import { describe, expect, test } from "bun:test"; import { canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references"; import type { ProjectAsset, ProjectDetail } from "@/services/api/projects"; import { buildShotAssetReferenceContext, ensureShotAssetMentionPrompt, resolveShotAssetMentionPrompt } from "@/pages/projects/detail/workflow-shot-references"; describe("workflow shot asset references", () => { test("builds ordered image references for bound media and characters", () => { const context = buildShotAssetReferenceContext(detail(), "shot-1"); expect(context.mentionReferences.map((reference) => reference.label)).toEqual(["雨夜街道", "林默"]); expect(context.referenceImages.map((image) => image.id)).toEqual(["asset-scene", "asset-character"]); expect(context.referenceImages[1]?.storageKey).toBe("resource:character-cover"); }); test("resolves asset tokens to the submitted reference image order", () => { const context = buildShotAssetReferenceContext(detail(), "shot-1"); const [scene, character] = context.mentionReferences; const prompt = `让 ${canvasResourceMentionToken(character)} 从 ${canvasResourceMentionToken(scene)} 走近镜头`; expect(resolveShotAssetMentionPrompt(prompt, context)).toContain("让 图片2 从 图片1 走近镜头"); }); test("rejects stale mentions after an asset is unbound", () => { const context = buildShotAssetReferenceContext(detail(), "shot-1"); expect(() => resolveShotAssetMentionPrompt("跟随 @[asset:missing]", context)).toThrow("未绑定到当前镜头"); }); test("adds bound assets as editable mention tokens without duplicating existing mentions", () => { const context = buildShotAssetReferenceContext(detail(), "shot-1"); const prompt = ensureShotAssetMentionPrompt("让 @[asset:asset-character] 走近镜头", context.mentionReferences); expect(prompt).toContain("@[asset:asset-character]"); expect(prompt.match(/@\[asset:asset-character\]/g)).toHaveLength(1); expect(prompt).toContain("【场景与道具参考】\n雨夜街道:@[asset:asset-scene]"); }); }); function detail(): ProjectDetail { const scene = { id: "asset-scene", title: "雨夜街道", mediaType: "image", category: "environment", status: "confirmed", primaryVersionId: "version-scene", versionCount: 1, usages: [], position: 0, storageKey: "resource:scene-image", updatedAt: "2026-08-29T00:00:00Z", } satisfies ProjectAsset; const character = { id: "asset-character", title: "林默", mediaType: "entity", category: "character", status: "confirmed", primaryVersionId: "version-character", versionCount: 1, usages: [], position: 1, updatedAt: "2026-08-29T00:00:00Z", character: { versionId: "version-character", version: 1, definition: {}, representations: [{ id: "representation-1", resourceId: "character-cover", mediaType: "image", role: "primary" }], visualStatus: "ready", voiceStatus: "missing", }, } satisfies ProjectAsset; const audio = { id: "asset-audio", title: "雨声", mediaType: "audio", category: "other", status: "confirmed", primaryVersionId: "version-audio", versionCount: 1, usages: [], position: 2, storageKey: "resource:rain-audio", updatedAt: "2026-08-29T00:00:00Z", } satisfies ProjectAsset; return { assets: [scene, character, audio], shotReferences: [ { id: "reference-scene", shotId: "shot-1", assetVersionId: "version-scene", role: "reference", status: "linked", createdAt: "2026-08-29T00:00:00Z" }, { id: "reference-character", shotId: "shot-1", assetVersionId: "version-character", role: "reference", status: "linked", createdAt: "2026-08-29T00:00:01Z" }, { id: "reference-audio", shotId: "shot-1", assetVersionId: "version-audio", role: "reference", status: "linked", createdAt: "2026-08-29T00:00:02Z" }, { id: "reference-other-shot", shotId: "shot-2", assetVersionId: "version-scene", role: "reference", status: "linked", createdAt: "2026-08-29T00:00:03Z" }, ], } as ProjectDetail; }