Files
lindyson-ai/apps/api/tests/tkPromptEngineRepository.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

1317 lines
50 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { DbClient } from '../src/db/pool.js';
import {
createTkPromptEngineRepository,
productionTkPromptCreditCallbacks,
readTkPromptEngineV6WritesRetired,
tkPromptEngineRepository,
tkPromptCreditEventKey,
tkPromptProviderRequestKey,
type TkPromptCreditCallbacks,
} from '../src/domain/tkPromptEngineRepository.js';
const userId = '10000000-0000-4000-8000-000000000001';
const runId = '20000000-0000-4000-8000-000000000001';
const generationId = '30000000-0000-4000-8000-000000000001';
const runClaimToken = '50000000-0000-4000-8000-000000000001';
const generationClaimToken = '50000000-0000-4000-8000-000000000002';
const optionClaimToken = '50000000-0000-4000-8000-000000000003';
const replacementClaimToken = '50000000-0000-4000-8000-000000000004';
const imageId = '90000000-0000-4000-8000-000000000001';
const optionIds = {
safe: '40000000-0000-4000-8000-000000000001',
native: '40000000-0000-4000-8000-000000000002',
creative: '40000000-0000-4000-8000-000000000003',
};
const past = '2026-08-16T00:00:00.000Z';
const now = '2026-08-17T00:00:00.000Z';
const future = '2099-08-17T00:00:00.000Z';
describe('TK Prompt Engine repository', () => {
it('生产实例接入 TK 专用积分回调并校验整批 allocation 归属与金额', async () => {
expect(tkPromptEngineRepository).toBeDefined();
const client = scriptedClient((sql) => {
if (sql.includes('FROM credit_allocations')) return rows([{ amount: 6 }]);
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(productionTkPromptCreditCallbacks.settleWithClient({
client,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
credits: 7,
})).rejects.toMatchObject({ code: 'TK_PROMPT_CREDIT_ALLOCATION_MISMATCH' });
expect(client.calls[0]?.sql).toContain('related_tk_prompt_generation_id = $1');
expect(client.calls[0]?.params).toEqual([
generationId,
tkPromptCreditEventKey(generationId),
]);
await expect(productionTkPromptCreditCallbacks.refundWithClient({
client,
userId,
generationId,
eventKey: 'wrong-event-key',
credits: 7,
})).rejects.toMatchObject({ code: 'TK_PROMPT_CREDIT_EVENT_KEY_MISMATCH' });
});
it('设置、分析 claim/complete/fail 和 resume 都只读写 TK1 独立表', async () => {
const settingsClient = scriptedClient((sql, params) => {
if (sql.startsWith('SELECT schema_version')) {
return rows([{ schema_version: 1, generation_credits: 7, available: false, updated_at: now }]);
}
if (sql.startsWith('UPDATE tk_prompt_engine_settings')) {
expect(sql).not.toContain('v6_retired_at');
expect(params).toEqual([8, false, userId]);
return rows([{ schema_version: 1, generation_credits: 8, available: false, updated_at: now }]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const settingsRepository = repositoryWith(settingsClient);
await expect(settingsRepository.getSettings()).resolves.toMatchObject({
schemaVersion: 1,
generationCredits: 7,
available: false,
});
await expect(settingsRepository.updateSettings({
updatedBy: userId,
generationCredits: 8,
available: false,
})).resolves.toMatchObject({ generationCredits: 8, available: false });
await expect(settingsRepository.updateSettings({
updatedBy: userId,
available: true,
})).rejects.toMatchObject({ code: 'TK_PROMPT_ENABLE_REQUIRES_FINALIZER', statusCode: 409 });
const retirementClient = scriptedClient((sql) => {
expect(sql).toContain('v6_retired_at IS NOT NULL');
return rows([{ v6_retired: true }]);
});
await expect(readTkPromptEngineV6WritesRetired(retirementClient)).resolves.toBe(true);
const analysisClient = scriptedClient((sql) => {
if (sql.includes('attempt_count = attempt_count + 1')) {
expect(sql).toContain('attempt_count + 1 = $3');
return rows([runRow({
started_at: now,
attempt_count: 1,
claim_token: runClaimToken,
})]);
}
if (sql.includes("SET status = 'ready'")) return rows([readyRunRow()]);
if (sql.includes("SET status = 'failed'")) {
return rows([runRow({ status: 'failed', failure_reason: 'analysis failed', completed_at: now })]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const analysisRepository = repositoryWith(analysisClient);
await expect(analysisRepository.claimRunAnalysis({
runId,
staleBefore: new Date(past),
expectedAttempt: 1,
})).resolves.toMatchObject({ status: 'analyzing', claimToken: runClaimToken });
await expect(analysisRepository.completeRunAnalysis({
runId,
userId,
claimToken: runClaimToken,
evidenceProfile: evidenceProfile(),
})).resolves.toMatchObject({ status: 'ready' });
await expect(analysisRepository.failRunAnalysis({
runId,
userId,
claimToken: runClaimToken,
failureReason: 'analysis failed',
})).resolves.toMatchObject({ status: 'failed', failureReason: 'analysis failed' });
const resumeClient = scriptedClient((sql) => {
if (sql.includes('FROM tk_prompt_runs run')) {
expect(sql).toContain("generation.status IN ('queued', 'strategizing', 'expanding')");
return rows([readyRunRow()]);
}
if (sql.includes('FROM tk_prompt_generations')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(resumeClient).resume(userId)).resolves.toMatchObject({
run: { id: runId, status: 'ready' },
generation: null,
});
});
it('读取历史方案时剔除发布字段,保留口播与执行提示词且不改写历史数据', async () => {
const creative = { ...generationInput().creativeInput, publishLanguage: '日语' };
const storedPlan = { ...plan(), publishPack: { title: '历史发布标题' } };
const completed = generationRow({
status: 'completed',
creative_input: creative,
strategy_set: strategySet(),
completed_at: now,
});
const client = scriptedClient(sql => {
if (sql.includes('FROM tk_prompt_runs run')) return rows([readyRunRow()]);
if (sql.includes('FROM tk_prompt_generations')) return rows([completed]);
if (sql.includes('FROM tk_prompt_options')) return rows(TK_SLOTS.map(slot => optionRow(slot, {
status: 'completed', plan: storedPlan, completed_at: now,
})));
throw new Error(`未处理 SQL: ${sql}`);
});
const resumed = await repositoryWith(client).resume(userId);
expect(resumed.generation?.creativeInput).not.toHaveProperty('publishLanguage');
expect(resumed.generation?.creativeInput.spokenLanguage).toBe('英语');
for (const option of resumed.generation!.options) {
expect(option.plan).not.toHaveProperty('publishPack');
expect(option.plan?.executionPrompt).toBe(storedPlan.executionPrompt);
}
expect(creative.publishLanguage).toBe('日语');
expect(storedPlan.publishPack.title).toBe('历史发布标题');
});
it('resume 在更新的失败换批之后仍恢复最近一次成功三案', async () => {
const completedGenerationId = '30000000-0000-4000-8000-000000000011';
const failedGenerationId = '30000000-0000-4000-8000-000000000012';
const completed = generationRow({
id: completedGenerationId,
ordinal: 1,
status: 'completed',
strategy_set: strategySet(),
completed_at: now,
});
const newerFailed = generationRow({
id: failedGenerationId,
ordinal: 2,
status: 'failed',
failure_reason: 'provider failed',
completed_at: now,
});
const client = scriptedClient((sql, params) => {
if (sql.includes('FROM tk_prompt_runs run')) return rows([readyRunRow()]);
if (sql.includes('FROM tk_prompt_generations')) {
expect(sql).toContain("WHEN status IN ('queued', 'strategizing', 'expanding') THEN 0");
expect(sql).toContain("WHEN status = 'completed' THEN 1");
expect(sql).toContain('END ASC, ordinal DESC');
expect(newerFailed.ordinal).toBeGreaterThan(completed.ordinal as number);
return rows([completed]);
}
if (sql.includes('FROM tk_prompt_options')) {
expect(params).toEqual([completedGenerationId]);
return rows((['safe', 'native', 'creative'] as const).map(slot => optionRow(slot, {
generation_id: completedGenerationId,
status: 'completed',
plan: plan(),
completed_at: now,
})));
}
throw new Error(`未处理 SQL: ${sql}`);
});
const resumed = await repositoryWith(client).resume(userId);
expect(resumed.generation).toMatchObject({
id: completedGenerationId,
status: 'completed',
});
expect(resumed.generation?.options).toHaveLength(3);
expect(resumed.generation?.options.every(option => option.plan !== null)).toBe(true);
});
it('resume 始终让 active 批次优先于 completed 与 failed', async () => {
const activeGenerationId = '30000000-0000-4000-8000-000000000023';
const client = scriptedClient((sql, params) => {
if (sql.includes('FROM tk_prompt_runs run')) return rows([readyRunRow()]);
if (sql.includes('FROM tk_prompt_generations')) {
expect(sql).toContain("WHEN status IN ('queued', 'strategizing', 'expanding') THEN 0");
expect(sql).toContain("WHEN status = 'completed' THEN 1");
return rows([generationRow({
id: activeGenerationId,
ordinal: 3,
status: 'expanding',
strategy_set: strategySet(),
})]);
}
if (sql.includes('FROM tk_prompt_options')) {
expect(params).toEqual([activeGenerationId]);
return rows((['safe', 'native', 'creative'] as const).map(slot => optionRow(slot, {
generation_id: activeGenerationId,
})));
}
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(client).resume(userId)).resolves.toMatchObject({
generation: { id: activeGenerationId, status: 'expanding' },
});
});
it('创建 run 使用事务级幂等锁、稳定请求哈希并锁定 1–9 张图片', async () => {
const client = scriptedClient((sql) => {
if (sql.includes('pg_advisory_xact_lock')) return rows([]);
if (sql.includes('FROM tk_prompt_runs') && sql.includes('idempotency_key')) return rows([]);
if (sql.includes('INSERT INTO tk_prompt_runs')) return rows([runRow()]);
throw new Error(`未处理 SQL: ${sql}`);
});
const repository = repositoryWith(client);
const run = await repository.createRun({
userId,
idempotencyKey: 'run-idempotency-1',
imageAssetIds: [imageId],
});
expect(run).toMatchObject({ id: runId, status: 'analyzing', imageAssetIds: [imageId] });
const insert = client.calls.find(call => call.sql.includes('INSERT INTO tk_prompt_runs'))!;
expect(insert.params?.[2]).toMatch(/^[0-9a-f]{64}$/u);
await expect(repository.createRun({
userId,
idempotencyKey: 'too-many',
imageAssetIds: Array.from({ length: 10 }, () => imageId),
})).rejects.toMatchObject({ code: 'TK_PROMPT_IMAGE_COUNT_INVALID' });
const conflicting = scriptedClient((sql) => {
if (sql.includes('pg_advisory_xact_lock')) return rows([]);
if (sql.includes('FROM tk_prompt_runs')) return rows([runRow({ request_hash: 'f'.repeat(64) })]);
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(conflicting).createRun({
userId,
idempotencyKey: 'run-idempotency-1',
imageAssetIds: [imageId],
})).rejects.toMatchObject({ code: 'TK_PROMPT_RUN_IDEMPOTENCY_CONFLICT' });
});
it('首个成功批次免费;后续批次按整批价格规划并在同一事务冻结积分', async () => {
const holdWithClient = vi.fn(async () => undefined);
const callbacks = creditCallbacks({ holdWithClient });
const client = scriptedClient((sql, params) => {
if (sql.includes('pg_advisory_xact_lock')) return rows([]);
if (sql.includes('JOIN tk_prompt_runs') && sql.includes('idempotency_key')) return rows([]);
if (sql.includes('SELECT * FROM tk_prompt_runs')) return rows([readyRunRow()]);
if (sql.includes('SELECT 1 FROM tk_prompt_generations')) return rows([]);
if (sql.includes('FROM tk_prompt_engine_settings')) {
return rows([{ schema_version: 1, generation_credits: 7, available: true, updated_at: now }]);
}
if (sql.includes('SELECT id FROM users')) return rows([{ id: userId }]);
if (sql.includes('FROM credit_lots')) return rows([]);
if (sql.includes('bool_or(status')) return rows([{ next_ordinal: 2, has_completed: true }]);
if (sql.includes('INSERT INTO tk_prompt_generations')) {
expect(params?.slice(8, 13)).toEqual([false, 7, 'held', 5, 2]);
return rows([generationRow({
ordinal: 2,
is_free: false,
credits: 7,
credit_status: 'held',
main_credits_used: 5,
addon_credits_used: 2,
})]);
}
if (sql.includes('UPDATE tk_prompt_runs')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
const planner = vi.fn(async (plannerClient: DbClient, plannerUserId: string) => {
await plannerClient.query(
'SELECT id FROM credit_lots WHERE user_id = $1 FOR UPDATE',
[plannerUserId],
);
return [{ mainAmount: 5, addonAmount: 2 }];
});
const repository = repositoryWith(client, { callbacks, planner });
const generation = await repository.createGeneration(generationInput());
expect(generation).toMatchObject({
id: generationId,
isFree: false,
credits: 7,
creditStatus: 'held',
});
expect(planner).toHaveBeenCalledWith(client, userId, [7]);
expect(holdWithClient).toHaveBeenCalledWith({
client,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
mainAmount: 5,
addonAmount: 2,
});
const userLockIndex = client.calls.findIndex(call => call.sql.includes('SELECT id FROM users'));
const lotLockIndex = client.calls.findIndex(call => call.sql.includes('FROM credit_lots'));
expect(userLockIndex).toBeGreaterThanOrEqual(0);
expect(lotLockIndex).toBeGreaterThan(userLockIndex);
const freeClient = scriptedClient((sql, params) => {
if (sql.includes('pg_advisory_xact_lock')) return rows([]);
if (sql.includes('JOIN tk_prompt_runs') && sql.includes('idempotency_key')) return rows([]);
if (sql.includes('SELECT * FROM tk_prompt_runs')) return rows([readyRunRow()]);
if (sql.includes('SELECT 1 FROM tk_prompt_generations')) return rows([]);
if (sql.includes('FROM tk_prompt_engine_settings')) {
return rows([{ schema_version: 1, generation_credits: 7, available: true, updated_at: now }]);
}
if (sql.includes('SELECT id FROM users')) return rows([{ id: userId }]);
if (sql.includes('bool_or(status')) return rows([{ next_ordinal: 3, has_completed: false }]);
if (sql.includes('INSERT INTO tk_prompt_generations')) {
expect(params?.slice(8, 13)).toEqual([true, 0, 'none', 0, 0]);
return rows([generationRow({ ordinal: 3 })]);
}
if (sql.includes('UPDATE tk_prompt_runs')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
const freePlanner = vi.fn(async () => [{ mainAmount: 7, addonAmount: 0 }]);
const freeRepository = repositoryWith(freeClient, { callbacks, planner: freePlanner });
const free = await freeRepository.createGeneration({
...generationInput(),
idempotencyKey: 'generation-free-retry',
});
expect(free).toMatchObject({ isFree: true, credits: 0, creditStatus: 'none' });
expect(freePlanner).not.toHaveBeenCalled();
expect(holdWithClient).toHaveBeenCalledTimes(1);
});
it('策略与三个 option 原子创建,第三案完成时才在同一事务结算整批', async () => {
const strategies = strategySet();
const strategyClient = scriptedClient((sql) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'strategizing',
started_at: now,
claim_token: generationClaimToken,
})]);
}
if (sql.includes('UPDATE tk_prompt_generations')) {
return rows([generationRow({ status: 'expanding', strategy_set: strategies })]);
}
if (sql.includes('INSERT INTO tk_prompt_options')) {
return rows(TK_SLOTS.map(slot => optionRow(slot, {
strategy_blueprint: strategies.find(strategy => strategy.slot === slot),
})));
}
throw new Error(`未处理 SQL: ${sql}`);
});
const created = await repositoryWith(strategyClient).saveStrategiesAndCreateOptions({
generationId,
userId,
claimToken: generationClaimToken,
strategies,
});
expect(created.status).toBe('expanding');
expect(created.options.map(option => option.slot)).toEqual(TK_SLOTS);
expect(strategyClient.calls.some(call => call.sql.includes('jsonb_array_elements'))).toBe(true);
const settleWithClient = vi.fn(async () => undefined);
const completionClient = scriptedClient((sql) => {
if (sql.includes('SELECT generation_id FROM tk_prompt_options')) {
return rows([{ generation_id: generationId }]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe', {
status: 'completed', plan: plan(), completed_at: now,
}),
optionRow('native', {
status: 'completed', plan: plan(), completed_at: now,
}),
optionRow('creative', {
status: 'processing',
strategy_blueprint: strategies[2],
started_at: now,
claim_token: optionClaimToken,
}),
]);
}
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
is_free: false,
credits: 7,
credit_status: 'held',
main_credits_used: 5,
addon_credits_used: 2,
strategy_set: strategies,
})]);
}
if (sql.includes("SET status = 'completed', plan")) {
return rows([optionRow('creative', {
status: 'completed',
strategy_blueprint: strategies[2],
plan: plan(),
completed_at: now,
})]);
}
if (sql.includes('count(*) AS completed_count')) return rows([{ completed_count: 3 }]);
if (sql.includes("SET status = 'completed'")) {
return rows([generationRow({
status: 'completed',
is_free: false,
credits: 7,
credit_status: 'settled',
main_credits_used: 5,
addon_credits_used: 2,
strategy_set: strategies,
completed_at: now,
})]);
}
if (sql.includes('UPDATE tk_prompt_runs')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
const completion = await repositoryWith(completionClient, {
callbacks: creditCallbacks({ settleWithClient }),
}).completeOption({
optionId: optionIds.creative,
userId,
claimToken: optionClaimToken,
plan: plan(),
});
expect(completion.generationCompleted).toBe(true);
expect(settleWithClient).toHaveBeenCalledWith({
client: completionClient,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
credits: 7,
});
const completionGenerationLock = completionClient.calls.findIndex(call => (
call.sql.includes('SELECT * FROM tk_prompt_generations') && call.sql.includes('FOR UPDATE')
));
const completionOptionsLock = completionClient.calls.findIndex(call => (
call.sql.includes('SELECT * FROM tk_prompt_options') && call.sql.includes('FOR UPDATE')
));
expect(completionGenerationLock).toBeGreaterThanOrEqual(0);
expect(completionOptionsLock).toBeGreaterThan(completionGenerationLock);
const claimClient = scriptedClient((sql) => {
if (sql.includes("SET status = 'strategizing'")) {
expect(sql).toContain("status IN ('queued', 'strategizing')");
expect(sql).toContain('attempt_count + 1 = $3');
return rows([generationRow({
status: 'strategizing',
started_at: now,
attempt_count: 1,
claim_token: generationClaimToken,
})]);
}
if (sql.includes("SET status = 'processing'")) {
expect(sql).toContain("generation.status = 'expanding'");
expect(sql).toContain('option.attempt_count + 1 = $3');
return rows([optionRow('safe', {
status: 'processing',
strategy_blueprint: strategies[0],
started_at: now,
attempt_count: 1,
claim_token: optionClaimToken,
})]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const claimRepository = repositoryWith(claimClient);
await expect(claimRepository.claimGenerationStrategy({
generationId,
staleBefore: new Date(past),
expectedAttempt: 1,
})).resolves.toMatchObject({
status: 'strategizing',
claimToken: generationClaimToken,
});
await expect(claimRepository.claimOption({
optionId: optionIds.safe,
staleBefore: new Date(past),
expectedAttempt: 1,
})).resolves.toMatchObject({
status: 'processing',
slot: 'safe',
claimToken: optionClaimToken,
});
});
it('任一方案失败会原子终止其余方案并全额退款;TTL 仅清理终态内容', async () => {
const refundWithClient = vi.fn(async () => undefined);
const client = scriptedClient((sql) => {
if (sql.includes('SELECT generation_id FROM tk_prompt_options')) {
return rows([{ generation_id: generationId }]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe'),
optionRow('native', {
status: 'processing',
started_at: now,
claim_token: optionClaimToken,
}),
optionRow('creative'),
]);
}
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
is_free: false,
credits: 7,
credit_status: 'held',
main_credits_used: 7,
strategy_set: strategySet(),
})]);
}
if (sql.includes('UPDATE tk_prompt_options')) return rows([], 2);
if (sql.includes('UPDATE tk_prompt_generations')) return rows([], 1);
if (sql.includes('UPDATE tk_prompt_runs')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
await repositoryWith(client, {
callbacks: creditCallbacks({ refundWithClient }),
}).failOption({
optionId: optionIds.native,
userId,
claimToken: optionClaimToken,
failureReason: 'provider failed',
});
expect(refundWithClient).toHaveBeenCalledWith({
client,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
credits: 7,
});
expect(client.calls.some(call => (
call.sql.includes('UPDATE tk_prompt_options')
&& call.sql.includes('content_purged_at IS NULL')
&& call.sql.includes('plan = NULL')
))).toBe(true);
const failureGenerationLock = client.calls.findIndex(call => (
call.sql.includes('SELECT * FROM tk_prompt_generations') && call.sql.includes('FOR UPDATE')
));
const failureOptionsLock = client.calls.findIndex(call => (
call.sql.includes('SELECT * FROM tk_prompt_options') && call.sql.includes('FOR UPDATE')
));
expect(failureGenerationLock).toBeGreaterThanOrEqual(0);
expect(failureOptionsLock).toBeGreaterThan(failureGenerationLock);
const purgeClient = scriptedClient((sql) => {
if (sql.includes('UPDATE tk_prompt_generations generation')) {
expect(sql).toContain("status IN ('completed', 'failed')");
expect(sql).toContain('FOR UPDATE SKIP LOCKED');
expect(sql.indexOf('UPDATE tk_prompt_options')).toBeLessThan(sql.indexOf('UPDATE tk_prompt_generations'));
return rows([{ id: generationId }]);
}
if (sql.includes('UPDATE tk_prompt_runs run')) {
expect(sql).toContain("run.status IN ('ready', 'failed')");
expect(sql).toContain("status IN ('queued', 'strategizing', 'expanding')");
expect(sql).toContain('generation.expires_at > now()');
return rows([{ id: runId }]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(purgeClient).purgeExpired(25)).resolves.toEqual({
generations: 1,
runs: 1,
});
});
it('旧 worker 的 claim token 不能覆盖新 attempt 的策略或方案结果', async () => {
const staleStrategyClient = scriptedClient((sql) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'strategizing',
started_at: now,
claim_token: replacementClaimToken,
attempt_count: 2,
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) return rows([]);
throw new Error(`旧策略 worker 不应执行写 SQL: ${sql}`);
});
await expect(repositoryWith(staleStrategyClient).failGeneration({
generationId,
userId,
claimToken: generationClaimToken,
failureReason: 'stale strategy failure',
})).rejects.toMatchObject({ code: 'TK_PROMPT_GENERATION_CLAIM_STALE' });
expect(staleStrategyClient.calls.every(call => !call.sql.startsWith('UPDATE'))).toBe(true);
const staleCompletionClient = staleOptionAttemptClient();
await expect(repositoryWith(staleCompletionClient).completeOption({
optionId: optionIds.native,
userId,
claimToken: optionClaimToken,
plan: plan(),
})).rejects.toMatchObject({ code: 'TK_PROMPT_OPTION_CLAIM_STALE' });
expect(staleCompletionClient.calls.every(call => !call.sql.startsWith('UPDATE'))).toBe(true);
const staleFailureClient = staleOptionAttemptClient();
await expect(repositoryWith(staleFailureClient).failOption({
optionId: optionIds.native,
userId,
claimToken: optionClaimToken,
failureReason: 'stale option failure',
})).rejects.toMatchObject({ code: 'TK_PROMPT_OPTION_CLAIM_STALE' });
expect(staleFailureClient.calls.every(call => !call.sql.startsWith('UPDATE'))).toBe(true);
});
it('过期活动批次使用 SKIP LOCKED 单次收口并在同一事务全额退款', async () => {
const refundWithClient = vi.fn(async () => undefined);
const client = scriptedClient((sql) => {
if (
sql.startsWith('SELECT id, user_id FROM tk_prompt_generations')
&& sql.includes('FOR UPDATE SKIP LOCKED')
) {
expect(sql).toContain('expires_at <= clock_timestamp()');
return rows([{ id: generationId, user_id: userId }]);
}
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
is_free: false,
credits: 7,
credit_status: 'held',
main_credits_used: 7,
strategy_set: strategySet(),
expires_at: past,
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe', { status: 'completed', plan: plan(), completed_at: now }),
optionRow('native', {
status: 'processing',
started_at: now,
claim_token: optionClaimToken,
}),
optionRow('creative'),
]);
}
if (sql.startsWith('UPDATE tk_prompt_options')) {
expect(sql).toContain('claim_token = NULL');
return rows([], 3);
}
if (sql.startsWith('UPDATE tk_prompt_generations')) {
expect(sql).toContain("credit_status = CASE WHEN credits > 0 THEN 'refunded'");
expect(sql).toContain('claim_token = NULL');
return rows([], 1);
}
if (sql.startsWith('UPDATE tk_prompt_runs SET expires_at')) return rows([]);
if (sql.startsWith('WITH candidates AS')) {
expect(sql).toContain("status = 'analyzing'");
expect(sql).toContain('FOR UPDATE SKIP LOCKED');
expect(sql).toContain('claim_token = NULL');
expect(sql).toContain('clock_timestamp()');
return rows([{ id: runId }]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const repository = repositoryWith(client, {
callbacks: creditCallbacks({ refundWithClient }),
});
await expect(repository.expireActive(20)).resolves.toEqual({
generations: 1,
runs: 1,
});
expect(refundWithClient).toHaveBeenCalledTimes(1);
expect(refundWithClient).toHaveBeenCalledWith({
client,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
credits: 7,
});
const competingSweeper = scriptedClient((sql) => {
if (sql.startsWith('SELECT id, user_id FROM tk_prompt_generations')) return rows([]);
if (sql.startsWith('WITH candidates AS')) return rows([]);
throw new Error(`竞争 sweeper 不应执行其他 SQL: ${sql}`);
});
await expect(repositoryWith(competingSweeper, {
callbacks: creditCallbacks({ refundWithClient }),
}).expireActive(20)).resolves.toEqual({ generations: 0, runs: 0 });
expect(refundWithClient).toHaveBeenCalledTimes(1);
});
it('stale claim 未产生 Provider 事实时先撤销旧 claim,再只投递精确下一 attempt', async () => {
const client = scriptedClient((sql) => {
if (sql.startsWith('SELECT id FROM tk_prompt_runs')) return rows([]);
if (sql.startsWith('SELECT id FROM tk_prompt_generations')) return rows([{ id: generationId }]);
if (sql.startsWith('SELECT DISTINCT generation.id')) return rows([]);
if (sql.startsWith('SELECT user_id FROM tk_prompt_generations')) return rows([{ user_id: userId }]);
if (sql.startsWith('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'strategizing',
started_at: past,
attempt_count: 1,
claim_token: generationClaimToken,
})]);
}
if (sql.startsWith('SELECT * FROM tk_prompt_options')) return rows([]);
if (sql.startsWith('SELECT expires_at > clock_timestamp() AS live')) {
return rows([{ live: true }]);
}
if (sql.startsWith('SELECT 1 FROM tk_prompt_provider_requests')) {
expect(sql).toContain('job_attempt = $5 AND claim_token = $6');
return rows([]);
}
if (sql.startsWith('UPDATE tk_prompt_generations')) {
expect(sql).toContain("SET status = 'queued', started_at = NULL, claim_token = NULL");
return rows([], 1);
}
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(client).recoverableWork(new Date(now))).resolves.toEqual({
runs: [],
generations: [{ id: generationId, attempt: 2 }],
options: [],
});
const providerCheck = client.calls.findIndex(call => call.sql.includes('tk_prompt_provider_requests'));
const reset = client.calls.findIndex(call => call.sql.startsWith('UPDATE tk_prompt_generations'));
expect(reset).toBeGreaterThan(providerCheck);
});
it('stale option claim 已有任一 Provider 事实时整批失败退款,绝不重新投递付费调用', async () => {
const refundWithClient = vi.fn(async () => undefined);
const client = scriptedClient((sql) => {
if (sql.startsWith('SELECT id FROM tk_prompt_runs')) return rows([]);
if (sql.startsWith('SELECT id FROM tk_prompt_generations')) return rows([]);
if (sql.startsWith('SELECT DISTINCT generation.id')) return rows([{ id: generationId }]);
if (sql.startsWith('SELECT user_id FROM tk_prompt_generations')) return rows([{ user_id: userId }]);
if (sql.startsWith('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
is_free: false,
credits: 7,
credit_status: 'held',
main_credits_used: 7,
strategy_set: strategySet(),
})]);
}
if (sql.startsWith('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe', {
status: 'processing',
started_at: past,
attempt_count: 1,
claim_token: optionClaimToken,
}),
optionRow('native'),
optionRow('creative'),
]);
}
if (sql.startsWith('SELECT expires_at > clock_timestamp() AS live')) {
return rows([{ live: true }]);
}
if (sql.startsWith('SELECT 1 FROM tk_prompt_provider_requests')) return rows([{ exists: 1 }]);
if (sql.startsWith('UPDATE tk_prompt_options')) return rows([], 3);
if (sql.startsWith('UPDATE tk_prompt_generations')) return rows([], 1);
if (sql.startsWith('UPDATE tk_prompt_runs')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(client, {
callbacks: creditCallbacks({ refundWithClient }),
}).recoverableWork(new Date(now))).resolves.toEqual({
runs: [],
generations: [],
options: [],
});
expect(refundWithClient).toHaveBeenCalledWith({
client,
userId,
generationId,
eventKey: tkPromptCreditEventKey(generationId),
credits: 7,
});
expect(client.calls.some(call => call.params?.some(param => (
typeof param === 'string' && param.includes('TK_PROMPT_PROVIDER_ATTEMPT_NOT_REPLAYABLE')
)))).toBe(true);
expect(client.calls.some(call => call.sql.includes("SET status = 'queued'"))).toBe(false);
});
it('expanding 批次清空 strategy claim 后仍可作为 option 的只读上下文', async () => {
const client = scriptedClient((sql) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
strategy_set: strategySet(),
claim_token: null,
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) return rows([]);
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(client).getGenerationWork(userId, generationId))
.resolves.toMatchObject({ id: generationId, status: 'expanding' });
});
it('三案完成前公共 generation 不暴露内部策略、蓝图或已先完成方案正文', async () => {
const client = scriptedClient((sql) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
strategy_set: strategySet(),
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe', { status: 'completed', plan: plan(), completed_at: now }),
optionRow('native', {
status: 'processing',
started_at: now,
claim_token: optionClaimToken,
}),
optionRow('creative'),
]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const generation = await repositoryWith(client).getGeneration(userId, generationId);
expect(generation.strategySet).toEqual([]);
expect(generation.options).toHaveLength(3);
expect(generation.options.every(option => option.strategyBlueprint === null)).toBe(true);
expect(generation.options.every(option => option.plan === null)).toBe(true);
expect(generation.options.find(option => option.slot === 'safe')).toMatchObject({
status: 'processing',
completedAt: null,
});
});
it('超出请求预算的 started Provider 账本会原子收口为未知成本终态', async () => {
const staleBefore = new Date('2026-08-17T01:00:00.000Z');
const client = scriptedClient((sql, params) => {
if (sql.includes('UPDATE tk_prompt_provider_requests request')) {
expect(sql).toContain("request_status = 'started'");
expect(sql).toContain('FOR UPDATE SKIP LOCKED');
expect(sql).toContain("request_status = 'failed'");
expect(sql).toContain("error_code = 'TK_PROMPT_PROVIDER_OUTCOME_UNKNOWN'");
expect(sql).toContain("cost_status = 'unknown'");
expect(sql).toContain('completed_at = clock_timestamp()');
expect(params).toEqual([staleBefore, 25]);
return rows([
{ id: '60000000-0000-4000-8000-000000000010' },
{ id: '60000000-0000-4000-8000-000000000011' },
]);
}
throw new Error(`未处理 SQL: ${sql}`);
});
await expect(repositoryWith(client).expireStartedProviderRequests(staleBefore, 25))
.resolves.toBe(2);
});
it('Provider 请求只允许唯一 started 记录写入一次终态', async () => {
let terminal = false;
const client = scriptedClient((sql, params) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
expect(sql).toContain("status = 'expanding'");
expect(sql).toContain('expires_at > clock_timestamp()');
return rows([generationRow({ status: 'expanding', strategy_set: strategySet() })]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
expect(sql).toContain('claim_token = $4 AND attempt_count = $5');
return rows([optionRow('safe', {
status: 'processing',
started_at: now,
claim_token: optionClaimToken,
attempt_count: 1,
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_runs')) return rows([readyRunRow()]);
if (sql.includes('INSERT INTO tk_prompt_provider_requests')) {
expect(sql).toContain('job_attempt, claim_token, repair_round');
expect(params?.[8]).toBe(optionClaimToken);
return rows([{ id: '60000000-0000-4000-8000-000000000001' }]);
}
if (sql.includes('SELECT id, request_key, request_status')) {
return rows([{
id: '60000000-0000-4000-8000-000000000001',
request_key: 'option:1',
request_status: terminal ? 'succeeded' : 'started',
}]);
}
if (sql.includes('UPDATE tk_prompt_provider_requests')) {
expect(sql).toContain('input_tokens = $9, cached_input_tokens = $10, output_tokens = $11');
expect(sql).toContain('cost_cny = $18, cost_status = $19');
expect(params).toEqual([
'60000000-0000-4000-8000-000000000001',
'succeeded',
'provider-request',
'provider-response',
'log-id',
200,
null,
null,
100,
0,
200,
null,
null,
null,
null,
null,
null,
null,
'unknown',
]);
expect(params).toHaveLength(19);
terminal = true;
return rows([], 1);
}
throw new Error(`未处理 SQL: ${sql}`);
});
const repository = repositoryWith(client);
const logicalAttempt = {
runId,
generationId,
optionId: optionIds.safe,
userId,
stage: 'option' as const,
action: 'generate-option',
jobAttempt: 1,
claimToken: optionClaimToken,
repairRound: 0 as const,
transportAttempt: 1,
provider: 'volcengine-ark',
model: 'doubao-seed-2-0-lite-260428',
serviceTier: 'default',
};
const requestId = await repository.beginProviderRequest({
...logicalAttempt,
requestKey: tkPromptProviderRequestKey(logicalAttempt),
});
const generationLock = client.calls.findIndex(call => call.sql.includes('FROM tk_prompt_generations'));
const optionLock = client.calls.findIndex(call => call.sql.includes('FROM tk_prompt_options'));
const runLock = client.calls.findIndex(call => call.sql.includes('FROM tk_prompt_runs'));
const ledgerInsert = client.calls.findIndex(call => call.sql.includes('INSERT INTO tk_prompt_provider_requests'));
expect(optionLock).toBeGreaterThan(generationLock);
expect(runLock).toBeGreaterThan(optionLock);
expect(ledgerInsert).toBeGreaterThan(runLock);
const terminalInput = {
status: 'succeeded' as const,
providerRequestId: 'provider-request',
providerResponseId: 'provider-response',
xTtLogId: 'log-id',
httpStatus: 200,
errorCode: null,
errorClassification: null,
inputTokens: 100,
cachedInputTokens: 0,
outputTokens: 200,
costStatus: 'unknown' as const,
};
await expect(repository.completeProviderRequest(requestId, terminalInput)).resolves.toBe(true);
await expect(repository.completeProviderRequest(requestId, terminalInput)).resolves.toBe(false);
expect(client.calls.filter(call => call.sql.includes('UPDATE tk_prompt_provider_requests'))).toHaveLength(1);
});
it('兄弟方案整批失败清 claim 后,旧 option worker 在付费调用前被 fencing 拒绝', async () => {
const logicalAttempt = {
runId,
generationId,
optionId: optionIds.native,
userId,
stage: 'option' as const,
action: 'repair-option',
jobAttempt: 1,
claimToken: optionClaimToken,
repairRound: 1 as const,
transportAttempt: 1,
provider: 'volcengine-ark',
model: 'doubao-seed-2-0-lite-260428',
serviceTier: 'default',
};
const client = scriptedClient((sql) => {
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({ status: 'expanding', strategy_set: strategySet() })]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) return rows([]);
throw new Error(`claim 失效后不应读取 run 或写 Provider 账本: ${sql}`);
});
await expect(repositoryWith(client).beginProviderRequest({
...logicalAttempt,
requestKey: tkPromptProviderRequestKey(logicalAttempt),
})).rejects.toMatchObject({ code: 'TK_PROMPT_PROVIDER_CLAIM_STALE' });
expect(client.calls.some(call => call.sql.includes('tk_prompt_provider_requests'))).toBe(false);
});
});
const TK_SLOTS = ['safe', 'native', 'creative'] as const;
function repositoryWith(
client: ReturnType<typeof scriptedClient>,
options: {
callbacks?: TkPromptCreditCallbacks;
planner?: (client: DbClient, userId: string, amounts: number[]) => Promise<Array<{
mainAmount: number;
addonAmount: number;
}>>;
} = {},
) {
return createTkPromptEngineRepository({
reader: client,
transaction: callback => callback(client),
creditCallbacks: options.callbacks,
planCreditsWithClient: options.planner,
});
}
function scriptedClient(
handler: (sql: string, params: unknown[] | undefined) => ReturnType<typeof rows>,
) {
const calls: Array<{ sql: string; params?: unknown[] }> = [];
return {
calls,
async query<T>(text: string, params?: unknown[]) {
const sql = text.replace(/\s+/gu, ' ').trim();
calls.push({ sql, params });
return handler(sql, params) as unknown as {
rows: T[];
rowCount: number;
};
},
} as unknown as DbClient & { calls: Array<{ sql: string; params?: unknown[] }> };
}
function staleOptionAttemptClient() {
return scriptedClient((sql) => {
if (sql.includes('SELECT generation_id FROM tk_prompt_options')) {
return rows([{ generation_id: generationId }]);
}
if (sql.includes('SELECT * FROM tk_prompt_generations')) {
return rows([generationRow({
status: 'expanding',
strategy_set: strategySet(),
})]);
}
if (sql.includes('SELECT * FROM tk_prompt_options')) {
return rows([
optionRow('safe'),
optionRow('native', {
status: 'processing',
started_at: now,
claim_token: replacementClaimToken,
attempt_count: 2,
}),
optionRow('creative'),
]);
}
throw new Error(`旧方案 worker 不应执行写 SQL: ${sql}`);
});
}
function rows<T>(items: T[], rowCount = items.length) {
return { rows: items, rowCount };
}
function creditCallbacks(overrides: Partial<TkPromptCreditCallbacks> = {}): TkPromptCreditCallbacks {
return {
holdWithClient: overrides.holdWithClient ?? vi.fn(async () => undefined),
settleWithClient: overrides.settleWithClient ?? vi.fn(async () => undefined),
refundWithClient: overrides.refundWithClient ?? vi.fn(async () => undefined),
};
}
function generationInput() {
return {
userId,
runId,
idempotencyKey: 'generation-idempotency-1',
expectedGenerationCredits: 7,
confirmedFactPack: {
schemaVersion: 1,
factPack: { facts: [{ factId: 'visible-001', text: '白色主体可见' }] },
evidenceReview: {
productName: '自动喂食器',
category: '宠物用品',
rejectedVisibleFactIds: [],
userConfirmedFacts: [],
forbiddenClaims: [],
},
evidenceFlags: {},
},
creativeInput: {
targetMarket: 'United States',
targetAudience: 'Pet owners',
spokenLanguage: '英语' as const,
verifiedOffer: '',
appearance: { mode: 'product_closeup' as const },
availableScenes: ['home'],
availableAssets: ['product'],
},
videoConfig: {
model: 'seedance-2.0' as const,
aspectRatio: '9:16' as const,
resolution: '720p' as const,
durationSeconds: 10 as const,
},
};
}
function runRow(overrides: Record<string, unknown> = {}) {
return {
id: runId,
user_id: userId,
schema_version: 1,
idempotency_key: 'run-idempotency-1',
request_hash: 'a'.repeat(64),
status: 'analyzing',
image_asset_ids: [imageId],
evidence_profile: null,
failure_reason: null,
attempt_count: 0,
claim_token: null,
content_purged_at: null,
started_at: null,
completed_at: null,
expires_at: future,
created_at: now,
updated_at: now,
...overrides,
};
}
function readyRunRow() {
return runRow({
status: 'ready',
evidence_profile: evidenceProfile(),
completed_at: now,
});
}
function generationRow(overrides: Record<string, unknown> = {}) {
return {
id: generationId,
run_id: runId,
user_id: userId,
schema_version: 1,
ordinal: 1,
idempotency_key: 'generation-idempotency-1',
request_hash: 'b'.repeat(64),
status: 'queued',
confirmed_fact_pack: generationInput().confirmedFactPack,
creative_input: generationInput().creativeInput,
video_config: generationInput().videoConfig,
strategy_set: null,
is_free: true,
credits: 0,
credit_status: 'none',
main_credits_used: 0,
addon_credits_used: 0,
failure_reason: null,
attempt_count: 0,
claim_token: null,
content_purged_at: null,
started_at: null,
completed_at: null,
expires_at: future,
created_at: now,
updated_at: now,
...overrides,
};
}
function optionRow(slot: typeof TK_SLOTS[number], overrides: Record<string, unknown> = {}) {
return {
id: optionIds[slot],
generation_id: generationId,
user_id: userId,
slot,
status: 'queued',
strategy_blueprint: strategySet().find(strategy => strategy.slot === slot),
plan: null,
failure_reason: null,
attempt_count: 0,
claim_token: null,
content_purged_at: null,
started_at: null,
completed_at: null,
created_at: now,
updated_at: now,
...overrides,
};
}
function evidenceProfile() {
return {
schemaVersion: 1 as const,
productIdentity: { name: 'Pet feeder', category: 'Pet supplies', confidence: 'high' as const },
visibleFacts: [{
factId: 'visible-001',
text: 'White body is visible',
sourceImageAssetIds: [imageId],
confidence: 'high' as const,
}],
inferredContexts: [],
demonstrableActions: [{
actionId: 'action-001',
text: 'show the product',
supportingFactIds: ['visible-001'],
sourceImageAssetIds: [imageId],
}],
availableMaterials: ['product image'],
forbiddenClaims: ['unverified performance'],
};
}
function strategySet() {
const base = {
labelZh: '最稳',
objective: 'conversion' as const,
styleForm: 'product' as const,
style: 'unboxing' as const,
hookPatternId: 'sound_hook' as const,
firstFrameId: 'sound_sync' as const,
firstFrame: '首帧展示商品动作',
openingAction: '手部立即操作商品',
openingLine: 'See this?',
relevanceBeat: '展示真实使用问题',
productBridge: '动作连接商品结构',
buyingMotivationId: 'problem_relief' as const,
buyingMotivation: '减少日常不便',
proofMethodId: 'visible_operation' as const,
proofMethod: '连续操作证明',
ctaStrategy: '查看商品详情',
requiredMaterials: ['product'],
allowedFactIds: ['visible-001'],
};
return [
{ ...base, slot: 'safe' as const, videoMode: 'product_closeup' as const },
{
...base,
slot: 'native' as const,
labelZh: '最原生',
objective: 'trust' as const,
styleForm: 'scene' as const,
style: 'scene' as const,
videoMode: 'scene_demo' as const,
hookPatternId: 'identity' as const,
firstFrameId: 'pov_entry' as const,
buyingMotivationId: 'identity_fit' as const,
proofMethodId: 'context_demo' as const,
},
{
...base,
slot: 'creative' as const,
labelZh: '最抓眼',
objective: 'attention' as const,
styleForm: 'drama' as const,
style: 'product_pov' as const,
videoMode: 'product_closeup' as const,
hookPatternId: 'unexpected' as const,
firstFrameId: 'structure_reveal' as const,
buyingMotivationId: 'curiosity' as const,
proofMethodId: 'structure_closeup' as const,
},
];
}
function plan() {
return {
schemaVersion: 1 as const,
title: 'Product proof',
whyItFits: 'Visible action supports the idea',
audioMode: 'voiceover' as const,
executionPrompt: 'Show @图片1 in one continuous action.',
beats: [{
startSeconds: 0,
endSeconds: 10,
environment: 'home counter',
subject: '@图片1 product',
action: 'operate the visible structure',
shotSize: 'close-up',
cameraMovement: 'slow push-in',
lighting: 'natural indoor light',
editingCue: 'cut on action',
soundDesign: 'real handling sound',
voiceoverTarget: 'See how this works.',
voiceoverZh: '看看它如何工作。',
factIds: ['visible-001'],
}],
factsUsed: ['visible-001'],
assumptions: [],
requiredMaterials: ['product'],
warnings: [],
};
}