vicky469_aside/tests/commentSyncPolicy.test.ts
vicky469 6b9939cef1 fix(comments): preserve agent and trash state
Keep annotation requests anchored, retry agent runs after note renames, and persist soft-deleted threads so trash mode can recover them.
2026-07-05 19:28:41 +08:00

138 lines
4.8 KiB
TypeScript

import * as assert from "node:assert/strict";
import test from "node:test";
import { CommentManager, commentToThread, threadToComment, type Comment, type CommentThread } from "../src/commentManager";
import {
chooseCommentStateForOpenEditor,
shouldDeferManagedCommentPersist,
syncLoadedCommentsForCurrentNote,
} from "../src/core/rules/commentSyncPolicy";
function createComment(overrides: Partial<Comment> = {}): Comment {
return {
id: "comment-1",
filePath: "note.md",
startLine: 3,
startChar: 4,
endLine: 3,
endChar: 9,
selectedText: "alpha",
selectedTextHash: "hash-alpha",
comment: "hello",
timestamp: 1710000000000,
...overrides,
};
}
test("defer managed comment persistence while the markdown editor is focused", () => {
assert.equal(
shouldDeferManagedCommentPersist({
isEditorFocused: true,
fileContent: "Body\n",
rewrittenContent: "Body\n\nUpdated sidecar-derived state\n",
}),
true,
);
});
test("allow managed comment persistence immediately when the editor is not focused", () => {
assert.equal(
shouldDeferManagedCommentPersist({
isEditorFocused: false,
fileContent: "Body\n",
rewrittenContent: "Body\n\nUpdated sidecar-derived state\n",
}),
false,
);
});
test("prefer in-memory comments over parsed on-disk comments for the open editor", () => {
const parsedComments = [createComment({ startLine: 1, startChar: 0, endLine: 1, endChar: 5 })];
const liveComments = [createComment({ startLine: 8, startChar: 2, endLine: 8, endChar: 7 })];
const chosen = chooseCommentStateForOpenEditor(liveComments, parsedComments);
assert.deepEqual(chosen, liveComments);
});
test("fall back to parsed comments when no live in-memory comments exist yet", () => {
const parsedComments = [createComment({ startLine: 1, startChar: 0, endLine: 1, endChar: 5 })];
const chosen = chooseCommentStateForOpenEditor([], parsedComments);
assert.deepEqual(chosen, parsedComments);
});
test("syncLoadedCommentsForCurrentNote re-resolves stale parsed coordinates before returning them", async () => {
const manager = new CommentManager([]);
let indexedFilePath = "";
let indexedThreads: CommentThread[] = [];
const syncedState = await syncLoadedCommentsForCurrentNote(
"note.md",
"preamble\nAlpha target omega\n",
[commentToThread(createComment({
startLine: 0,
startChar: 6,
endLine: 0,
endChar: 12,
selectedText: "target",
selectedTextHash: "hash-target",
}))],
manager,
{
updateFile(filePath, items) {
indexedFilePath = filePath;
const threads = items as CommentThread[];
indexedThreads = threads.map((thread) => ({
...thread,
entries: thread.entries.map((entry) => ({ ...entry })),
}));
},
},
);
assert.equal(syncedState.threads.length, 1);
assert.equal(syncedState.comments.length, 1);
assert.equal(syncedState.comments[0].startLine, 1);
assert.equal(syncedState.comments[0].startChar, 6);
assert.equal(syncedState.comments[0].endLine, 1);
assert.equal(syncedState.comments[0].endChar, 12);
const managerComments = manager.getCommentsForFile("note.md");
assert.equal(managerComments.length, 1);
assert.equal(managerComments[0].startLine, 1);
assert.equal(indexedFilePath, "note.md");
assert.equal(indexedThreads.length, 1);
assert.equal(threadToComment(indexedThreads[0]).startLine, 1);
});
test("syncLoadedCommentsForCurrentNote preserves soft-deleted threads for sidecar persistence", async () => {
const manager = new CommentManager([]);
let indexedThreads: CommentThread[] = [];
const deletedAt = Date.now();
const syncedState = await syncLoadedCommentsForCurrentNote(
"note.md",
"Alpha target omega\n",
[commentToThread(createComment({
selectedText: "target",
selectedTextHash: "hash-target",
deletedAt,
}))],
manager,
{
updateFile(_filePath, items) {
indexedThreads = (items as CommentThread[]).map((thread) => ({
...thread,
entries: thread.entries.map((entry) => ({ ...entry })),
}));
},
},
);
assert.equal(syncedState.threads.length, 1);
assert.equal(syncedState.threads[0].deletedAt, deletedAt);
assert.equal(manager.getThreadsForFile("note.md", { includeDeleted: true })[0]?.deletedAt, deletedAt);
assert.equal(indexedThreads[0]?.deletedAt, deletedAt);
});