// tests/api/04-aliases.ts // 3-Tier Alias Resolution System Tests import { join } from 'path'; import { api, log, runTest, uploadFile, waitForGeneration, testContext, resolveAlias, createTestImage, exitWithTestResults } 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() .then(() => exitWithTestResults()) .catch((error) => { console.error('Unexpected error:', error); process.exit(1); });