fix: skip no-op scene revision bumps

This commit is contained in:
2026-05-31 12:35:57 +00:00
parent d7a0a5d076
commit f349617427
4 changed files with 133 additions and 14 deletions
+40
View File
@@ -84,6 +84,46 @@ describe("api", () => {
cleanup();
});
test("does not increment revisions for equivalent normalized scenes", async () => {
const { api, cleanup } = withApi();
const created = await api.createDrawing().json();
await api.updateDrawing(
Object.assign(
new Request(`http://local/api/drawings/${created.id}`, {
method: "PUT",
body: JSON.stringify({
elements: [{ id: "shape" }],
appState: { gridSize: 20 },
files: {},
expectedRevision: 0,
}),
}),
{ params: { id: created.id } },
),
);
const response = await api.updateDrawing(
Object.assign(
new Request(`http://local/api/drawings/${created.id}`, {
method: "PUT",
body: JSON.stringify({
elements: [{ id: "shape" }],
appState: { gridSize: 20, selectedElementIds: { shape: true } },
files: {},
expectedRevision: 0,
}),
}),
{ params: { id: created.id } },
),
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.drawing.revision).toBe(1);
cleanup();
});
test("updates scene and title with a matching expected revision", async () => {
const { api, cleanup } = withApi();
const created = await api.createDrawing().json();
+53
View File
@@ -82,4 +82,57 @@ describe("drawing store", () => {
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "first" }]);
});
test("keeps the revision unchanged when the scene is already stored", () => {
const store = createTempStore();
const created = store.createDrawing();
const scene = {
elements: [{ id: "same" }],
appState: {},
files: {},
};
const first = store.updateDrawing(created.id, { scene });
expect(first.ok ? first.drawing.revision : null).toBe(1);
const repeated = store.updateDrawing(created.id, { scene });
expect(repeated.ok).toBe(true);
expect(repeated.ok ? repeated.drawing.revision : null).toBe(1);
expect(store.getDrawing(created.id)?.revision).toBe(1);
});
test("accepts stale expected revisions when the scene is already stored", () => {
const store = createTempStore();
const created = store.createDrawing();
const scene = {
elements: [{ id: "same" }],
appState: {},
files: {},
};
const first = store.updateDrawing(created.id, { expectedRevision: 0, scene });
expect(first.ok ? first.drawing.revision : null).toBe(1);
const repeated = store.updateDrawing(created.id, { expectedRevision: 0, scene });
expect(repeated.ok).toBe(true);
expect(repeated.ok ? repeated.drawing.revision : null).toBe(1);
expect(store.getDrawing(created.id)?.scene.elements).toEqual([{ id: "same" }]);
});
test("increments the revision when the title changes but the scene does not", () => {
const store = createTempStore();
const created = store.createDrawing();
const scene = {
elements: [{ id: "same" }],
appState: {},
files: {},
};
const first = store.updateDrawing(created.id, { expectedRevision: 0, scene });
expect(first.ok ? first.drawing.revision : null).toBe(1);
const renamed = store.updateDrawing(created.id, { expectedRevision: 1, title: "Renamed", scene });
expect(renamed.ok).toBe(true);
expect(renamed.ok ? renamed.drawing.title : null).toBe("Renamed");
expect(renamed.ok ? renamed.drawing.revision : null).toBe(2);
});
});
+28 -14
View File
@@ -190,32 +190,46 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
): DrawingUpdateResult {
const hasScene = update.scene !== undefined;
const hasTitle = update.title !== undefined;
const existingRow = getQuery.get(id) as DrawingRow | null | undefined;
if (!existingRow) {
return { ok: false, reason: "not_found" };
}
const existing = readRow(existingRow)!;
if (!hasScene && !hasTitle) {
const existing = getDrawing(id);
return existing ? { ok: true, drawing: existing } : { ok: false, reason: "not_found" };
return { ok: true, drawing: existing };
}
const nextTitle = hasTitle ? normalizeTitle(update.title) : existingRow.title;
const nextScene = hasScene ? JSON.stringify(update.scene) : existingRow.data;
const titleChanged = nextTitle !== existingRow.title;
const sceneChanged = nextScene !== existingRow.data;
if (!titleChanged && !sceneChanged) {
return { ok: true, drawing: existing };
}
if (update.expectedRevision !== undefined && update.expectedRevision !== existingRow.revision) {
return { ok: false, reason: "conflict", drawing: existing };
}
let changes: number;
if (hasScene && hasTitle) {
const title = normalizeTitle(update.title);
const scene = JSON.stringify(update.scene);
if (sceneChanged && titleChanged) {
changes =
update.expectedRevision === undefined
? Number(updateBothQuery.run(id, title, scene).changes)
: Number(updateBothExpectedQuery.run(id, title, scene, update.expectedRevision).changes);
} else if (hasScene) {
const scene = JSON.stringify(update.scene);
? Number(updateBothQuery.run(id, nextTitle, nextScene).changes)
: Number(updateBothExpectedQuery.run(id, nextTitle, nextScene, update.expectedRevision).changes);
} else if (sceneChanged) {
changes =
update.expectedRevision === undefined
? Number(updateSceneQuery.run(id, scene).changes)
: Number(updateSceneExpectedQuery.run(id, scene, update.expectedRevision).changes);
? Number(updateSceneQuery.run(id, nextScene).changes)
: Number(updateSceneExpectedQuery.run(id, nextScene, update.expectedRevision).changes);
} else {
const title = normalizeTitle(update.title);
changes =
update.expectedRevision === undefined
? Number(updateTitleQuery.run(id, title).changes)
: Number(updateTitleExpectedQuery.run(id, title, update.expectedRevision).changes);
? Number(updateTitleQuery.run(id, nextTitle).changes)
: Number(updateTitleExpectedQuery.run(id, nextTitle, update.expectedRevision).changes);
}
const drawing = getDrawing(id);
+12
View File
@@ -95,6 +95,18 @@ describe("mcp tool handlers", () => {
expect(store.getDrawing(result.id)?.scene).toEqual(nextScene);
});
test("replace_drawing keeps the revision when the scene is already stored", () => {
const { store, tools } = createTempTools();
const scene = testScene();
const result = tools.create_drawing({ title: "replace noop", scene });
const replaced = tools.replace_drawing({ id: result.id, scene });
expect(replaced.revision).toBe(1);
expect(store.getDrawing(result.id)?.revision).toBe(1);
expect(store.getDrawing(result.id)?.scene).toEqual(scene);
});
test("patch_drawing replaces an element property and removes an element", () => {
const { store, tools } = createTempTools();
const result = tools.create_drawing({ title: "patch", scene: testScene() });