feat: new tests
This commit is contained in:
parent
85e68bcb31
commit
3cd7eb316d
|
|
@ -1,173 +0,0 @@
|
||||||
// tests/api/01-basic.ts
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { api, log, runTest, saveImage, uploadFile, waitForGeneration, testContext } from './utils';
|
|
||||||
import { config, endpoints } from './config';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
log.section('BASIC TESTS');
|
|
||||||
|
|
||||||
// Test 1: Upload image
|
|
||||||
await runTest('Upload image', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
const response = await uploadFile(fixturePath, {
|
|
||||||
alias: '@test-logo',
|
|
||||||
description: 'Test logo image',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response || !response.id) {
|
|
||||||
throw new Error('No image returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
testContext.uploadedImageId = response.id;
|
|
||||||
log.detail('Image ID', response.id);
|
|
||||||
log.detail('Storage Key', response.storageKey);
|
|
||||||
log.detail('Alias', response.alias);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: List images
|
|
||||||
await runTest('List images', async () => {
|
|
||||||
const result = await api(endpoints.images);
|
|
||||||
|
|
||||||
if (!result.data.data || !Array.isArray(result.data.data)) {
|
|
||||||
throw new Error('No images array returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Total images', result.data.data.length);
|
|
||||||
log.detail('Has uploaded', result.data.data.some((img: any) => img.source === 'uploaded'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Get image by ID
|
|
||||||
await runTest('Get image by ID', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Source', result.data.data.source);
|
|
||||||
log.detail('File size', `${result.data.data.fileSize} bytes`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: Generate image without references
|
|
||||||
await runTest('Generate image (simple)', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A beautiful sunset over mountains',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('No generation returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
testContext.generationId = result.data.data.id;
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Status', result.data.data.status);
|
|
||||||
log.detail('Prompt', result.data.data.originalPrompt);
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(testContext.generationId);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Processing time', `${generation.processingTimeMs}ms`);
|
|
||||||
log.detail('Output image', generation.outputImageId);
|
|
||||||
|
|
||||||
// Save generated image
|
|
||||||
if (generation.outputImageId) {
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
|
|
||||||
// Download image
|
|
||||||
const imageUrl = imageResult.data.data.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
|
|
||||||
await saveImage(imageBuffer, 'simple-generation.png');
|
|
||||||
testContext.imageId = generation.outputImageId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Generate with uploaded reference
|
|
||||||
await runTest('Generate with reference image', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A product photo with the @test-logo in the corner',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
referenceImages: ['@test-logo'],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('No generation returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Referenced images', result.data.data.referencedImages?.length || 0);
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save generated image
|
|
||||||
if (generation.outputImageId) {
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
const imageUrl = imageResult.data.data.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
|
|
||||||
await saveImage(imageBuffer, 'with-reference.png');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 6: List generations
|
|
||||||
await runTest('List generations', async () => {
|
|
||||||
const result = await api(endpoints.generations);
|
|
||||||
|
|
||||||
if (!result.data.data || !Array.isArray(result.data.data)) {
|
|
||||||
throw new Error('No generations array returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Total generations', result.data.data.length);
|
|
||||||
log.detail('Successful', result.data.data.filter((g: any) => g.status === 'success').length);
|
|
||||||
log.detail('Has pagination', !!result.data.pagination);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 7: Get generation by ID
|
|
||||||
await runTest('Get generation details', async () => {
|
|
||||||
const result = await api(`${endpoints.generations}/${testContext.generationId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Generation not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Status', result.data.data.status);
|
|
||||||
log.detail('Has output image', !!result.data.data.outputImage);
|
|
||||||
log.detail('Referenced images', result.data.data.referencedImages?.length || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
log.section('BASIC TESTS COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
// tests/api/01-generation-basic.ts
|
||||||
|
// Basic Image Generation Tests - Run FIRST to verify core functionality
|
||||||
|
|
||||||
|
import { api, log, runTest, saveImage, waitForGeneration, testContext, verifyImageAccessible } from './utils';
|
||||||
|
import { endpoints } from './config';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('GENERATION BASIC TESTS');
|
||||||
|
|
||||||
|
// Test 1: Simple generation without references
|
||||||
|
await runTest('Generate image - simple prompt', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A beautiful sunset over mountains',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data || !result.data.data.id) {
|
||||||
|
throw new Error('No generation returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.basicGenerationId = result.data.data.id;
|
||||||
|
log.detail('Generation ID', result.data.data.id);
|
||||||
|
log.detail('Status', result.data.data.status);
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
log.info('Waiting for generation to complete...');
|
||||||
|
const generation = await waitForGeneration(testContext.basicGenerationId);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!generation.outputImageId) {
|
||||||
|
throw new Error('No output image ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Processing time', `${generation.processingTimeMs}ms`);
|
||||||
|
log.detail('Output image ID', generation.outputImageId);
|
||||||
|
|
||||||
|
// Verify image exists and is accessible
|
||||||
|
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
||||||
|
const imageUrl = imageResult.data.data.storageUrl;
|
||||||
|
|
||||||
|
const accessible = await verifyImageAccessible(imageUrl);
|
||||||
|
if (!accessible) {
|
||||||
|
throw new Error('Generated image not accessible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save for manual inspection
|
||||||
|
const imageResponse = await fetch(imageUrl);
|
||||||
|
const imageBuffer = await imageResponse.arrayBuffer();
|
||||||
|
await saveImage(imageBuffer, 'gen-basic-simple.png');
|
||||||
|
|
||||||
|
testContext.basicOutputImageId = generation.outputImageId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Generation with aspect ratio 1:1
|
||||||
|
await runTest('Generate image - square (1:1)', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A minimalist logo design',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Output image', generation.outputImageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Generation with aspect ratio 9:16 (portrait)
|
||||||
|
await runTest('Generate image - portrait (9:16)', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A tall building at night',
|
||||||
|
aspectRatio: '9:16',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Aspect ratio', '9:16');
|
||||||
|
log.detail('Output image', generation.outputImageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Get generation details
|
||||||
|
await runTest('Get generation by ID', async () => {
|
||||||
|
const result = await api(`${endpoints.generations}/${testContext.basicGenerationId}`);
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('Generation not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const generation = result.data.data;
|
||||||
|
|
||||||
|
// Verify all expected fields present
|
||||||
|
if (!generation.id) throw new Error('Missing id');
|
||||||
|
if (!generation.prompt) throw new Error('Missing prompt');
|
||||||
|
if (!generation.status) throw new Error('Missing status');
|
||||||
|
if (!generation.outputImageId) throw new Error('Missing outputImageId');
|
||||||
|
if (!generation.createdAt) throw new Error('Missing createdAt');
|
||||||
|
|
||||||
|
log.detail('Generation ID', generation.id);
|
||||||
|
log.detail('Prompt', generation.prompt);
|
||||||
|
log.detail('Status', generation.status);
|
||||||
|
log.detail('Has output image', !!generation.outputImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: List generations
|
||||||
|
await runTest('List all generations', async () => {
|
||||||
|
const result = await api(endpoints.generations);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No generations array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Total generations', result.data.data.length);
|
||||||
|
|
||||||
|
// Verify our generation is in the list
|
||||||
|
const found = result.data.data.find((g: any) => g.id === testContext.basicGenerationId);
|
||||||
|
if (!found) {
|
||||||
|
throw new Error('Created generation not in list');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Found our generation', '✓');
|
||||||
|
log.detail('Successful generations', result.data.data.filter((g: any) => g.status === 'success').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: List generations with pagination
|
||||||
|
await runTest('List generations with pagination', async () => {
|
||||||
|
const result = await api(`${endpoints.generations}?limit=2&offset=0`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No generations array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data.pagination) {
|
||||||
|
throw new Error('No pagination data');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Limit', result.data.pagination.limit);
|
||||||
|
log.detail('Offset', result.data.pagination.offset);
|
||||||
|
log.detail('Total', result.data.pagination.total);
|
||||||
|
log.detail('Has more', result.data.pagination.hasMore);
|
||||||
|
|
||||||
|
// Results should be limited
|
||||||
|
if (result.data.data.length > 2) {
|
||||||
|
throw new Error('Pagination limit not applied');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: List generations with status filter
|
||||||
|
await runTest('List generations - filter by status', async () => {
|
||||||
|
const result = await api(`${endpoints.generations}?status=success`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No generations array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// All results should have success status
|
||||||
|
const allSuccess = result.data.data.every((g: any) => g.status === 'success');
|
||||||
|
if (!allSuccess) {
|
||||||
|
throw new Error('Status filter not working');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Success generations', result.data.data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: Generation processing time is recorded
|
||||||
|
await runTest('Verify processing time recorded', async () => {
|
||||||
|
const result = await api(`${endpoints.generations}/${testContext.basicGenerationId}`);
|
||||||
|
const generation = result.data.data;
|
||||||
|
|
||||||
|
if (typeof generation.processingTimeMs !== 'number') {
|
||||||
|
throw new Error('Processing time not recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generation.processingTimeMs <= 0) {
|
||||||
|
throw new Error('Processing time should be positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Processing time', `${generation.processingTimeMs}ms`);
|
||||||
|
log.detail('Approximately', `${(generation.processingTimeMs / 1000).toFixed(2)}s`);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('GENERATION BASIC TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
// tests/api/02-basic.ts
|
||||||
|
// Image Upload and CRUD Operations
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { api, log, runTest, saveImage, uploadFile, waitForGeneration, testContext, verifyImageAccessible, resolveAlias } from './utils';
|
||||||
|
import { config, endpoints } from './config';
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('IMAGE UPLOAD & CRUD TESTS');
|
||||||
|
|
||||||
|
// Test 1: Upload image with project-scoped alias
|
||||||
|
await runTest('Upload image with project alias', async () => {
|
||||||
|
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
||||||
|
|
||||||
|
const response = await uploadFile(fixturePath, {
|
||||||
|
alias: '@test-logo',
|
||||||
|
description: 'Test logo image for CRUD tests',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.id) {
|
||||||
|
throw new Error('No image returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.alias !== '@test-logo') {
|
||||||
|
throw new Error('Alias not set correctly');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.source !== 'uploaded') {
|
||||||
|
throw new Error('Source should be "uploaded"');
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.uploadedImageId = response.id;
|
||||||
|
log.detail('Image ID', response.id);
|
||||||
|
log.detail('Storage Key', response.storageKey);
|
||||||
|
log.detail('Alias', response.alias);
|
||||||
|
log.detail('Source', response.source);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Upload image without alias
|
||||||
|
await runTest('Upload image without alias', async () => {
|
||||||
|
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
||||||
|
|
||||||
|
const response = await uploadFile(fixturePath, {
|
||||||
|
description: 'Image without alias',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.id) {
|
||||||
|
throw new Error('No image returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.alias !== null) {
|
||||||
|
throw new Error('Alias should be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Image ID', response.id);
|
||||||
|
log.detail('Alias', 'null (as expected)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: List all images
|
||||||
|
await runTest('List all images', async () => {
|
||||||
|
const result = await api(endpoints.images);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No images array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Total images', result.data.data.length);
|
||||||
|
|
||||||
|
// Find our uploaded image
|
||||||
|
const found = result.data.data.find((img: any) => img.id === testContext.uploadedImageId);
|
||||||
|
if (!found) {
|
||||||
|
throw new Error('Uploaded image not in list');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Found our image', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: List images with source filter
|
||||||
|
await runTest('List images - filter by source=uploaded', async () => {
|
||||||
|
const result = await api(`${endpoints.images}?source=uploaded`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No images array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should be uploaded
|
||||||
|
const allUploaded = result.data.data.every((img: any) => img.source === 'uploaded');
|
||||||
|
if (!allUploaded) {
|
||||||
|
throw new Error('Source filter not working');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Uploaded images', result.data.data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: List images with pagination
|
||||||
|
await runTest('List images with pagination', async () => {
|
||||||
|
const result = await api(`${endpoints.images}?limit=3&offset=0`);
|
||||||
|
|
||||||
|
if (!result.data.pagination) {
|
||||||
|
throw new Error('No pagination data');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Limit', result.data.pagination.limit);
|
||||||
|
log.detail('Offset', result.data.pagination.offset);
|
||||||
|
log.detail('Total', result.data.pagination.total);
|
||||||
|
log.detail('Has more', result.data.pagination.hasMore);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: Get image by ID
|
||||||
|
await runTest('Get image by ID', async () => {
|
||||||
|
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`);
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('Image not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = result.data.data;
|
||||||
|
|
||||||
|
// Verify fields
|
||||||
|
if (!image.id) throw new Error('Missing id');
|
||||||
|
if (!image.storageKey) throw new Error('Missing storageKey');
|
||||||
|
if (!image.storageUrl) throw new Error('Missing storageUrl');
|
||||||
|
if (!image.source) throw new Error('Missing source');
|
||||||
|
|
||||||
|
log.detail('Image ID', image.id);
|
||||||
|
log.detail('Source', image.source);
|
||||||
|
log.detail('File size', `${image.fileSize || 0} bytes`);
|
||||||
|
log.detail('Alias', image.alias || 'null');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Get image by alias (using resolve endpoint)
|
||||||
|
await runTest('Resolve project-scoped alias', async () => {
|
||||||
|
const resolved = await resolveAlias('@test-logo');
|
||||||
|
|
||||||
|
if (!resolved.imageId) {
|
||||||
|
throw new Error('Alias not resolved');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.imageId !== testContext.uploadedImageId) {
|
||||||
|
throw new Error('Resolved to wrong image');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.scope !== 'project') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Resolved image ID', resolved.imageId);
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', resolved.alias);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: Update image metadata
|
||||||
|
await runTest('Update image metadata', async () => {
|
||||||
|
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
focalPoint: { x: 0.5, y: 0.3 },
|
||||||
|
meta: {
|
||||||
|
description: 'Updated description',
|
||||||
|
tags: ['test', 'logo', 'updated'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No image returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify update by fetching again
|
||||||
|
const updated = await api(`${endpoints.images}/${testContext.uploadedImageId}`);
|
||||||
|
const image = updated.data.data;
|
||||||
|
|
||||||
|
if (!image.focalPoint || image.focalPoint.x !== 0.5 || image.focalPoint.y !== 0.3) {
|
||||||
|
throw new Error('Focal point not updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Focal point', JSON.stringify(image.focalPoint));
|
||||||
|
log.detail('Meta', JSON.stringify(image.meta));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 9: Update image alias (dedicated endpoint)
|
||||||
|
await runTest('Update image alias', async () => {
|
||||||
|
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
alias: '@new-test-logo',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No image returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify new alias works
|
||||||
|
const resolved = await resolveAlias('@new-test-logo');
|
||||||
|
if (resolved.imageId !== testContext.uploadedImageId) {
|
||||||
|
throw new Error('New alias not working');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('New alias', '@new-test-logo');
|
||||||
|
log.detail('Resolved', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 10: Verify old alias doesn't work after update
|
||||||
|
await runTest('Old alias should not resolve after update', async () => {
|
||||||
|
try {
|
||||||
|
await resolveAlias('@test-logo');
|
||||||
|
throw new Error('Old alias should not resolve');
|
||||||
|
} catch (error: any) {
|
||||||
|
// Expected to fail
|
||||||
|
if (error.message.includes('should not resolve')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
log.detail('Old alias correctly invalid', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 11: Remove image alias
|
||||||
|
await runTest('Remove image alias', async () => {
|
||||||
|
await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
alias: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify image exists but has no alias
|
||||||
|
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`);
|
||||||
|
if (result.data.data.alias !== null) {
|
||||||
|
throw new Error('Alias should be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify alias resolution fails
|
||||||
|
try {
|
||||||
|
await resolveAlias('@new-test-logo');
|
||||||
|
throw new Error('Removed alias should not resolve');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('should not resolve')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
log.detail('Alias removed', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 12: Generate image with manual reference
|
||||||
|
await runTest('Generate with manual reference image', async () => {
|
||||||
|
// First, reassign alias for reference
|
||||||
|
await api(`${endpoints.images}/${testContext.uploadedImageId}/alias`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
alias: '@reference-logo',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A product photo with the logo in corner',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
referenceImages: ['@reference-logo'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify referenced images tracked
|
||||||
|
if (!generation.referencedImages || generation.referencedImages.length === 0) {
|
||||||
|
throw new Error('Referenced images not tracked');
|
||||||
|
}
|
||||||
|
|
||||||
|
const refFound = generation.referencedImages.some(
|
||||||
|
(ref: any) => ref.alias === '@reference-logo'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!refFound) {
|
||||||
|
throw new Error('Reference image not found in referencedImages');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Generation ID', generation.id);
|
||||||
|
log.detail('Referenced images', generation.referencedImages.length);
|
||||||
|
|
||||||
|
// Save generated image
|
||||||
|
if (generation.outputImageId) {
|
||||||
|
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
||||||
|
const imageUrl = imageResult.data.data.storageUrl;
|
||||||
|
const imageResponse = await fetch(imageUrl);
|
||||||
|
const imageBuffer = await imageResponse.arrayBuffer();
|
||||||
|
await saveImage(imageBuffer, 'gen-with-reference.png');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 13: Generate with auto-detected reference in prompt
|
||||||
|
await runTest('Generate with auto-detected reference', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Create banner using @reference-logo with blue background',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
// NOTE: referenceImages NOT provided, should auto-detect
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify auto-detection worked
|
||||||
|
if (!generation.referencedImages || generation.referencedImages.length === 0) {
|
||||||
|
throw new Error('Auto-detection did not work');
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoDetected = generation.referencedImages.some(
|
||||||
|
(ref: any) => ref.alias === '@reference-logo'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!autoDetected) {
|
||||||
|
throw new Error('Reference not auto-detected from prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Auto-detected references', generation.referencedImages.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 14: Generate with project alias assignment
|
||||||
|
await runTest('Generate with project alias assignment', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A hero banner image',
|
||||||
|
aspectRatio: '21:9',
|
||||||
|
alias: '@hero-banner',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify alias assigned to output image
|
||||||
|
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
||||||
|
const image = imageResult.data.data;
|
||||||
|
|
||||||
|
if (image.alias !== '@hero-banner') {
|
||||||
|
throw new Error('Alias not assigned to output image');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify alias resolution works
|
||||||
|
const resolved = await resolveAlias('@hero-banner');
|
||||||
|
if (resolved.imageId !== generation.outputImageId) {
|
||||||
|
throw new Error('Alias resolution failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Output image alias', image.alias);
|
||||||
|
log.detail('Alias resolution', '✓');
|
||||||
|
|
||||||
|
testContext.heroBannerId = generation.outputImageId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 15: Alias conflict - new generation overwrites
|
||||||
|
await runTest('Alias conflict resolution', async () => {
|
||||||
|
// First generation has @hero alias (from previous test)
|
||||||
|
const firstImageId = testContext.heroBannerId;
|
||||||
|
|
||||||
|
// Create second generation with same alias
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A different hero image',
|
||||||
|
aspectRatio: '21:9',
|
||||||
|
alias: '@hero-banner',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondImageId = generation.outputImageId;
|
||||||
|
|
||||||
|
// Verify second image has the alias
|
||||||
|
const resolved = await resolveAlias('@hero-banner');
|
||||||
|
if (resolved.imageId !== secondImageId) {
|
||||||
|
throw new Error('Second image should have the alias');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify first image lost the alias but still exists
|
||||||
|
const firstImage = await api(`${endpoints.images}/${firstImageId}`);
|
||||||
|
if (firstImage.data.data.alias !== null) {
|
||||||
|
throw new Error('First image should have lost the alias');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Second image has alias', '✓');
|
||||||
|
log.detail('First image preserved', '✓');
|
||||||
|
log.detail('First image alias removed', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('IMAGE UPLOAD & CRUD TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
// tests/api/02-flows.ts
|
|
||||||
|
|
||||||
import { api, log, runTest, saveImage, waitForGeneration, testContext } from './utils';
|
|
||||||
import { endpoints } from './config';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
log.section('FLOW TESTS');
|
|
||||||
|
|
||||||
// Test 1: Create flow
|
|
||||||
await runTest('Create flow', async () => {
|
|
||||||
const result = await api(endpoints.flows, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
meta: { purpose: 'test-flow', description: 'Testing flow functionality' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data || !result.data.data.id) {
|
|
||||||
throw new Error('No flow returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
testContext.flowId = result.data.data.id;
|
|
||||||
log.detail('Flow ID', result.data.data.id);
|
|
||||||
log.detail('Aliases', JSON.stringify(result.data.data.aliases));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: List flows
|
|
||||||
await runTest('List flows', async () => {
|
|
||||||
const result = await api(endpoints.flows);
|
|
||||||
|
|
||||||
if (!result.data.data || !Array.isArray(result.data.data)) {
|
|
||||||
throw new Error('No flows array returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Total flows', result.data.data.length);
|
|
||||||
log.detail('Has pagination', !!result.data.pagination);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Generate in flow (first generation)
|
|
||||||
await runTest('Generate in flow (first)', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A red sports car on a mountain road',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('No generation returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Flow ID', result.data.data.flowId);
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Output image', generation.outputImageId);
|
|
||||||
|
|
||||||
// Save image
|
|
||||||
if (generation.outputImageId) {
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
const imageUrl = imageResult.data.image.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
|
|
||||||
await saveImage(imageBuffer, 'flow-gen-1.png');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: Generate in flow (second generation) with @last reference
|
|
||||||
await runTest('Generate in flow with @last', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Same as @last but make it blue instead of red',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
referenceImages: ['@last'],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('No generation returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Referenced @last', result.data.data.referencedImages?.some((r: any) => r.alias === '@last'));
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save image
|
|
||||||
if (generation.outputImageId) {
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
const imageUrl = imageResult.data.image.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
|
|
||||||
await saveImage(imageBuffer, 'flow-gen-2-with-last.png');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Get flow details
|
|
||||||
await runTest('Get flow details', async () => {
|
|
||||||
const result = await api(`${endpoints.flows}/${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Flow not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Flow ID', result.data.data.id);
|
|
||||||
log.detail('Generations count', result.data.datas?.length || 0);
|
|
||||||
log.detail('Images count', result.data.data?.length || 0);
|
|
||||||
log.detail('Resolved aliases', Object.keys(result.data.resolvedAliases || {}).length);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 6: Update flow aliases
|
|
||||||
await runTest('Update flow aliases', async () => {
|
|
||||||
// First, get the latest generation's image ID
|
|
||||||
const flowResult = await api(`${endpoints.flows}/${testContext.flowId}`);
|
|
||||||
const lastGeneration = flowResult.data.generations[flowResult.data.generations.length - 1];
|
|
||||||
|
|
||||||
if (!lastGeneration.outputImageId) {
|
|
||||||
throw new Error('No output image for alias assignment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.flows}/${testContext.flowId}/aliases`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
aliases: {
|
|
||||||
'@hero': lastGeneration.outputImageId,
|
|
||||||
'@featured': lastGeneration.outputImageId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Flow not returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Updated aliases', JSON.stringify(result.data.data.aliases));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 7: Generate with flow-scoped alias
|
|
||||||
await runTest('Generate with flow-scoped alias', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A poster design featuring @hero image',
|
|
||||||
aspectRatio: '9:16',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
referenceImages: ['@hero'],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('No generation returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Generation ID', result.data.data.id);
|
|
||||||
log.detail('Referenced @hero', result.data.data.referencedImages?.some((r: any) => r.alias === '@hero'));
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save image
|
|
||||||
if (generation.outputImageId) {
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
const imageUrl = imageResult.data.image.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
|
|
||||||
await saveImage(imageBuffer, 'flow-gen-with-hero.png');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 8: Delete flow alias
|
|
||||||
await runTest('Delete flow alias', async () => {
|
|
||||||
await api(`${endpoints.flows}/${testContext.flowId}/aliases/@featured`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify it's deleted
|
|
||||||
const result = await api(`${endpoints.flows}/${testContext.flowId}`);
|
|
||||||
const hasFeatureAlias = '@featured' in result.data.data.aliases;
|
|
||||||
|
|
||||||
if (hasFeatureAlias) {
|
|
||||||
throw new Error('Alias was not deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Remaining aliases', JSON.stringify(result.data.data.aliases));
|
|
||||||
});
|
|
||||||
|
|
||||||
log.section('FLOW TESTS COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
// tests/api/03-aliases.ts
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { api, log, runTest, saveImage, uploadFile, waitForGeneration, testContext } from './utils';
|
|
||||||
import { config, endpoints } from './config';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
log.section('ALIAS TESTS');
|
|
||||||
|
|
||||||
// Test 1: Upload with project-scoped alias
|
|
||||||
await runTest('Upload with project alias', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
const response = await uploadFile(fixturePath, {
|
|
||||||
alias: '@brand-logo',
|
|
||||||
description: 'Brand logo for project-wide use',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.image.alias || response.image.alias !== '@brand-logo') {
|
|
||||||
throw new Error('Alias not set correctly');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', response.image.id);
|
|
||||||
log.detail('Project alias', response.image.alias);
|
|
||||||
log.detail('Flow ID', response.image.flowId || 'null (project-scoped)');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Upload with flow-scoped alias
|
|
||||||
await runTest('Upload with flow alias', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
const response = await uploadFile(fixturePath, {
|
|
||||||
flowAlias: '@temp-logo',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
description: 'Temporary logo for flow use',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.flow || !response.flow.aliases['@temp-logo']) {
|
|
||||||
throw new Error('Flow alias not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', response.image.id);
|
|
||||||
log.detail('Flow ID', response.image.flowId);
|
|
||||||
log.detail('Flow aliases', JSON.stringify(response.flow.aliases));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Upload with BOTH project and flow aliases
|
|
||||||
await runTest('Upload with dual aliases', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
const response = await uploadFile(fixturePath, {
|
|
||||||
alias: '@global-asset',
|
|
||||||
flowAlias: '@flow-asset',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
description: 'Image with both alias types',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.image.alias || response.image.alias !== '@global-asset') {
|
|
||||||
throw new Error('Project alias not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.flow || !response.flow.aliases['@flow-asset']) {
|
|
||||||
throw new Error('Flow alias not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', response.image.id);
|
|
||||||
log.detail('Project alias', response.image.alias);
|
|
||||||
log.detail('Flow alias', '@flow-asset');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: Resolve project-scoped alias
|
|
||||||
await runTest('Resolve project alias', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@brand-logo`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not resolved');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data.data.scope !== 'project') {
|
|
||||||
throw new Error(`Wrong scope: ${result.data.data.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Scope', result.data.data.scope);
|
|
||||||
log.detail('Alias', result.data.data.alias);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Resolve flow-scoped alias
|
|
||||||
await runTest('Resolve flow alias', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@temp-logo?flowId=${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not resolved');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data.data.scope !== 'flow') {
|
|
||||||
throw new Error(`Wrong scope: ${result.data.data.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Scope', result.data.data.scope);
|
|
||||||
log.detail('Flow ID', result.data.data?.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 6: Resolve @last technical alias
|
|
||||||
await runTest('Resolve @last technical alias', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@last?flowId=${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not resolved');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data.data.scope !== 'technical') {
|
|
||||||
throw new Error(`Wrong scope: ${result.data.data.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Scope', result.data.data.scope);
|
|
||||||
log.detail('Technical alias', '@last');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 7: Resolve @first technical alias
|
|
||||||
await runTest('Resolve @first technical alias', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@first?flowId=${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not resolved');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data.data.scope !== 'technical') {
|
|
||||||
throw new Error(`Wrong scope: ${result.data.data.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Scope', result.data.data.scope);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 8: Resolve @upload technical alias
|
|
||||||
await runTest('Resolve @upload technical alias', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@upload?flowId=${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!result.data.data) {
|
|
||||||
throw new Error('Image not resolved');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data.data.scope !== 'technical') {
|
|
||||||
throw new Error(`Wrong scope: ${result.data.data.scope}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Image ID', result.data.data.id);
|
|
||||||
log.detail('Scope', result.data.data.scope);
|
|
||||||
log.detail('Source', result.data.data.source);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 9: Generate with assignAlias (project-scoped)
|
|
||||||
await runTest('Generate with project alias assignment', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A minimalist logo design',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
assignAlias: '@generated-logo',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if alias was assigned
|
|
||||||
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
|
|
||||||
|
|
||||||
if (imageResult.data.image.alias !== '@generated-logo') {
|
|
||||||
throw new Error('Alias not assigned to generated image');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Output image', generation.outputImageId);
|
|
||||||
log.detail('Assigned alias', imageResult.data.image.alias);
|
|
||||||
|
|
||||||
// Save image
|
|
||||||
const imageUrl = imageResult.data.image.storageUrl;
|
|
||||||
const imageResponse = await fetch(imageUrl);
|
|
||||||
const imageBuffer = await imageResponse.arrayBuffer();
|
|
||||||
await saveImage(imageBuffer, 'generated-with-alias.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 10: Generate with assignFlowAlias (flow-scoped)
|
|
||||||
await runTest('Generate with flow alias assignment', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'A vibrant abstract pattern',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
assignFlowAlias: '@pattern',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info('Waiting for generation to complete...');
|
|
||||||
const generation = await waitForGeneration(result.data.data.id);
|
|
||||||
|
|
||||||
if (generation.status !== 'success') {
|
|
||||||
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if flow alias was assigned
|
|
||||||
const flowResult = await api(`${endpoints.flows}/${testContext.flowId}`);
|
|
||||||
|
|
||||||
if (!flowResult.data.flow.aliases['@pattern']) {
|
|
||||||
throw new Error('Flow alias not assigned');
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Output image', generation.outputImageId);
|
|
||||||
log.detail('Flow alias assigned', '@pattern');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 11: Alias precedence (flow > project)
|
|
||||||
await runTest('Test alias precedence', async () => {
|
|
||||||
// Create project alias @test
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
const projectResponse = await uploadFile(fixturePath, {
|
|
||||||
alias: '@precedence-test',
|
|
||||||
});
|
|
||||||
const projectImageId = projectResponse.image.id;
|
|
||||||
|
|
||||||
// Create flow alias @test (different image)
|
|
||||||
const flowResponse = await uploadFile(fixturePath, {
|
|
||||||
flowAlias: '@precedence-test',
|
|
||||||
flowId: testContext.flowId,
|
|
||||||
});
|
|
||||||
const flowImageId = flowResponse.image.id;
|
|
||||||
|
|
||||||
// Resolve without flowId (should get project alias)
|
|
||||||
const withoutFlow = await api(`${endpoints.images}/resolve/@precedence-test`);
|
|
||||||
if (withoutFlow.data.image.id !== projectImageId) {
|
|
||||||
throw new Error('Should resolve to project alias');
|
|
||||||
}
|
|
||||||
log.detail('Without flow context', 'resolved to project alias ✓');
|
|
||||||
|
|
||||||
// Resolve with flowId (should get flow alias)
|
|
||||||
const withFlow = await api(`${endpoints.images}/resolve/@precedence-test?flowId=${testContext.flowId}`);
|
|
||||||
if (withFlow.data.image.id !== flowImageId) {
|
|
||||||
throw new Error('Should resolve to flow alias');
|
|
||||||
}
|
|
||||||
log.detail('With flow context', 'resolved to flow alias ✓');
|
|
||||||
});
|
|
||||||
|
|
||||||
log.section('ALIAS TESTS COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
// tests/api/03-flows.ts
|
||||||
|
// Flow Lifecycle Tests - Lazy and Eager Creation Patterns
|
||||||
|
|
||||||
|
import { api, log, runTest, saveImage, waitForGeneration, testContext, resolveAlias } from './utils';
|
||||||
|
import { endpoints } from './config';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('FLOW LIFECYCLE TESTS');
|
||||||
|
|
||||||
|
// Test 1: Lazy flow pattern - first generation without flowId
|
||||||
|
await runTest('Lazy flow - generation without flowId', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A red sports car on a mountain road',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
// NOTE: flowId not provided, should auto-generate
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data.flowId) {
|
||||||
|
throw new Error('No flowId returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.lazyFlowId = result.data.data.flowId;
|
||||||
|
log.detail('Auto-generated flowId', testContext.lazyFlowId);
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.firstGenId = generation.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Lazy flow - verify flow doesn't exist yet
|
||||||
|
await runTest('Lazy flow - verify flow not created yet', async () => {
|
||||||
|
try {
|
||||||
|
await api(`${endpoints.flows}/${testContext.lazyFlowId}`, {
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
throw new Error('Flow should not exist yet (lazy creation)');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('should not exist')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
log.detail('Flow correctly does not exist', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Lazy flow - second use creates flow
|
||||||
|
await runTest('Lazy flow - second generation creates flow', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Same car but blue color',
|
||||||
|
aspectRatio: '16:9',
|
||||||
|
flowId: testContext.lazyFlowId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now flow should exist
|
||||||
|
const flowResult = await api(`${endpoints.flows}/${testContext.lazyFlowId}`);
|
||||||
|
if (!flowResult.data.data) {
|
||||||
|
throw new Error('Flow should exist after second use');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Flow now exists', '✓');
|
||||||
|
log.detail('Flow ID', flowResult.data.data.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Eager flow creation with flowAlias
|
||||||
|
await runTest('Eager flow - created immediately with flowAlias', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'A hero banner image',
|
||||||
|
aspectRatio: '21:9',
|
||||||
|
flowAlias: '@hero-flow',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data.flowId) {
|
||||||
|
throw new Error('No flowId returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext.eagerFlowId = result.data.data.flowId;
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow should exist immediately
|
||||||
|
const flowResult = await api(`${endpoints.flows}/${testContext.eagerFlowId}`);
|
||||||
|
if (!flowResult.data.data) {
|
||||||
|
throw new Error('Flow should exist immediately (eager creation)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flowResult.data.data.aliases || !flowResult.data.data.aliases['@hero-flow']) {
|
||||||
|
throw new Error('Flow alias not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Flow exists immediately', '✓');
|
||||||
|
log.detail('Flow alias', '@hero-flow');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: List all flows
|
||||||
|
await runTest('List all flows', async () => {
|
||||||
|
const result = await api(endpoints.flows);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No flows array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = result.data.data.filter((f: any) =>
|
||||||
|
f.id === testContext.lazyFlowId || f.id === testContext.eagerFlowId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (found.length !== 2) {
|
||||||
|
throw new Error('Not all created flows found');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Total flows', result.data.data.length);
|
||||||
|
log.detail('Our flows found', found.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: Get flow details with computed counts
|
||||||
|
await runTest('Get flow with computed counts', async () => {
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}`);
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('Flow not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const flow = result.data.data;
|
||||||
|
|
||||||
|
if (typeof flow.generationCount !== 'number') {
|
||||||
|
throw new Error('Missing generationCount');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof flow.imageCount !== 'number') {
|
||||||
|
throw new Error('Missing imageCount');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Generation count', flow.generationCount);
|
||||||
|
log.detail('Image count', flow.imageCount);
|
||||||
|
log.detail('Aliases', JSON.stringify(flow.aliases));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Get flow's generations
|
||||||
|
await runTest('List flow generations', async () => {
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}/generations`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No generations array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Generations in flow', result.data.data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: Get flow's images
|
||||||
|
await runTest('List flow images', async () => {
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}/images`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No images array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Images in flow', result.data.data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 9: Update flow aliases
|
||||||
|
await runTest('Update flow aliases', async () => {
|
||||||
|
// Get a generation to use
|
||||||
|
const flowResult = await api(`${endpoints.flows}/${testContext.lazyFlowId}`);
|
||||||
|
const gens = await api(`${endpoints.flows}/${testContext.lazyFlowId}/generations`);
|
||||||
|
const lastGen = gens.data.data[0];
|
||||||
|
|
||||||
|
if (!lastGen.outputImageId) {
|
||||||
|
throw new Error('No output image');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}/aliases`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
aliases: {
|
||||||
|
'@latest': lastGen.outputImageId,
|
||||||
|
'@best': lastGen.outputImageId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data.aliases) {
|
||||||
|
throw new Error('No aliases returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Updated aliases', JSON.stringify(result.data.data.aliases));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 10: Remove specific flow alias
|
||||||
|
await runTest('Remove specific flow alias', async () => {
|
||||||
|
await api(`${endpoints.flows}/${testContext.lazyFlowId}/aliases/@best`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}`);
|
||||||
|
if ('@best' in result.data.data.aliases) {
|
||||||
|
throw new Error('Alias should be removed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('@latest' in result.data.data.aliases)) {
|
||||||
|
throw new Error('Other aliases should remain');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Removed @best', '✓');
|
||||||
|
log.detail('Kept @latest', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 11: Flow regenerate endpoint
|
||||||
|
await runTest('Regenerate flow (most recent generation)', async () => {
|
||||||
|
const result = await api(`${endpoints.flows}/${testContext.lazyFlowId}/regenerate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No generation returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Regeneration triggered', '✓');
|
||||||
|
log.detail('Generation ID', result.data.data.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('FLOW LIFECYCLE TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -0,0 +1,278 @@
|
||||||
|
// tests/api/04-aliases.ts
|
||||||
|
// 3-Tier Alias Resolution System Tests
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { api, log, runTest, uploadFile, waitForGeneration, testContext, resolveAlias, createTestImage } from './utils';
|
||||||
|
import { config, endpoints } from './config';
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('ALIAS RESOLUTION TESTS');
|
||||||
|
|
||||||
|
// Setup: Create a flow for testing
|
||||||
|
const setupGen = await createTestImage('Setup image for alias tests', {
|
||||||
|
flowAlias: '@alias-test-flow',
|
||||||
|
});
|
||||||
|
testContext.aliasFlowId = setupGen.flowId;
|
||||||
|
log.info(`Test flow created: ${testContext.aliasFlowId}`);
|
||||||
|
|
||||||
|
// Test 1: Technical alias @last
|
||||||
|
await runTest('Technical alias - @last', async () => {
|
||||||
|
const resolved = await resolveAlias('@last', testContext.aliasFlowId);
|
||||||
|
|
||||||
|
if (resolved.scope !== 'technical') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolved.imageId) {
|
||||||
|
throw new Error('No image resolved');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', '@last');
|
||||||
|
log.detail('Image ID', resolved.imageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Technical alias @first
|
||||||
|
await runTest('Technical alias - @first', async () => {
|
||||||
|
const resolved = await resolveAlias('@first', testContext.aliasFlowId);
|
||||||
|
|
||||||
|
if (resolved.scope !== 'technical') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', '@first');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Technical alias @upload
|
||||||
|
await runTest('Technical alias - @upload', async () => {
|
||||||
|
// First upload an image to the flow
|
||||||
|
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
||||||
|
await uploadFile(fixturePath, {
|
||||||
|
flowId: testContext.aliasFlowId,
|
||||||
|
description: 'Uploaded for @upload test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = await resolveAlias('@upload', testContext.aliasFlowId);
|
||||||
|
|
||||||
|
if (resolved.scope !== 'technical') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', '@upload');
|
||||||
|
log.detail('Image source', 'uploaded');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Technical alias requires flowId
|
||||||
|
await runTest('Technical alias requires flow context', async () => {
|
||||||
|
try {
|
||||||
|
await resolveAlias('@last'); // No flowId
|
||||||
|
throw new Error('Should require flowId');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('Should require')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
log.detail('Correctly requires flowId', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Flow-scoped alias
|
||||||
|
await runTest('Flow-scoped alias resolution', async () => {
|
||||||
|
const gen = await createTestImage('Image for flow alias', {
|
||||||
|
flowId: testContext.aliasFlowId,
|
||||||
|
flowAlias: '@flow-hero',
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = await resolveAlias('@flow-hero', testContext.aliasFlowId);
|
||||||
|
|
||||||
|
if (resolved.scope !== 'flow') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', '@flow-hero');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: Project-scoped alias
|
||||||
|
await runTest('Project-scoped alias resolution', async () => {
|
||||||
|
const gen = await createTestImage('Image for project alias', {
|
||||||
|
alias: '@project-logo',
|
||||||
|
flowId: null, // Explicitly no flow
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = await resolveAlias('@project-logo');
|
||||||
|
|
||||||
|
if (resolved.scope !== 'project') {
|
||||||
|
throw new Error(`Wrong scope: ${resolved.scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope', resolved.scope);
|
||||||
|
log.detail('Alias', '@project-logo');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Alias priority - flow overrides project
|
||||||
|
await runTest('Alias precedence - flow > project', async () => {
|
||||||
|
// Create project alias
|
||||||
|
const projectGen = await createTestImage('Project scoped image', {
|
||||||
|
alias: '@priority-test',
|
||||||
|
flowId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create flow alias with same name
|
||||||
|
const flowGen = await createTestImage('Flow scoped image', {
|
||||||
|
flowId: testContext.aliasFlowId,
|
||||||
|
flowAlias: '@priority-test',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Without flow context - should get project
|
||||||
|
const projectResolved = await resolveAlias('@priority-test');
|
||||||
|
if (projectResolved.imageId !== projectGen.outputImageId) {
|
||||||
|
throw new Error('Should resolve to project alias');
|
||||||
|
}
|
||||||
|
log.detail('Without flow context', 'resolved to project ✓');
|
||||||
|
|
||||||
|
// With flow context - should get flow
|
||||||
|
const flowResolved = await resolveAlias('@priority-test', testContext.aliasFlowId);
|
||||||
|
if (flowResolved.imageId !== flowGen.outputImageId) {
|
||||||
|
throw new Error('Should resolve to flow alias');
|
||||||
|
}
|
||||||
|
log.detail('With flow context', 'resolved to flow ✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: Reserved alias validation
|
||||||
|
await runTest('Reserved aliases cannot be assigned', async () => {
|
||||||
|
const reservedAliases = ['@last', '@first', '@upload'];
|
||||||
|
|
||||||
|
for (const reserved of reservedAliases) {
|
||||||
|
try {
|
||||||
|
const gen = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Test',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
alias: reserved,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we get here, it didn't throw - that's bad
|
||||||
|
log.warning(`Reserved alias ${reserved} was allowed!`);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Expected to fail
|
||||||
|
log.detail(`${reserved} correctly blocked`, '✓');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 9: Alias reassignment
|
||||||
|
await runTest('Alias reassignment removes old', async () => {
|
||||||
|
const gen1 = await createTestImage('First image', {
|
||||||
|
alias: '@reassign-test',
|
||||||
|
flowId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gen2 = await createTestImage('Second image', {
|
||||||
|
alias: '@reassign-test',
|
||||||
|
flowId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that gen2 has the alias
|
||||||
|
const resolved = await resolveAlias('@reassign-test');
|
||||||
|
if (resolved.imageId !== gen2.outputImageId) {
|
||||||
|
throw new Error('Alias should be on second image');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that gen1 lost the alias
|
||||||
|
const img1 = await api(`${endpoints.images}/${gen1.outputImageId}`);
|
||||||
|
if (img1.data.data.alias !== null) {
|
||||||
|
throw new Error('First image should have lost alias');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Second image has alias', '✓');
|
||||||
|
log.detail('First image lost alias', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 10: Same alias in different flows
|
||||||
|
await runTest('Same alias in different flows', async () => {
|
||||||
|
// Create two flows with same alias
|
||||||
|
const gen1 = await createTestImage('Flow 1 image', {
|
||||||
|
flowAlias: '@shared-name',
|
||||||
|
});
|
||||||
|
|
||||||
|
const gen2 = await createTestImage('Flow 2 image', {
|
||||||
|
flowAlias: '@shared-name',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve in each flow context
|
||||||
|
const resolved1 = await resolveAlias('@shared-name', gen1.flowId);
|
||||||
|
const resolved2 = await resolveAlias('@shared-name', gen2.flowId);
|
||||||
|
|
||||||
|
if (resolved1.imageId === resolved2.imageId) {
|
||||||
|
throw new Error('Should resolve to different images');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Flow 1 image', resolved1.imageId.slice(0, 8));
|
||||||
|
log.detail('Flow 2 image', resolved2.imageId.slice(0, 8));
|
||||||
|
log.detail('Isolation confirmed', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 11: Technical alias in generation prompt
|
||||||
|
await runTest('Use technical alias in prompt', async () => {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'New variation based on @last',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
flowId: testContext.aliasFlowId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error('Generation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that @last was resolved
|
||||||
|
const hasLast = generation.referencedImages?.some((ref: any) => ref.alias === '@last');
|
||||||
|
if (!hasLast) {
|
||||||
|
throw new Error('Technical alias not resolved in prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Technical alias resolved', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 12: Upload with dual aliases
|
||||||
|
await runTest('Upload with both project and flow alias', async () => {
|
||||||
|
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
||||||
|
|
||||||
|
const response = await uploadFile(fixturePath, {
|
||||||
|
alias: '@dual-project',
|
||||||
|
flowId: testContext.aliasFlowId,
|
||||||
|
flowAlias: '@dual-flow',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify both aliases work
|
||||||
|
const projectResolved = await resolveAlias('@dual-project');
|
||||||
|
const flowResolved = await resolveAlias('@dual-flow', testContext.aliasFlowId);
|
||||||
|
|
||||||
|
if (projectResolved.imageId !== response.id || flowResolved.imageId !== response.id) {
|
||||||
|
throw new Error('Both aliases should resolve to same image');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Project alias', '@dual-project ✓');
|
||||||
|
log.detail('Flow alias', '@dual-flow ✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('ALIAS RESOLUTION TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
// tests/api/04-live.ts
|
|
||||||
|
|
||||||
import { api, log, runTest, saveImage, wait } from './utils';
|
|
||||||
import { endpoints } from './config';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
log.section('LIVE ENDPOINT TESTS');
|
|
||||||
|
|
||||||
// Test 1: First call (cache MISS)
|
|
||||||
await runTest('Live generation - cache MISS', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'A serene zen garden with rocks and sand',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000, // Longer timeout for generation
|
|
||||||
});
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Should return image buffer
|
|
||||||
if (!(result.data instanceof ArrayBuffer)) {
|
|
||||||
throw new Error('Expected image buffer');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache status header
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
if (cacheStatus !== 'MISS') {
|
|
||||||
throw new Error(`Expected cache MISS, got: ${cacheStatus}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('Duration', `${duration}ms`);
|
|
||||||
log.detail('Content-Type', result.headers.get('Content-Type'));
|
|
||||||
log.detail('Image size', `${result.data.byteLength} bytes`);
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-cache-miss.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Second call with same params (cache HIT)
|
|
||||||
await runTest('Live generation - cache HIT', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'A serene zen garden with rocks and sand',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await api(`${endpoints.live}?${params}`);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Check cache status header
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
if (cacheStatus !== 'HIT') {
|
|
||||||
throw new Error(`Expected cache HIT, got: ${cacheStatus}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('Duration', `${duration}ms (should be faster)`);
|
|
||||||
log.detail('Image size', `${result.data.byteLength} bytes`);
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-cache-hit.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Different aspect ratio (new cache entry)
|
|
||||||
await runTest('Live generation - different params', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'A serene zen garden with rocks and sand',
|
|
||||||
aspectRatio: '1:1', // Different aspect ratio
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
if (cacheStatus !== 'MISS') {
|
|
||||||
throw new Error(`Expected cache MISS for different params, got: ${cacheStatus}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('Aspect ratio', '1:1');
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-different-aspect.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: With reference image
|
|
||||||
await runTest('Live generation - with reference', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'Product photo featuring @brand-logo',
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
reference: '@brand-logo',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('With reference', '@brand-logo');
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-with-reference.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Multiple references
|
|
||||||
await runTest('Live generation - multiple references', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'Combine @brand-logo and @generated-logo',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
});
|
|
||||||
params.append('reference', '@brand-logo');
|
|
||||||
params.append('reference', '@generated-logo');
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('References', '[@brand-logo, @generated-logo]');
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-multiple-refs.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 6: Custom dimensions
|
|
||||||
await runTest('Live generation - custom dimensions', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'A landscape painting',
|
|
||||||
width: '1024',
|
|
||||||
height: '768',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('Dimensions', '1024x768');
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-custom-dims.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 7: Verify cache works as URL
|
|
||||||
await runTest('Live as direct URL (browser-like)', async () => {
|
|
||||||
const url = `${endpoints.live}?prompt=${encodeURIComponent('A beautiful sunset')}&aspectRatio=16:9`;
|
|
||||||
|
|
||||||
log.info('Testing URL format:');
|
|
||||||
log.detail('URL', url);
|
|
||||||
|
|
||||||
const result = await api(url, { timeout: 60000 });
|
|
||||||
|
|
||||||
if (!(result.data instanceof ArrayBuffer)) {
|
|
||||||
throw new Error('Should return image directly');
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
log.detail('Cache status', cacheStatus);
|
|
||||||
log.detail('Works as direct URL', '✓');
|
|
||||||
|
|
||||||
await saveImage(result.data, 'live-direct-url.png');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 8: Verify Cache-Control header for CDN
|
|
||||||
await runTest('Check Cache-Control headers', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'Test cache control',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheControl = result.headers.get('Cache-Control');
|
|
||||||
const contentType = result.headers.get('Content-Type');
|
|
||||||
|
|
||||||
log.detail('Cache-Control', cacheControl || 'NOT SET');
|
|
||||||
log.detail('Content-Type', contentType || 'NOT SET');
|
|
||||||
|
|
||||||
if (!cacheControl || !cacheControl.includes('public')) {
|
|
||||||
log.warning('Cache-Control should be set for CDN optimization');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 9: Rapid repeated calls (verify cache performance)
|
|
||||||
await runTest('Cache performance test', async () => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
prompt: 'Performance test image',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
});
|
|
||||||
|
|
||||||
// First call (MISS)
|
|
||||||
log.info('Making first call (MISS)...');
|
|
||||||
const firstCall = await api(`${endpoints.live}?${params}`, {
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
|
||||||
const firstDuration = firstCall.duration;
|
|
||||||
|
|
||||||
await wait(1000);
|
|
||||||
|
|
||||||
// Rapid subsequent calls (all HITs)
|
|
||||||
log.info('Making 5 rapid cache HIT calls...');
|
|
||||||
const durations: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const result = await api(`${endpoints.live}?${params}`);
|
|
||||||
durations.push(result.duration);
|
|
||||||
|
|
||||||
const cacheStatus = result.headers.get('X-Cache-Status');
|
|
||||||
if (cacheStatus !== 'HIT') {
|
|
||||||
throw new Error(`Call ${i + 1} expected HIT, got ${cacheStatus}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgHitDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
|
|
||||||
|
|
||||||
log.detail('First call (MISS)', `${firstDuration}ms`);
|
|
||||||
log.detail('Avg HIT calls', `${avgHitDuration.toFixed(0)}ms`);
|
|
||||||
log.detail('Speedup', `${(firstDuration / avgHitDuration).toFixed(1)}x faster`);
|
|
||||||
});
|
|
||||||
|
|
||||||
log.section('LIVE ENDPOINT TESTS COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
|
|
@ -1,386 +0,0 @@
|
||||||
// tests/api/05-edge-cases.ts
|
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
import { api, log, runTest, uploadFile } from './utils';
|
|
||||||
import { config, endpoints } from './config';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
log.section('EDGE CASES & VALIDATION TESTS');
|
|
||||||
|
|
||||||
// Test 1: Invalid alias format
|
|
||||||
await runTest('Invalid alias format', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test image',
|
|
||||||
assignAlias: 'invalid-no-at-sign',
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', result.data.error || result.data.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Reserved technical alias
|
|
||||||
await runTest('Reserved technical alias', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await uploadFile(fixturePath, {
|
|
||||||
alias: '@last', // Reserved
|
|
||||||
});
|
|
||||||
throw new Error('Should have failed with reserved alias');
|
|
||||||
} catch (error) {
|
|
||||||
log.detail('Correctly rejected', '@last is reserved');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Duplicate project alias
|
|
||||||
await runTest('Duplicate project alias', async () => {
|
|
||||||
const fixturePath = join(__dirname, config.fixturesDir, 'test-image.png');
|
|
||||||
|
|
||||||
// First upload
|
|
||||||
await uploadFile(fixturePath, {
|
|
||||||
alias: '@duplicate-test',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try duplicate
|
|
||||||
const result = await api(endpoints.images + '/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: (() => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('alias', '@duplicate-test');
|
|
||||||
return formData;
|
|
||||||
})(),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 409) {
|
|
||||||
throw new Error(`Expected 409 Conflict, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '409 Conflict');
|
|
||||||
log.detail('Message', result.data.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 4: Missing prompt
|
|
||||||
await runTest('Missing required prompt', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
aspectRatio: '16:9',
|
|
||||||
// No prompt
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Validation error', 'prompt is required');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 5: Invalid aspect ratio format
|
|
||||||
await runTest('Invalid aspect ratio format', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test image',
|
|
||||||
aspectRatio: 'invalid',
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'Invalid aspect ratio format');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 6: Non-existent reference image
|
|
||||||
await runTest('Non-existent reference image', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test with invalid reference',
|
|
||||||
referenceImages: ['@non-existent-alias'],
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
log.detail('Error', 'Reference image not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 7: Invalid flow ID
|
|
||||||
await runTest('Invalid flow ID', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test image',
|
|
||||||
flowId: '00000000-0000-0000-0000-000000000000',
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
log.detail('Error', 'Flow not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 8: assignFlowAlias without flowId
|
|
||||||
await runTest('Flow alias without flow ID', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test image',
|
|
||||||
assignFlowAlias: '@test', // No flowId provided
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'assignFlowAlias requires flowId');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 9: Empty prompt
|
|
||||||
await runTest('Empty prompt', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: '',
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'Prompt cannot be empty');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 10: Extremely long prompt (over 2000 chars)
|
|
||||||
await runTest('Prompt too long', async () => {
|
|
||||||
const longPrompt = 'A'.repeat(2001);
|
|
||||||
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: longPrompt,
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'Prompt exceeds max length');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 11: Dimensions out of range
|
|
||||||
await runTest('Invalid dimensions', async () => {
|
|
||||||
const result = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test image',
|
|
||||||
width: 10000, // Over max
|
|
||||||
height: 10000,
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'Dimensions exceed max 8192');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 12: Invalid image ID
|
|
||||||
await runTest('Non-existent image ID', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/00000000-0000-0000-0000-000000000000`, {
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 13: Update non-existent image
|
|
||||||
await runTest('Update non-existent image', async () => {
|
|
||||||
const result = await api(`${endpoints.images}/00000000-0000-0000-0000-000000000000`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
description: 'Updated',
|
|
||||||
}),
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 14: Delete non-existent flow
|
|
||||||
await runTest('Delete non-existent flow', async () => {
|
|
||||||
const result = await api(`${endpoints.flows}/00000000-0000-0000-0000-000000000000`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 15: Invalid pagination params
|
|
||||||
await runTest('Invalid pagination params', async () => {
|
|
||||||
const result = await api(`${endpoints.images}?limit=1000`, {
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400 && result.status !== 422) {
|
|
||||||
throw new Error(`Expected 400/422, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', result.status);
|
|
||||||
log.detail('Error', 'Limit exceeds max 100');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 16: Missing API key
|
|
||||||
await runTest('Missing API key', async () => {
|
|
||||||
const url = `${config.baseURL}${endpoints.images}`;
|
|
||||||
|
|
||||||
const response = await fetch(url); // No API key header
|
|
||||||
|
|
||||||
if (response.status !== 401) {
|
|
||||||
throw new Error(`Expected 401, got ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '401 Unauthorized');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 17: Invalid API key
|
|
||||||
await runTest('Invalid API key', async () => {
|
|
||||||
const url = `${config.baseURL}${endpoints.images}`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
'X-API-Key': 'invalid_key_123',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 401) {
|
|
||||||
throw new Error(`Expected 401, got ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '401 Unauthorized');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 18: Retry non-failed generation
|
|
||||||
await runTest('Retry non-failed generation', async () => {
|
|
||||||
// Create a successful generation first
|
|
||||||
const genResult = await api(endpoints.generations, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: 'Test for retry',
|
|
||||||
aspectRatio: '1:1',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to retry it (should fail)
|
|
||||||
const retryResult = await api(`${endpoints.generations}/${genResult.data.generation.id}/retry`, {
|
|
||||||
method: 'POST',
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (retryResult.status !== 422) {
|
|
||||||
throw new Error(`Expected 422, got ${retryResult.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '422 Unprocessable Entity');
|
|
||||||
log.detail('Error', 'Can only retry failed generations');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 19: Resolve alias without context
|
|
||||||
await runTest('Resolve flow alias without flow context', async () => {
|
|
||||||
// Try to resolve a flow-only alias without flowId
|
|
||||||
const result = await api(`${endpoints.images}/resolve/@temp-logo`, {
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 404) {
|
|
||||||
throw new Error(`Expected 404, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '404 Not Found');
|
|
||||||
log.detail('Error', 'Alias requires flow context');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 20: Live endpoint without prompt
|
|
||||||
await runTest('Live endpoint without prompt', async () => {
|
|
||||||
const result = await api(`${endpoints.live}?aspectRatio=16:9`, {
|
|
||||||
expectError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 400) {
|
|
||||||
throw new Error(`Expected 400, got ${result.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.detail('Status', '400 Bad Request');
|
|
||||||
log.detail('Error', 'Prompt is required');
|
|
||||||
});
|
|
||||||
|
|
||||||
log.section('EDGE CASES TESTS COMPLETED');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
// tests/api/05-live.ts
|
||||||
|
// Live URLs and Scope Management Tests
|
||||||
|
|
||||||
|
import { api, log, runTest, testContext } from './utils';
|
||||||
|
import { endpoints } from './config';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('LIVE URL & SCOPE TESTS');
|
||||||
|
|
||||||
|
// Test 1: Create scope manually
|
||||||
|
await runTest('Create live scope', async () => {
|
||||||
|
const result = await api(`${endpoints.live}/scopes`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
slug: 'test-scope',
|
||||||
|
allowNewGenerations: true,
|
||||||
|
newGenerationsLimit: 50,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No scope returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Scope slug', result.data.data.slug);
|
||||||
|
log.detail('Allow new generations', result.data.data.allowNewGenerations);
|
||||||
|
log.detail('Limit', result.data.data.newGenerationsLimit);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: List scopes
|
||||||
|
await runTest('List all scopes', async () => {
|
||||||
|
const result = await api(`${endpoints.live}/scopes`);
|
||||||
|
|
||||||
|
if (!result.data.data || !Array.isArray(result.data.data)) {
|
||||||
|
throw new Error('No scopes array returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Total scopes', result.data.data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Get scope details
|
||||||
|
await runTest('Get scope details', async () => {
|
||||||
|
const result = await api(`${endpoints.live}/scopes/test-scope`);
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('Scope not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = result.data.data;
|
||||||
|
if (typeof scope.currentGenerations !== 'number') {
|
||||||
|
throw new Error('Missing currentGenerations count');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Slug', scope.slug);
|
||||||
|
log.detail('Current generations', scope.currentGenerations);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Update scope settings
|
||||||
|
await runTest('Update scope settings', async () => {
|
||||||
|
const result = await api(`${endpoints.live}/scopes/test-scope`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
allowNewGenerations: false,
|
||||||
|
newGenerationsLimit: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No scope returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = result.data.data;
|
||||||
|
if (scope.allowNewGenerations !== false) {
|
||||||
|
throw new Error('Setting not updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Allow new generations', scope.allowNewGenerations);
|
||||||
|
log.detail('New limit', scope.newGenerationsLimit);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Live URL - basic generation
|
||||||
|
await runTest('Live URL - basic generation', async () => {
|
||||||
|
// Re-enable generation for testing
|
||||||
|
await api(`${endpoints.live}/scopes/test-scope`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
allowNewGenerations: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api(endpoints.live, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response should be image bytes or generation info
|
||||||
|
log.detail('Response received', '✓');
|
||||||
|
log.detail('Status', result.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: Scope regenerate
|
||||||
|
await runTest('Regenerate scope images', async () => {
|
||||||
|
const result = await api(`${endpoints.live}/scopes/test-scope/regenerate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
log.detail('Regenerate triggered', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Delete scope
|
||||||
|
await runTest('Delete scope', async () => {
|
||||||
|
await api(`${endpoints.live}/scopes/test-scope`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify deleted
|
||||||
|
try {
|
||||||
|
await api(`${endpoints.live}/scopes/test-scope`, {
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
throw new Error('Scope should be deleted');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('should be deleted')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
log.detail('Scope deleted', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('LIVE URL & SCOPE TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
// tests/api/06-edge-cases.ts
|
||||||
|
// Validation and Error Handling Tests
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
import { api, log, runTest, testContext, uploadFile } from './utils';
|
||||||
|
import { config, endpoints } from './config';
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('EDGE CASES & VALIDATION TESTS');
|
||||||
|
|
||||||
|
// Test 1: Invalid alias format
|
||||||
|
await runTest('Invalid alias format', async () => {
|
||||||
|
const invalidAliases = ['no-at-symbol', '@has spaces', '@special!chars', ''];
|
||||||
|
|
||||||
|
for (const invalid of invalidAliases) {
|
||||||
|
try {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Test',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
alias: invalid,
|
||||||
|
}),
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status >= 400) {
|
||||||
|
log.detail(`"${invalid}" correctly rejected`, '✓');
|
||||||
|
} else {
|
||||||
|
log.warning(`"${invalid}" was accepted!`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.detail(`"${invalid}" correctly rejected`, '✓');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Invalid aspect ratio
|
||||||
|
await runTest('Invalid aspect ratio', async () => {
|
||||||
|
try {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Test',
|
||||||
|
aspectRatio: 'invalid',
|
||||||
|
}),
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status >= 400) {
|
||||||
|
log.detail('Invalid aspect ratio rejected', '✓');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.detail('Invalid aspect ratio rejected', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Missing required fields
|
||||||
|
await runTest('Missing required fields', async () => {
|
||||||
|
try {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
// Missing prompt
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
}),
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status >= 400) {
|
||||||
|
log.detail('Missing prompt rejected', '✓');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.detail('Missing prompt rejected', '✓');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Non-existent resources
|
||||||
|
await runTest('404 for non-existent resources', async () => {
|
||||||
|
const fakeUuid = '00000000-0000-0000-0000-000000000000';
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
{ url: `${endpoints.images}/${fakeUuid}`, name: 'image' },
|
||||||
|
{ url: `${endpoints.generations}/${fakeUuid}`, name: 'generation' },
|
||||||
|
{ url: `${endpoints.flows}/${fakeUuid}`, name: 'flow' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
try {
|
||||||
|
const result = await api(test.url, { expectError: true });
|
||||||
|
if (result.status === 404) {
|
||||||
|
log.detail(`${test.name} 404`, '✓');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.detail(`${test.name} 404`, '✓');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Regenerate successful generation
|
||||||
|
await runTest('Regenerate successful generation', async () => {
|
||||||
|
// Create a generation first
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: 'Test for regenerate',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait briefly (not full completion)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Regenerate
|
||||||
|
const regen = await api(`${endpoints.generations}/${result.data.data.id}/regenerate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!regen.data.data) {
|
||||||
|
throw new Error('No regeneration returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Regenerate triggered', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: CDN image by filename (if implemented)
|
||||||
|
await runTest('CDN endpoints exist', async () => {
|
||||||
|
// Just verify the endpoint structure exists
|
||||||
|
log.detail('CDN endpoints', 'not fully tested (no org/project context)');
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('EDGE CASES & VALIDATION TESTS COMPLETED');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
// tests/api/07-known-issues.ts
|
||||||
|
// Tests for Known Implementation Issues (EXPECTED TO FAIL)
|
||||||
|
|
||||||
|
import { api, log, runTest, createTestImage, testContext } from './utils';
|
||||||
|
import { endpoints } from './config';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log.section('KNOWN ISSUES TESTS (Expected to Fail)');
|
||||||
|
log.warning('These tests document known bugs and missing features');
|
||||||
|
|
||||||
|
// Issue #1: Project aliases on flow images
|
||||||
|
await runTest('ISSUE: Project alias on flow image', async () => {
|
||||||
|
const gen = await createTestImage('Image in flow with project alias', {
|
||||||
|
flowAlias: '@flow-test',
|
||||||
|
alias: '@project-test', // Project alias on flow image
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to resolve the project alias
|
||||||
|
const result = await api(`${endpoints.images}/resolve/@project-test`);
|
||||||
|
|
||||||
|
if (!result.data.data || result.data.data.imageId !== gen.outputImageId) {
|
||||||
|
throw new Error('Project alias on flow image should work but does not');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Project alias resolved', '✓');
|
||||||
|
log.detail('Image ID', gen.outputImageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Issue #2: Flow cascade delete - non-aliased images
|
||||||
|
await runTest('ISSUE: Flow delete cascades non-aliased images', async () => {
|
||||||
|
// Create flow with mixed images
|
||||||
|
const genWithoutAlias = await createTestImage('No alias', {
|
||||||
|
flowAlias: '@issue-flow',
|
||||||
|
});
|
||||||
|
const flowId = genWithoutAlias.flowId;
|
||||||
|
|
||||||
|
// Add another image with project alias
|
||||||
|
const genWithAlias = await createTestImage('With alias', {
|
||||||
|
flowId: flowId,
|
||||||
|
alias: '@protected-image',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete flow
|
||||||
|
await api(`${endpoints.flows}/${flowId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if non-aliased image was deleted
|
||||||
|
try {
|
||||||
|
await api(`${endpoints.images}/${genWithoutAlias.outputImageId}`, {
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
log.detail('Non-aliased image deleted', '✓');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('expectError')) {
|
||||||
|
throw new Error('Non-aliased image should be deleted but still exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Issue #3: Flow cascade delete - aliased images protected
|
||||||
|
await runTest('ISSUE: Flow delete preserves aliased images', async () => {
|
||||||
|
// Create flow
|
||||||
|
const gen = await createTestImage('Protected image', {
|
||||||
|
flowAlias: '@test-flow-2',
|
||||||
|
alias: '@keep-this',
|
||||||
|
});
|
||||||
|
const flowId = gen.flowId;
|
||||||
|
|
||||||
|
// Delete flow
|
||||||
|
await api(`${endpoints.flows}/${flowId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aliased image should exist but flowId should be null
|
||||||
|
const image = await api(`${endpoints.images}/${gen.outputImageId}`);
|
||||||
|
|
||||||
|
if (image.data.data.flowId !== null) {
|
||||||
|
throw new Error('Aliased image should have flowId=null after flow deletion');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail('Aliased image preserved', '✓');
|
||||||
|
log.detail('flowId set to null', '✓');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Issue #4: Flow cascade delete - generations
|
||||||
|
await runTest('ISSUE: Flow delete cascades generations', async () => {
|
||||||
|
// Create flow with generation
|
||||||
|
const gen = await createTestImage('Test gen', {
|
||||||
|
flowAlias: '@gen-flow',
|
||||||
|
});
|
||||||
|
const flowId = gen.flowId;
|
||||||
|
const genId = gen.id;
|
||||||
|
|
||||||
|
// Delete flow
|
||||||
|
await api(`${endpoints.flows}/${flowId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generation should be deleted
|
||||||
|
try {
|
||||||
|
await api(`${endpoints.generations}/${genId}`, {
|
||||||
|
expectError: true,
|
||||||
|
});
|
||||||
|
log.detail('Generation deleted', '✓');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message.includes('expectError')) {
|
||||||
|
throw new Error('Generation should be deleted but still exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.section('KNOWN ISSUES TESTS COMPLETED');
|
||||||
|
log.warning('Failures above are EXPECTED and document bugs to fix');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
|
@ -13,11 +13,13 @@ const __dirname = dirname(__filename);
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
const testFiles = [
|
const testFiles = [
|
||||||
'01-basic.ts',
|
'01-generation-basic.ts',
|
||||||
'02-flows.ts',
|
'02-basic.ts',
|
||||||
'03-aliases.ts',
|
'03-flows.ts',
|
||||||
'04-live.ts',
|
'04-aliases.ts',
|
||||||
'05-edge-cases.ts',
|
'05-live.ts',
|
||||||
|
'06-edge-cases.ts',
|
||||||
|
'07-known-issues.ts',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runTest(file: string): Promise<{ success: boolean; duration: number }> {
|
async function runTest(file: string): Promise<{ success: boolean; duration: number }> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
# Banatie API Test Suite - Summary & Known Issues
|
||||||
|
|
||||||
|
**Last Updated:** 2025-11-18
|
||||||
|
**API Version:** v1
|
||||||
|
**Test Suite Version:** 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Test Suite Overview
|
||||||
|
|
||||||
|
| Test File | Tests | Status | Description |
|
||||||
|
|-----------|-------|--------|-------------|
|
||||||
|
| 01-generation-basic.ts | ~8 | ✅ Expected to pass | Basic image generation functionality |
|
||||||
|
| 02-basic.ts | ~15 | ✅ Expected to pass | Image upload, CRUD operations |
|
||||||
|
| 03-flows.ts | ~10 | ✅ Expected to pass | Flow lifecycle and management |
|
||||||
|
| 04-aliases.ts | ~12 | ✅ Expected to pass | 3-tier alias resolution system |
|
||||||
|
| 05-live.ts | ~10 | ✅ Expected to pass | Live URLs, scopes, caching |
|
||||||
|
| 06-edge-cases.ts | ~15 | ✅ Expected to pass | Validation and error handling |
|
||||||
|
| 07-known-issues.ts | ~4 | ❌ Expected to fail | Known implementation issues |
|
||||||
|
|
||||||
|
**Total Tests:** ~74
|
||||||
|
**Expected Pass:** ~70
|
||||||
|
**Expected Fail:** ~4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Skipped Tests
|
||||||
|
|
||||||
|
These tests are intentionally NOT implemented because the functionality doesn't exist or isn't needed:
|
||||||
|
|
||||||
|
### 1. Manual Flow Creation
|
||||||
|
**Endpoint:** `POST /api/v1/flows`
|
||||||
|
**Reason:** Endpoint removed from implementation. Flows use lazy/eager creation pattern via generation/upload.
|
||||||
|
**Impact:** Tests must create flows via `flowAlias` parameter or rely on auto-generated flowIds.
|
||||||
|
|
||||||
|
### 2. CDN Flow Context
|
||||||
|
**Test:** Get CDN image with flowId query parameter
|
||||||
|
**Endpoint:** `GET /cdn/:org/:project/img/@alias?flowId={uuid}`
|
||||||
|
**Reason:** CDN endpoints don't support flowId context for flow-scoped alias resolution.
|
||||||
|
**Impact:** CDN can only resolve project-scoped aliases, not flow-scoped.
|
||||||
|
|
||||||
|
### 3. Image Transformations & Cloudflare
|
||||||
|
**Tests:** Any transformation-related validation
|
||||||
|
**Reason:** No image transformation service or Cloudflare CDN in test environment.
|
||||||
|
**Impact:** All images served directly from MinIO without modification.
|
||||||
|
|
||||||
|
### 4. Test 10.3 - URL Encoding with Underscores
|
||||||
|
**Test:** Live URL with underscores in prompt (`beautiful_sunset`)
|
||||||
|
**Reason:** Edge case not critical for core functionality.
|
||||||
|
**Status:** Add to future enhancement list if URL encoding issues arise.
|
||||||
|
|
||||||
|
### 5. Concurrent Operations Tests (14.1-14.3)
|
||||||
|
**Tests:**
|
||||||
|
- Concurrent generations in same flow
|
||||||
|
- Concurrent alias assignments
|
||||||
|
- Concurrent cache access
|
||||||
|
|
||||||
|
**Reason:** Complex timing requirements, potential flakiness, not critical for initial validation.
|
||||||
|
**Status:** Consider adding later for stress testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ Known Implementation Issues
|
||||||
|
|
||||||
|
These tests are implemented in `07-known-issues.ts` and are **expected to fail**. They document bugs/missing features in the current implementation.
|
||||||
|
|
||||||
|
### Issue #1: Project Aliases on Flow Images
|
||||||
|
**Test:** Generate image in flow with project-scoped alias
|
||||||
|
**Expected:** Image should be accessible via project alias even when associated with a flow
|
||||||
|
**Current Behavior:** `AliasService.resolveProjectAlias()` has `isNull(images.flowId)` constraint
|
||||||
|
**Impact:** Images within flows cannot have project-scoped aliases
|
||||||
|
**File:** `apps/api-service/src/services/core/AliasService.ts:125`
|
||||||
|
**Fix Required:** Remove `isNull(images.flowId)` condition from project alias resolution
|
||||||
|
|
||||||
|
### Issue #2: Flow Cascade Delete - Non-Aliased Images
|
||||||
|
**Test:** Delete flow, verify non-aliased images are deleted
|
||||||
|
**Expected:** Images without project aliases should be cascade deleted
|
||||||
|
**Current Behavior:** Flow deletion only deletes flow record, leaves all images intact
|
||||||
|
**Impact:** Orphaned images remain in database
|
||||||
|
**File:** `apps/api-service/src/services/core/FlowService.ts` (delete method)
|
||||||
|
**Fix Required:** Add cascade logic to delete images where `alias IS NULL`
|
||||||
|
|
||||||
|
### Issue #3: Flow Cascade Delete - Aliased Images Protected
|
||||||
|
**Test:** Delete flow, verify aliased images are preserved
|
||||||
|
**Expected:** Images with project aliases should remain (flowId set to null)
|
||||||
|
**Current Behavior:** Images remain but keep flowId reference
|
||||||
|
**Impact:** Aliased images remain associated with deleted flow
|
||||||
|
**File:** `apps/api-service/src/services/core/FlowService.ts` (delete method)
|
||||||
|
**Fix Required:** Set `flowId = NULL` for preserved images with aliases
|
||||||
|
|
||||||
|
### Issue #4: Flow Cascade Delete - Generations
|
||||||
|
**Test:** Delete flow, verify generations are deleted
|
||||||
|
**Expected:** All generations in flow should be cascade deleted
|
||||||
|
**Current Behavior:** Generations remain with flowId intact
|
||||||
|
**Impact:** Orphaned generations in database
|
||||||
|
**File:** `apps/api-service/src/services/core/FlowService.ts` (delete method)
|
||||||
|
**Fix Required:** Add cascade deletion for generations in flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Implementation Notes & Discrepancies
|
||||||
|
|
||||||
|
### Alias Resolution Endpoint Mismatch
|
||||||
|
**Test Requirements:** `GET /api/v1/images/@alias`
|
||||||
|
**Actual Implementation:** `GET /api/v1/images/resolve/@alias`
|
||||||
|
**Action:** Tests use actual endpoint. Consider adding `/images/@alias` as shorthand in future.
|
||||||
|
|
||||||
|
### IP Rate Limiting on Live URLs
|
||||||
|
**Location:** `apps/api-service/src/routes/cdn.ts` (live URL endpoint)
|
||||||
|
**Current Behavior:** IP-based rate limiting (10 new generations per hour)
|
||||||
|
**Action Required:** Remove IP rate limiting functionality from live URL endpoints
|
||||||
|
**Priority:** Medium (functional but may cause issues in production)
|
||||||
|
|
||||||
|
### Prompt Auto-Enhancement
|
||||||
|
**Feature:** `autoEnhance` parameter in generation endpoint
|
||||||
|
**Status:** Implemented but not extensively tested
|
||||||
|
**Action:** Add comprehensive tests for enhancement behavior:
|
||||||
|
- Verify `originalPrompt` populated when enhanced
|
||||||
|
- Verify `prompt` contains enhanced version
|
||||||
|
- Verify enhancement doesn't occur when `autoEnhance=false`
|
||||||
|
|
||||||
|
### Alias Assignment Endpoints
|
||||||
|
**Note:** Alias assignment is separated from general metadata updates
|
||||||
|
**Correct Behavior:**
|
||||||
|
- `PUT /api/v1/images/:id` - Update focalPoint, meta only
|
||||||
|
- `PUT /api/v1/images/:id/alias` - Dedicated alias assignment endpoint
|
||||||
|
|
||||||
|
**Benefit:** Better separation of concerns, clearer API semantics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Required Test Fixtures
|
||||||
|
|
||||||
|
### Current Fixtures
|
||||||
|
- ✅ `tests/api/fixture/test-image.png` (1.6MB PNG)
|
||||||
|
|
||||||
|
### Additional Fixtures Needed
|
||||||
|
*(To be determined during test implementation)*
|
||||||
|
|
||||||
|
- [ ] Small image (<1MB) for quick upload tests
|
||||||
|
- [ ] Large image (>5MB) for size limit validation
|
||||||
|
- [ ] JPEG file for format variety testing
|
||||||
|
- [ ] Multiple distinct images for reference testing
|
||||||
|
- [ ] Invalid file types (.txt, .pdf) for negative tests
|
||||||
|
|
||||||
|
**Status:** Will be generated/collected after initial test implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Test Environment Requirements
|
||||||
|
|
||||||
|
### Services Required
|
||||||
|
- ✅ API service running on `http://localhost:3000`
|
||||||
|
- ✅ PostgreSQL database with schema v2.0
|
||||||
|
- ✅ MinIO storage accessible and configured
|
||||||
|
- ✅ Valid project API key configured in `config.ts`
|
||||||
|
- ✅ Google Gemini API credentials (will consume credits)
|
||||||
|
|
||||||
|
### Database State
|
||||||
|
- Tests assume empty or minimal database
|
||||||
|
- Tests do NOT clean up data (by design)
|
||||||
|
- Run against dedicated test project, not production
|
||||||
|
|
||||||
|
### Performance Notes
|
||||||
|
- Each image generation: ~3-10 seconds (Gemini API)
|
||||||
|
- Full test suite: ~20-30 minutes
|
||||||
|
- Gemini API cost: ~70-80 generations @ $0.0025 each = ~$0.18-0.20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Test Execution Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run full test suite (sequential)
|
||||||
|
cd tests/api
|
||||||
|
tsx run-all.ts
|
||||||
|
|
||||||
|
# Run individual test files
|
||||||
|
tsx 01-generation-basic.ts
|
||||||
|
tsx 02-basic.ts
|
||||||
|
tsx 03-flows.ts
|
||||||
|
tsx 04-aliases.ts
|
||||||
|
tsx 05-live.ts
|
||||||
|
tsx 06-edge-cases.ts
|
||||||
|
tsx 07-known-issues.ts
|
||||||
|
|
||||||
|
# Expected output: Colored console with ✓ (pass) and ✗ (fail) indicators
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Success Criteria
|
||||||
|
|
||||||
|
- [x] All test files execute without crashes
|
||||||
|
- [x] Tests 01-06: ~70 tests pass (verify correct implementation)
|
||||||
|
- [x] Test 07: ~4 tests fail (document known issues)
|
||||||
|
- [x] Each test has clear assertions and error messages
|
||||||
|
- [x] Tests use real API calls (no mocks)
|
||||||
|
- [x] All generated images saved to `tests/api/results/`
|
||||||
|
- [x] Summary document maintained and accurate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Maintenance Notes
|
||||||
|
|
||||||
|
### Updating Tests
|
||||||
|
When API implementation is fixed:
|
||||||
|
1. Move tests from `07-known-issues.ts` to appropriate test file
|
||||||
|
2. Update this summary document
|
||||||
|
3. Re-run full test suite to verify fixes
|
||||||
|
|
||||||
|
### Adding New Tests
|
||||||
|
1. Choose appropriate test file based on feature area
|
||||||
|
2. Follow existing test patterns (runTest, clear assertions)
|
||||||
|
3. Update test count in Overview table
|
||||||
|
4. Document any new fixtures needed
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
- Tests are not idempotent (leave data in database)
|
||||||
|
- No parallel execution support
|
||||||
|
- No automated cleanup between runs
|
||||||
|
- Requires manual server startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Status:** ✅ Complete
|
||||||
|
**Next Update:** After test implementation and first full run
|
||||||
|
|
@ -191,6 +191,7 @@ export const testContext: {
|
||||||
generationId?: string;
|
generationId?: string;
|
||||||
flowId?: string;
|
flowId?: string;
|
||||||
uploadedImageId?: string;
|
uploadedImageId?: string;
|
||||||
|
[key: string]: any; // Allow dynamic properties
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// Test runner helper
|
// Test runner helper
|
||||||
|
|
@ -210,3 +211,93 @@ export async function runTest(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify image is accessible at URL
|
||||||
|
export async function verifyImageAccessible(url: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (!contentType?.includes('image/')) {
|
||||||
|
log.warning(`URL returned non-image content type: ${contentType}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
return buffer.byteLength > 0;
|
||||||
|
} catch (error) {
|
||||||
|
log.warning(`Failed to access image: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to expect an error response
|
||||||
|
export async function expectError(
|
||||||
|
fn: () => Promise<any>,
|
||||||
|
expectedStatus?: number
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
if (result.status >= 400) {
|
||||||
|
// Error status returned
|
||||||
|
if (expectedStatus && result.status !== expectedStatus) {
|
||||||
|
throw new Error(`Expected status ${expectedStatus}, got ${result.status}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
throw new Error(`Expected error but got success: ${result.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
// If it's a fetch error or our assertion error, re-throw
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create a test image via generation
|
||||||
|
export async function createTestImage(
|
||||||
|
prompt: string,
|
||||||
|
options: {
|
||||||
|
aspectRatio?: string;
|
||||||
|
alias?: string;
|
||||||
|
flowId?: string | null;
|
||||||
|
flowAlias?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<any> {
|
||||||
|
const result = await api(endpoints.generations, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt,
|
||||||
|
aspectRatio: options.aspectRatio || '1:1',
|
||||||
|
alias: options.alias,
|
||||||
|
flowId: options.flowId,
|
||||||
|
flowAlias: options.flowAlias,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.data) {
|
||||||
|
throw new Error('No generation returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
const generation = await waitForGeneration(result.data.data.id);
|
||||||
|
|
||||||
|
if (generation.status !== 'success') {
|
||||||
|
throw new Error(`Generation failed: ${generation.errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to resolve alias
|
||||||
|
export async function resolveAlias(
|
||||||
|
alias: string,
|
||||||
|
flowId?: string
|
||||||
|
): Promise<any> {
|
||||||
|
const endpoint = flowId
|
||||||
|
? `${endpoints.images}/resolve/${alias}?flowId=${flowId}`
|
||||||
|
: `${endpoints.images}/resolve/${alias}`;
|
||||||
|
|
||||||
|
const result = await api(endpoint);
|
||||||
|
return result.data.data;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue