chapterhouse 0.5.0 → 0.5.2
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 +31 -3
- package/dist/api/server.test.js +48 -5
- package/dist/cli.js +4 -2
- package/dist/config.js +75 -13
- package/dist/config.test.js +44 -0
- package/dist/copilot/orchestrator.js +17 -6
- package/dist/copilot/orchestrator.test.js +50 -3
- package/dist/copilot/router.js +43 -8
- package/dist/copilot/router.test.js +30 -18
- package/dist/copilot/system-message.js +3 -3
- package/dist/copilot/system-message.test.js +10 -0
- package/dist/copilot/tools.js +79 -12
- package/dist/copilot/tools.memory.test.js +94 -6
- package/dist/daemon.js +7 -2
- package/dist/integrations/team-push.js +8 -1
- package/dist/integrations/teams-notify.js +8 -1
- package/dist/memory/active-scope.test.js +7 -2
- package/dist/memory/eot.js +96 -8
- package/dist/memory/eot.test.js +186 -5
- package/dist/memory/hot-tier.test.js +14 -4
- package/dist/memory/housekeeping.js +73 -25
- package/dist/memory/housekeeping.test.js +115 -16
- package/dist/memory/inbox.test.js +178 -0
- package/dist/memory/scopes.test.js +0 -24
- package/dist/memory/tiering.test.js +323 -0
- package/dist/mode-context.js +28 -0
- package/dist/mode-context.test.js +42 -0
- package/dist/setup.js +152 -95
- package/dist/setup.test.js +122 -0
- package/dist/store/db.js +0 -18
- package/dist/wiki/team-sync.js +8 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-BfHqP3-C.js → index-CPaILy2j.js} +69 -69
- package/web/dist/assets/index-CPaILy2j.js.map +1 -0
- package/web/dist/assets/index-Cs7AGeaL.css +10 -0
- package/web/dist/index.html +2 -2
- package/agents/bellonda.agent.md +0 -11
- package/agents/hwi-noree.agent.md +0 -12
- package/web/dist/assets/index-BfHqP3-C.js.map +0 -1
- package/web/dist/assets/index-_O6AoWOS.css +0 -10
|
@@ -16,14 +16,65 @@ function resetSandbox() {
|
|
|
16
16
|
async function loadModules() {
|
|
17
17
|
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
18
18
|
const memoryModule = await import(new URL("./index.js", import.meta.url).href);
|
|
19
|
-
const housekeepingModule = await import(new URL(
|
|
19
|
+
const housekeepingModule = await import(new URL(`./housekeeping.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
20
20
|
return { dbModule, memoryModule, housekeepingModule };
|
|
21
21
|
}
|
|
22
|
+
async function loadMockedHousekeepingModule(t, options = {}) {
|
|
23
|
+
t.mock.module("../config.js", {
|
|
24
|
+
namedExports: {
|
|
25
|
+
config: {
|
|
26
|
+
memoryDecayDays: 30,
|
|
27
|
+
memoryInboxRetentionDays: 7,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
t.mock.module("../store/db.js", {
|
|
32
|
+
namedExports: {
|
|
33
|
+
getDb: () => {
|
|
34
|
+
throw new Error("getDb should not be called in this test");
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
t.mock.module("../util/logger.js", {
|
|
39
|
+
namedExports: {
|
|
40
|
+
childLogger: () => ({
|
|
41
|
+
info: () => { },
|
|
42
|
+
warn: () => { },
|
|
43
|
+
error: () => { },
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
t.mock.module("./active-scope.js", {
|
|
48
|
+
namedExports: {
|
|
49
|
+
getActiveScope: () => (options.activeScopeId === undefined ? null : { id: options.activeScopeId }),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
t.mock.module("./scopes.js", {
|
|
53
|
+
namedExports: {
|
|
54
|
+
listScopes: () => options.scopes ?? [],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
t.mock.module("./tiering.js", {
|
|
58
|
+
namedExports: {
|
|
59
|
+
tieringPass: (scopeId) => options.tieringPass?.(scopeId) ?? { pass: "tieringPass", examined: scopeId, modified: 1, errors: [] },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return await import(new URL(`./housekeeping.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
63
|
+
}
|
|
22
64
|
function getFunction(module, name) {
|
|
23
65
|
const value = module[name];
|
|
24
66
|
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
25
67
|
return value;
|
|
26
68
|
}
|
|
69
|
+
function createTestScope(memoryModule, slug) {
|
|
70
|
+
const createScope = getFunction(memoryModule, "createScope");
|
|
71
|
+
return createScope({
|
|
72
|
+
slug,
|
|
73
|
+
title: slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "),
|
|
74
|
+
description: `${slug} test scope`,
|
|
75
|
+
keywords: [slug],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
27
78
|
test.beforeEach(async () => {
|
|
28
79
|
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
29
80
|
dbModule.closeDb();
|
|
@@ -42,8 +93,8 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
|
|
|
42
93
|
const getScope = getFunction(memoryModule, "getScope");
|
|
43
94
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
44
95
|
const chapterhouse = getScope("chapterhouse");
|
|
45
|
-
const team =
|
|
46
|
-
assert.ok(chapterhouse
|
|
96
|
+
const team = createTestScope(memoryModule, "team");
|
|
97
|
+
assert.ok(chapterhouse);
|
|
47
98
|
const first = recordObservation({
|
|
48
99
|
scope_id: team.id,
|
|
49
100
|
content: "The worker event stream uses server sent events for live task output.",
|
|
@@ -88,9 +139,8 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
|
|
|
88
139
|
const getScope = getFunction(memoryModule, "getScope");
|
|
89
140
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
90
141
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
91
|
-
const team =
|
|
92
|
-
const infra =
|
|
93
|
-
assert.ok(team && infra);
|
|
142
|
+
const team = createTestScope(memoryModule, "team");
|
|
143
|
+
const infra = createTestScope(memoryModule, "infra");
|
|
94
144
|
const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
|
|
95
145
|
const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
|
|
96
146
|
const oldDecision = recordDecision({
|
|
@@ -139,8 +189,8 @@ test("orphanCleanupPass clears missing observation entity references without tou
|
|
|
139
189
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
140
190
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
141
191
|
const chapterhouse = getScope("chapterhouse");
|
|
142
|
-
const team =
|
|
143
|
-
assert.ok(chapterhouse
|
|
192
|
+
const team = createTestScope(memoryModule, "team");
|
|
193
|
+
assert.ok(chapterhouse);
|
|
144
194
|
const entity = upsertEntity({ scope_id: chapterhouse.id, kind: "tool", name: "sqlite" });
|
|
145
195
|
const valid = recordObservation({ scope_id: chapterhouse.id, entity_id: entity.id, content: "Valid entity reference", source: "test" });
|
|
146
196
|
const orphan = recordObservation({ scope_id: chapterhouse.id, content: "Will become orphaned", source: "test" });
|
|
@@ -164,8 +214,8 @@ test("decayPass archives old low-confidence observations only in scope and compa
|
|
|
164
214
|
const getScope = getFunction(memoryModule, "getScope");
|
|
165
215
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
166
216
|
const chapterhouse = getScope("chapterhouse");
|
|
167
|
-
const team =
|
|
168
|
-
assert.ok(chapterhouse
|
|
217
|
+
const team = createTestScope(memoryModule, "team");
|
|
218
|
+
assert.ok(chapterhouse);
|
|
169
219
|
const archiveMe = recordObservation({ scope_id: chapterhouse.id, content: "Old low confidence", source: "test", confidence: 0.2 });
|
|
170
220
|
const highConfidence = recordObservation({ scope_id: chapterhouse.id, content: "Old high confidence", source: "test", confidence: 0.9 });
|
|
171
221
|
const fresh = recordObservation({ scope_id: chapterhouse.id, content: "Fresh low confidence", source: "test", confidence: 0.2 });
|
|
@@ -201,23 +251,73 @@ test("runHousekeeping defaults to the active scope and can target all active sco
|
|
|
201
251
|
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
202
252
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
203
253
|
const chapterhouse = getScope("chapterhouse");
|
|
204
|
-
const team =
|
|
205
|
-
assert.ok(chapterhouse
|
|
254
|
+
const team = createTestScope(memoryModule, "team");
|
|
255
|
+
assert.ok(chapterhouse);
|
|
206
256
|
const chapterhouseOld = recordObservation({ scope_id: chapterhouse.id, content: "Chapterhouse old low", source: "test", confidence: 0.1 });
|
|
207
257
|
const teamOld = recordObservation({ scope_id: team.id, content: "Team old low", source: "test", confidence: 0.1 });
|
|
208
258
|
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?)`).run(chapterhouseOld.id, teamOld.id);
|
|
209
259
|
setActiveScope("chapterhouse");
|
|
210
|
-
const activeOnly = housekeepingModule.runHousekeeping({ passes: ["decay"] });
|
|
260
|
+
const activeOnly = await housekeepingModule.runHousekeeping({ passes: ["decay"] });
|
|
211
261
|
assert.deepEqual(activeOnly.scopeIds, [chapterhouse.id]);
|
|
212
262
|
assert.equal(activeOnly.summaries.length, 1);
|
|
213
263
|
assert.equal(activeOnly.summaries[0]?.modified, 1);
|
|
214
264
|
assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(chapterhouseOld.id).archived_at);
|
|
215
265
|
assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at, null);
|
|
216
|
-
const allScopes = housekeepingModule.runHousekeeping({ allScopes: true, passes: ["decay"] });
|
|
266
|
+
const allScopes = await housekeepingModule.runHousekeeping({ allScopes: true, passes: ["decay"] });
|
|
217
267
|
assert.ok(allScopes.scopeIds.includes(team.id));
|
|
218
268
|
assert.equal(allScopes.summaries.some((summary) => summary.modified === 1), true);
|
|
219
269
|
assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at);
|
|
220
270
|
});
|
|
271
|
+
test("runHousekeeping starts all scoped passes before awaiting completion", async (t) => {
|
|
272
|
+
const releases = new Map();
|
|
273
|
+
const startedScopes = [];
|
|
274
|
+
const housekeepingModule = await loadMockedHousekeepingModule(t, {
|
|
275
|
+
scopes: [
|
|
276
|
+
{ id: 11, active: true },
|
|
277
|
+
{ id: 22, active: true },
|
|
278
|
+
],
|
|
279
|
+
tieringPass: async (scopeId) => {
|
|
280
|
+
startedScopes.push(scopeId);
|
|
281
|
+
await new Promise((resolve) => {
|
|
282
|
+
releases.set(scopeId, resolve);
|
|
283
|
+
});
|
|
284
|
+
return { pass: `tieringPass:${scopeId}`, examined: 1, modified: 1, errors: [] };
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const pending = housekeepingModule.runHousekeeping({ allScopes: true, passes: ["tiering"] });
|
|
288
|
+
assert.equal(typeof pending.then, "function");
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
assert.deepEqual(startedScopes.sort((left, right) => left - right), [11, 22]);
|
|
291
|
+
releases.get(11)?.();
|
|
292
|
+
releases.get(22)?.();
|
|
293
|
+
const result = await pending;
|
|
294
|
+
assert.deepEqual(result.scopeIds, [11, 22]);
|
|
295
|
+
assert.deepEqual(result.summaries.map((summary) => summary.pass), ["tieringPass:11", "tieringPass:22"]);
|
|
296
|
+
});
|
|
297
|
+
test("runHousekeeping rejects overlapping runs that share an in-flight scope", async (t) => {
|
|
298
|
+
const releases = new Map();
|
|
299
|
+
const housekeepingModule = await loadMockedHousekeepingModule(t, {
|
|
300
|
+
scopes: [
|
|
301
|
+
{ id: 11, active: true },
|
|
302
|
+
{ id: 22, active: true },
|
|
303
|
+
],
|
|
304
|
+
tieringPass: async (scopeId) => {
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
releases.set(scopeId, resolve);
|
|
307
|
+
});
|
|
308
|
+
return { pass: `tieringPass:${scopeId}`, examined: 1, modified: 1, errors: [] };
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
const firstRun = housekeepingModule.runHousekeeping({ scopeIds: [11, 22], passes: ["tiering"] });
|
|
312
|
+
await Promise.resolve();
|
|
313
|
+
assert.equal(housekeepingModule.isHousekeepingInFlight([22], ["tiering"]), true);
|
|
314
|
+
const secondRun = await housekeepingModule.runHousekeeping({ scopeIds: [22], passes: ["tiering"] });
|
|
315
|
+
assert.deepEqual(secondRun.scopeIds, [22]);
|
|
316
|
+
assert.match(secondRun.summaries[0]?.errors[0] ?? "", /already in flight/i);
|
|
317
|
+
releases.get(11)?.();
|
|
318
|
+
releases.get(22)?.();
|
|
319
|
+
await firstRun;
|
|
320
|
+
});
|
|
221
321
|
test("tieringPass promotes and demotes rows from lifecycle signals and is idempotent", async () => {
|
|
222
322
|
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
223
323
|
const db = dbModule.getDb();
|
|
@@ -225,8 +325,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
225
325
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
226
326
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
227
327
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
228
|
-
const team =
|
|
229
|
-
assert.ok(team);
|
|
328
|
+
const team = createTestScope(memoryModule, "team");
|
|
230
329
|
const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
|
|
231
330
|
const referencedObservation = recordObservation({
|
|
232
331
|
scope_id: team.id,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const repoRoot = process.cwd();
|
|
6
|
+
const sandboxRoot = join(repoRoot, ".test-work", `memory-inbox-${process.pid}`);
|
|
7
|
+
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
8
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
9
|
+
function resetSandbox() {
|
|
10
|
+
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
11
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(chapterhouseHome, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
async function loadRealModules(cacheBust = `${Date.now()}-${Math.random()}`) {
|
|
15
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
16
|
+
const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
|
|
17
|
+
const inboxModule = await import(new URL(`./inbox.js?case=${cacheBust}`, import.meta.url).href);
|
|
18
|
+
return { dbModule, memoryModule, inboxModule };
|
|
19
|
+
}
|
|
20
|
+
async function loadMockedInboxModule(t, options) {
|
|
21
|
+
t.mock.module("../store/db.js", {
|
|
22
|
+
namedExports: {
|
|
23
|
+
getDb: () => ({ prepare: options.prepare }),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
t.mock.module("./active-scope.js", {
|
|
27
|
+
namedExports: {
|
|
28
|
+
getActiveScope: () => options.activeScope ?? null,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
t.mock.module("./scopes.js", {
|
|
32
|
+
namedExports: {
|
|
33
|
+
getScope: (slug) => options.explicitScopes?.[slug],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return await import(new URL(`./inbox.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
37
|
+
}
|
|
38
|
+
function getFunction(module, name) {
|
|
39
|
+
const value = module[name];
|
|
40
|
+
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function createTestScope(memoryModule, slug) {
|
|
44
|
+
const createScope = getFunction(memoryModule, "createScope");
|
|
45
|
+
return createScope({
|
|
46
|
+
slug,
|
|
47
|
+
title: slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "),
|
|
48
|
+
description: `${slug} test scope`,
|
|
49
|
+
keywords: [slug],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
test.beforeEach(async () => {
|
|
53
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
54
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
55
|
+
dbModule.closeDb();
|
|
56
|
+
resetSandbox();
|
|
57
|
+
});
|
|
58
|
+
test.after(async () => {
|
|
59
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
60
|
+
dbModule.closeDb();
|
|
61
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
test("resolveProposalScope returns explicit scopes and falls back to the active scope for missing or empty values", async () => {
|
|
64
|
+
const { dbModule, memoryModule, inboxModule } = await loadRealModules("resolve-fallback");
|
|
65
|
+
dbModule.getDb();
|
|
66
|
+
const docs = createTestScope(memoryModule, "docs");
|
|
67
|
+
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
68
|
+
const active = createTestScope(memoryModule, "team-memory");
|
|
69
|
+
setActiveScope(active.slug);
|
|
70
|
+
assert.deepEqual(inboxModule.resolveProposalScope(docs.slug), { scopeId: docs.id, scopeSlug: docs.slug });
|
|
71
|
+
assert.deepEqual(inboxModule.resolveProposalScope(undefined), { scopeId: active.id, scopeSlug: active.slug });
|
|
72
|
+
assert.deepEqual(inboxModule.resolveProposalScope(""), { scopeId: active.id, scopeSlug: active.slug });
|
|
73
|
+
});
|
|
74
|
+
test("resolveProposalScope rejects invalid explicit scopes and missing default scopes", async () => {
|
|
75
|
+
const { dbModule, inboxModule } = await loadRealModules("resolve-errors");
|
|
76
|
+
dbModule.getDb();
|
|
77
|
+
assert.throws(() => inboxModule.resolveProposalScope("does-not-exist"), /Unknown memory scope 'does-not-exist'\./);
|
|
78
|
+
assert.throws(() => inboxModule.resolveProposalScope(), /No active memory scope is set\. Use memory_set_scope or pass scope_slug explicitly\./);
|
|
79
|
+
});
|
|
80
|
+
test("queueMemoryProposal enqueues explicit-scope proposals with the expected envelope defaults", async () => {
|
|
81
|
+
const { dbModule, memoryModule, inboxModule } = await loadRealModules("explicit-queue");
|
|
82
|
+
const db = dbModule.getDb();
|
|
83
|
+
const docs = createTestScope(memoryModule, "docs-queue");
|
|
84
|
+
const queued = inboxModule.queueMemoryProposal({
|
|
85
|
+
kind: "observation",
|
|
86
|
+
scopeSlug: docs.slug,
|
|
87
|
+
payload: {
|
|
88
|
+
content: "Inbox tests should verify the stored proposal envelope.",
|
|
89
|
+
source: "oracle",
|
|
90
|
+
},
|
|
91
|
+
sourceAgent: "oracle",
|
|
92
|
+
sourceTaskId: "task-inbox-explicit",
|
|
93
|
+
});
|
|
94
|
+
assert.equal(typeof queued.id, "number");
|
|
95
|
+
assert.equal(queued.scopeId, docs.id);
|
|
96
|
+
assert.equal(queued.kind, "memory_proposal");
|
|
97
|
+
assert.equal(queued.status, "pending");
|
|
98
|
+
assert.equal(queued.sourceAgent, "oracle");
|
|
99
|
+
assert.equal(queued.sourceTaskId, "task-inbox-explicit");
|
|
100
|
+
assert.match(queued.createdAt, /\d{4}-\d{2}-\d{2}/);
|
|
101
|
+
const persisted = db.prepare(`
|
|
102
|
+
SELECT payload
|
|
103
|
+
FROM mem_inbox
|
|
104
|
+
WHERE id = ?
|
|
105
|
+
`).get(queued.id);
|
|
106
|
+
assert.ok(persisted, "queueMemoryProposal should persist a mem_inbox row");
|
|
107
|
+
assert.deepEqual(JSON.parse(persisted.payload), {
|
|
108
|
+
kind: "observation",
|
|
109
|
+
scope_slug: docs.slug,
|
|
110
|
+
confidence: 0.5,
|
|
111
|
+
payload: {
|
|
112
|
+
content: "Inbox tests should verify the stored proposal envelope.",
|
|
113
|
+
source: "oracle",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
assert.deepEqual(inboxModule.getInboxItem(queued.id), queued);
|
|
117
|
+
});
|
|
118
|
+
test("queueMemoryProposal falls back to the active scope, does not deduplicate duplicates, and pending lists exclude resolved items", async () => {
|
|
119
|
+
const { dbModule, memoryModule, inboxModule } = await loadRealModules("active-scope-queue");
|
|
120
|
+
dbModule.getDb();
|
|
121
|
+
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
122
|
+
setActiveScope("chapterhouse");
|
|
123
|
+
const first = inboxModule.queueMemoryProposal({
|
|
124
|
+
kind: "observation",
|
|
125
|
+
scopeSlug: "",
|
|
126
|
+
payload: { content: "Duplicate proposal payloads should still queue separately." },
|
|
127
|
+
confidence: 0.8,
|
|
128
|
+
reason: "First proposal",
|
|
129
|
+
sourceAgent: "oracle",
|
|
130
|
+
sourceTaskId: "task-inbox-duplicates",
|
|
131
|
+
});
|
|
132
|
+
const second = inboxModule.queueMemoryProposal({
|
|
133
|
+
kind: "observation",
|
|
134
|
+
payload: { content: "Duplicate proposal payloads should still queue separately." },
|
|
135
|
+
confidence: 0.8,
|
|
136
|
+
reason: "Second proposal",
|
|
137
|
+
sourceAgent: "oracle",
|
|
138
|
+
sourceTaskId: "task-inbox-duplicates",
|
|
139
|
+
});
|
|
140
|
+
assert.notEqual(first.id, second.id);
|
|
141
|
+
assert.equal(first.scopeId, second.scopeId);
|
|
142
|
+
assert.deepEqual(inboxModule.listPendingMemoryProposalsForTask("task-inbox-duplicates").map((item) => item.id), [first.id, second.id]);
|
|
143
|
+
inboxModule.resolveInboxItem(first.id, "accepted", "Durable enough to keep");
|
|
144
|
+
const resolved = inboxModule.getInboxItem(first.id);
|
|
145
|
+
assert.equal(resolved?.status, "accepted");
|
|
146
|
+
assert.equal(resolved?.resolutionReason, "Durable enough to keep");
|
|
147
|
+
assert.match(resolved?.resolvedAt ?? "", /\d{4}-\d{2}-\d{2}/);
|
|
148
|
+
assert.deepEqual(inboxModule.listPendingMemoryProposalsForTask("task-inbox-duplicates").map((item) => item.id), [second.id]);
|
|
149
|
+
});
|
|
150
|
+
test("queueMemoryProposal surfaces insert failures such as a full queue", async (t) => {
|
|
151
|
+
const fullError = new Error("database or disk is full");
|
|
152
|
+
const seenStatements = [];
|
|
153
|
+
const inboxModule = await loadMockedInboxModule(t, {
|
|
154
|
+
activeScope: { id: 99, slug: "chapterhouse" },
|
|
155
|
+
prepare: (sql) => {
|
|
156
|
+
seenStatements.push(sql);
|
|
157
|
+
if (sql.includes("INSERT INTO mem_inbox")) {
|
|
158
|
+
return {
|
|
159
|
+
run: () => {
|
|
160
|
+
throw fullError;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
run: () => ({ changes: 0 }),
|
|
166
|
+
get: () => undefined,
|
|
167
|
+
all: () => [],
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
assert.throws(() => inboxModule.queueMemoryProposal({
|
|
172
|
+
kind: "observation",
|
|
173
|
+
payload: { content: "This proposal should fail before it is persisted." },
|
|
174
|
+
sourceAgent: "oracle",
|
|
175
|
+
}), /database or disk is full/);
|
|
176
|
+
assert.equal(seenStatements.some((sql) => sql.includes("INSERT INTO mem_inbox")), true);
|
|
177
|
+
});
|
|
178
|
+
//# sourceMappingURL=inbox.test.js.map
|
|
@@ -68,13 +68,6 @@ test("getDb seeds canonical memory scopes on first run", async () => {
|
|
|
68
68
|
ORDER BY slug
|
|
69
69
|
`).all();
|
|
70
70
|
assert.deepEqual(rows, [
|
|
71
|
-
{
|
|
72
|
-
slug: "brian",
|
|
73
|
-
title: "Brian",
|
|
74
|
-
description: "Brian's preferences, context, working style",
|
|
75
|
-
keywords: JSON.stringify(["brian"]),
|
|
76
|
-
active: 1,
|
|
77
|
-
},
|
|
78
71
|
{
|
|
79
72
|
slug: "chapterhouse",
|
|
80
73
|
title: "Chapterhouse",
|
|
@@ -89,20 +82,6 @@ test("getDb seeds canonical memory scopes on first run", async () => {
|
|
|
89
82
|
keywords: JSON.stringify(["everywhere", "general"]),
|
|
90
83
|
active: 1,
|
|
91
84
|
},
|
|
92
|
-
{
|
|
93
|
-
slug: "infra",
|
|
94
|
-
title: "Infra",
|
|
95
|
-
description: "Infrastructure, hosting, deployment, CI/CD",
|
|
96
|
-
keywords: JSON.stringify(["infra"]),
|
|
97
|
-
active: 1,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
slug: "team",
|
|
101
|
-
title: "Team",
|
|
102
|
-
description: "Team processes, rituals, OKRs",
|
|
103
|
-
keywords: JSON.stringify(["team"]),
|
|
104
|
-
active: 1,
|
|
105
|
-
},
|
|
106
85
|
]);
|
|
107
86
|
}
|
|
108
87
|
finally {
|
|
@@ -123,11 +102,8 @@ test("memory schema initialization is idempotent", async () => {
|
|
|
123
102
|
ORDER BY slug
|
|
124
103
|
`).all();
|
|
125
104
|
assert.deepEqual(counts, [
|
|
126
|
-
{ slug: "brian", count: 1 },
|
|
127
105
|
{ slug: "chapterhouse", count: 1 },
|
|
128
106
|
{ slug: "global", count: 1 },
|
|
129
|
-
{ slug: "infra", count: 1 },
|
|
130
|
-
{ slug: "team", count: 1 },
|
|
131
107
|
]);
|
|
132
108
|
reopened.dbModule.closeDb();
|
|
133
109
|
}
|