chapterhouse 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/korg.agent.md +65 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +238 -2
- package/dist/api/server.test.js +199 -0
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agents.js +3 -4
- package/dist/copilot/agents.test.js +12 -1
- package/dist/copilot/orchestrator.js +12 -1
- package/dist/copilot/orchestrator.test.js +3 -7
- package/dist/copilot/system-message.js +12 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +193 -375
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +80 -59
- package/dist/copilot/turn-event-log-env.test.js +11 -15
- package/dist/daemon.js +19 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/eot.js +30 -8
- package/dist/memory/eot.test.js +220 -6
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +108 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/paths.js +31 -11
- package/dist/store/db.js +187 -4
- package/dist/store/db.test.js +66 -2
- package/dist/test/helpers/reset-singletons.js +8 -0
- package/dist/test/helpers/reset-singletons.test.js +37 -0
- package/dist/test/setup-env.js +9 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +143 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/fs.js +22 -13
- package/dist/wiki/index-manager.js +305 -330
- package/dist/wiki/index-manager.test.js +265 -144
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/log-manager.js +8 -5
- package/dist/wiki/log-manager.test.js +4 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +1 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/{index-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
- package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-DytB69KC.js.map +0 -1
|
@@ -1,160 +1,281 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { resetSingletons } from "../test/helpers/reset-singletons.js";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import test from "node:test";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
process.
|
|
8
|
-
|
|
6
|
+
// Sandbox: every test gets a fresh CHAPTERHOUSE_HOME
|
|
7
|
+
function makeSandbox() {
|
|
8
|
+
const dir = mkdtempSync(join(process.cwd(), ".test-work", "wiki-idx-"));
|
|
9
|
+
process.env.CHAPTERHOUSE_HOME = dir;
|
|
10
|
+
resetSingletons();
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
async function loadModules(sandbox) {
|
|
9
14
|
const nonce = `${Date.now()}-${Math.random()}`;
|
|
10
|
-
const indexManager = await import(new URL(`./index-manager.js?
|
|
11
|
-
const wikiFs = await import(new URL(`./fs.js?
|
|
15
|
+
const indexManager = await import(new URL(`./index-manager.js?c=${nonce}`, import.meta.url).href);
|
|
16
|
+
const wikiFs = await import(new URL(`./fs.js?c=${nonce}`, import.meta.url).href);
|
|
12
17
|
return { indexManager, wikiFs };
|
|
13
18
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
function resetWikiState(indexManager, wikiFs) {
|
|
20
|
+
rmSync(wikiFs.getWikiDir(), { recursive: true, force: true });
|
|
21
|
+
for (const entry of indexManager.parseIndex()) {
|
|
22
|
+
indexManager.removeFromIndex(entry.path);
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
async function loadModulesWithMocks(t, sandbox, options = {}) {
|
|
26
|
+
const warnings = [];
|
|
27
|
+
const infos = [];
|
|
28
|
+
t.mock.module("../util/logger.js", {
|
|
29
|
+
namedExports: {
|
|
30
|
+
childLogger: () => ({
|
|
31
|
+
info: (obj, msg) => infos.push({ obj, msg }),
|
|
32
|
+
warn: (obj, msg) => warnings.push({ obj, msg }),
|
|
33
|
+
error: () => { },
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (options.malformedMarker) {
|
|
38
|
+
const actualFrontmatter = await import(new URL(`./frontmatter.js?actual=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
39
|
+
t.mock.module("./frontmatter.js", {
|
|
40
|
+
namedExports: {
|
|
41
|
+
parseWikiFrontmatter: (content) => {
|
|
42
|
+
if (content.includes(options.malformedMarker)) {
|
|
43
|
+
throw new Error("Malformed wiki page");
|
|
44
|
+
}
|
|
45
|
+
return actualFrontmatter.parseWikiFrontmatter(content);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { ...(await loadModules(sandbox)), warnings, infos };
|
|
51
|
+
}
|
|
52
|
+
test.before(() => {
|
|
53
|
+
mkdirSync(join(process.cwd(), ".test-work"), { recursive: true });
|
|
20
54
|
});
|
|
21
|
-
test
|
|
22
|
-
|
|
55
|
+
test("wikiSearch returns FTS5 results for matching query", async () => {
|
|
56
|
+
const sandbox = makeSandbox();
|
|
57
|
+
try {
|
|
58
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
59
|
+
wikiFs.writePage("pages/topics/rust/index.md", "---\ntitle: Rust Programming\nsummary: Systems programming with async support\ntags: [rust, async]\nupdated: 2026-05-01\n---\n\n# Rust\n\nSystems language.\n");
|
|
60
|
+
wikiFs.writePage("pages/topics/typescript/index.md", "---\ntitle: TypeScript\nsummary: Typed JavaScript for large projects\ntags: [ts, web]\nupdated: 2026-05-02\n---\n\n# TypeScript\n\nJS with types.\n");
|
|
61
|
+
indexManager.rebuildWikiIndex();
|
|
62
|
+
const results = indexManager.wikiSearch("rust async");
|
|
63
|
+
assert.ok(results.length > 0, "Should return results for 'rust async'");
|
|
64
|
+
assert.ok(results.some((r) => r.path === "pages/topics/rust/index.md"), "Should include rust page");
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
68
|
+
}
|
|
23
69
|
});
|
|
24
|
-
test("
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tags: undefined,
|
|
42
|
-
updated: undefined,
|
|
43
|
-
},
|
|
44
|
-
]);
|
|
70
|
+
test("wikiSearch empty query returns most recently updated pages", async () => {
|
|
71
|
+
const sandbox = makeSandbox();
|
|
72
|
+
try {
|
|
73
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
74
|
+
wikiFs.writePage("pages/topics/alpha/index.md", "---\ntitle: Alpha\nsummary: First topic\nupdated: 2026-01-01\n---\n\n# Alpha\n");
|
|
75
|
+
wikiFs.writePage("pages/topics/beta/index.md", "---\ntitle: Beta\nsummary: Second topic\nupdated: 2026-05-14\n---\n\n# Beta\n");
|
|
76
|
+
indexManager.rebuildWikiIndex();
|
|
77
|
+
const results = indexManager.wikiSearch("", 10);
|
|
78
|
+
assert.ok(results.length >= 2, "Should return pages for empty query");
|
|
79
|
+
// Most recent first
|
|
80
|
+
const betaIdx = results.findIndex((r) => r.path === "pages/topics/beta/index.md");
|
|
81
|
+
const alphaIdx = results.findIndex((r) => r.path === "pages/topics/alpha/index.md");
|
|
82
|
+
assert.ok(betaIdx < alphaIdx, "More recently updated page should come first");
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
86
|
+
}
|
|
45
87
|
});
|
|
46
|
-
test("
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
88
|
+
test("rebuildWikiIndex populates wiki_pages from filesystem", async () => {
|
|
89
|
+
const sandbox = makeSandbox();
|
|
90
|
+
try {
|
|
91
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
92
|
+
wikiFs.writePage("pages/projects/chapterhouse/index.md", "---\ntitle: Chapterhouse\nsummary: AI orchestrator\ntags: [ai, orchestration]\nupdated: 2026-05-10\n---\n\n# Chapterhouse\n");
|
|
93
|
+
wikiFs.writePage("pages/projects/chapterhouse/decisions.md", "---\ntitle: Decisions\nsummary: Architectural decisions\nupdated: 2026-05-09\n---\n\n# Decisions\n");
|
|
94
|
+
indexManager.rebuildWikiIndex();
|
|
95
|
+
const entries = indexManager.parseIndex();
|
|
96
|
+
const paths = entries.map((e) => e.path).sort();
|
|
97
|
+
assert.ok(paths.includes("pages/projects/chapterhouse/index.md"), "Should include index.md");
|
|
98
|
+
assert.ok(paths.includes("pages/projects/chapterhouse/decisions.md"), "Should include decisions.md");
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
102
|
+
}
|
|
58
103
|
});
|
|
59
|
-
test("
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
summary: "Shared direction for the team.",
|
|
78
|
-
section: "Knowledge",
|
|
79
|
-
tags: undefined,
|
|
80
|
-
updated: undefined,
|
|
81
|
-
},
|
|
82
|
-
]);
|
|
83
|
-
assert.match(wikiFs.readIndexFile(), /\[Vision\]\(pages\/team\/vision\.md\)/);
|
|
104
|
+
test("ensureWikiIndexPopulated rebuilds from disk when wiki_pages starts empty", async () => {
|
|
105
|
+
const sandbox = makeSandbox();
|
|
106
|
+
try {
|
|
107
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
108
|
+
wikiFs.writePage("pages/topics/rust/index.md", "---\ntitle: Rust\nsummary: Systems programming\nupdated: 2026-05-10\n---\n\n# Rust\n");
|
|
109
|
+
for (const entry of indexManager.parseIndex()) {
|
|
110
|
+
indexManager.removeFromIndex(entry.path);
|
|
111
|
+
}
|
|
112
|
+
assert.equal(indexManager.parseIndex().length, 0, "Precondition: wiki_pages should start empty");
|
|
113
|
+
const result = indexManager.ensureWikiIndexPopulated();
|
|
114
|
+
assert.equal(result.reindexed, true);
|
|
115
|
+
assert.ok(result.diskPageCount >= 1);
|
|
116
|
+
assert.ok(result.indexedPageCount >= 1);
|
|
117
|
+
assert.ok(indexManager.parseIndex().some((entry) => entry.path === "pages/topics/rust/index.md"));
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
121
|
+
}
|
|
84
122
|
});
|
|
85
|
-
test("
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"pages/projects/chapterhouse/decisions.md",
|
|
101
|
-
"pages/projects/chapterhouse/index.md",
|
|
102
|
-
]);
|
|
123
|
+
test("upsertWikiPage inserts and updates correctly", async () => {
|
|
124
|
+
const sandbox = makeSandbox();
|
|
125
|
+
try {
|
|
126
|
+
const { indexManager } = await loadModules(sandbox);
|
|
127
|
+
indexManager.upsertWikiPage("pages/people/ada/index.md", { title: "Ada Lovelace", summary: "Mathematician", tags: ["math"], updated: "2026-05-01", metadata: {} }, "First programmer");
|
|
128
|
+
const results = indexManager.wikiSearch("Ada");
|
|
129
|
+
assert.ok(results.some((r) => r.title === "Ada Lovelace"), "Should find Ada");
|
|
130
|
+
// Update
|
|
131
|
+
indexManager.upsertWikiPage("pages/people/ada/index.md", { title: "Ada Lovelace", summary: "Mathematician and programmer", tags: ["math", "history"], updated: "2026-05-02", metadata: {} }, "First programmer and mathematician");
|
|
132
|
+
const updated = indexManager.wikiSearch("programmer");
|
|
133
|
+
assert.ok(updated.length > 0, "Should find updated page");
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
137
|
+
}
|
|
103
138
|
});
|
|
104
|
-
test("
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
]);
|
|
124
|
-
const metadataHit = indexManager.searchIndex("api", 1);
|
|
125
|
-
const bodyFallback = indexManager.searchIndex("telemetry", 1);
|
|
126
|
-
assert.deepEqual(metadataHit.map((entry) => entry.path), ["pages/team/api.md"]);
|
|
127
|
-
assert.deepEqual(bodyFallback.map((entry) => entry.path), ["pages/team/api.md"]);
|
|
139
|
+
test("FTS search returns results under 50ms", async () => {
|
|
140
|
+
const sandbox = makeSandbox();
|
|
141
|
+
try {
|
|
142
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
143
|
+
// Populate with 20 pages
|
|
144
|
+
for (let i = 0; i < 20; i++) {
|
|
145
|
+
wikiFs.writePage(`pages/topics/topic-${i}/index.md`, `---\ntitle: Topic ${i}\nsummary: Description for topic ${i} covering various subjects\ntags: [topic${i}]\nupdated: 2026-05-01\n---\n\n# Topic ${i}\n\nContent.\n`);
|
|
146
|
+
}
|
|
147
|
+
indexManager.rebuildWikiIndex();
|
|
148
|
+
const start = Date.now();
|
|
149
|
+
const results = indexManager.wikiSearch("topic description");
|
|
150
|
+
const elapsed = Date.now() - start;
|
|
151
|
+
assert.ok(results.length > 0, "Should return results");
|
|
152
|
+
assert.ok(elapsed < 50, `FTS search should complete in <50ms, took ${elapsed}ms`);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
156
|
+
}
|
|
128
157
|
});
|
|
129
|
-
test("
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
test("removeFromIndex removes from wiki_pages", async () => {
|
|
159
|
+
const sandbox = makeSandbox();
|
|
160
|
+
try {
|
|
161
|
+
const { indexManager } = await loadModules(sandbox);
|
|
162
|
+
indexManager.upsertWikiPage("pages/people/test/index.md", { title: "Test Person", summary: "A test", tags: [], updated: "2026-05-01", metadata: {} }, "A test");
|
|
163
|
+
const before = indexManager.wikiSearch("Test Person");
|
|
164
|
+
assert.ok(before.length > 0, "Should exist before removal");
|
|
165
|
+
const removed = indexManager.removeFromIndex("pages/people/test/index.md");
|
|
166
|
+
assert.equal(removed, true);
|
|
167
|
+
const after = indexManager.wikiSearch("Test Person");
|
|
168
|
+
assert.equal(after.length, 0, "Should not exist after removal");
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
test("searchIndex delegates to wikiSearch and returns IndexEntry shape", async () => {
|
|
175
|
+
const sandbox = makeSandbox();
|
|
176
|
+
try {
|
|
177
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
178
|
+
wikiFs.writePage("pages/team/api.md", "---\ntitle: API Docs\nsummary: API documentation\ntags: [api]\nupdated: 2026-05-01\n---\n\n# API\n");
|
|
179
|
+
indexManager.rebuildWikiIndex();
|
|
180
|
+
const results = indexManager.searchIndex("api");
|
|
181
|
+
assert.ok(results.length > 0);
|
|
182
|
+
assert.ok("section" in results[0], "Should have section field");
|
|
183
|
+
assert.ok("title" in results[0], "Should have title field");
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
test("rebuildWikiIndex removes stale entries not on disk", async () => {
|
|
190
|
+
const sandbox = makeSandbox();
|
|
191
|
+
try {
|
|
192
|
+
const { indexManager, wikiFs } = await loadModules(sandbox);
|
|
193
|
+
wikiFs.writePage("pages/topics/keep/index.md", "---\ntitle: Keep\nsummary: Keep this\nupdated: 2026-05-01\n---\n\n# Keep\n");
|
|
194
|
+
// Insert stale entry directly
|
|
195
|
+
indexManager.upsertWikiPage("pages/topics/stale/index.md", { title: "Stale", summary: "Should be removed", tags: [], updated: "2026-01-01", metadata: {} }, "Stale");
|
|
196
|
+
// Rebuild syncs disk → DB
|
|
197
|
+
indexManager.rebuildWikiIndex();
|
|
198
|
+
const entries = indexManager.parseIndex();
|
|
199
|
+
const paths = entries.map((e) => e.path);
|
|
200
|
+
assert.ok(paths.includes("pages/topics/keep/index.md"), "Should keep on-disk page");
|
|
201
|
+
assert.ok(!paths.includes("pages/topics/stale/index.md"), "Should remove stale entry");
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
test("reindexWikiPages skips unreadable pages and continues indexing others", async (t) => {
|
|
208
|
+
const sandbox = makeSandbox();
|
|
209
|
+
try {
|
|
210
|
+
const { indexManager, wikiFs, warnings } = await loadModulesWithMocks(t, sandbox);
|
|
211
|
+
resetWikiState(indexManager, wikiFs);
|
|
212
|
+
const unreadablePath = join(wikiFs.getWikiDir(), "pages", "topics", "blocked", "index.md");
|
|
213
|
+
wikiFs.writePage("pages/topics/alpha/index.md", "---\ntitle: Alpha\nsummary: First topic\nupdated: 2026-05-01\n---\n\n# Alpha\n");
|
|
214
|
+
wikiFs.writePage("pages/topics/blocked/index.md", "---\ntitle: Blocked\nsummary: Unreadable topic\nupdated: 2026-05-02\n---\n\n# Blocked\n");
|
|
215
|
+
wikiFs.writePage("pages/topics/gamma/index.md", "---\ntitle: Gamma\nsummary: Third topic\nupdated: 2026-05-03\n---\n\n# Gamma\n");
|
|
216
|
+
chmodSync(unreadablePath, 0o000);
|
|
217
|
+
try {
|
|
218
|
+
const result = indexManager.reindexWikiPages();
|
|
219
|
+
const paths = indexManager.parseIndex().map((entry) => entry.path).sort();
|
|
220
|
+
assert.deepEqual(paths, [
|
|
221
|
+
"pages/topics/alpha/index.md",
|
|
222
|
+
"pages/topics/gamma/index.md",
|
|
223
|
+
]);
|
|
224
|
+
assert.equal(result.diskPageCount, 3);
|
|
225
|
+
assert.equal(result.indexedPageCount, 2);
|
|
226
|
+
assert.ok(warnings.some((entry) => entry.obj.path === "pages/topics/blocked/index.md"), "Expected a warning for the unreadable page");
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
chmodSync(unreadablePath, 0o644);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
test("reindexWikiPages skips malformed pages and continues indexing others", async (t) => {
|
|
237
|
+
const sandbox = makeSandbox();
|
|
238
|
+
try {
|
|
239
|
+
const { indexManager, wikiFs, warnings } = await loadModulesWithMocks(t, sandbox, { malformedMarker: "UNPARSEABLE" });
|
|
240
|
+
resetWikiState(indexManager, wikiFs);
|
|
241
|
+
wikiFs.writePage("pages/topics/alpha/index.md", "---\ntitle: Alpha\nsummary: First topic\nupdated: 2026-05-01\n---\n\n# Alpha\n");
|
|
242
|
+
wikiFs.writePage("pages/topics/bad/index.md", "---\ntitle: Bad\nsummary: Broken topic\nupdated: 2026-05-02\n---\n\nUNPARSEABLE\n");
|
|
243
|
+
wikiFs.writePage("pages/topics/gamma/index.md", "---\ntitle: Gamma\nsummary: Third topic\nupdated: 2026-05-03\n---\n\n# Gamma\n");
|
|
244
|
+
const result = indexManager.reindexWikiPages();
|
|
245
|
+
const paths = indexManager.parseIndex().map((entry) => entry.path).sort();
|
|
246
|
+
assert.deepEqual(paths, [
|
|
247
|
+
"pages/topics/alpha/index.md",
|
|
248
|
+
"pages/topics/gamma/index.md",
|
|
249
|
+
]);
|
|
250
|
+
assert.equal(result.diskPageCount, 3);
|
|
251
|
+
assert.equal(result.indexedPageCount, 2);
|
|
252
|
+
assert.ok(warnings.some((entry) => entry.obj.path === "pages/topics/bad/index.md"), "Expected a warning for the malformed page");
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
test("reindexWikiPages logs a summary with indexed and skipped counts", async (t) => {
|
|
259
|
+
const sandbox = makeSandbox();
|
|
260
|
+
try {
|
|
261
|
+
const { indexManager, wikiFs, infos } = await loadModulesWithMocks(t, sandbox);
|
|
262
|
+
resetWikiState(indexManager, wikiFs);
|
|
263
|
+
const unreadablePath = join(wikiFs.getWikiDir(), "pages", "topics", "blocked", "index.md");
|
|
264
|
+
wikiFs.writePage("pages/topics/alpha/index.md", "---\ntitle: Alpha\nsummary: First topic\nupdated: 2026-05-01\n---\n\n# Alpha\n");
|
|
265
|
+
wikiFs.writePage("pages/topics/blocked/index.md", "---\ntitle: Blocked\nsummary: Unreadable topic\nupdated: 2026-05-02\n---\n\n# Blocked\n");
|
|
266
|
+
wikiFs.writePage("pages/topics/gamma/index.md", "---\ntitle: Gamma\nsummary: Third topic\nupdated: 2026-05-03\n---\n\n# Gamma\n");
|
|
267
|
+
chmodSync(unreadablePath, 0o000);
|
|
268
|
+
try {
|
|
269
|
+
const result = indexManager.reindexWikiPages();
|
|
270
|
+
assert.equal(result.indexedPageCount, 2);
|
|
271
|
+
assert.ok(infos.some((entry) => /Reindexed 2 pages, skipped 1/.test(entry.msg)), "Expected a summary log with indexed and skipped counts");
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
chmodSync(unreadablePath, 0o644);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
279
|
+
}
|
|
159
280
|
});
|
|
160
281
|
//# sourceMappingURL=index-manager.test.js.map
|