chapterhouse 0.3.22 → 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.js +71 -29
- package/dist/api/server.test.js +67 -94
- package/dist/api/worker-events-sse.integration.test.js +336 -0
- package/dist/copilot/orchestrator.js +10 -5
- package/dist/copilot/orchestrator.test.js +6 -1
- package/dist/copilot/task-event-log.js +6 -5
- 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/web/dist/assets/{index-Ch4AYrQP.js → index-BK-hInnO.js} +87 -84
- package/web/dist/assets/index-BK-hInnO.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-Ch4AYrQP.js.map +0 -1
|
@@ -574,10 +574,16 @@ async function executeOnSession(manager, item) {
|
|
|
574
574
|
});
|
|
575
575
|
const unsubSubDoneDb = session.on("subagent.completed", (event) => {
|
|
576
576
|
try {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
577
|
+
const doneData = event.data;
|
|
578
|
+
const taskId = String(doneData.toolCallId ?? "");
|
|
579
|
+
const finalResult = typeof doneData.result?.detailedContent === "string"
|
|
580
|
+
? doneData.result.detailedContent
|
|
581
|
+
: typeof doneData.result?.content === "string"
|
|
582
|
+
? doneData.result.content
|
|
583
|
+
: null;
|
|
584
|
+
spawnArgsMap.delete(taskId);
|
|
585
|
+
activeSubagentTaskIds.delete(taskId);
|
|
586
|
+
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(finalResult?.slice(0, 10000) ?? null, taskId);
|
|
581
587
|
const taskRow = db.prepare(`SELECT agent_slug FROM agent_tasks WHERE task_id = ?`).get(taskId);
|
|
582
588
|
void agentEventBus.emit({
|
|
583
589
|
type: "session:destroyed",
|
|
@@ -587,7 +593,6 @@ async function executeOnSession(manager, item) {
|
|
|
587
593
|
timestamp: new Date(),
|
|
588
594
|
});
|
|
589
595
|
// Emit turn:delta with subagent completed part (coexistence — #130)
|
|
590
|
-
const doneData = event.data;
|
|
591
596
|
const donePart = {
|
|
592
597
|
type: "subagent",
|
|
593
598
|
toolCallId: String(doneData.toolCallId ?? ""),
|
|
@@ -914,7 +914,7 @@ test("S5-01: subagent.started event inserts an adhoc row into agent_tasks", asyn
|
|
|
914
914
|
// Resolve the pending sendAndWait so the test can clean up
|
|
915
915
|
state.pendingReject?.(new Error("test teardown"));
|
|
916
916
|
});
|
|
917
|
-
test("S5-01: subagent.completed event
|
|
917
|
+
test("S5-01: subagent.completed event persists final result text to agent_tasks", async (t) => {
|
|
918
918
|
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
919
919
|
sendResult: "__PENDING__",
|
|
920
920
|
});
|
|
@@ -934,10 +934,15 @@ test("S5-01: subagent.completed event updates agent_tasks status to completed",
|
|
|
934
934
|
agentName: "Wash",
|
|
935
935
|
agentDisplayName: "Wash — Frontend Dev",
|
|
936
936
|
durationMs: 1234,
|
|
937
|
+
result: {
|
|
938
|
+
detailedContent: "Workers tab refreshes with persisted final output",
|
|
939
|
+
},
|
|
937
940
|
});
|
|
938
941
|
const updateWrite = state.dbWrites.find((w) => w.sql.includes("UPDATE") && w.sql.includes("agent_tasks") && w.sql.includes("completed"));
|
|
939
942
|
assert.ok(updateWrite, "subagent.completed must UPDATE agent_tasks to completed");
|
|
943
|
+
assert.ok(updateWrite.sql.includes("result = ?"), "completed subagent updates must persist the final result text");
|
|
940
944
|
assert.ok(JSON.stringify(updateWrite.args).includes("subagent-call-002"), "UPDATE must target the correct task_id");
|
|
945
|
+
assert.ok(JSON.stringify(updateWrite.args).includes("Workers tab refreshes with persisted final output"), "UPDATE must store the final result text in agent_tasks.result");
|
|
941
946
|
state.pendingReject?.(new Error("test teardown"));
|
|
942
947
|
});
|
|
943
948
|
test("S5-01: subagent.failed event updates agent_tasks status to error", async (t) => {
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* in sync without the caller needing to wire anything extra.
|
|
8
8
|
*
|
|
9
9
|
* Consumers:
|
|
10
|
-
* 1. `GET /api/workers/:taskId/events` —
|
|
11
|
-
* The ring buffer is checked first (fast path, in-memory);
|
|
12
|
-
* for completed tasks whose ring buffer has been
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* 1. `GET /api/workers/:taskId/events` — SSE backlog replay on connect and
|
|
11
|
+
* reconnect. The ring buffer is checked first (fast path, in-memory);
|
|
12
|
+
* SQLite is the fallback for completed tasks whose ring buffer has been
|
|
13
|
+
* cleared.
|
|
14
|
+
* 2. Per-task SSE subscribers — `subscribeTaskLog` delivers live events as
|
|
15
|
+
* they arrive so the SSE frame fires immediately (no SQLite round-trip).
|
|
15
16
|
*
|
|
16
17
|
* Lifecycle:
|
|
17
18
|
* - `initTaskEventLog()` must be called once from `initOrchestrator()`.
|
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