284 lines
8.9 KiB
TypeScript
284 lines
8.9 KiB
TypeScript
// 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);
|
|
});
|