新增的批次接口把一个 UUID 正则写成了 8-4-4-12(少一组 -{4}),标准 UUID 是
8-4-4-4-12。结果是任何真实项目编号和请求编号都过不了校验:查询接口恒返 400
「项目编号无效」,提交接口恒返 400「任务编号或项目编号无效」,详情替换完全无法使用。
- 校验抽成 isDetailReplacementUuid 并修正为规范的 8-4-4-4-12,路由不再自带正则
- 回归测试覆盖真实线上项目编号、浏览器生成的请求编号,以及导致故障的 8-4-4-12 形状
- 增加源码守卫,防止该路由再次出现自带的错误 UUID 正则
- 前端在项目尚未保存时给出明确提示,而不是把空编号丢给接口
106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
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\}\$/);
|
|
}
|
|
});
|