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

115 lines
3.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
poolQuery: vi.fn(),
assets: vi.fn(),
signedUrls: vi.fn(),
objectSize: vi.fn(),
}));
vi.mock('../src/db/pool.js', () => ({
pool: { query: mocks.poolQuery },
withTransaction: vi.fn(),
}));
vi.mock('../src/domain/assetReferences.js', () => ({
readOwnedAssetReferencesByIds: mocks.assets,
}));
vi.mock('../src/domain/assetReadUrlSigner.js', () => ({
signOwnedObjectReadUrls: mocks.signedUrls,
}));
vi.mock('../src/storage/storageService.js', () => ({
getStoredObjectSize: mocks.objectSize,
}));
import { resolveTkPromptEngineImages } from '../src/domain/tkPromptEngineService.js';
const userId = '00000000-0000-4000-8000-000000000901';
const assetId = '00000000-0000-4000-8000-000000000902';
describe('TK Prompt Engine 商品图 MIME 解析', () => {
beforeEach(() => {
mocks.poolQuery.mockReset();
mocks.assets.mockReset();
mocks.signedUrls.mockReset();
mocks.objectSize.mockReset();
const asset = imageAsset('users/user/uploads/session/product');
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
mocks.signedUrls.mockResolvedValue(new Map([[
asset.object_key,
'https://assets.example.com/product?signature=redacted',
]]));
mocks.objectSize.mockResolvedValue(2_048);
});
it('无后缀商品图必须由完成上传会话与 ready 缩略图链提供 JPEG 类型', async () => {
mocks.poolQuery.mockResolvedValue({
rows: [{ asset_id: assetId, content_type: 'image/jpeg' }],
rowCount: 1,
});
await expect(resolveTkPromptEngineImages(userId, [assetId])).resolves.toEqual([
expect.objectContaining({ assetId, mimeType: 'image/jpeg', sizeBytes: 2_048 }),
]);
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).toContain(
"session.status = 'completed'",
);
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).toContain(
"asset.thumbnail_status = 'ready'",
);
expect(mocks.poolQuery.mock.calls[0]?.[1]).toEqual([userId, [assetId]]);
});
it('完成会话记录伪造类型时即使对象键有图片后缀也拒绝', async () => {
const asset = imageAsset('users/user/uploads/session/product.jpg');
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
mocks.signedUrls.mockResolvedValue(new Map([[
asset.object_key,
'https://assets.example.com/product.jpg?signature=redacted',
]]));
mocks.poolQuery.mockResolvedValue({
rows: [{ asset_id: assetId, content_type: 'text/plain' }],
rowCount: 1,
});
await expect(resolveTkPromptEngineImages(userId, [assetId])).rejects.toMatchObject({
code: 'TK_PROMPT_IMAGE_MIME_INVALID',
});
});
it('没有完成会话时仅允许对象键的安全后缀回退,无后缀保持关闭', async () => {
mocks.poolQuery.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(resolveTkPromptEngineImages(userId, [assetId])).rejects.toMatchObject({
code: 'TK_PROMPT_IMAGE_MIME_INVALID',
});
const asset = imageAsset('users/user/historical/product.png');
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
mocks.signedUrls.mockResolvedValue(new Map([[
asset.object_key,
'https://assets.example.com/product.png?signature=redacted',
]]));
await expect(resolveTkPromptEngineImages(userId, [assetId])).resolves.toEqual([
expect.objectContaining({ mimeType: 'image/png' }),
]);
});
});
function imageAsset(objectKey: string) {
return {
id: assetId,
user_id: userId,
name: '商品图片',
object_key: objectKey,
thumbnail_key: 'users/user/upload-thumbnails/session.webp',
thumbnail_status: 'ready',
type: 'image',
duration_seconds: null,
size_bytes: 2_048,
width: 1_080,
height: 1_080,
};
}