test: cover stale canvas autosave conflicts

This commit is contained in:
134 2026-06-23 13:21:31 +08:00
parent 084a6225e7
commit 7f3ce72468
3 changed files with 72 additions and 20 deletions

View File

@ -4,6 +4,10 @@ import { db } from '@/lib/db';
import { canvasProject } from '@/lib/db/schema';
import { syncCanvasNodesForProject } from '@/lib/db/queries';
import { eq, and } from 'drizzle-orm';
import {
buildStaleCanvasSaveConflictPayload,
isStaleCanvasSaveBaseRevision,
} from '@/lib/canvas/canvas-save-conflict';
function resolveWorkspaceType(input: unknown) {
return input === "agent" || input === "video" ? input : "canvas";
@ -13,19 +17,6 @@ function getDefaultWorkspaceTitle(workspaceType: ReturnType<typeof resolveWorksp
return workspaceType === "video" ? "未命名视频画布" : "未命名画布";
}
function parseRevisionDate(value: unknown) {
const raw = String(value || "").trim();
if (!raw) return null;
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function isStaleBaseRevision(baseRevision: unknown, currentUpdatedAt: Date) {
const baseDate = parseRevisionDate(baseRevision);
if (!baseDate) return false;
return baseDate.getTime() + 1000 < currentUpdatedAt.getTime();
}
// 获取单个画布
export async function GET(
request: NextRequest,
@ -95,15 +86,14 @@ export async function PUT(
if (
canvasData !== undefined &&
baseCanvasRevision &&
isStaleBaseRevision(baseCanvasRevision, existingProject.updatedAt)
isStaleCanvasSaveBaseRevision({
baseCanvasRevision: body?.baseCanvasRevision,
baseCanvasUpdatedAt: body?.baseCanvasUpdatedAt,
currentUpdatedAt: existingProject.updatedAt,
})
) {
return NextResponse.json(
{
success: false,
error: "画布已更新,请刷新后再保存",
code: "STALE_CANVAS_REVISION",
canvasUpdatedAt: existingProject.updatedAt.toISOString(),
},
buildStaleCanvasSaveConflictPayload(existingProject.updatedAt),
{ status: 409 }
);
}

View File

@ -0,0 +1,29 @@
export const STALE_CANVAS_REVISION_CODE = "STALE_CANVAS_REVISION";
export function parseCanvasRevisionDate(value: unknown) {
const raw = String(value || "").trim();
if (!raw) return null;
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
export function isStaleCanvasSaveBaseRevision(params: {
baseCanvasRevision?: unknown;
baseCanvasUpdatedAt?: unknown;
currentUpdatedAt: Date;
}) {
const baseDate = parseCanvasRevisionDate(
params.baseCanvasUpdatedAt || params.baseCanvasRevision
);
if (!baseDate) return false;
return baseDate.getTime() + 1000 < params.currentUpdatedAt.getTime();
}
export function buildStaleCanvasSaveConflictPayload(currentUpdatedAt: Date) {
return {
success: false,
error: "画布已更新,请刷新后再保存",
code: STALE_CANVAS_REVISION_CODE,
canvasUpdatedAt: currentUpdatedAt.toISOString(),
};
}

View File

@ -64,6 +64,11 @@ import {
getCanvasAgentOperationErrorMessage,
normalizeCanvasAgentOperationErrorCode,
} from "@/lib/canvas/agent-operation-errors";
import {
buildStaleCanvasSaveConflictPayload,
isStaleCanvasSaveBaseRevision,
parseCanvasRevisionDate,
} from "@/lib/canvas/canvas-save-conflict";
import {
applyCanvasOperationPlanToCanvasData,
CanvasAgentMutationError,
@ -125,6 +130,34 @@ import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
import type { ChatMessage } from "@/lib/types";
test.describe("canvas chat pure helpers", () => {
test("rejects stale autosave bases without blocking fresh or missing revisions", () => {
const currentUpdatedAt = new Date("2026-06-22T00:00:05.500Z");
expect(parseCanvasRevisionDate("not-a-date")).toBeNull();
expect(
isStaleCanvasSaveBaseRevision({
baseCanvasRevision: "2026-06-22T00:00:00.000Z",
currentUpdatedAt,
})
).toBe(true);
expect(
isStaleCanvasSaveBaseRevision({
baseCanvasUpdatedAt: "2026-06-22T00:00:05.000Z",
currentUpdatedAt,
})
).toBe(false);
expect(
isStaleCanvasSaveBaseRevision({
currentUpdatedAt,
})
).toBe(false);
expect(buildStaleCanvasSaveConflictPayload(currentUpdatedAt)).toMatchObject({
success: false,
code: "STALE_CANVAS_REVISION",
canvasUpdatedAt: "2026-06-22T00:00:05.500Z",
});
});
const plannerContext: CanvasAgentContext = {
version: 1,
source: "canvas",