2026-04-05 17:21:52 +00:00
|
|
|
import * as assert from "node:assert/strict";
|
|
|
|
|
import test from "node:test";
|
2026-04-16 05:12:32 +00:00
|
|
|
import type { Comment, CommentThread } from "../src/commentManager";
|
2026-04-05 17:21:52 +00:00
|
|
|
import {
|
|
|
|
|
buildIndexFileFilterGraph,
|
|
|
|
|
getIndexFileFilterConnectedComponent,
|
|
|
|
|
} from "../src/core/derived/indexFileFilterGraph";
|
|
|
|
|
|
|
|
|
|
function createComment(overrides: Partial<Comment> = {}): Comment {
|
|
|
|
|
return {
|
|
|
|
|
id: overrides.id ?? "comment-1",
|
|
|
|
|
filePath: overrides.filePath ?? "docs/a.md",
|
|
|
|
|
startLine: overrides.startLine ?? 0,
|
|
|
|
|
startChar: overrides.startChar ?? 0,
|
|
|
|
|
endLine: overrides.endLine ?? 0,
|
|
|
|
|
endChar: overrides.endChar ?? 0,
|
|
|
|
|
selectedText: overrides.selectedText ?? "comment",
|
|
|
|
|
selectedTextHash: overrides.selectedTextHash ?? "hash:comment",
|
|
|
|
|
comment: overrides.comment ?? "",
|
|
|
|
|
timestamp: overrides.timestamp ?? 100,
|
|
|
|
|
anchorKind: overrides.anchorKind ?? "selection",
|
|
|
|
|
orphaned: overrides.orphaned ?? false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 05:12:32 +00:00
|
|
|
function createThread(overrides: Partial<CommentThread> = {}): CommentThread {
|
|
|
|
|
return {
|
|
|
|
|
id: overrides.id ?? "thread-1",
|
|
|
|
|
filePath: overrides.filePath ?? "docs/a.md",
|
|
|
|
|
startLine: overrides.startLine ?? 0,
|
|
|
|
|
startChar: overrides.startChar ?? 0,
|
|
|
|
|
endLine: overrides.endLine ?? 0,
|
|
|
|
|
endChar: overrides.endChar ?? 0,
|
|
|
|
|
selectedText: overrides.selectedText ?? "comment",
|
|
|
|
|
selectedTextHash: overrides.selectedTextHash ?? "hash:comment",
|
|
|
|
|
anchorKind: overrides.anchorKind ?? "selection",
|
|
|
|
|
orphaned: overrides.orphaned ?? false,
|
|
|
|
|
entries: overrides.entries ?? [],
|
|
|
|
|
createdAt: overrides.createdAt ?? 100,
|
|
|
|
|
updatedAt: overrides.updatedAt ?? 200,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 17:21:52 +00:00
|
|
|
function createResolver(resolvedPathsByKey: Record<string, string | null>) {
|
|
|
|
|
return (linkPath: string) => resolvedPathsByKey[linkPath] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortedNeighbors(graphValue: Map<string, Set<string>>, filePath: string): string[] {
|
|
|
|
|
return Array.from(graphValue.get(filePath) ?? []).sort((left, right) => left.localeCompare(right));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("buildIndexFileFilterGraph builds full connected components from outgoing and incoming wiki links", () => {
|
|
|
|
|
const graph = buildIndexFileFilterGraph([
|
|
|
|
|
createComment({
|
|
|
|
|
id: "a-1",
|
|
|
|
|
filePath: "docs/a.md",
|
|
|
|
|
comment: "See [[B]]",
|
|
|
|
|
}),
|
|
|
|
|
createComment({
|
|
|
|
|
id: "b-1",
|
|
|
|
|
filePath: "docs/b.md",
|
|
|
|
|
comment: "Then [[C]]",
|
|
|
|
|
}),
|
|
|
|
|
createComment({
|
|
|
|
|
id: "d-1",
|
|
|
|
|
filePath: "docs/d.md",
|
|
|
|
|
comment: "Back to [[A]]",
|
|
|
|
|
}),
|
|
|
|
|
createComment({
|
|
|
|
|
id: "e-1",
|
|
|
|
|
filePath: "docs/e.md",
|
|
|
|
|
comment: "Isolated",
|
|
|
|
|
}),
|
|
|
|
|
createComment({
|
|
|
|
|
id: "c-1",
|
|
|
|
|
filePath: "docs/c.md",
|
|
|
|
|
comment: "No links",
|
|
|
|
|
}),
|
|
|
|
|
], {
|
|
|
|
|
resolveWikiLinkPath: createResolver({
|
|
|
|
|
A: "docs/a.md",
|
|
|
|
|
B: "docs/b.md",
|
|
|
|
|
C: "docs/c.md",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(graph.availableFiles, [
|
|
|
|
|
"docs/a.md",
|
|
|
|
|
"docs/b.md",
|
|
|
|
|
"docs/c.md",
|
|
|
|
|
"docs/d.md",
|
|
|
|
|
"docs/e.md",
|
|
|
|
|
]);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/a.md"), [
|
|
|
|
|
"docs/a.md",
|
|
|
|
|
"docs/b.md",
|
|
|
|
|
"docs/c.md",
|
|
|
|
|
"docs/d.md",
|
|
|
|
|
]);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/d.md"), [
|
|
|
|
|
"docs/a.md",
|
|
|
|
|
"docs/b.md",
|
|
|
|
|
"docs/c.md",
|
|
|
|
|
"docs/d.md",
|
|
|
|
|
]);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/e.md"), [
|
|
|
|
|
"docs/e.md",
|
|
|
|
|
]);
|
|
|
|
|
assert.deepEqual(sortedNeighbors(graph.outgoingAdjacency, "docs/a.md"), ["docs/b.md"]);
|
|
|
|
|
assert.deepEqual(sortedNeighbors(graph.undirectedAdjacency, "docs/a.md"), ["docs/b.md", "docs/d.md"]);
|
|
|
|
|
assert.equal(graph.componentSizeByFile.get("docs/a.md"), 4);
|
|
|
|
|
assert.equal(graph.componentSizeByFile.get("docs/e.md"), 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("buildIndexFileFilterGraph ignores targets without side notes, self-links, and the index note", () => {
|
|
|
|
|
const graph = buildIndexFileFilterGraph([
|
|
|
|
|
createComment({
|
|
|
|
|
id: "a-1",
|
|
|
|
|
filePath: "docs/a.md",
|
|
|
|
|
comment: "Self [[A]] missing [[Missing]] index [[Index]] real [[B]]",
|
|
|
|
|
}),
|
|
|
|
|
createComment({
|
|
|
|
|
id: "b-1",
|
|
|
|
|
filePath: "docs/b.md",
|
|
|
|
|
comment: "",
|
|
|
|
|
}),
|
|
|
|
|
], {
|
2026-05-13 18:44:11 +00:00
|
|
|
allCommentsNotePath: "Aside index.md",
|
2026-04-05 17:21:52 +00:00
|
|
|
resolveWikiLinkPath: createResolver({
|
|
|
|
|
A: "docs/a.md",
|
|
|
|
|
B: "docs/b.md",
|
2026-05-13 18:44:11 +00:00
|
|
|
Index: "Aside index.md",
|
2026-04-05 17:21:52 +00:00
|
|
|
Missing: "docs/missing.md",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(sortedNeighbors(graph.outgoingAdjacency, "docs/a.md"), ["docs/b.md"]);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/a.md"), [
|
|
|
|
|
"docs/a.md",
|
|
|
|
|
"docs/b.md",
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("getIndexFileFilterConnectedComponent returns empty for missing roots", () => {
|
|
|
|
|
const graph = buildIndexFileFilterGraph([
|
|
|
|
|
createComment({
|
|
|
|
|
id: "a-1",
|
|
|
|
|
filePath: "docs/a.md",
|
|
|
|
|
comment: "",
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, null), []);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/missing.md"), []);
|
|
|
|
|
});
|
2026-04-16 05:12:32 +00:00
|
|
|
|
|
|
|
|
test("buildIndexFileFilterGraph keeps thread links from older child entries", () => {
|
|
|
|
|
const graph = buildIndexFileFilterGraph([
|
|
|
|
|
createThread({
|
|
|
|
|
id: "a-thread",
|
|
|
|
|
filePath: "docs/a.md",
|
|
|
|
|
entries: [
|
|
|
|
|
{ id: "a-entry-1", body: "Older child links [[B]].", timestamp: 100 },
|
|
|
|
|
{ id: "a-entry-2", body: "Latest child has no link.", timestamp: 200 },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
createThread({
|
|
|
|
|
id: "b-thread",
|
|
|
|
|
filePath: "docs/b.md",
|
|
|
|
|
entries: [
|
|
|
|
|
{ id: "b-entry-1", body: "No links", timestamp: 300 },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
], {
|
|
|
|
|
resolveWikiLinkPath: createResolver({
|
|
|
|
|
B: "docs/b.md",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(sortedNeighbors(graph.outgoingAdjacency, "docs/a.md"), ["docs/b.md"]);
|
|
|
|
|
assert.deepEqual(getIndexFileFilterConnectedComponent(graph, "docs/a.md"), [
|
|
|
|
|
"docs/a.md",
|
|
|
|
|
"docs/b.md",
|
|
|
|
|
]);
|
|
|
|
|
});
|