vicky469_aside/tests/commentManager.idTargeting.test.ts
vicky469 e94254ee2d feat(publish): add local Pages publishing
Add experimental Cloudflare Pages publishing through local Wrangler, pane-header publish actions, public HTML preview support, artifact safety checks, and state cleanup for rename/delete/unpublish flows.
2026-07-09 15:51:02 +08:00

339 lines
12 KiB
TypeScript

import * as assert from "node:assert/strict";
import test from "node:test";
import { Comment, CommentManager, commentToThread } from "../src/commentManager";
function createComment(id: string, timestamp: number, text: string): Comment {
return {
id,
filePath: "note.md",
startLine: 0,
startChar: 0,
endLine: 0,
endChar: 5,
selectedText: "hello",
selectedTextHash: "hash",
comment: text,
timestamp,
};
}
test("CommentManager edits/deletes by id under timestamp collision", () => {
const sameTimestamp = Date.now();
const first = createComment("id-1", sameTimestamp, "first");
const second = createComment("id-2", sameTimestamp, "second");
const manager = new CommentManager([first, second]);
manager.editComment("id-2", "second-updated");
const comments = manager.getCommentsForFile("note.md");
assert.equal(comments.find((comment) => comment.id === "id-1")?.comment, "first");
assert.equal(comments.find((comment) => comment.id === "id-2")?.comment, "second-updated");
manager.deleteComment("id-1", sameTimestamp + 5);
const remaining = manager.getCommentsForFile("note.md");
assert.equal(remaining.length, 1);
assert.equal(remaining[0].id, "id-2");
assert.equal(manager.getCommentById("id-1")?.deletedAt, sameTimestamp + 5);
});
test("CommentManager targets child thread entries by id", () => {
const baseTimestamp = Date.now();
const thread = commentToThread(createComment("thread-1", baseTimestamp, "parent"));
thread.entries.push({
id: "entry-2",
body: "child",
timestamp: baseTimestamp + 1000,
});
const manager = new CommentManager([thread]);
assert.equal(manager.getCommentById("thread-1")?.comment, "parent");
assert.equal(manager.getCommentById("entry-2")?.comment, "child");
manager.editComment("entry-2", "child-updated");
assert.equal(manager.getCommentById("entry-2")?.comment, "child-updated");
manager.appendEntry("entry-2", {
id: "entry-3",
body: "grandchild",
timestamp: baseTimestamp + 2000,
});
assert.equal(manager.getCommentById("entry-3")?.comment, "grandchild");
manager.deleteComment("entry-2", baseTimestamp + 3000);
assert.equal(manager.getCommentById("entry-2")?.deletedAt, baseTimestamp + 3000);
assert.equal(manager.getThreadById("entry-3")?.entries.length, 3);
assert.deepEqual(
manager.getThreadsForFile("note.md")[0]?.entries.map((entry) => entry.id),
["thread-1", "entry-3"],
);
});
test("CommentManager can permanently clear soft-deleted comments for one file", () => {
const baseTimestamp = Date.now();
const activeThread = commentToThread(createComment("thread-1", baseTimestamp, "parent"));
activeThread.entries.push({
id: "entry-2",
body: "deleted child",
timestamp: baseTimestamp + 1000,
deletedAt: baseTimestamp + 2000,
});
const deletedThread = {
...commentToThread(createComment("thread-2", baseTimestamp + 3000, "deleted thread")),
deletedAt: baseTimestamp + 4000,
};
const otherFileThread = {
...commentToThread(createComment("thread-3", baseTimestamp + 5000, "other file")),
filePath: "other.md",
};
const manager = new CommentManager([activeThread, deletedThread, otherFileThread]);
assert.equal(manager.clearDeletedCommentsForFile("note.md", baseTimestamp + 6000), true);
assert.deepEqual(
manager.getThreadsForFile("note.md", { includeDeleted: true }).map((thread) => ({
id: thread.id,
entryIds: thread.entries.map((entry) => entry.id),
})),
[{
id: "thread-1",
entryIds: ["thread-1"],
}],
);
assert.equal(manager.getThreadById("thread-2"), undefined);
assert.equal(manager.getThreadById("thread-3")?.filePath, "other.md");
assert.equal(manager.clearDeletedCommentsForFile("note.md", baseTimestamp + 7000), false);
});
test("CommentManager reorders root threads within the same file", () => {
const first = createComment("thread-1", 1710000000000, "first");
const second = createComment("thread-2", 1710000001000, "second");
const third = createComment("thread-3", 1710000002000, "third");
const manager = new CommentManager([first, second, third]);
assert.equal(manager.reorderThreadsForFile("note.md", "thread-3", "thread-1", "before"), true);
assert.deepEqual(
manager.getThreadsForFile("note.md").map((thread) => thread.id),
["thread-3", "thread-1", "thread-2"],
);
assert.equal(manager.reorderThreadsForFile("note.md", "thread-3", "thread-2", "after"), true);
assert.deepEqual(
manager.getThreadsForFile("note.md").map((thread) => thread.id),
["thread-1", "thread-2", "thread-3"],
);
assert.equal(manager.reorderThreadsForFile("note.md", "thread-3", "thread-3", "before"), false);
});
test("CommentManager can read threads across paired public markdown and html paths", () => {
const manager = new CommentManager([
{
...createComment("markdown-thread", 1710000000000, "markdown"),
filePath: "public/page.md",
},
{
...createComment("html-thread", 1710000001000, "html"),
filePath: "public/page.html",
},
{
...createComment("other-thread", 1710000002000, "other"),
filePath: "public/other.md",
},
]);
assert.deepEqual(
manager.getThreadsForFiles(["public/page.md", "public/page.html"]).map((thread) => thread.id),
["markdown-thread", "html-thread"],
);
});
test("CommentManager reorders child entries only within their parent thread", () => {
const thread = commentToThread(createComment("thread-1", 1710000000000, "parent"));
thread.entries.push({
id: "entry-2",
body: "second",
timestamp: 1710000001000,
});
thread.entries.push({
id: "entry-3",
body: "third",
timestamp: 1710000002000,
});
thread.entries.push({
id: "entry-4",
body: "fourth",
timestamp: 1710000003000,
});
const manager = new CommentManager([thread]);
assert.equal(manager.reorderThreadEntries("thread-1", "entry-4", "entry-2", "before"), true);
assert.deepEqual(
manager.getThreadById("thread-1")?.entries.map((entry) => entry.id),
["thread-1", "entry-4", "entry-2", "entry-3"],
);
assert.equal(manager.reorderThreadEntries("thread-1", "entry-3", "thread-1", "after"), true);
assert.deepEqual(
manager.getThreadById("thread-1")?.entries.map((entry) => entry.id),
["thread-1", "entry-3", "entry-4", "entry-2"],
);
assert.equal(manager.reorderThreadEntries("thread-1", "thread-1", "entry-2", "before"), false);
assert.equal(manager.reorderThreadEntries("thread-1", "entry-4", "thread-1", "before"), false);
});
test("CommentManager nests a top-level anchored thread under another thread", () => {
const mainThread = commentToThread(createComment("thread-main", 1710000000000, "main"));
const pointThread = commentToThread({
...createComment("thread-point", 1710000001000, "point"),
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 13,
selectedText: "point anchor",
selectedTextHash: "hash-point",
});
pointThread.entries.push({
id: "entry-follow-up",
body: "follow up",
timestamp: 1710000002000,
});
const manager = new CommentManager([mainThread, pointThread]);
assert.equal(manager.nestThreadUnderThread("note.md", "thread-point", "thread-main"), true);
const threads = manager.getThreadsForFile("note.md");
assert.deepEqual(threads.map((thread) => thread.id), ["thread-main"]);
assert.deepEqual(
threads[0].entries.map((entry) => entry.id),
["thread-main", "thread-point", "entry-follow-up"],
);
assert.deepEqual(threads[0].entries[1]?.anchor, {
filePath: "note.md",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 13,
selectedText: "point anchor",
selectedTextHash: "hash-point",
anchorKind: "selection",
});
assert.equal(manager.getCommentById("thread-point")?.selectedText, "point anchor");
});
test("CommentManager deduplicates child entries and preserves the richest copy", () => {
const thread = commentToThread(createComment("thread-main", 1710000000000, "main"));
thread.entries.push({
id: "entry-point",
body: "",
timestamp: 1710000001000,
});
thread.entries.push({
id: "entry-point",
body: "visible body",
timestamp: 1710000002000,
anchor: {
filePath: "note.md",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 13,
selectedText: "point anchor",
selectedTextHash: "hash-point",
anchorKind: "selection",
},
});
const manager = new CommentManager([thread]);
const entries = manager.getThreadById("thread-main")?.entries ?? [];
assert.deepEqual(entries.map((entry) => entry.id), ["thread-main", "entry-point"]);
assert.equal(entries[1]?.body, "visible body");
assert.deepEqual(entries[1]?.anchor, {
filePath: "note.md",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 13,
selectedText: "point anchor",
selectedTextHash: "hash-point",
anchorKind: "selection",
});
});
test("CommentManager rejects nesting page-note threads", () => {
const mainThread = commentToThread(createComment("thread-main", 1710000000000, "main"));
const pageThread = commentToThread({
...createComment("thread-page", 1710000001000, "page"),
startLine: 0,
startChar: 0,
endLine: 0,
endChar: 0,
selectedText: "note.md",
selectedTextHash: "hash-page",
anchorKind: "page",
});
const manager = new CommentManager([mainThread, pageThread]);
assert.equal(manager.nestThreadUnderThread("note.md", "thread-page", "thread-main"), false);
assert.deepEqual(
manager.getThreadsForFile("note.md").map((thread) => thread.id),
["thread-main", "thread-page"],
);
});
test("CommentManager keeps id and file lookups current after replacement and rename", () => {
const manager = new CommentManager([
createComment("old-thread", 1710000000000, "old"),
]);
const nextThread = commentToThread(createComment("new-thread", 1710000001000, "new"));
nextThread.entries.push({
id: "new-child",
body: "child",
timestamp: 1710000002000,
});
manager.replaceThreadsForFile("note.md", [nextThread]);
assert.equal(manager.getThreadById("old-thread"), undefined);
assert.equal(manager.getCommentById("new-child")?.comment, "child");
manager.renameFile("note.md", "renamed.md");
assert.equal(manager.getThreadsForFile("note.md").length, 0);
assert.equal(manager.getThreadById("new-child")?.filePath, "renamed.md");
assert.deepEqual(
manager.getThreadsForFile("renamed.md").map((thread) => thread.id),
["new-thread"],
);
});
test("CommentManager preserves a comment when its anchor text is gone", async () => {
const manager = new CommentManager([
createComment("id-1", 1710000000000, "first"),
]);
await manager.updateCommentCoordinatesForFile("goodbye", "note.md");
const remaining = manager.getCommentsForFile("note.md");
assert.equal(remaining.length, 1);
assert.equal(remaining[0].id, "id-1");
assert.equal(remaining[0].selectedText, "hello");
assert.equal(remaining[0].orphaned, true);
});
test("CommentManager clears orphaned when the anchor text returns", async () => {
const manager = new CommentManager([
{
...createComment("id-1", 1710000000000, "first"),
orphaned: true,
},
]);
await manager.updateCommentCoordinatesForFile("hello again", "note.md");
const remaining = manager.getCommentsForFile("note.md");
assert.equal(remaining[0].orphaned, false);
});