import { expect, test } from "@playwright/test"; import { completedDetailReplacementCardIds, detailReplacementCardMessageId, detailReplacementSubmissionKey, isDetailReplacementBatchRequest, isDetailReplacementUuid, type DetailReplacementBatchResult, } from "@/lib/agents/detail-replacement-batch"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; test("recognizes only durable detail replacement batches", () => { expect(isDetailReplacementBatchRequest({ taskKind: "agent-detail-replacement-batch" })).toBe(true); expect(isDetailReplacementBatchRequest({ taskKind: "style-pro" })).toBe(false); expect(isDetailReplacementBatchRequest(null)).toBe(false); }); test("card message identity stays stable across retries and page reloads", () => { const id = detailReplacementCardMessageId("batch-a", "card-01"); expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); expect(detailReplacementCardMessageId("batch-a", "card-01")).toBe(id); expect(detailReplacementCardMessageId("batch-a", "card-02")).not.toBe(id); }); test("restart skips successful cards but retries failed ones", () => { const result: DetailReplacementBatchResult = { taskKind: "agent-detail-replacement-batch", currentCardId: null, cards: [ { cardId: "one", status: "completed", imageUrl: "https://example.com/one.png", thumbnailUrl: "https://example.com/one.png", text: "" }, { cardId: "two", status: "fallback", imageUrl: "https://example.com/two.png", thumbnailUrl: "https://example.com/two.png", text: "生成失败" }, { cardId: "three", status: "passthrough", imageUrl: "https://example.com/three.png", thumbnailUrl: "https://example.com/three.png", text: "" }, ], }; expect([...completedDetailReplacementCardIds(result)]).toEqual(["one", "three"]); }); const submission = { projectId: "11111111-1111-1111-1111-111111111111", model: "gpt-image-2.5", prompt: "保持版式,替换产品主体", userPrompt: "详情替换", imageSize: "2K", replacementStrategy: "scene_replace", productImageUrls: ["https://example.com/p2.png", "https://example.com/p1.png"], modelImageUrl: null, references: [ { id: "card-01", label: "首屏", imageUrl: "https://example.com/a.png" }, { id: "card-02", label: "第二屏", imageUrl: "https://example.com/b.png" }, ], }; test("an identical double click resolves to the same submission key", () => { expect(detailReplacementSubmissionKey(submission)).toBe( detailReplacementSubmissionKey({ ...submission, productImageUrls: [...submission.productImageUrls].reverse(), references: [...submission.references].reverse(), }), ); }); test("re-running a single card while a batch runs is not treated as a duplicate", () => { const full = detailReplacementSubmissionKey(submission); expect(detailReplacementSubmissionKey({ ...submission, references: [submission.references[0]!], })).not.toBe(full); expect(detailReplacementSubmissionKey({ ...submission, prompt: "换一个背景", })).not.toBe(full); }); test("accepts the canonical 8-4-4-4-12 UUIDs the API receives", () => { // Real project ids taken from production requests that returned 400 in the field. expect(isDetailReplacementUuid("b1fa322a-dc36-49a0-9d07-8f06ffdab680")).toBe(true); expect(isDetailReplacementUuid("15f4bdc7-2487-4926-bf38-87df559aea6a")).toBe(true); // Browser generated request ids look like this. expect(isDetailReplacementUuid("3f2504e0-4f89-41d3-9a0c-0305e82c3301")).toBe(true); // The scoped chat id the route derives for a batch project is a UUID too. expect(isDetailReplacementUuid("b1fa322a-dc36-49a0-9d07-a93e17a60001")).toBe(true); }); test("rejects malformed ids, including the 8-4-4-12 shape that broke polling", () => { expect(isDetailReplacementUuid("")).toBe(false); expect(isDetailReplacementUuid(null)).toBe(false); expect(isDetailReplacementUuid(undefined)).toBe(false); // Missing the fourth group: this is exactly the bug that made every request 400. expect(isDetailReplacementUuid("b1fa322a-dc36-49a0-8f06ffdab680")).toBe(false); expect(isDetailReplacementUuid("b1fa322adc3649a09d078f06ffdab680")).toBe(false); expect(isDetailReplacementUuid("b1fa322a-dc36-49a0-9d07-8f06ffdab680-extra")).toBe(false); }); test("the batch route keeps no stale UUID regex of its own", () => { const sources = [ "app/api/agents/detail-replacement/batches/route.ts", "lib/agents/detail-replacement-batch-runner.ts", ].map((file) => readFileSync(resolve(process.cwd(), file), "utf8")); for (const source of sources) { // A local 8-4-4-12 literal is the typo this feature shipped with. expect(source).not.toMatch(/\^\[0-9a-f\]\{8\}-\[0-9a-f\]\{4\}-\[0-9a-f\]\{4\}-\[0-9a-f\]\{12\}\$/); } });