Files
lindyson-ai/apps/api/tests/tkPromptEngineRoutes.test.ts
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、
迁移脚本、测试与自托管 docker-compose 示例。

开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读
publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为
VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注
Apache-2.0。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 02:10:46 +08:00

250 lines
8.2 KiB
TypeScript

import fastify from 'fastify';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
requireAuth: vi.fn(),
readSettings: vi.fn(),
createRun: vi.fn(),
readRun: vi.fn(),
createGeneration: vi.fn(),
readGeneration: vi.fn(),
readResume: vi.fn(),
enqueueAnalysis: vi.fn(),
enqueueStrategy: vi.fn(),
}));
vi.mock('../src/auth/session.js', () => ({ requireAuth: mocks.requireAuth }));
vi.mock('../src/domain/tkPromptEngineService.js', () => ({
readTkPromptEngineSettings: mocks.readSettings,
createTkPromptRun: mocks.createRun,
readTkPromptRun: mocks.readRun,
createTkPromptGeneration: mocks.createGeneration,
readTkPromptGeneration: mocks.readGeneration,
readTkPromptResume: mocks.readResume,
}));
vi.mock('../src/queues/tkPromptEngineQueue.js', () => ({
enqueueTkPromptAnalysis: mocks.enqueueAnalysis,
enqueueTkPromptStrategy: mocks.enqueueStrategy,
}));
import { registerTkPromptEngineRoutes } from '../src/routes/tkPromptEngineRoutes.js';
const userId = '00000000-0000-4000-8000-000000000901';
const runId = '00000000-0000-4000-8000-000000000902';
const generationId = '00000000-0000-4000-8000-000000000903';
const imageId = '00000000-0000-4000-8000-000000000904';
const now = '2026-08-17T00:00:00.000Z';
describe('TK Prompt Engine 1.0 路由状态机边界', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.requireAuth.mockResolvedValue({ id: userId, email: 'member@example.com', role: 'user' });
mocks.enqueueAnalysis.mockResolvedValue(undefined);
mocks.enqueueStrategy.mockResolvedValue(undefined);
mocks.readSettings.mockResolvedValue({
schemaVersion: 1,
generationCredits: 7,
available: true,
updatedAt: now,
});
mocks.createRun.mockResolvedValue(run());
mocks.readRun.mockResolvedValue(run());
mocks.createGeneration.mockResolvedValue(generation());
mocks.readGeneration.mockResolvedValue(generation());
mocks.readResume.mockResolvedValue({ run: run(), generation: generation() });
});
it('POST /runs 只创建事实分析 run,并以实体 ID 投递 analysis job', async () => {
const app = makeApp();
try {
const response = await app.inject({
method: 'POST',
url: '/api/ai/tk-prompt-engine/runs',
payload: { idempotencyKey: 'analysis-0001', imageAssetIds: [imageId] },
});
expect(response.statusCode).toBe(202);
expect(response.json()).toEqual({ run: run() });
expect(mocks.createRun).toHaveBeenCalledWith({
userId,
idempotencyKey: 'analysis-0001',
imageAssetIds: [imageId],
});
expect(mocks.enqueueAnalysis).toHaveBeenCalledWith(runId, 1);
expect(mocks.createGeneration).not.toHaveBeenCalled();
} finally {
await app.close();
}
});
it('POST /runs/:id/generations 创建整批并只投递 strategy job', async () => {
const app = makeApp();
const payload = {
idempotencyKey: 'generation-0001',
expectedGenerationCredits: 7,
evidenceReview: {
productName: '参考商品',
category: '家居用品',
rejectedVisibleFactIds: [],
userConfirmedFacts: [],
forbiddenClaims: [],
},
creativeInput: {
targetMarket: 'United States',
targetAudience: 'Pet owners',
spokenLanguage: '英语',
verifiedOffer: '',
appearance: { mode: 'auto' },
availableScenes: [],
availableAssets: ['产品参考图片1'],
},
videoConfig: {
model: 'seedance-2.0',
aspectRatio: '9:16',
resolution: '480p',
durationSeconds: 10,
},
};
try {
const response = await app.inject({
method: 'POST',
url: `/api/ai/tk-prompt-engine/runs/${runId}/generations`,
payload,
});
expect(response.statusCode).toBe(202);
expect(response.json()).toEqual({ generation: generation() });
expect(mocks.createGeneration).toHaveBeenCalledWith({ userId, runId, ...payload });
expect(mocks.enqueueStrategy).toHaveBeenCalledWith(generationId, 1);
} finally {
await app.close();
}
});
it('Redis 首次投递失败不把已落库 run/已 hold 批次伪装成 HTTP 500', async () => {
const app = makeApp();
mocks.enqueueAnalysis.mockRejectedValueOnce(new Error('redis unavailable'));
const paidGeneration = {
...generation(),
isFree: false,
credits: 7,
creditStatus: 'held',
};
mocks.createGeneration.mockResolvedValueOnce(paidGeneration);
mocks.enqueueStrategy.mockRejectedValueOnce(new Error('redis unavailable'));
try {
const runResponse = await app.inject({
method: 'POST',
url: '/api/ai/tk-prompt-engine/runs',
payload: { idempotencyKey: 'analysis-enqueue-failure', imageAssetIds: [imageId] },
});
expect(runResponse.statusCode).toBe(202);
expect(runResponse.json()).toEqual({ run: run() });
const generationResponse = await app.inject({
method: 'POST',
url: `/api/ai/tk-prompt-engine/runs/${runId}/generations`,
payload: generationCreatePayload('generation-enqueue-failure'),
});
expect(generationResponse.statusCode).toBe(202);
expect(generationResponse.json()).toEqual({ generation: paidGeneration });
expect(mocks.enqueueAnalysis).toHaveBeenCalledWith(runId, 1);
expect(mocks.enqueueStrategy).toHaveBeenCalledWith(generationId, 1);
} finally {
await app.close();
}
});
it('settings、generation 与 resume 均按冻结公共路径读取', async () => {
const app = makeApp();
try {
const [settings, generationResponse, resume] = await Promise.all([
app.inject({ method: 'GET', url: '/api/ai/tk-prompt-engine/settings' }),
app.inject({ method: 'GET', url: `/api/ai/tk-prompt-engine/generations/${generationId}` }),
app.inject({ method: 'GET', url: '/api/ai/tk-prompt-engine/resume' }),
]);
expect(settings.statusCode).toBe(200);
expect(settings.json()).toHaveProperty('settings.generationCredits', 7);
expect(generationResponse.json()).toEqual({ generation: generation() });
expect(resume.json()).toEqual({ run: run(), generation: generation() });
expect(mocks.readGeneration).toHaveBeenCalledWith(userId, generationId);
expect(mocks.readResume).toHaveBeenCalledWith(userId);
} finally {
await app.close();
}
});
});
function run() {
return {
id: runId,
schemaVersion: 1,
status: 'analyzing',
imageAssetIds: [imageId],
evidenceProfile: null,
failureReason: null,
createdAt: now,
completedAt: null,
};
}
function generation() {
return {
id: generationId,
runId,
schemaVersion: 1,
status: 'queued',
evidenceReview: generationCreatePayload('resume-snapshot').evidenceReview,
creativeInput: generationCreatePayload('resume-snapshot').creativeInput,
videoConfig: generationCreatePayload('resume-snapshot').videoConfig,
isFree: true,
credits: 0,
creditStatus: 'none',
strategySet: [],
options: [],
failureReason: null,
createdAt: now,
completedAt: null,
};
}
function generationCreatePayload(idempotencyKey: string) {
return {
idempotencyKey,
expectedGenerationCredits: 7,
evidenceReview: {
productName: '参考商品',
category: '家居用品',
rejectedVisibleFactIds: [],
userConfirmedFacts: [],
forbiddenClaims: [],
},
creativeInput: {
targetMarket: 'United States',
targetAudience: 'Pet owners',
spokenLanguage: '英语',
verifiedOffer: '',
appearance: { mode: 'auto' },
availableScenes: [],
availableAssets: ['产品参考图片1'],
},
videoConfig: {
model: 'seedance-2.0',
aspectRatio: '9:16',
resolution: '480p',
durationSeconds: 10,
},
};
}
function makeApp() {
const app = fastify({ logger: false });
app.register(registerTkPromptEngineRoutes);
app.setErrorHandler((error, _request, reply) => {
const value = error as { statusCode?: number; name?: string; message?: string; code?: string };
return reply.code(value.statusCode ?? (value.name === 'ZodError' ? 400 : 500)).send({
message: value.message,
code: value.code,
});
});
return app;
}