mirror of
https://github.com/vicky469/aside.git
synced 2026-07-22 07:01:57 +00:00
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.
339 lines
12 KiB
TypeScript
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);
|
|
});
|