chapterhouse 0.3.23 → 0.3.24
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/dist/api/server.test.js +67 -94
- package/dist/store/db.js +8 -0
- package/dist/store/db.test.js +1 -0
- package/dist/wiki/project-registry.js +74 -61
- package/dist/wiki/project-registry.test.js +96 -48
- package/package.json +1 -1
package/dist/api/server.test.js
CHANGED
|
@@ -68,6 +68,47 @@ test("formats named SSE status events", async () => {
|
|
|
68
68
|
assert.equal(sse.formatSseEvent("status", { status: "dreaming", message: "Consolidating memories..." }), 'event: status\ndata: {"status":"dreaming","message":"Consolidating memories..."}\n\n');
|
|
69
69
|
});
|
|
70
70
|
const repoRoot = process.cwd();
|
|
71
|
+
function getProjectDbPath(testRoot) {
|
|
72
|
+
return join(testRoot, ".chapterhouse", "chapterhouse.db");
|
|
73
|
+
}
|
|
74
|
+
function seedProjectRegistry(testRoot, registry) {
|
|
75
|
+
mkdirSync(join(testRoot, ".chapterhouse"), { recursive: true });
|
|
76
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
77
|
+
try {
|
|
78
|
+
db.exec(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
80
|
+
slug TEXT PRIMARY KEY,
|
|
81
|
+
cwd TEXT NOT NULL,
|
|
82
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
83
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
84
|
+
)
|
|
85
|
+
`);
|
|
86
|
+
db.prepare("DELETE FROM projects").run();
|
|
87
|
+
const insert = db.prepare(`
|
|
88
|
+
INSERT INTO projects (slug, cwd, created_at, updated_at)
|
|
89
|
+
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
90
|
+
`);
|
|
91
|
+
for (const [slug, cwd] of Object.entries(registry).sort(([left], [right]) => left.localeCompare(right))) {
|
|
92
|
+
insert.run(slug, cwd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function readProjectRegistryRows(testRoot) {
|
|
100
|
+
const dbPath = getProjectDbPath(testRoot);
|
|
101
|
+
if (!existsSync(dbPath)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const db = new Database(dbPath, { readonly: true });
|
|
105
|
+
try {
|
|
106
|
+
return db.prepare("SELECT slug, cwd FROM projects ORDER BY slug").all();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
db.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
71
112
|
async function getFreePort() {
|
|
72
113
|
const server = createServer();
|
|
73
114
|
await new Promise((resolve) => {
|
|
@@ -402,19 +443,10 @@ test("server projects route returns an empty list when the registry is missing",
|
|
|
402
443
|
});
|
|
403
444
|
test("server projects route sorts by slug and summarizes rule counts", async () => {
|
|
404
445
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
+ "summary: Canonical project registry.\n"
|
|
410
|
-
+ "updated: 2026-05-12\n"
|
|
411
|
-
+ "---\n\n"
|
|
412
|
-
+ "# Projects\n\n"
|
|
413
|
-
+ "## Project Registry\n\n"
|
|
414
|
-
+ "```yaml\n"
|
|
415
|
-
+ "zeta: /srv/zeta\n"
|
|
416
|
-
+ "alpha: /srv/alpha\n"
|
|
417
|
-
+ "```\n", "utf-8");
|
|
446
|
+
seedProjectRegistry(testRoot, {
|
|
447
|
+
zeta: "/srv/zeta",
|
|
448
|
+
alpha: "/srv/alpha",
|
|
449
|
+
});
|
|
418
450
|
const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
|
|
419
451
|
mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
|
|
420
452
|
writeFileSync(alphaRulesPath, "---\n"
|
|
@@ -448,18 +480,9 @@ test("server projects route sorts by slug and summarizes rule counts", async ()
|
|
|
448
480
|
});
|
|
449
481
|
test("server project detail route returns cwd plus hard and soft rules", async () => {
|
|
450
482
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
+ "title: Projects\n"
|
|
455
|
-
+ "summary: Canonical project registry.\n"
|
|
456
|
-
+ "updated: 2026-05-12\n"
|
|
457
|
-
+ "---\n\n"
|
|
458
|
-
+ "# Projects\n\n"
|
|
459
|
-
+ "## Project Registry\n\n"
|
|
460
|
-
+ "```yaml\n"
|
|
461
|
-
+ "alpha: /srv/alpha\n"
|
|
462
|
-
+ "```\n", "utf-8");
|
|
483
|
+
seedProjectRegistry(testRoot, {
|
|
484
|
+
alpha: "/srv/alpha",
|
|
485
|
+
});
|
|
463
486
|
const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
|
|
464
487
|
mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
|
|
465
488
|
writeFileSync(alphaRulesPath, "---\n"
|
|
@@ -509,18 +532,9 @@ test("server project detail route returns 404 for an unknown slug", async () =>
|
|
|
509
532
|
});
|
|
510
533
|
test("server projects create route rejects a duplicate slug", async () => {
|
|
511
534
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
+ "title: Projects\n"
|
|
516
|
-
+ "summary: Canonical project registry.\n"
|
|
517
|
-
+ "updated: 2026-05-12\n"
|
|
518
|
-
+ "---\n\n"
|
|
519
|
-
+ "# Projects\n\n"
|
|
520
|
-
+ "## Project Registry\n\n"
|
|
521
|
-
+ "```yaml\n"
|
|
522
|
-
+ "alpha: /srv/original\n"
|
|
523
|
-
+ "```\n", "utf-8");
|
|
535
|
+
seedProjectRegistry(testRoot, {
|
|
536
|
+
alpha: "/srv/original",
|
|
537
|
+
});
|
|
524
538
|
const response = await fetch(`${baseUrl}/api/projects`, {
|
|
525
539
|
method: "POST",
|
|
526
540
|
headers: {
|
|
@@ -534,33 +548,17 @@ test("server projects create route rejects a duplicate slug", async () => {
|
|
|
534
548
|
});
|
|
535
549
|
assert.equal(response.status, 400);
|
|
536
550
|
assert.deepEqual(await response.json(), { error: "Project 'alpha' already exists" });
|
|
537
|
-
assert.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
+ "updated: 2026-05-12\n"
|
|
541
|
-
+ "---\n\n"
|
|
542
|
-
+ "# Projects\n\n"
|
|
543
|
-
+ "## Project Registry\n\n"
|
|
544
|
-
+ "```yaml\n"
|
|
545
|
-
+ "alpha: /srv/original\n"
|
|
546
|
-
+ "```\n");
|
|
551
|
+
assert.deepEqual(readProjectRegistryRows(testRoot), [
|
|
552
|
+
{ slug: "alpha", cwd: "/srv/original" },
|
|
553
|
+
]);
|
|
547
554
|
}, {}, 60_000);
|
|
548
555
|
});
|
|
549
556
|
test("server projects delete route removes the registry entry and rules page", async () => {
|
|
550
557
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
+ "summary: Canonical project registry.\n"
|
|
556
|
-
+ "updated: 2026-05-12\n"
|
|
557
|
-
+ "---\n\n"
|
|
558
|
-
+ "# Projects\n\n"
|
|
559
|
-
+ "## Project Registry\n\n"
|
|
560
|
-
+ "```yaml\n"
|
|
561
|
-
+ "alpha: /srv/alpha\n"
|
|
562
|
-
+ "beta: /srv/beta\n"
|
|
563
|
-
+ "```\n", "utf-8");
|
|
558
|
+
seedProjectRegistry(testRoot, {
|
|
559
|
+
alpha: "/srv/alpha",
|
|
560
|
+
beta: "/srv/beta",
|
|
561
|
+
});
|
|
564
562
|
const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
|
|
565
563
|
mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
|
|
566
564
|
writeFileSync(alphaRulesPath, "---\n"
|
|
@@ -574,16 +572,9 @@ test("server projects delete route removes the registry entry and rules page", a
|
|
|
574
572
|
});
|
|
575
573
|
assert.equal(response.status, 200);
|
|
576
574
|
assert.deepEqual(await response.json(), { ok: true, slug: "alpha" });
|
|
577
|
-
assert.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
+ "updated: 2026-05-12\n"
|
|
581
|
-
+ "---\n\n"
|
|
582
|
-
+ "# Projects\n\n"
|
|
583
|
-
+ "## Project Registry\n\n"
|
|
584
|
-
+ "```yaml\n"
|
|
585
|
-
+ "beta: /srv/beta\n"
|
|
586
|
-
+ "```\n");
|
|
575
|
+
assert.deepEqual(readProjectRegistryRows(testRoot), [
|
|
576
|
+
{ slug: "beta", cwd: "/srv/beta" },
|
|
577
|
+
]);
|
|
587
578
|
assert.equal(existsSync(alphaRulesPath), false);
|
|
588
579
|
}, {}, 60_000);
|
|
589
580
|
});
|
|
@@ -599,18 +590,9 @@ test("server projects delete route returns 404 for an unknown slug", async () =>
|
|
|
599
590
|
});
|
|
600
591
|
test("server project hard-rules update route rewrites only hard-rule frontmatter fields", async () => {
|
|
601
592
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
+ "title: Projects\n"
|
|
606
|
-
+ "summary: Canonical project registry.\n"
|
|
607
|
-
+ "updated: 2026-05-12\n"
|
|
608
|
-
+ "---\n\n"
|
|
609
|
-
+ "# Projects\n\n"
|
|
610
|
-
+ "## Project Registry\n\n"
|
|
611
|
-
+ "```yaml\n"
|
|
612
|
-
+ "alpha: /srv/alpha\n"
|
|
613
|
-
+ "```\n", "utf-8");
|
|
593
|
+
seedProjectRegistry(testRoot, {
|
|
594
|
+
alpha: "/srv/alpha",
|
|
595
|
+
});
|
|
614
596
|
const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
|
|
615
597
|
mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
|
|
616
598
|
writeFileSync(alphaRulesPath, "---\n"
|
|
@@ -688,18 +670,9 @@ test("server project hard-rules update route rewrites only hard-rule frontmatter
|
|
|
688
670
|
});
|
|
689
671
|
test("server project soft-rules update route rewrites the body while preserving frontmatter", async () => {
|
|
690
672
|
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
+ "title: Projects\n"
|
|
695
|
-
+ "summary: Canonical project registry.\n"
|
|
696
|
-
+ "updated: 2026-05-12\n"
|
|
697
|
-
+ "---\n\n"
|
|
698
|
-
+ "# Projects\n\n"
|
|
699
|
-
+ "## Project Registry\n\n"
|
|
700
|
-
+ "```yaml\n"
|
|
701
|
-
+ "alpha: /srv/alpha\n"
|
|
702
|
-
+ "```\n", "utf-8");
|
|
673
|
+
seedProjectRegistry(testRoot, {
|
|
674
|
+
alpha: "/srv/alpha",
|
|
675
|
+
});
|
|
703
676
|
const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
|
|
704
677
|
mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
|
|
705
678
|
writeFileSync(alphaRulesPath, "---\n"
|
package/dist/store/db.js
CHANGED
|
@@ -44,6 +44,14 @@ export function getDb() {
|
|
|
44
44
|
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
45
|
completed_at DATETIME
|
|
46
46
|
)
|
|
47
|
+
`);
|
|
48
|
+
db.exec(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
50
|
+
slug TEXT PRIMARY KEY,
|
|
51
|
+
cwd TEXT NOT NULL,
|
|
52
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
54
|
+
)
|
|
47
55
|
`);
|
|
48
56
|
db.exec(`
|
|
49
57
|
CREATE TABLE IF NOT EXISTS max_state (
|
package/dist/store/db.test.js
CHANGED
|
@@ -1,47 +1,91 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { getDb } from "../store/db.js";
|
|
3
|
+
import { childLogger } from "../util/logger.js";
|
|
4
|
+
import { deletePage, pageExists, readPage } from "./fs.js";
|
|
5
|
+
const log = childLogger("project-registry");
|
|
6
|
+
const LEGACY_PROJECTS_INDEX_PATH = "pages/projects/index.md";
|
|
4
7
|
const REGISTRY_HEADING = "## Project Registry";
|
|
5
8
|
const OPENING_FENCE = "```yaml";
|
|
6
9
|
const CLOSING_FENCE = "```";
|
|
7
10
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
8
11
|
export function loadRegistry() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return {};
|
|
15
|
-
return parseRegistryBlock(section.blockLines);
|
|
12
|
+
ensureRegistryMigrated();
|
|
13
|
+
const rows = getDb()
|
|
14
|
+
.prepare("SELECT slug, cwd FROM projects ORDER BY slug")
|
|
15
|
+
.all();
|
|
16
|
+
return Object.fromEntries(rows.map(({ slug, cwd }) => [slug, cwd]));
|
|
16
17
|
}
|
|
17
18
|
export function saveRegistry(registry) {
|
|
18
19
|
validateRegistry(registry);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
ensureRegistryMigrated();
|
|
21
|
+
const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
|
|
22
|
+
const db = getDb();
|
|
23
|
+
const save = db.transaction(() => {
|
|
24
|
+
db.prepare("DELETE FROM projects").run();
|
|
25
|
+
const insert = db.prepare(`
|
|
26
|
+
INSERT INTO projects (slug, cwd, created_at, updated_at)
|
|
27
|
+
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
28
|
+
`);
|
|
29
|
+
for (const [slug, cwd] of entries) {
|
|
30
|
+
insert.run(slug, cwd);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
save();
|
|
34
|
+
removeLegacyRegistryFile("Removed legacy wiki registry after SQLite save");
|
|
35
|
+
}
|
|
36
|
+
export function assertValidProjectSlug(slug) {
|
|
37
|
+
if (!SLUG_RE.test(slug)) {
|
|
38
|
+
throw new Error(`Project registry has invalid project slug '${slug}'. Expected a lowercase slug.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function ensureRegistryMigrated() {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
let migratedRegistry;
|
|
44
|
+
const migrate = db.transaction(() => {
|
|
45
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM projects").get();
|
|
46
|
+
if (row.count > 0 || !pageExists(LEGACY_PROJECTS_INDEX_PATH)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const legacyContent = readPage(LEGACY_PROJECTS_INDEX_PATH);
|
|
50
|
+
if (!legacyContent) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const registry = parseLegacyRegistry(legacyContent);
|
|
54
|
+
if (!registry) {
|
|
55
|
+
log.warn({ path: LEGACY_PROJECTS_INDEX_PATH }, "Legacy projects page had no registry section; skipping SQLite migration");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const insert = db.prepare(`
|
|
59
|
+
INSERT INTO projects (slug, cwd, created_at, updated_at)
|
|
60
|
+
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
61
|
+
`);
|
|
62
|
+
for (const [slug, cwd] of Object.entries(registry).sort(([left], [right]) => left.localeCompare(right))) {
|
|
63
|
+
insert.run(slug, cwd);
|
|
64
|
+
}
|
|
65
|
+
migratedRegistry = registry;
|
|
66
|
+
});
|
|
67
|
+
migrate.immediate();
|
|
68
|
+
if (!migratedRegistry) {
|
|
23
69
|
return;
|
|
24
70
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
writePage(PROJECTS_INDEX_PATH, `${prefix}${renderedSection}\n`);
|
|
71
|
+
removeLegacyRegistryFile("Migrated project registry from wiki to SQLite", Object.keys(migratedRegistry).length);
|
|
72
|
+
}
|
|
73
|
+
function removeLegacyRegistryFile(message, count) {
|
|
74
|
+
if (!deletePage(LEGACY_PROJECTS_INDEX_PATH)) {
|
|
30
75
|
return;
|
|
31
76
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (before) {
|
|
36
|
-
pieces.push(before);
|
|
37
|
-
pieces.push("");
|
|
77
|
+
if (typeof count === "number") {
|
|
78
|
+
log.info({ count, path: LEGACY_PROJECTS_INDEX_PATH }, message);
|
|
79
|
+
return;
|
|
38
80
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
log.info({ path: LEGACY_PROJECTS_INDEX_PATH }, message);
|
|
82
|
+
}
|
|
83
|
+
function parseLegacyRegistry(content) {
|
|
84
|
+
const section = parseRegistrySection(content);
|
|
85
|
+
if (!section) {
|
|
86
|
+
return undefined;
|
|
43
87
|
}
|
|
44
|
-
|
|
88
|
+
return parseRegistryBlock(section.blockLines);
|
|
45
89
|
}
|
|
46
90
|
function parseRegistrySection(content) {
|
|
47
91
|
const normalized = normalizeLineEndings(content);
|
|
@@ -81,11 +125,7 @@ function parseRegistrySection(content) {
|
|
|
81
125
|
throw new Error("Project registry is malformed: unexpected content after the fenced block.");
|
|
82
126
|
}
|
|
83
127
|
}
|
|
84
|
-
return {
|
|
85
|
-
before: lines.slice(0, headingIndex),
|
|
86
|
-
blockLines,
|
|
87
|
-
after: lines.slice(sectionEnd),
|
|
88
|
-
};
|
|
128
|
+
return { blockLines };
|
|
89
129
|
}
|
|
90
130
|
function parseRegistryBlock(lines) {
|
|
91
131
|
const registry = {};
|
|
@@ -114,28 +154,11 @@ function validateRegistry(registry) {
|
|
|
114
154
|
validatePath(path);
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
|
-
export function assertValidProjectSlug(slug) {
|
|
118
|
-
if (!SLUG_RE.test(slug)) {
|
|
119
|
-
throw new Error(`Project registry has invalid project slug '${slug}'. Expected a lowercase slug.`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
157
|
function validatePath(path) {
|
|
123
158
|
if (!path || !isAbsolute(path)) {
|
|
124
159
|
throw new Error(`Project registry path '${path}' must be an absolute path.`);
|
|
125
160
|
}
|
|
126
161
|
}
|
|
127
|
-
function renderRegistrySection(registry) {
|
|
128
|
-
const lines = [
|
|
129
|
-
REGISTRY_HEADING,
|
|
130
|
-
"",
|
|
131
|
-
OPENING_FENCE,
|
|
132
|
-
...Object.keys(registry)
|
|
133
|
-
.sort()
|
|
134
|
-
.map((slug) => `${slug}: ${registry[slug]}`),
|
|
135
|
-
CLOSING_FENCE,
|
|
136
|
-
];
|
|
137
|
-
return lines.join("\n");
|
|
138
|
-
}
|
|
139
162
|
function findNextHeading(lines, startIndex) {
|
|
140
163
|
for (let index = startIndex; index < lines.length; index += 1) {
|
|
141
164
|
if (/^##\s+/.test(lines[index])) {
|
|
@@ -147,14 +170,4 @@ function findNextHeading(lines, startIndex) {
|
|
|
147
170
|
function normalizeLineEndings(content) {
|
|
148
171
|
return content.replace(/\r\n/g, "\n");
|
|
149
172
|
}
|
|
150
|
-
function stripTrailingBlankLines(content) {
|
|
151
|
-
return content.replace(/\n+$/g, "");
|
|
152
|
-
}
|
|
153
|
-
function stripLeadingBlankLines(lines) {
|
|
154
|
-
let start = 0;
|
|
155
|
-
while (start < lines.length && lines[start].trim() === "") {
|
|
156
|
-
start += 1;
|
|
157
|
-
}
|
|
158
|
-
return lines.slice(start);
|
|
159
|
-
}
|
|
160
173
|
//# sourceMappingURL=project-registry.js.map
|
|
@@ -6,67 +6,115 @@ const repoRoot = process.cwd();
|
|
|
6
6
|
const sandboxRoot = join(repoRoot, ".test-work", `wiki-project-registry-${process.pid}`);
|
|
7
7
|
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
8
8
|
async function loadModules() {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
return { projectRegistry, wikiFs };
|
|
9
|
+
const projectRegistry = await import(new URL("./project-registry.js", import.meta.url).href);
|
|
10
|
+
const wikiFs = await import(new URL("./fs.js", import.meta.url).href);
|
|
11
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
12
|
+
return { projectRegistry, wikiFs, dbModule };
|
|
13
13
|
}
|
|
14
14
|
function resetSandbox() {
|
|
15
15
|
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
16
16
|
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
17
17
|
}
|
|
18
|
-
test.beforeEach(() => {
|
|
18
|
+
test.beforeEach(async () => {
|
|
19
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
20
|
+
dbModule.closeDb();
|
|
19
21
|
resetSandbox();
|
|
20
22
|
});
|
|
21
|
-
test.after(() => {
|
|
23
|
+
test.after(async () => {
|
|
24
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
25
|
+
dbModule.closeDb();
|
|
22
26
|
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
23
27
|
});
|
|
24
|
-
test("loadRegistry returns an empty object when
|
|
25
|
-
const { projectRegistry } = await loadModules();
|
|
26
|
-
|
|
28
|
+
test("loadRegistry returns an empty object when the projects table is empty", async () => {
|
|
29
|
+
const { projectRegistry, dbModule } = await loadModules();
|
|
30
|
+
try {
|
|
31
|
+
assert.deepEqual(projectRegistry.loadRegistry(), {});
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
dbModule.closeDb();
|
|
35
|
+
}
|
|
27
36
|
});
|
|
28
|
-
test("
|
|
29
|
-
const { projectRegistry, wikiFs } = await loadModules();
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
test("saveRegistry persists the registry in SQLite", async () => {
|
|
38
|
+
const { projectRegistry, dbModule, wikiFs } = await loadModules();
|
|
39
|
+
try {
|
|
40
|
+
projectRegistry.saveRegistry({
|
|
41
|
+
"docs-site": "/home/bjk/projects/docs-site",
|
|
42
|
+
chapterhouse: "/home/bjk/projects/chapterhouse",
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(projectRegistry.loadRegistry(), {
|
|
45
|
+
chapterhouse: "/home/bjk/projects/chapterhouse",
|
|
46
|
+
"docs-site": "/home/bjk/projects/docs-site",
|
|
47
|
+
});
|
|
48
|
+
assert.equal(wikiFs.readPage("pages/projects/index.md"), undefined);
|
|
49
|
+
assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
|
|
50
|
+
{ slug: "chapterhouse", cwd: "/home/bjk/projects/chapterhouse" },
|
|
51
|
+
{ slug: "docs-site", cwd: "/home/bjk/projects/docs-site" },
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
dbModule.closeDb();
|
|
56
|
+
}
|
|
32
57
|
});
|
|
33
|
-
test("
|
|
34
|
-
const { projectRegistry,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
test("saveRegistry replaces prior SQLite-backed registry contents", async () => {
|
|
59
|
+
const { projectRegistry, dbModule } = await loadModules();
|
|
60
|
+
try {
|
|
61
|
+
projectRegistry.saveRegistry({
|
|
62
|
+
alpha: "/srv/alpha",
|
|
63
|
+
zeta: "/srv/zeta",
|
|
64
|
+
});
|
|
65
|
+
projectRegistry.saveRegistry({
|
|
66
|
+
beta: "/srv/beta",
|
|
67
|
+
alpha: "/srv/alpha",
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(projectRegistry.loadRegistry(), {
|
|
70
|
+
alpha: "/srv/alpha",
|
|
71
|
+
beta: "/srv/beta",
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
|
|
74
|
+
{ slug: "alpha", cwd: "/srv/alpha" },
|
|
75
|
+
{ slug: "beta", cwd: "/srv/beta" },
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
dbModule.closeDb();
|
|
80
|
+
}
|
|
40
81
|
});
|
|
41
|
-
test("loadRegistry
|
|
42
|
-
const { projectRegistry, wikiFs } = await loadModules();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
test("loadRegistry migrates the legacy wiki registry into SQLite and removes the file", async () => {
|
|
83
|
+
const { projectRegistry, wikiFs, dbModule } = await loadModules();
|
|
84
|
+
try {
|
|
85
|
+
wikiFs.writePage("pages/projects/index.md", "---\n"
|
|
86
|
+
+ "title: Projects\n"
|
|
87
|
+
+ "summary: Canonical project registry.\n"
|
|
88
|
+
+ "updated: 2026-05-12\n"
|
|
89
|
+
+ "---\n\n"
|
|
90
|
+
+ "# Projects\n\n"
|
|
91
|
+
+ "## Project Registry\n\n"
|
|
92
|
+
+ "```yaml\n"
|
|
93
|
+
+ "chapterhouse: /home/bjk/projects/chapterhouse\n"
|
|
94
|
+
+ "docs-site: /home/bjk/Documents/docs site\n"
|
|
95
|
+
+ "```\n");
|
|
96
|
+
assert.deepEqual(projectRegistry.loadRegistry(), {
|
|
97
|
+
chapterhouse: "/home/bjk/projects/chapterhouse",
|
|
98
|
+
"docs-site": "/home/bjk/Documents/docs site",
|
|
99
|
+
});
|
|
100
|
+
assert.equal(wikiFs.pageExists("pages/projects/index.md"), false);
|
|
101
|
+
assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
|
|
102
|
+
{ slug: "chapterhouse", cwd: "/home/bjk/projects/chapterhouse" },
|
|
103
|
+
{ slug: "docs-site", cwd: "/home/bjk/Documents/docs site" },
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
dbModule.closeDb();
|
|
108
|
+
}
|
|
66
109
|
});
|
|
67
110
|
test("saveRegistry rejects invalid slugs and non-absolute paths", async () => {
|
|
68
|
-
const { projectRegistry } = await loadModules();
|
|
69
|
-
|
|
70
|
-
|
|
111
|
+
const { projectRegistry, dbModule } = await loadModules();
|
|
112
|
+
try {
|
|
113
|
+
assert.throws(() => projectRegistry.saveRegistry({ ChapterHouse: "/home/bjk/projects/chapterhouse" }), /invalid project slug/);
|
|
114
|
+
assert.throws(() => projectRegistry.saveRegistry({ chapterhouse: "./relative" }), /absolute path/);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
dbModule.closeDb();
|
|
118
|
+
}
|
|
71
119
|
});
|
|
72
120
|
//# sourceMappingURL=project-registry.test.js.map
|
package/package.json
CHANGED