@remnic/core 9.3.651 → 9.3.653
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/access-cli.js +10 -10
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +12 -12
- package/dist/access-mcp.d.ts +3 -2
- package/dist/access-mcp.js +11 -11
- package/dist/access-schema.d.ts +3 -0
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-DIZRHQ7Q.d.ts → access-service-CdJFd3_b.d.ts} +23 -2
- package/dist/access-service.d.ts +3 -2
- package/dist/access-service.js +9 -9
- package/dist/bootstrap.d.ts +2 -1
- package/dist/briefing.js +1 -1
- package/dist/{chunk-QT4THOLT.js → chunk-2DGQLOOM.js} +1 -1
- package/dist/chunk-2DGQLOOM.js.map +1 -0
- package/dist/{chunk-FOVPSMGI.js → chunk-7WEB3FLJ.js} +2 -2
- package/dist/{chunk-IJHLC5CH.js → chunk-BNFRL6QW.js} +31 -21
- package/dist/{chunk-IJHLC5CH.js.map → chunk-BNFRL6QW.js.map} +1 -1
- package/dist/{chunk-SLYD3AH4.js → chunk-E3J6O6N7.js} +177 -5
- package/dist/chunk-E3J6O6N7.js.map +1 -0
- package/dist/{chunk-MF32AL7N.js → chunk-EW52H5EM.js} +4 -4
- package/dist/{chunk-WJK75OCH.js → chunk-GI45G4BK.js} +2 -2
- package/dist/{chunk-76QTEJ2Q.js → chunk-JBHXMCYN.js} +2 -2
- package/dist/{chunk-4PTKFBST.js → chunk-JVRPJ7D4.js} +126 -26
- package/dist/chunk-JVRPJ7D4.js.map +1 -0
- package/dist/{chunk-TQUWNX7C.js → chunk-JX2RINDR.js} +2 -2
- package/dist/{chunk-RSS2KWN6.js → chunk-NOBL7OUP.js} +14 -6
- package/dist/chunk-NOBL7OUP.js.map +1 -0
- package/dist/{chunk-I4COC5XW.js → chunk-PYWNNF2I.js} +47 -9
- package/dist/chunk-PYWNNF2I.js.map +1 -0
- package/dist/{chunk-RKN5J4RO.js → chunk-QQHIQ7JD.js} +6 -6
- package/dist/{chunk-5WSDHTBO.js → chunk-SPMZZUEJ.js} +8 -5
- package/dist/chunk-SPMZZUEJ.js.map +1 -0
- package/dist/{chunk-4HYSMH7D.js → chunk-UAU5U5ML.js} +3 -2
- package/dist/chunk-UAU5U5ML.js.map +1 -0
- package/dist/{chunk-LFTLXOFX.js → chunk-WTI35CVJ.js} +2 -2
- package/dist/{chunk-6UKL6IXM.js → chunk-YM3LR4LS.js} +5 -5
- package/dist/{cli-BG4ybtJr.d.ts → cli-DDo7Qgs-.d.ts} +2 -2
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +15 -15
- package/dist/explicit-capture.d.ts +2 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +16 -16
- package/dist/mcp-memory-inspector-app.d.ts +3 -2
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/search.d.ts +18 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/operator-toolkit.js +9 -9
- package/dist/{orchestrator-CX-oqwJq.d.ts → orchestrator-8fTZsa0y.d.ts} +2 -0
- package/dist/orchestrator.d.ts +2 -1
- package/dist/orchestrator.js +9 -9
- package/dist/qmd.d.ts +2 -1
- package/dist/qmd.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +6 -6
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +6 -0
- package/dist/search/port.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/access-mcp.test.ts +70 -1
- package/src/access-mcp.ts +12 -2
- package/src/access-schema.ts +1 -0
- package/src/access-service-health.test.ts +402 -0
- package/src/access-service.ts +274 -2
- package/src/briefing.test.ts +70 -0
- package/src/briefing.ts +30 -20
- package/src/namespaces/search.test.ts +258 -3
- package/src/namespaces/search.ts +184 -30
- package/src/orchestrator.ts +11 -1
- package/src/qmd.test.ts +102 -0
- package/src/qmd.ts +54 -7
- package/src/search/port.ts +6 -0
- package/dist/chunk-4HYSMH7D.js.map +0 -1
- package/dist/chunk-4PTKFBST.js.map +0 -1
- package/dist/chunk-5WSDHTBO.js.map +0 -1
- package/dist/chunk-I4COC5XW.js.map +0 -1
- package/dist/chunk-QT4THOLT.js.map +0 -1
- package/dist/chunk-RSS2KWN6.js.map +0 -1
- package/dist/chunk-SLYD3AH4.js.map +0 -1
- /package/dist/{chunk-FOVPSMGI.js.map → chunk-7WEB3FLJ.js.map} +0 -0
- /package/dist/{chunk-MF32AL7N.js.map → chunk-EW52H5EM.js.map} +0 -0
- /package/dist/{chunk-WJK75OCH.js.map → chunk-GI45G4BK.js.map} +0 -0
- /package/dist/{chunk-76QTEJ2Q.js.map → chunk-JBHXMCYN.js.map} +0 -0
- /package/dist/{chunk-TQUWNX7C.js.map → chunk-JX2RINDR.js.map} +0 -0
- /package/dist/{chunk-RKN5J4RO.js.map → chunk-QQHIQ7JD.js.map} +0 -0
- /package/dist/{chunk-LFTLXOFX.js.map → chunk-WTI35CVJ.js.map} +0 -0
- /package/dist/{chunk-6UKL6IXM.js.map → chunk-YM3LR4LS.js.map} +0 -0
package/src/access-mcp.test.ts
CHANGED
|
@@ -371,11 +371,80 @@ test("MCP capsule tools tolerate client-injected cwd/projectTag (#1434)", async
|
|
|
371
371
|
}
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
+
test("MCP capsule list tolerates client-injected sessionKey (#1513)", async () => {
|
|
375
|
+
let received: Record<string, unknown> | undefined;
|
|
376
|
+
const service = {
|
|
377
|
+
...makeMockService(),
|
|
378
|
+
capsuleList: async (args: Record<string, unknown>) => {
|
|
379
|
+
received = args;
|
|
380
|
+
return { capsules: [] };
|
|
381
|
+
},
|
|
382
|
+
} as unknown as EngramAccessService;
|
|
383
|
+
const server = new EngramMcpServer(service);
|
|
384
|
+
|
|
385
|
+
const response = await server.handleRequest(
|
|
386
|
+
makeToolRequest("engram.capsule_list", {
|
|
387
|
+
namespace: "team",
|
|
388
|
+
sessionKey: "pi-injected-session",
|
|
389
|
+
cwd: "/x",
|
|
390
|
+
projectTag: "t",
|
|
391
|
+
}),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
assert.deepEqual(received, {
|
|
395
|
+
namespace: "team",
|
|
396
|
+
principal: undefined,
|
|
397
|
+
});
|
|
398
|
+
assert.equal((response as Record<string, unknown> & { result?: { isError?: boolean } }).result?.isError, false);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("MCP capsule list derives namespace principal from client-injected sessionKey (#1513)", async () => {
|
|
402
|
+
let received: Record<string, unknown> | undefined;
|
|
403
|
+
const service = {
|
|
404
|
+
...makeMockService(),
|
|
405
|
+
configRef: parseConfig({
|
|
406
|
+
memoryDir: "/tmp/remnic-mcp-capsule-list-session-principal",
|
|
407
|
+
namespacesEnabled: true,
|
|
408
|
+
defaultNamespace: "default",
|
|
409
|
+
principalFromSessionKeyMode: "map",
|
|
410
|
+
principalFromSessionKeyRules: [{ match: "pi-session", principal: "pi-agent" }],
|
|
411
|
+
namespacePolicies: [
|
|
412
|
+
{ name: "team", readPrincipals: ["pi-agent"], writePrincipals: [] },
|
|
413
|
+
],
|
|
414
|
+
}),
|
|
415
|
+
capsuleList: async (args: Record<string, unknown>) => {
|
|
416
|
+
received = args;
|
|
417
|
+
return { capsules: [] };
|
|
418
|
+
},
|
|
419
|
+
} as unknown as EngramAccessService;
|
|
420
|
+
const server = new EngramMcpServer(service);
|
|
421
|
+
|
|
422
|
+
const response = await server.handleRequest(
|
|
423
|
+
makeToolRequest("engram.capsule_list", {
|
|
424
|
+
namespace: "team",
|
|
425
|
+
sessionKey: "pi-session",
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
assert.deepEqual(received, {
|
|
430
|
+
namespace: "team",
|
|
431
|
+
principal: "pi-agent",
|
|
432
|
+
});
|
|
433
|
+
assert.equal((response as Record<string, unknown> & { result?: { isError?: boolean } }).result?.isError, false);
|
|
434
|
+
});
|
|
435
|
+
|
|
374
436
|
test("MCP session override is injected only into tools that accept sessionKey", async () => {
|
|
375
437
|
let capsuleListArgs: Record<string, unknown> | undefined;
|
|
376
438
|
let observeArgs: Record<string, unknown> | undefined;
|
|
377
439
|
const service = {
|
|
378
440
|
...makeMockService(),
|
|
441
|
+
configRef: parseConfig({
|
|
442
|
+
memoryDir: "/tmp/remnic-mcp-session-override-test",
|
|
443
|
+
namespacesEnabled: true,
|
|
444
|
+
defaultNamespace: "default",
|
|
445
|
+
principalFromSessionKeyMode: "map",
|
|
446
|
+
principalFromSessionKeyRules: [{ match: "adapter-session", principal: "adapter-agent" }],
|
|
447
|
+
}),
|
|
379
448
|
capsuleList: async (args: Record<string, unknown>) => {
|
|
380
449
|
capsuleListArgs = args;
|
|
381
450
|
return { capsules: [] };
|
|
@@ -400,7 +469,7 @@ test("MCP session override is injected only into tools that accept sessionKey",
|
|
|
400
469
|
|
|
401
470
|
assert.deepEqual(capsuleListArgs, {
|
|
402
471
|
namespace: undefined,
|
|
403
|
-
principal:
|
|
472
|
+
principal: "adapter-agent",
|
|
404
473
|
});
|
|
405
474
|
assert.deepEqual(observeArgs, {
|
|
406
475
|
sessionKey: "adapter-session",
|
package/src/access-mcp.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { validateBriefingFormat } from "./briefing.js";
|
|
|
20
20
|
import { buildCitationGuidance, type CitationMetadata } from "./citations.js";
|
|
21
21
|
import { projectTagProjectId } from "./coding/coding-namespace.js";
|
|
22
22
|
import { expandTildePath } from "./utils/path.js";
|
|
23
|
+
import { resolvePrincipal } from "./namespaces/principal.js";
|
|
23
24
|
import {
|
|
24
25
|
REMNIC_CHATGPT_MEMORY_INSPECTOR_MIME_TYPE,
|
|
25
26
|
REMNIC_CHATGPT_MEMORY_INSPECTOR_TOOL,
|
|
@@ -155,7 +156,7 @@ const STRICT_MCP_SCHEMA_KEYS: Partial<Record<SchemaName, readonly string[]>> = {
|
|
|
155
156
|
"projectTag",
|
|
156
157
|
],
|
|
157
158
|
capsuleImport: ["archivePath", "namespace", "mode", "passphrase", "cwd", "projectTag"],
|
|
158
|
-
capsuleList: ["namespace", "cwd", "projectTag"],
|
|
159
|
+
capsuleList: ["namespace", "sessionKey", "cwd", "projectTag"],
|
|
159
160
|
};
|
|
160
161
|
|
|
161
162
|
// Shared JSON-schema fragments for the client-injected git/project context
|
|
@@ -852,6 +853,10 @@ export class EngramMcpServer {
|
|
|
852
853
|
type: "object",
|
|
853
854
|
properties: {
|
|
854
855
|
namespace: { type: "string" },
|
|
856
|
+
sessionKey: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "Optional session key used to derive namespace principal when no trusted transport principal is present.",
|
|
859
|
+
},
|
|
855
860
|
...MCP_GIT_CONTEXT_SCHEMA_PROPS_IGNORED,
|
|
856
861
|
},
|
|
857
862
|
additionalProperties: false,
|
|
@@ -2694,9 +2699,14 @@ export class EngramMcpServer {
|
|
|
2694
2699
|
}
|
|
2695
2700
|
case "engram.capsule_list": {
|
|
2696
2701
|
const body: CapsuleListRequest = parseMcpRequest("capsuleList", args);
|
|
2702
|
+
const requestPrincipal =
|
|
2703
|
+
effectivePrincipal ??
|
|
2704
|
+
(body.sessionKey && this.service.configRef
|
|
2705
|
+
? resolvePrincipal(body.sessionKey, this.service.configRef)
|
|
2706
|
+
: undefined);
|
|
2697
2707
|
return this.service.capsuleList({
|
|
2698
2708
|
namespace: body.namespace,
|
|
2699
|
-
principal:
|
|
2709
|
+
principal: requestPrincipal,
|
|
2700
2710
|
});
|
|
2701
2711
|
}
|
|
2702
2712
|
case "engram.memory_governance_run":
|
package/src/access-schema.ts
CHANGED
|
@@ -410,6 +410,7 @@ export const capsuleImportRequestSchema = z
|
|
|
410
410
|
export const capsuleListRequestSchema = z
|
|
411
411
|
.object({
|
|
412
412
|
namespace: namespaceSchema,
|
|
413
|
+
sessionKey: sessionKeySchema,
|
|
413
414
|
});
|
|
414
415
|
|
|
415
416
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { EngramAccessService } from "./access-service.js";
|
|
8
|
+
import { parseConfig } from "./config.js";
|
|
9
|
+
import { namespaceCollectionName } from "./namespaces/search.js";
|
|
10
|
+
import type { Orchestrator } from "./orchestrator.js";
|
|
11
|
+
import type { SearchBackend } from "./search/port.js";
|
|
12
|
+
|
|
13
|
+
function makeQmd(overrides: Partial<SearchBackend> & Record<string, unknown>): SearchBackend {
|
|
14
|
+
return {
|
|
15
|
+
async probe() {
|
|
16
|
+
return false;
|
|
17
|
+
},
|
|
18
|
+
isAvailable() {
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
debugStatus() {
|
|
22
|
+
return "backend=noop";
|
|
23
|
+
},
|
|
24
|
+
async search() {
|
|
25
|
+
return [];
|
|
26
|
+
},
|
|
27
|
+
async searchGlobal() {
|
|
28
|
+
return [];
|
|
29
|
+
},
|
|
30
|
+
async bm25Search() {
|
|
31
|
+
return [];
|
|
32
|
+
},
|
|
33
|
+
async vectorSearch() {
|
|
34
|
+
return [];
|
|
35
|
+
},
|
|
36
|
+
async hybridSearch() {
|
|
37
|
+
return [];
|
|
38
|
+
},
|
|
39
|
+
async update() {},
|
|
40
|
+
async updateCollection() {},
|
|
41
|
+
async embed() {},
|
|
42
|
+
async embedCollection() {},
|
|
43
|
+
async ensureCollection() {
|
|
44
|
+
return "skipped";
|
|
45
|
+
},
|
|
46
|
+
...overrides,
|
|
47
|
+
} as SearchBackend;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("health reports active QMD version and collection state", async () => {
|
|
51
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-"));
|
|
52
|
+
try {
|
|
53
|
+
const config = parseConfig({
|
|
54
|
+
memoryDir,
|
|
55
|
+
searchBackend: "qmd",
|
|
56
|
+
qmdEnabled: true,
|
|
57
|
+
qmdCollection: "remnic-memory",
|
|
58
|
+
});
|
|
59
|
+
const qmd = makeQmd({
|
|
60
|
+
probe: async () => true,
|
|
61
|
+
isAvailable: () => true,
|
|
62
|
+
debugStatus: () => "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
63
|
+
checkCollection: async (collection, execution) => {
|
|
64
|
+
assert.equal(collection, "remnic-memory");
|
|
65
|
+
assert.ok(execution?.signal instanceof AbortSignal);
|
|
66
|
+
return "present";
|
|
67
|
+
},
|
|
68
|
+
getVersionStatus: () => ({
|
|
69
|
+
installedVersion: "qmd 2.5.3",
|
|
70
|
+
supportedVersion: "2.5.3",
|
|
71
|
+
supported: true,
|
|
72
|
+
newerThanSupported: false,
|
|
73
|
+
upgradeAvailable: false,
|
|
74
|
+
capabilities: { doctor: true },
|
|
75
|
+
}),
|
|
76
|
+
isDaemonMode: () => false,
|
|
77
|
+
});
|
|
78
|
+
const service = new EngramAccessService({
|
|
79
|
+
config,
|
|
80
|
+
qmd,
|
|
81
|
+
async getStorage() {
|
|
82
|
+
return { dir: memoryDir };
|
|
83
|
+
},
|
|
84
|
+
} as unknown as Orchestrator);
|
|
85
|
+
|
|
86
|
+
const health = await service.health();
|
|
87
|
+
|
|
88
|
+
assert.equal(health.qmdEnabled, true);
|
|
89
|
+
assert.deepEqual(health.qmd, {
|
|
90
|
+
enabled: true,
|
|
91
|
+
active: true,
|
|
92
|
+
degraded: false,
|
|
93
|
+
mode: "cli",
|
|
94
|
+
collection: "remnic-memory",
|
|
95
|
+
collectionState: "present",
|
|
96
|
+
installedVersion: "qmd 2.5.3",
|
|
97
|
+
supportedVersion: "2.5.3",
|
|
98
|
+
supported: true,
|
|
99
|
+
upgradeAvailable: false,
|
|
100
|
+
doctorAvailable: true,
|
|
101
|
+
debugStatus: "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
102
|
+
});
|
|
103
|
+
} finally {
|
|
104
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("health marks QMD as degraded when configured search falls back to noop", async () => {
|
|
109
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-degraded-"));
|
|
110
|
+
try {
|
|
111
|
+
const config = parseConfig({
|
|
112
|
+
memoryDir,
|
|
113
|
+
searchBackend: "qmd",
|
|
114
|
+
qmdEnabled: true,
|
|
115
|
+
qmdCollection: "remnic-memory",
|
|
116
|
+
});
|
|
117
|
+
const service = new EngramAccessService({
|
|
118
|
+
config,
|
|
119
|
+
qmd: makeQmd({}),
|
|
120
|
+
async getStorage() {
|
|
121
|
+
return { dir: memoryDir };
|
|
122
|
+
},
|
|
123
|
+
} as unknown as Orchestrator);
|
|
124
|
+
|
|
125
|
+
const health = await service.health();
|
|
126
|
+
|
|
127
|
+
assert.equal(health.qmd.active, false);
|
|
128
|
+
assert.equal(health.qmd.degraded, true);
|
|
129
|
+
assert.equal(health.qmd.mode, "fallback");
|
|
130
|
+
assert.equal(health.qmd.collectionState, "unknown");
|
|
131
|
+
assert.equal(health.qmd.debugStatus, "backend=noop");
|
|
132
|
+
} finally {
|
|
133
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("health marks QMD as degraded when the checked collection is missing", async () => {
|
|
138
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-missing-"));
|
|
139
|
+
try {
|
|
140
|
+
const config = parseConfig({
|
|
141
|
+
memoryDir,
|
|
142
|
+
searchBackend: "qmd",
|
|
143
|
+
qmdEnabled: true,
|
|
144
|
+
qmdCollection: "remnic-memory",
|
|
145
|
+
});
|
|
146
|
+
const service = new EngramAccessService({
|
|
147
|
+
config,
|
|
148
|
+
qmd: makeQmd({
|
|
149
|
+
probe: async () => true,
|
|
150
|
+
isAvailable: () => true,
|
|
151
|
+
debugStatus: () => "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
152
|
+
checkCollection: async () => "missing",
|
|
153
|
+
isDaemonMode: () => false,
|
|
154
|
+
}),
|
|
155
|
+
async getStorage() {
|
|
156
|
+
return { dir: memoryDir };
|
|
157
|
+
},
|
|
158
|
+
} as unknown as Orchestrator);
|
|
159
|
+
|
|
160
|
+
const health = await service.health();
|
|
161
|
+
|
|
162
|
+
assert.equal(health.qmd.collectionState, "missing");
|
|
163
|
+
assert.equal(health.qmd.active, false);
|
|
164
|
+
assert.equal(health.qmd.degraded, true);
|
|
165
|
+
assert.equal(health.qmd.mode, "fallback");
|
|
166
|
+
} finally {
|
|
167
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("health reports degraded diagnostics when read-only root QMD probe fails", async () => {
|
|
172
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-stale-probe-"));
|
|
173
|
+
try {
|
|
174
|
+
const config = parseConfig({
|
|
175
|
+
memoryDir,
|
|
176
|
+
searchBackend: "qmd",
|
|
177
|
+
qmdEnabled: true,
|
|
178
|
+
qmdCollection: "remnic-memory",
|
|
179
|
+
});
|
|
180
|
+
const service = new EngramAccessService({
|
|
181
|
+
config,
|
|
182
|
+
qmd: makeQmd({
|
|
183
|
+
probe: async () => false,
|
|
184
|
+
isAvailable: () => true,
|
|
185
|
+
debugStatus: () => "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
186
|
+
checkCollection: async () => "present",
|
|
187
|
+
isDaemonMode: () => false,
|
|
188
|
+
}),
|
|
189
|
+
async getStorage() {
|
|
190
|
+
return { dir: memoryDir };
|
|
191
|
+
},
|
|
192
|
+
} as unknown as Orchestrator);
|
|
193
|
+
|
|
194
|
+
const health = await service.health();
|
|
195
|
+
|
|
196
|
+
assert.equal(health.qmd.active, true);
|
|
197
|
+
assert.equal(health.qmd.degraded, true);
|
|
198
|
+
assert.equal(health.qmd.mode, "cli");
|
|
199
|
+
assert.equal(health.qmd.collectionState, "unknown");
|
|
200
|
+
} finally {
|
|
201
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("health uses read-only root QMD availability checks", async () => {
|
|
206
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-readonly-"));
|
|
207
|
+
try {
|
|
208
|
+
const config = parseConfig({
|
|
209
|
+
memoryDir,
|
|
210
|
+
searchBackend: "qmd",
|
|
211
|
+
qmdEnabled: true,
|
|
212
|
+
qmdCollection: "remnic-memory",
|
|
213
|
+
});
|
|
214
|
+
let checkAvailabilityCalled = false;
|
|
215
|
+
let probeCalled = false;
|
|
216
|
+
const service = new EngramAccessService({
|
|
217
|
+
config,
|
|
218
|
+
qmd: makeQmd({
|
|
219
|
+
probe: async () => {
|
|
220
|
+
probeCalled = true;
|
|
221
|
+
return true;
|
|
222
|
+
},
|
|
223
|
+
checkAvailability: async (execution) => {
|
|
224
|
+
checkAvailabilityCalled = true;
|
|
225
|
+
assert.ok(execution?.signal instanceof AbortSignal);
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
isAvailable: () => true,
|
|
229
|
+
debugStatus: () => "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
230
|
+
checkCollection: async () => "present",
|
|
231
|
+
isDaemonMode: () => false,
|
|
232
|
+
}),
|
|
233
|
+
async getStorage() {
|
|
234
|
+
return { dir: memoryDir };
|
|
235
|
+
},
|
|
236
|
+
} as unknown as Orchestrator);
|
|
237
|
+
|
|
238
|
+
const health = await service.health();
|
|
239
|
+
|
|
240
|
+
assert.equal(checkAvailabilityCalled, true);
|
|
241
|
+
assert.equal(probeCalled, false);
|
|
242
|
+
assert.equal(health.qmd.active, true);
|
|
243
|
+
assert.equal(health.qmd.degraded, false);
|
|
244
|
+
} finally {
|
|
245
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("health reports namespace-scoped QMD backend state", async () => {
|
|
250
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-ns-"));
|
|
251
|
+
try {
|
|
252
|
+
const teamDir = path.join(rootDir, "namespaces", "team");
|
|
253
|
+
const config = parseConfig({
|
|
254
|
+
memoryDir: rootDir,
|
|
255
|
+
namespacesEnabled: true,
|
|
256
|
+
defaultNamespace: "default",
|
|
257
|
+
searchBackend: "qmd",
|
|
258
|
+
qmdEnabled: true,
|
|
259
|
+
qmdCollection: "remnic-memory",
|
|
260
|
+
namespacePolicies: [{ name: "team", readPrincipals: [], writePrincipals: [] }],
|
|
261
|
+
});
|
|
262
|
+
const expectedCollection = namespaceCollectionName("remnic-memory", "team", {
|
|
263
|
+
defaultNamespace: "default",
|
|
264
|
+
});
|
|
265
|
+
const service = new EngramAccessService({
|
|
266
|
+
config,
|
|
267
|
+
qmd: makeQmd({}),
|
|
268
|
+
async getStorage(namespace: string) {
|
|
269
|
+
return { dir: namespace === "team" ? teamDir : rootDir };
|
|
270
|
+
},
|
|
271
|
+
async searchHealthForNamespace(namespace: string, execution?: { signal?: AbortSignal }) {
|
|
272
|
+
assert.equal(namespace, "team");
|
|
273
|
+
assert.ok(execution?.signal instanceof AbortSignal);
|
|
274
|
+
return {
|
|
275
|
+
collection: expectedCollection,
|
|
276
|
+
available: true,
|
|
277
|
+
collectionState: "present",
|
|
278
|
+
debugStatus: "cli=true daemon=false cliPath=qmd cliVersion=qmd 2.5.3",
|
|
279
|
+
installedVersion: "qmd 2.5.3",
|
|
280
|
+
supportedVersion: "2.5.3",
|
|
281
|
+
supported: true,
|
|
282
|
+
upgradeAvailable: false,
|
|
283
|
+
doctorAvailable: true,
|
|
284
|
+
daemonMode: false,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
} as unknown as Orchestrator);
|
|
288
|
+
|
|
289
|
+
const health = await service.health("team");
|
|
290
|
+
|
|
291
|
+
assert.equal(health.memoryDir, teamDir);
|
|
292
|
+
assert.equal(health.qmd.active, true);
|
|
293
|
+
assert.equal(health.qmd.degraded, false);
|
|
294
|
+
assert.equal(health.qmd.collection, expectedCollection);
|
|
295
|
+
assert.equal(health.qmd.collectionState, "present");
|
|
296
|
+
assert.equal(health.qmd.installedVersion, "qmd 2.5.3");
|
|
297
|
+
} finally {
|
|
298
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("health marks namespace QMD unknown collection state degraded", async () => {
|
|
303
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-ns-unknown-"));
|
|
304
|
+
try {
|
|
305
|
+
const teamDir = path.join(rootDir, "namespaces", "team");
|
|
306
|
+
const config = parseConfig({
|
|
307
|
+
memoryDir: rootDir,
|
|
308
|
+
namespacesEnabled: true,
|
|
309
|
+
defaultNamespace: "default",
|
|
310
|
+
searchBackend: "qmd",
|
|
311
|
+
qmdEnabled: true,
|
|
312
|
+
qmdCollection: "remnic-memory",
|
|
313
|
+
namespacePolicies: [{ name: "team", readPrincipals: [], writePrincipals: [] }],
|
|
314
|
+
});
|
|
315
|
+
const expectedCollection = namespaceCollectionName("remnic-memory", "team", {
|
|
316
|
+
defaultNamespace: "default",
|
|
317
|
+
});
|
|
318
|
+
const service = new EngramAccessService({
|
|
319
|
+
config,
|
|
320
|
+
qmd: makeQmd({}),
|
|
321
|
+
async getStorage(namespace: string) {
|
|
322
|
+
return { dir: namespace === "team" ? teamDir : rootDir };
|
|
323
|
+
},
|
|
324
|
+
async searchHealthForNamespace(namespace: string, execution?: { signal?: AbortSignal }) {
|
|
325
|
+
assert.equal(namespace, "team");
|
|
326
|
+
assert.ok(execution?.signal instanceof AbortSignal);
|
|
327
|
+
return {
|
|
328
|
+
collection: expectedCollection,
|
|
329
|
+
available: true,
|
|
330
|
+
collectionState: "unknown",
|
|
331
|
+
debugStatus: "cli=true collection=unknown",
|
|
332
|
+
installedVersion: "qmd 2.5.3",
|
|
333
|
+
supportedVersion: "2.5.3",
|
|
334
|
+
supported: true,
|
|
335
|
+
upgradeAvailable: false,
|
|
336
|
+
doctorAvailable: true,
|
|
337
|
+
daemonMode: false,
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
} as unknown as Orchestrator);
|
|
341
|
+
|
|
342
|
+
const health = await service.health("team");
|
|
343
|
+
|
|
344
|
+
assert.equal(health.memoryDir, teamDir);
|
|
345
|
+
assert.equal(health.qmd.active, true);
|
|
346
|
+
assert.equal(health.qmd.degraded, true);
|
|
347
|
+
assert.equal(health.qmd.collection, expectedCollection);
|
|
348
|
+
assert.equal(health.qmd.collectionState, "unknown");
|
|
349
|
+
assert.equal(health.qmd.mode, "cli");
|
|
350
|
+
} finally {
|
|
351
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("health keeps namespace QMD failures scoped to the namespace", async () => {
|
|
356
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "remnic-health-qmd-ns-fail-"));
|
|
357
|
+
try {
|
|
358
|
+
const teamDir = path.join(rootDir, "namespaces", "team");
|
|
359
|
+
const config = parseConfig({
|
|
360
|
+
memoryDir: rootDir,
|
|
361
|
+
namespacesEnabled: true,
|
|
362
|
+
defaultNamespace: "default",
|
|
363
|
+
searchBackend: "qmd",
|
|
364
|
+
qmdEnabled: true,
|
|
365
|
+
qmdCollection: "remnic-memory",
|
|
366
|
+
namespacePolicies: [{ name: "team", readPrincipals: [], writePrincipals: [] }],
|
|
367
|
+
});
|
|
368
|
+
const expectedCollection = namespaceCollectionName("remnic-memory", "team", {
|
|
369
|
+
defaultNamespace: "default",
|
|
370
|
+
});
|
|
371
|
+
const service = new EngramAccessService({
|
|
372
|
+
config,
|
|
373
|
+
qmd: makeQmd({
|
|
374
|
+
probe: async () => true,
|
|
375
|
+
isAvailable: () => true,
|
|
376
|
+
debugStatus: () => "root-qmd-should-not-leak",
|
|
377
|
+
checkCollection: async () => "present",
|
|
378
|
+
}),
|
|
379
|
+
async getStorage(namespace: string) {
|
|
380
|
+
return { dir: namespace === "team" ? teamDir : rootDir };
|
|
381
|
+
},
|
|
382
|
+
async searchHealthForNamespace() {
|
|
383
|
+
throw new Error(`namespace probe timed out at ${teamDir}/qmd/index.sqlite`);
|
|
384
|
+
},
|
|
385
|
+
} as unknown as Orchestrator);
|
|
386
|
+
|
|
387
|
+
const health = await service.health("team");
|
|
388
|
+
|
|
389
|
+
assert.equal(health.memoryDir, teamDir);
|
|
390
|
+
assert.equal(health.qmd.collection, expectedCollection);
|
|
391
|
+
assert.equal(health.qmd.active, false);
|
|
392
|
+
assert.equal(health.qmd.degraded, true);
|
|
393
|
+
assert.equal(health.qmd.collectionState, "unknown");
|
|
394
|
+
assert.equal(health.qmd.mode, "fallback");
|
|
395
|
+
assert.equal(health.qmd.debugStatus, "backend=namespace-unavailable error=Error");
|
|
396
|
+
assert.doesNotMatch(health.qmd.debugStatus, /root-qmd-should-not-leak/);
|
|
397
|
+
assert.doesNotMatch(health.qmd.debugStatus, /index\.sqlite/);
|
|
398
|
+
assert.doesNotMatch(health.qmd.debugStatus, /remnic-health-qmd-ns-fail/);
|
|
399
|
+
} finally {
|
|
400
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
});
|