fix: service and tests

This commit is contained in:
Oleg Proskurin 2025-11-10 00:57:06 +07:00
parent 4e7eb7b5b5
commit d6ca79152c
10 changed files with 98 additions and 95 deletions

View File

@ -1,6 +1,7 @@
import express, { Application } from 'express'; import express, { Application } from 'express';
import cors from 'cors'; import cors from 'cors';
import { config } from 'dotenv'; import { config } from 'dotenv';
import { randomUUID } from 'crypto';
import { Config } from './types/api'; import { Config } from './types/api';
import { textToImageRouter } from './routes/textToImage'; import { textToImageRouter } from './routes/textToImage';
import { imagesRouter } from './routes/images'; import { imagesRouter } from './routes/images';
@ -43,7 +44,7 @@ export const createApp = (): Application => {
// Request ID middleware for logging // Request ID middleware for logging
app.use((req, res, next) => { app.use((req, res, next) => {
req.requestId = Math.random().toString(36).substr(2, 9); req.requestId = randomUUID();
res.setHeader('X-Request-ID', req.requestId); res.setHeader('X-Request-ID', req.requestId);
next(); next();
}); });

View File

@ -78,6 +78,7 @@ export class ImageGenService {
filename: uploadResult.filename, filename: uploadResult.filename,
filepath: uploadResult.path, filepath: uploadResult.path,
url: uploadResult.url, url: uploadResult.url,
size: uploadResult.size,
model: this.primaryModel, model: this.primaryModel,
geminiParams, geminiParams,
...(generatedData.description && { ...(generatedData.description && {

View File

@ -122,7 +122,7 @@ export class GenerationService {
storageKey, storageKey,
storageUrl: genResult.url!, storageUrl: genResult.url!,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
fileSize: 0, // TODO: Get actual file size from storage fileSize: genResult.size || 0,
fileHash, fileHash,
source: 'generated', source: 'generated',
alias: params.assignAlias || null, alias: params.assignAlias || null,

View File

@ -94,6 +94,7 @@ export interface ImageGenerationResult {
filename?: string; filename?: string;
filepath?: string; filepath?: string;
url?: string; // API URL for accessing the image url?: string; // API URL for accessing the image
size?: number; // File size in bytes
description?: string; description?: string;
model: string; model: string;
geminiParams?: GeminiParams; // Gemini SDK parameters used for generation geminiParams?: GeminiParams; // Gemini SDK parameters used for generation

View File

@ -22,39 +22,39 @@ async function main() {
description: 'Test logo image', description: 'Test logo image',
}); });
if (!response.image || !response.image.id) { if (!response || !response.id) {
throw new Error('No image returned'); throw new Error('No image returned');
} }
testContext.uploadedImageId = response.image.id; testContext.uploadedImageId = response.id;
log.detail('Image ID', response.image.id); log.detail('Image ID', response.id);
log.detail('Storage Key', response.image.storageKey); log.detail('Storage Key', response.storageKey);
log.detail('Alias', response.image.alias); log.detail('Alias', response.alias);
}); });
// Test 2: List images // Test 2: List images
await runTest('List images', async () => { await runTest('List images', async () => {
const result = await api(endpoints.images); const result = await api(endpoints.images);
if (!result.data.images || !Array.isArray(result.data.images)) { if (!result.data.data || !Array.isArray(result.data.data)) {
throw new Error('No images array returned'); throw new Error('No images array returned');
} }
log.detail('Total images', result.data.images.length); log.detail('Total images', result.data.data.length);
log.detail('Has uploaded', result.data.images.some((img: any) => img.source === 'uploaded')); log.detail('Has uploaded', result.data.data.some((img: any) => img.source === 'uploaded'));
}); });
// Test 3: Get image by ID // Test 3: Get image by ID
await runTest('Get image by ID', async () => { await runTest('Get image by ID', async () => {
const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`); const result = await api(`${endpoints.images}/${testContext.uploadedImageId}`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not found'); throw new Error('Image not found');
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Source', result.data.image.source); log.detail('Source', result.data.data.source);
log.detail('File size', `${result.data.image.fileSize} bytes`); log.detail('File size', `${result.data.data.fileSize} bytes`);
}); });
// Test 4: Generate image without references // Test 4: Generate image without references
@ -68,14 +68,14 @@ async function main() {
}), }),
}); });
if (!result.data.generation) { if (!result.data.data) {
throw new Error('No generation returned'); throw new Error('No generation returned');
} }
testContext.generationId = result.data.generation.id; testContext.generationId = result.data.data.id;
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Status', result.data.generation.status); log.detail('Status', result.data.data.status);
log.detail('Prompt', result.data.generation.originalPrompt); log.detail('Prompt', result.data.data.originalPrompt);
// Wait for completion // Wait for completion
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
@ -93,7 +93,7 @@ async function main() {
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
// Download image // Download image
const imageUrl = imageResult.data.image.storageUrl; const imageUrl = imageResult.data.data.storageUrl;
const imageResponse = await fetch(imageUrl); const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer(); const imageBuffer = await imageResponse.arrayBuffer();
@ -114,16 +114,16 @@ async function main() {
}), }),
}); });
if (!result.data.generation) { if (!result.data.data) {
throw new Error('No generation returned'); throw new Error('No generation returned');
} }
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Referenced images', result.data.generation.referencedImages?.length || 0); log.detail('Referenced images', result.data.data.referencedImages?.length || 0);
// Wait for completion // Wait for completion
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);
@ -132,7 +132,7 @@ async function main() {
// Save generated image // Save generated image
if (generation.outputImageId) { if (generation.outputImageId) {
const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`); const imageResult = await api(`${endpoints.images}/${generation.outputImageId}`);
const imageUrl = imageResult.data.image.storageUrl; const imageUrl = imageResult.data.data.storageUrl;
const imageResponse = await fetch(imageUrl); const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer(); const imageBuffer = await imageResponse.arrayBuffer();
@ -144,12 +144,12 @@ async function main() {
await runTest('List generations', async () => { await runTest('List generations', async () => {
const result = await api(endpoints.generations); const result = await api(endpoints.generations);
if (!result.data.generations || !Array.isArray(result.data.generations)) { if (!result.data.data || !Array.isArray(result.data.data)) {
throw new Error('No generations array returned'); throw new Error('No generations array returned');
} }
log.detail('Total generations', result.data.generations.length); log.detail('Total generations', result.data.data.length);
log.detail('Successful', result.data.generations.filter((g: any) => g.status === 'success').length); log.detail('Successful', result.data.data.filter((g: any) => g.status === 'success').length);
log.detail('Has pagination', !!result.data.pagination); log.detail('Has pagination', !!result.data.pagination);
}); });
@ -157,14 +157,14 @@ async function main() {
await runTest('Get generation details', async () => { await runTest('Get generation details', async () => {
const result = await api(`${endpoints.generations}/${testContext.generationId}`); const result = await api(`${endpoints.generations}/${testContext.generationId}`);
if (!result.data.generation) { if (!result.data.data) {
throw new Error('Generation not found'); throw new Error('Generation not found');
} }
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Status', result.data.generation.status); log.detail('Status', result.data.data.status);
log.detail('Has image', !!result.data.image); log.detail('Has output image', !!result.data.data.outputImage);
log.detail('Referenced images', result.data.referencedImages?.length || 0); log.detail('Referenced images', result.data.data.referencedImages?.length || 0);
}); });
log.section('BASIC TESTS COMPLETED'); log.section('BASIC TESTS COMPLETED');

View File

@ -22,24 +22,24 @@ async function main() {
}), }),
}); });
if (!result.data.flow || !result.data.flow.id) { if (!result.data.data || !result.data.data.id) {
throw new Error('No flow returned'); throw new Error('No flow returned');
} }
testContext.flowId = result.data.flow.id; testContext.flowId = result.data.data.id;
log.detail('Flow ID', result.data.flow.id); log.detail('Flow ID', result.data.data.id);
log.detail('Aliases', JSON.stringify(result.data.flow.aliases)); log.detail('Aliases', JSON.stringify(result.data.data.aliases));
}); });
// Test 2: List flows // Test 2: List flows
await runTest('List flows', async () => { await runTest('List flows', async () => {
const result = await api(endpoints.flows); const result = await api(endpoints.flows);
if (!result.data.flows || !Array.isArray(result.data.flows)) { if (!result.data.data || !Array.isArray(result.data.data)) {
throw new Error('No flows array returned'); throw new Error('No flows array returned');
} }
log.detail('Total flows', result.data.flows.length); log.detail('Total flows', result.data.data.length);
log.detail('Has pagination', !!result.data.pagination); log.detail('Has pagination', !!result.data.pagination);
}); });
@ -55,16 +55,16 @@ async function main() {
}), }),
}); });
if (!result.data.generation) { if (!result.data.data) {
throw new Error('No generation returned'); throw new Error('No generation returned');
} }
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Flow ID', result.data.generation.flowId); log.detail('Flow ID', result.data.data.flowId);
// Wait for completion // Wait for completion
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);
@ -96,16 +96,16 @@ async function main() {
}), }),
}); });
if (!result.data.generation) { if (!result.data.data) {
throw new Error('No generation returned'); throw new Error('No generation returned');
} }
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Referenced @last', result.data.generation.referencedImages?.some((r: any) => r.alias === '@last')); log.detail('Referenced @last', result.data.data.referencedImages?.some((r: any) => r.alias === '@last'));
// Wait for completion // Wait for completion
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);
@ -126,13 +126,13 @@ async function main() {
await runTest('Get flow details', async () => { await runTest('Get flow details', async () => {
const result = await api(`${endpoints.flows}/${testContext.flowId}`); const result = await api(`${endpoints.flows}/${testContext.flowId}`);
if (!result.data.flow) { if (!result.data.data) {
throw new Error('Flow not found'); throw new Error('Flow not found');
} }
log.detail('Flow ID', result.data.flow.id); log.detail('Flow ID', result.data.data.id);
log.detail('Generations count', result.data.generations?.length || 0); log.detail('Generations count', result.data.datas?.length || 0);
log.detail('Images count', result.data.images?.length || 0); log.detail('Images count', result.data.data?.length || 0);
log.detail('Resolved aliases', Object.keys(result.data.resolvedAliases || {}).length); log.detail('Resolved aliases', Object.keys(result.data.resolvedAliases || {}).length);
}); });
@ -157,11 +157,11 @@ async function main() {
}), }),
}); });
if (!result.data.flow) { if (!result.data.data) {
throw new Error('Flow not returned'); throw new Error('Flow not returned');
} }
log.detail('Updated aliases', JSON.stringify(result.data.flow.aliases)); log.detail('Updated aliases', JSON.stringify(result.data.data.aliases));
}); });
// Test 7: Generate with flow-scoped alias // Test 7: Generate with flow-scoped alias
@ -177,16 +177,16 @@ async function main() {
}), }),
}); });
if (!result.data.generation) { if (!result.data.data) {
throw new Error('No generation returned'); throw new Error('No generation returned');
} }
log.detail('Generation ID', result.data.generation.id); log.detail('Generation ID', result.data.data.id);
log.detail('Referenced @hero', result.data.generation.referencedImages?.some((r: any) => r.alias === '@hero')); log.detail('Referenced @hero', result.data.data.referencedImages?.some((r: any) => r.alias === '@hero'));
// Wait for completion // Wait for completion
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);
@ -211,13 +211,13 @@ async function main() {
// Verify it's deleted // Verify it's deleted
const result = await api(`${endpoints.flows}/${testContext.flowId}`); const result = await api(`${endpoints.flows}/${testContext.flowId}`);
const hasFeatureAlias = '@featured' in result.data.flow.aliases; const hasFeatureAlias = '@featured' in result.data.data.aliases;
if (hasFeatureAlias) { if (hasFeatureAlias) {
throw new Error('Alias was not deleted'); throw new Error('Alias was not deleted');
} }
log.detail('Remaining aliases', JSON.stringify(result.data.flow.aliases)); log.detail('Remaining aliases', JSON.stringify(result.data.data.aliases));
}); });
log.section('FLOW TESTS COMPLETED'); log.section('FLOW TESTS COMPLETED');

View File

@ -78,50 +78,50 @@ async function main() {
await runTest('Resolve project alias', async () => { await runTest('Resolve project alias', async () => {
const result = await api(`${endpoints.images}/resolve/@brand-logo`); const result = await api(`${endpoints.images}/resolve/@brand-logo`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not resolved'); throw new Error('Image not resolved');
} }
if (result.data.scope !== 'project') { if (result.data.data.scope !== 'project') {
throw new Error(`Wrong scope: ${result.data.scope}`); throw new Error(`Wrong scope: ${result.data.data.scope}`);
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Scope', result.data.scope); log.detail('Scope', result.data.data.scope);
log.detail('Alias', result.data.image.alias); log.detail('Alias', result.data.data.alias);
}); });
// Test 5: Resolve flow-scoped alias // Test 5: Resolve flow-scoped alias
await runTest('Resolve flow alias', async () => { await runTest('Resolve flow alias', async () => {
const result = await api(`${endpoints.images}/resolve/@temp-logo?flowId=${testContext.flowId}`); const result = await api(`${endpoints.images}/resolve/@temp-logo?flowId=${testContext.flowId}`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not resolved'); throw new Error('Image not resolved');
} }
if (result.data.scope !== 'flow') { if (result.data.data.scope !== 'flow') {
throw new Error(`Wrong scope: ${result.data.scope}`); throw new Error(`Wrong scope: ${result.data.data.scope}`);
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Scope', result.data.scope); log.detail('Scope', result.data.data.scope);
log.detail('Flow ID', result.data.flow?.id); log.detail('Flow ID', result.data.data?.id);
}); });
// Test 6: Resolve @last technical alias // Test 6: Resolve @last technical alias
await runTest('Resolve @last technical alias', async () => { await runTest('Resolve @last technical alias', async () => {
const result = await api(`${endpoints.images}/resolve/@last?flowId=${testContext.flowId}`); const result = await api(`${endpoints.images}/resolve/@last?flowId=${testContext.flowId}`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not resolved'); throw new Error('Image not resolved');
} }
if (result.data.scope !== 'technical') { if (result.data.data.scope !== 'technical') {
throw new Error(`Wrong scope: ${result.data.scope}`); throw new Error(`Wrong scope: ${result.data.data.scope}`);
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Scope', result.data.scope); log.detail('Scope', result.data.data.scope);
log.detail('Technical alias', '@last'); log.detail('Technical alias', '@last');
}); });
@ -129,33 +129,33 @@ async function main() {
await runTest('Resolve @first technical alias', async () => { await runTest('Resolve @first technical alias', async () => {
const result = await api(`${endpoints.images}/resolve/@first?flowId=${testContext.flowId}`); const result = await api(`${endpoints.images}/resolve/@first?flowId=${testContext.flowId}`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not resolved'); throw new Error('Image not resolved');
} }
if (result.data.scope !== 'technical') { if (result.data.data.scope !== 'technical') {
throw new Error(`Wrong scope: ${result.data.scope}`); throw new Error(`Wrong scope: ${result.data.data.scope}`);
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Scope', result.data.scope); log.detail('Scope', result.data.data.scope);
}); });
// Test 8: Resolve @upload technical alias // Test 8: Resolve @upload technical alias
await runTest('Resolve @upload technical alias', async () => { await runTest('Resolve @upload technical alias', async () => {
const result = await api(`${endpoints.images}/resolve/@upload?flowId=${testContext.flowId}`); const result = await api(`${endpoints.images}/resolve/@upload?flowId=${testContext.flowId}`);
if (!result.data.image) { if (!result.data.data) {
throw new Error('Image not resolved'); throw new Error('Image not resolved');
} }
if (result.data.scope !== 'technical') { if (result.data.data.scope !== 'technical') {
throw new Error(`Wrong scope: ${result.data.scope}`); throw new Error(`Wrong scope: ${result.data.data.scope}`);
} }
log.detail('Image ID', result.data.image.id); log.detail('Image ID', result.data.data.id);
log.detail('Scope', result.data.scope); log.detail('Scope', result.data.data.scope);
log.detail('Source', result.data.image.source); log.detail('Source', result.data.data.source);
}); });
// Test 9: Generate with assignAlias (project-scoped) // Test 9: Generate with assignAlias (project-scoped)
@ -171,7 +171,7 @@ async function main() {
}); });
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);
@ -208,7 +208,7 @@ async function main() {
}); });
log.info('Waiting for generation to complete...'); log.info('Waiting for generation to complete...');
const generation = await waitForGeneration(result.data.generation.id); const generation = await waitForGeneration(result.data.data.id);
if (generation.status !== 'success') { if (generation.status !== 'success') {
throw new Error(`Generation failed: ${generation.errorMessage}`); throw new Error(`Generation failed: ${generation.errorMessage}`);

View File

@ -7,7 +7,7 @@ export const config = {
// Paths // Paths
resultsDir: '../../results', resultsDir: '../../results',
fixturesDir: './fixtures', fixturesDir: './fixture',
// Timeouts // Timeouts
requestTimeout: 30000, requestTimeout: 30000,

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -2,7 +2,7 @@
import { writeFile, mkdir } from 'fs/promises'; import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import { config } from './config'; import { config, endpoints } from './config';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
@ -150,7 +150,7 @@ export async function uploadFile(
formData.append(key, value); formData.append(key, value);
} }
const result = await api(config.endpoints.images + '/upload', { const result = await api(endpoints.images + '/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: { headers: {
@ -158,7 +158,7 @@ export async function uploadFile(
}, },
}); });
return result.data; return result.data.data;
} }
// Wait helper // Wait helper
@ -172,8 +172,8 @@ export async function waitForGeneration(
maxAttempts = 20 maxAttempts = 20
): Promise<any> { ): Promise<any> {
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
const result = await api(`${config.endpoints.generations}/${generationId}`); const result = await api(`${endpoints.generations}/${generationId}`);
const generation = result.data.generation; const generation = result.data.data;
if (generation.status === 'success' || generation.status === 'failed') { if (generation.status === 'success' || generation.status === 'failed') {
return generation; return generation;