@remnic/core 9.3.704 → 9.3.705
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-boundary.d.ts +1 -1
- package/dist/access-boundary.js +25 -25
- package/dist/access-cli.js +53 -52
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +30 -29
- package/dist/access-mcp.js +29 -28
- package/dist/access-operations-batch.d.ts +2 -0
- package/dist/access-operations-batch.js +120 -0
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-operations.js +28 -27
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +24 -24
- package/dist/access-surface-catalog.js +57 -54
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/{auto-sync-3AAP25FV.js → auto-sync-24NNS3GU.js} +3 -3
- package/dist/briefing.js +4 -4
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.js +8 -8
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-PJ5MDXOF.js → chunk-2SYPOX45.js} +10 -10
- package/dist/{chunk-7G4KPGR3.js → chunk-47LPHBIP.js} +2 -2
- package/dist/{chunk-RYDT7LAI.js → chunk-5DTWZ66X.js} +2 -2
- package/dist/chunk-5DTWZ66X.js.map +1 -0
- package/dist/{chunk-OCFNG4LB.js → chunk-6GJ2WQRK.js} +2 -2
- package/dist/{chunk-4NFVPDIL.js → chunk-74MYJSKB.js} +5 -5
- package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
- package/dist/{chunk-DE2OPIQD.js → chunk-7ARKLLCV.js} +6 -6
- package/dist/{chunk-3EHCV6YV.js → chunk-7L54YZUI.js} +63 -63
- package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
- package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
- package/dist/{chunk-S24J5LLQ.js → chunk-BKBSE3BU.js} +3 -3
- package/dist/{chunk-AE3C4PY6.js → chunk-BMKGG7GC.js} +2 -2
- package/dist/{chunk-NMW3T6T6.js → chunk-C2EHWNP2.js} +9 -9
- package/dist/{chunk-VKJHM6PL.js → chunk-DGTDRIUO.js} +5 -5
- package/dist/chunk-GI3MTYDU.js +1303 -0
- package/dist/chunk-GI3MTYDU.js.map +1 -0
- package/dist/{chunk-25FHSOEB.js → chunk-GT4VBKM4.js} +59 -5
- package/dist/chunk-GT4VBKM4.js.map +1 -0
- package/dist/{chunk-OOEVMJK4.js → chunk-HEHPVBWA.js} +6 -6
- package/dist/{chunk-YOI3ELXF.js → chunk-IF362TF6.js} +2 -2
- package/dist/{chunk-ABCGDW3A.js → chunk-IFR23TC5.js} +4 -4
- package/dist/{chunk-ZXFNSKPY.js → chunk-JHAYR2JY.js} +32 -32
- package/dist/{chunk-CTY6QSLZ.js → chunk-JZCKDTAS.js} +4 -4
- package/dist/{chunk-CTY6QSLZ.js.map → chunk-JZCKDTAS.js.map} +1 -1
- package/dist/{chunk-6AFVIOVZ.js → chunk-K22IJO4R.js} +2 -2
- package/dist/{chunk-QV5WFNZ2.js → chunk-KFU2SKDD.js} +2 -2
- package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
- package/dist/{chunk-TUPNAHHQ.js → chunk-KZMMAVR2.js} +2 -2
- package/dist/{chunk-6Q7JFY4P.js → chunk-LY4FJLPT.js} +2 -2
- package/dist/{chunk-XXDOJ5NL.js → chunk-M5MWSG4S.js} +7 -7
- package/dist/{chunk-YLB7NMLD.js → chunk-MNH2OODZ.js} +2 -2
- package/dist/{chunk-ORXVO6TH.js → chunk-NFGJBEPT.js} +6 -6
- package/dist/{chunk-QDIVMS25.js → chunk-NUWZFSKC.js} +2 -2
- package/dist/{chunk-6GHWF4QH.js → chunk-OM6ETLLH.js} +2 -2
- package/dist/{chunk-53EEQYE5.js → chunk-OMSTU233.js} +4 -4
- package/dist/{chunk-R64MNFPV.js → chunk-S3WYRPSA.js} +59 -59
- package/dist/{chunk-UOIRJ45Z.js → chunk-S5APSA3U.js} +2 -2
- package/dist/{chunk-WAHI7SPK.js → chunk-SY6WG6DB.js} +2 -2
- package/dist/{chunk-K7QRDDSR.js → chunk-ULVM34NP.js} +5 -5
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-QHNJVWF6.js → chunk-Y4PMATX2.js} +6 -6
- package/dist/{chunk-KF74X62T.js → chunk-YGKUAX2B.js} +4 -4
- package/dist/{chunk-YN4ZT4CW.js → chunk-YI7RZO7Y.js} +1 -1
- package/dist/cli.js +53 -52
- package/dist/compounding/engine.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/entity-retrieval.js +4 -4
- package/dist/extraction.js +2 -2
- package/dist/{graph-edge-decay-D7OESCBR.js → graph-edge-decay-JLOF3YW4.js} +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.js +95 -94
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/maintenance.js +6 -6
- package/dist/orchestrator.js +40 -40
- package/dist/resume-bundles.js +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/storage.js +3 -3
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/access-boundary.ts +102 -1
- package/src/access-http.ts +54 -0
- package/src/access-operations-batch.ts +1468 -0
- package/src/access-operations.ts +4 -0
- package/src/access-surface-catalog.test.ts +17 -1
- package/src/access-surface-catalog.ts +57 -54
- package/src/storage-contract/atomicity.test.ts +113 -0
- package/src/storage-contract/cache-coherence.test.ts +100 -0
- package/src/storage-contract/content-hash.test.ts +102 -0
- package/src/storage-contract/failure-semantics.test.ts +108 -0
- package/src/storage-contract/harness.ts +148 -0
- package/src/storage-contract/namespace-routing.test.ts +135 -0
- package/src/storage-contract/round-trip.test.ts +226 -0
- package/src/storage-contract/surface.test.ts +90 -0
- package/src/storage-contract/versioning.test.ts +139 -0
- package/dist/chunk-25FHSOEB.js.map +0 -1
- package/dist/chunk-RYDT7LAI.js.map +0 -1
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-operations-batch.js.map} +0 -0
- /package/dist/{auto-sync-3AAP25FV.js.map → auto-sync-24NNS3GU.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-PJ5MDXOF.js.map → chunk-2SYPOX45.js.map} +0 -0
- /package/dist/{chunk-7G4KPGR3.js.map → chunk-47LPHBIP.js.map} +0 -0
- /package/dist/{chunk-OCFNG4LB.js.map → chunk-6GJ2WQRK.js.map} +0 -0
- /package/dist/{chunk-4NFVPDIL.js.map → chunk-74MYJSKB.js.map} +0 -0
- /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
- /package/dist/{chunk-DE2OPIQD.js.map → chunk-7ARKLLCV.js.map} +0 -0
- /package/dist/{chunk-3EHCV6YV.js.map → chunk-7L54YZUI.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
- /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
- /package/dist/{chunk-S24J5LLQ.js.map → chunk-BKBSE3BU.js.map} +0 -0
- /package/dist/{chunk-AE3C4PY6.js.map → chunk-BMKGG7GC.js.map} +0 -0
- /package/dist/{chunk-NMW3T6T6.js.map → chunk-C2EHWNP2.js.map} +0 -0
- /package/dist/{chunk-VKJHM6PL.js.map → chunk-DGTDRIUO.js.map} +0 -0
- /package/dist/{chunk-OOEVMJK4.js.map → chunk-HEHPVBWA.js.map} +0 -0
- /package/dist/{chunk-YOI3ELXF.js.map → chunk-IF362TF6.js.map} +0 -0
- /package/dist/{chunk-ABCGDW3A.js.map → chunk-IFR23TC5.js.map} +0 -0
- /package/dist/{chunk-ZXFNSKPY.js.map → chunk-JHAYR2JY.js.map} +0 -0
- /package/dist/{chunk-6AFVIOVZ.js.map → chunk-K22IJO4R.js.map} +0 -0
- /package/dist/{chunk-QV5WFNZ2.js.map → chunk-KFU2SKDD.js.map} +0 -0
- /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-TUPNAHHQ.js.map → chunk-KZMMAVR2.js.map} +0 -0
- /package/dist/{chunk-6Q7JFY4P.js.map → chunk-LY4FJLPT.js.map} +0 -0
- /package/dist/{chunk-XXDOJ5NL.js.map → chunk-M5MWSG4S.js.map} +0 -0
- /package/dist/{chunk-YLB7NMLD.js.map → chunk-MNH2OODZ.js.map} +0 -0
- /package/dist/{chunk-ORXVO6TH.js.map → chunk-NFGJBEPT.js.map} +0 -0
- /package/dist/{chunk-QDIVMS25.js.map → chunk-NUWZFSKC.js.map} +0 -0
- /package/dist/{chunk-6GHWF4QH.js.map → chunk-OM6ETLLH.js.map} +0 -0
- /package/dist/{chunk-53EEQYE5.js.map → chunk-OMSTU233.js.map} +0 -0
- /package/dist/{chunk-R64MNFPV.js.map → chunk-S3WYRPSA.js.map} +0 -0
- /package/dist/{chunk-UOIRJ45Z.js.map → chunk-S5APSA3U.js.map} +0 -0
- /package/dist/{chunk-WAHI7SPK.js.map → chunk-SY6WG6DB.js.map} +0 -0
- /package/dist/{chunk-K7QRDDSR.js.map → chunk-ULVM34NP.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-QHNJVWF6.js.map → chunk-Y4PMATX2.js.map} +0 -0
- /package/dist/{chunk-KF74X62T.js.map → chunk-YGKUAX2B.js.map} +0 -0
- /package/dist/{chunk-YN4ZT4CW.js.map → chunk-YI7RZO7Y.js.map} +0 -0
- /package/dist/{graph-edge-decay-D7OESCBR.js.map → graph-edge-decay-JLOF3YW4.js.map} +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Storage contract test harness.
|
|
3
|
+
*
|
|
4
|
+
* Shared fixture helpers for the storage public-surface contract suite AND
|
|
5
|
+
* the #1522 catalog-touch fitness test (both live under `storage-contract/`
|
|
6
|
+
* per the issue's coordination note: "build both on ONE shared harness so the
|
|
7
|
+
* write-entry-point enumeration lives in exactly one place").
|
|
8
|
+
*
|
|
9
|
+
* Every helper creates a fresh temp directory and returns a `cleanup` that
|
|
10
|
+
* tears it down — tests never share state. `StorageManager.clearAllStaticCaches()`
|
|
11
|
+
* is called on setup and teardown so the module-level caches (readAllMemories
|
|
12
|
+
* in-flight, version sentinels, secure-store entity cache) do not leak between
|
|
13
|
+
* tests.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { StorageManager } from "../storage.js";
|
|
21
|
+
import {
|
|
22
|
+
NamespaceStorageRouter,
|
|
23
|
+
} from "../namespaces/storage.js";
|
|
24
|
+
import type { PluginConfig } from "../types.js";
|
|
25
|
+
|
|
26
|
+
/** Clear every module-level cache so tests start from a known-empty state. */
|
|
27
|
+
export function resetStaticCaches(): void {
|
|
28
|
+
StorageManager.clearAllStaticCaches();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a StorageManager backed by a fresh temp directory.
|
|
33
|
+
*
|
|
34
|
+
* Returns the manager, the on-disk base directory, and a `cleanup` that removes
|
|
35
|
+
* the directory and clears static caches. Callers MUST call `cleanup` in a
|
|
36
|
+
* `try/finally` (or via `node:test`'s `t.after`).
|
|
37
|
+
*/
|
|
38
|
+
export async function makeStorage(
|
|
39
|
+
prefix = "remnic-storage-contract-",
|
|
40
|
+
): Promise<{
|
|
41
|
+
storage: StorageManager;
|
|
42
|
+
baseDir: string;
|
|
43
|
+
cleanup: () => Promise<void>;
|
|
44
|
+
}> {
|
|
45
|
+
resetStaticCaches();
|
|
46
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
47
|
+
const storage = new StorageManager(baseDir);
|
|
48
|
+
await storage.ensureDirectories();
|
|
49
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
50
|
+
return {
|
|
51
|
+
storage,
|
|
52
|
+
baseDir,
|
|
53
|
+
cleanup: async () => {
|
|
54
|
+
resetStaticCaches();
|
|
55
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a minimal valid memory markdown body for raw seeding (bypasses
|
|
62
|
+
* writeMemory so the test controls exact on-disk bytes).
|
|
63
|
+
*/
|
|
64
|
+
export function rawMemoryMarkdown(
|
|
65
|
+
id: string,
|
|
66
|
+
category: string,
|
|
67
|
+
content: string,
|
|
68
|
+
extra: Record<string, string> = {},
|
|
69
|
+
): string {
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const lines = [
|
|
72
|
+
"---",
|
|
73
|
+
`id: ${id}`,
|
|
74
|
+
`category: ${category}`,
|
|
75
|
+
`created: ${now}`,
|
|
76
|
+
`updated: ${now}`,
|
|
77
|
+
"source: test",
|
|
78
|
+
"confidence: 0.9",
|
|
79
|
+
'tags: ["contract"]',
|
|
80
|
+
];
|
|
81
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
82
|
+
lines.push(`${key}: ${value}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push("---", "", content, "");
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Seed a raw markdown file at a path relative to `baseDir`. Used when a test
|
|
90
|
+
* needs to control exact placement (e.g. a specific category dir + date nest)
|
|
91
|
+
* without going through `writeMemory`.
|
|
92
|
+
*/
|
|
93
|
+
export async function seedRawFile(
|
|
94
|
+
baseDir: string,
|
|
95
|
+
relPath: string,
|
|
96
|
+
content: string,
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
const fullPath = path.join(baseDir, relPath);
|
|
99
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
100
|
+
await writeFile(fullPath, content, "utf-8");
|
|
101
|
+
return fullPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Minimal PluginConfig for NamespaceStorageRouter tests. Only the fields the
|
|
106
|
+
* router reads are populated; the rest default to values that keep the router
|
|
107
|
+
* in the namespaces-enabled mode the tests exercise.
|
|
108
|
+
*/
|
|
109
|
+
export function makeNamespaceConfig(
|
|
110
|
+
memoryDir: string,
|
|
111
|
+
overrides: Record<string, unknown> = {},
|
|
112
|
+
): PluginConfig {
|
|
113
|
+
return {
|
|
114
|
+
memoryDir,
|
|
115
|
+
namespacesEnabled: true,
|
|
116
|
+
defaultNamespace: "default",
|
|
117
|
+
entitySchemas: {},
|
|
118
|
+
inlineSourceAttributionFormat: undefined,
|
|
119
|
+
...overrides,
|
|
120
|
+
} as unknown as PluginConfig;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a NamespaceStorageRouter over a fresh temp dir with namespaces
|
|
125
|
+
* enabled. Returns the router, the memory dir, and a cleanup.
|
|
126
|
+
*/
|
|
127
|
+
export async function makeNamespaceRouter(
|
|
128
|
+
overrides: Partial<PluginConfig> = {},
|
|
129
|
+
): Promise<{
|
|
130
|
+
router: NamespaceStorageRouter;
|
|
131
|
+
memoryDir: string;
|
|
132
|
+
config: PluginConfig;
|
|
133
|
+
cleanup: () => Promise<void>;
|
|
134
|
+
}> {
|
|
135
|
+
resetStaticCaches();
|
|
136
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-ns-contract-"));
|
|
137
|
+
const config = makeNamespaceConfig(memoryDir, overrides);
|
|
138
|
+
const router = new NamespaceStorageRouter(config);
|
|
139
|
+
return {
|
|
140
|
+
router,
|
|
141
|
+
memoryDir,
|
|
142
|
+
config,
|
|
143
|
+
cleanup: async () => {
|
|
144
|
+
resetStaticCaches();
|
|
145
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: namespace routing.
|
|
3
|
+
*
|
|
4
|
+
* NamespaceStorageRouter must route the SAME operation through the correct
|
|
5
|
+
* on-disk root for default vs named namespaces, while rejecting escaping paths
|
|
6
|
+
* (the containment invariants from #1506 — a FILE at the namespaces root or at
|
|
7
|
+
* a token dir is rejected via statSync().isDirectory(), not existsSync; rule 24).
|
|
8
|
+
*
|
|
9
|
+
* Contracts pinned:
|
|
10
|
+
* - default namespace → memoryDir (legacy root)
|
|
11
|
+
* - named namespace → memoryDir/namespaces/<token>
|
|
12
|
+
* - unsafe namespace segments (/, \, .., absolute) are rejected
|
|
13
|
+
* - writes in one namespace are invisible to another
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import test from "node:test";
|
|
20
|
+
|
|
21
|
+
import { makeStorage, makeNamespaceRouter } from "./harness.js";
|
|
22
|
+
import { resolveNamespaceStorageRoot } from "../namespaces/storage.js";
|
|
23
|
+
import { namespaceIdentityToken } from "../namespaces/identity.js";
|
|
24
|
+
|
|
25
|
+
test("namespace-routing: default namespace resolves to the legacy memoryDir root", async () => {
|
|
26
|
+
const { router, memoryDir, config, cleanup } = await makeNamespaceRouter();
|
|
27
|
+
try {
|
|
28
|
+
const root = await resolveNamespaceStorageRoot(config, "default");
|
|
29
|
+
assert.equal(root, memoryDir);
|
|
30
|
+
|
|
31
|
+
const sm = await router.storageFor("default");
|
|
32
|
+
assert.equal(sm.dir, memoryDir);
|
|
33
|
+
} finally {
|
|
34
|
+
await cleanup();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("namespace-routing: named namespace resolves to a tokenized subdirectory", async () => {
|
|
39
|
+
const { router, memoryDir, config, cleanup } = await makeNamespaceRouter();
|
|
40
|
+
try {
|
|
41
|
+
const ns = "team-alpha";
|
|
42
|
+
const root = await resolveNamespaceStorageRoot(config, ns);
|
|
43
|
+
const token = namespaceIdentityToken(ns);
|
|
44
|
+
assert.ok(
|
|
45
|
+
root.includes(path.join("namespaces", token)),
|
|
46
|
+
`named namespace root ${root} should include namespaces/${token}`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const sm = await router.storageFor(ns);
|
|
50
|
+
assert.ok(sm.dir.startsWith(memoryDir), "named namespace must be under memoryDir");
|
|
51
|
+
assert.ok(sm.dir.includes("namespaces"), "named namespace must be under namespaces/");
|
|
52
|
+
} finally {
|
|
53
|
+
await cleanup();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("namespace-routing: two named namespaces resolve to DIFFERENT roots", async () => {
|
|
58
|
+
const { router, cleanup } = await makeNamespaceRouter();
|
|
59
|
+
try {
|
|
60
|
+
const sm1 = await router.storageFor("team-alpha");
|
|
61
|
+
const sm2 = await router.storageFor("team-beta");
|
|
62
|
+
assert.notEqual(sm1.dir, sm2.dir, "different namespaces must have different roots");
|
|
63
|
+
} finally {
|
|
64
|
+
await cleanup();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("namespace-routing: writes in one namespace are invisible to another (isolation)", async () => {
|
|
69
|
+
const { router, cleanup } = await makeNamespaceRouter();
|
|
70
|
+
try {
|
|
71
|
+
const sm1 = await router.storageFor("team-alpha");
|
|
72
|
+
const id = await sm1.writeMemory("fact", "alpha-only fact");
|
|
73
|
+
|
|
74
|
+
const sm2 = await router.storageFor("team-beta");
|
|
75
|
+
const found = await sm2.getMemoryById(id);
|
|
76
|
+
assert.equal(found, null, "beta namespace must not see alpha's memories");
|
|
77
|
+
} finally {
|
|
78
|
+
await cleanup();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("namespace-routing: resolveNamespaceStorageRoot rejects traversal segments", async () => {
|
|
83
|
+
const { config, cleanup } = await makeNamespaceRouter();
|
|
84
|
+
try {
|
|
85
|
+
for (const bad of ["../escape", "a/b", "a\\b", "/abs"]) {
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
() => resolveNamespaceStorageRoot(config, bad),
|
|
88
|
+
/unsafe namespace path segment/,
|
|
89
|
+
`should reject ${bad}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
await cleanup();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("namespace-routing: storageFor rejects unsafe non-default namespaces", async () => {
|
|
98
|
+
const { router, cleanup } = await makeNamespaceRouter();
|
|
99
|
+
try {
|
|
100
|
+
for (const bad of ["../escape", "a/b"]) {
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
() => router.storageFor(bad),
|
|
103
|
+
/unsafe namespace/,
|
|
104
|
+
`should reject ${bad}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
await cleanup();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("namespace-routing: namespaces disabled → all namespaces use memoryDir", async () => {
|
|
113
|
+
const { router, memoryDir, cleanup } = await makeNamespaceRouter({
|
|
114
|
+
namespacesEnabled: false,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const sm1 = await router.storageFor("default");
|
|
118
|
+
const sm2 = await router.storageFor("anything");
|
|
119
|
+
assert.equal(sm1.dir, memoryDir);
|
|
120
|
+
assert.equal(sm2.dir, memoryDir);
|
|
121
|
+
} finally {
|
|
122
|
+
await cleanup();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("namespace-routing: router caches StorageManager per namespace (same instance on repeat)", async () => {
|
|
127
|
+
const { router, cleanup } = await makeNamespaceRouter();
|
|
128
|
+
try {
|
|
129
|
+
const sm1 = await router.storageFor("team-alpha");
|
|
130
|
+
const sm2 = await router.storageFor("team-alpha");
|
|
131
|
+
assert.equal(sm1, sm2, "storageFor must return the cached instance for the same namespace");
|
|
132
|
+
} finally {
|
|
133
|
+
await cleanup();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: write/read/list/delete round-trips.
|
|
3
|
+
*
|
|
4
|
+
* For EVERY category directory (iterated from ALL_CATEGORY_DIRS, never hardcoded
|
|
5
|
+
* — rule 53's cousin), exercises the documented public surface end to end:
|
|
6
|
+
*
|
|
7
|
+
* writeMemory → readMemoryByPath → getMemoryById → readAllMemories → invalidateMemory
|
|
8
|
+
*
|
|
9
|
+
* Asserts content fidelity, frontmatter integrity, hash identity, and that
|
|
10
|
+
* deleted memories disappear from every read path. This is the round-trip
|
|
11
|
+
* contract that the 51 importers depend on; Phase B moves MUST keep it green.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import type { MemoryCategory } from "../types.js";
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { StorageManager } from "../storage.js";
|
|
19
|
+
import {
|
|
20
|
+
ALL_CATEGORY_DIRS,
|
|
21
|
+
ALL_CATEGORY_KEYS,
|
|
22
|
+
CATEGORY_DIR_MAP,
|
|
23
|
+
RECALL_NON_MEMORY_DIRS,
|
|
24
|
+
categoryDirName,
|
|
25
|
+
} from "../utils/category-dir.js";
|
|
26
|
+
import { makeStorage } from "./harness.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The singular category keys writeMemory accepts, minus "entity" (entities use
|
|
30
|
+
* a separate write path: writeEntity) and "question" (questions are a
|
|
31
|
+
* non-memory QUEUE dir — RECALL_NON_MEMORY_DIRS — surfaced through
|
|
32
|
+
* writeQuestion/readQuestions, NOT readAllMemories; see question-queue test).
|
|
33
|
+
*/
|
|
34
|
+
const ROUND_TRIP_CATEGORIES = ALL_CATEGORY_KEYS.filter(
|
|
35
|
+
(k) => k !== "entity" && k !== "question",
|
|
36
|
+
) as MemoryCategory[];
|
|
37
|
+
|
|
38
|
+
test("round-trip: writeMemory returns a stable id and persists to the category dir", async () => {
|
|
39
|
+
const { storage, cleanup } = await makeStorage();
|
|
40
|
+
try {
|
|
41
|
+
const id = await storage.writeMemory("fact", "The sky is blue", {
|
|
42
|
+
tags: ["contract"],
|
|
43
|
+
});
|
|
44
|
+
assert.equal(typeof id, "string");
|
|
45
|
+
assert.match(id, /^fact-/);
|
|
46
|
+
|
|
47
|
+
const byId = await storage.getMemoryById(id);
|
|
48
|
+
assert.ok(byId, "getMemoryById must find the just-written memory");
|
|
49
|
+
assert.equal(byId!.frontmatter.id, id);
|
|
50
|
+
assert.equal(byId!.content, "The sky is blue");
|
|
51
|
+
} finally {
|
|
52
|
+
await cleanup();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("round-trip: readAllMemories returns written memories from every category dir", async () => {
|
|
57
|
+
const { storage, cleanup } = await makeStorage();
|
|
58
|
+
try {
|
|
59
|
+
const written: string[] = [];
|
|
60
|
+
for (const category of ROUND_TRIP_CATEGORIES) {
|
|
61
|
+
const id = await storage.writeMemory(
|
|
62
|
+
category,
|
|
63
|
+
`content for ${category}`,
|
|
64
|
+
{ confidence: 0.9 },
|
|
65
|
+
);
|
|
66
|
+
written.push(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const all = await storage.readAllMemories();
|
|
70
|
+
const ids = new Set(all.map((m) => m.frontmatter.id));
|
|
71
|
+
for (const id of written) {
|
|
72
|
+
assert.ok(ids.has(id), `readAllMemories missing memory ${id} (${ROUND_TRIP_CATEGORIES.find((c) => id.startsWith(c))})`);
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
await cleanup();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
for (const category of ROUND_TRIP_CATEGORIES) {
|
|
80
|
+
test(`round-trip [${category}]: write → read-by-path → read-by-id → list → delete`, async () => {
|
|
81
|
+
const { storage, cleanup } = await makeStorage();
|
|
82
|
+
try {
|
|
83
|
+
const content = `fact body for category ${category}`;
|
|
84
|
+
const id = await storage.writeMemory(category, content, {
|
|
85
|
+
tags: ["round-trip"],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// getMemoryById
|
|
89
|
+
const byId = await storage.getMemoryById(id);
|
|
90
|
+
assert.ok(byId, "getMemoryById returned null");
|
|
91
|
+
assert.equal(byId!.content, content);
|
|
92
|
+
assert.equal(byId!.frontmatter.category, category);
|
|
93
|
+
|
|
94
|
+
// readMemoryByPath — the path comes from the byId result
|
|
95
|
+
const byPath = await storage.readMemoryByPath(byId!.path);
|
|
96
|
+
assert.ok(byPath, "readMemoryByPath returned null");
|
|
97
|
+
assert.equal(byPath!.content, content);
|
|
98
|
+
assert.equal(byPath!.frontmatter.id, id);
|
|
99
|
+
|
|
100
|
+
// The file landed under the right category dir
|
|
101
|
+
const expectedDir = categoryDirName(category);
|
|
102
|
+
assert.ok(
|
|
103
|
+
byId!.path.includes(`/${expectedDir}/`),
|
|
104
|
+
`${category} memory landed in ${byId!.path}, expected under ${expectedDir}/`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// list contains it
|
|
108
|
+
const all = await storage.readAllMemories();
|
|
109
|
+
assert.ok(
|
|
110
|
+
all.some((m) => m.frontmatter.id === id),
|
|
111
|
+
"readAllMemories did not include the written memory",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// delete (invalidate) removes it from every read path
|
|
115
|
+
const deleted = await storage.invalidateMemory(id);
|
|
116
|
+
assert.equal(deleted, true, "invalidateMemory should return true on success");
|
|
117
|
+
|
|
118
|
+
const afterDelete = await storage.getMemoryById(id);
|
|
119
|
+
assert.equal(afterDelete, null, "getMemoryById must return null after invalidateMemory");
|
|
120
|
+
|
|
121
|
+
const allAfter = await storage.readAllMemories();
|
|
122
|
+
assert.ok(
|
|
123
|
+
!allAfter.some((m) => m.frontmatter.id === id),
|
|
124
|
+
"readAllMemories must not include deleted memory",
|
|
125
|
+
);
|
|
126
|
+
} finally {
|
|
127
|
+
await cleanup();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
test("round-trip: invalidateMemory returns false for a non-existent id (not a throw)", async () => {
|
|
133
|
+
const { storage, cleanup } = await makeStorage();
|
|
134
|
+
try {
|
|
135
|
+
const result = await storage.invalidateMemory("does-not-exist-12345");
|
|
136
|
+
assert.equal(result, false);
|
|
137
|
+
} finally {
|
|
138
|
+
await cleanup();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("round-trip: getMemoryById returns null for a non-existent id", async () => {
|
|
143
|
+
const { storage, cleanup } = await makeStorage();
|
|
144
|
+
try {
|
|
145
|
+
const result = await storage.getMemoryById("missing-id");
|
|
146
|
+
assert.equal(result, null);
|
|
147
|
+
} finally {
|
|
148
|
+
await cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("round-trip: readMemoryByPath returns null for a non-existent path", async () => {
|
|
153
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
154
|
+
try {
|
|
155
|
+
const result = await storage.readMemoryByPath(
|
|
156
|
+
`${baseDir}/facts/2026-01-01/nonexistent.md`,
|
|
157
|
+
);
|
|
158
|
+
assert.equal(result, null);
|
|
159
|
+
} finally {
|
|
160
|
+
await cleanup();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("round-trip: ALL_CATEGORY_DIRS is exactly facts + every CATEGORY_DIR_MAP value", () => {
|
|
165
|
+
// ALL_CATEGORY_DIRS must contain directory NAMES (facts, decisions, ...) not
|
|
166
|
+
// singular keys (fact, decision, ...). This guards the parity between
|
|
167
|
+
// categoryDirName() and the scan roots — a divergence would silently drop
|
|
168
|
+
// a category from recall.
|
|
169
|
+
const expected = new Set(["facts", ...Object.values(CATEGORY_DIR_MAP)]);
|
|
170
|
+
assert.deepEqual(new Set(ALL_CATEGORY_DIRS), expected);
|
|
171
|
+
// No singular keys leaked in
|
|
172
|
+
for (const dir of ALL_CATEGORY_DIRS) {
|
|
173
|
+
assert.ok(!Object.hasOwn(CATEGORY_DIR_MAP, dir),
|
|
174
|
+
`${dir} is a singular category key, not a directory name`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("round-trip: StorageManager.dir exposes the base directory", async () => {
|
|
179
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
180
|
+
try {
|
|
181
|
+
assert.equal(storage.dir, baseDir);
|
|
182
|
+
} finally {
|
|
183
|
+
await cleanup();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("question-queue: writeQuestion → readQuestions → resolveQuestion round-trip (questions are NOT in readAllMemories)", async () => {
|
|
188
|
+
const { storage, cleanup } = await makeStorage();
|
|
189
|
+
try {
|
|
190
|
+
const id = await storage.writeQuestion("What database are we on?", "infra context", 3);
|
|
191
|
+
assert.equal(typeof id, "string");
|
|
192
|
+
assert.match(id, /^q-/);
|
|
193
|
+
|
|
194
|
+
// readQuestions returns it
|
|
195
|
+
const questions = await storage.readQuestions();
|
|
196
|
+
const q = questions.find((x) => x.id === id);
|
|
197
|
+
assert.ok(q, "readQuestions must include the just-written question");
|
|
198
|
+
assert.equal(q!.question, "What database are we on?");
|
|
199
|
+
assert.equal(q!.resolved, false);
|
|
200
|
+
|
|
201
|
+
// questions/ is a RECALL_NON_MEMORY_DIRS entry — it must NOT appear in readAllMemories
|
|
202
|
+
const all = await storage.readAllMemories();
|
|
203
|
+
assert.ok(
|
|
204
|
+
!all.some((m) => m.frontmatter.id === id),
|
|
205
|
+
"questions must not leak into the memory corpus (readAllMemories)",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// unresolvedOnly filter works
|
|
209
|
+
const unresolved = await storage.readQuestions({ unresolvedOnly: true });
|
|
210
|
+
assert.ok(unresolved.some((x) => x.id === id));
|
|
211
|
+
|
|
212
|
+
// resolve marks it resolved
|
|
213
|
+
const resolved = await storage.resolveQuestion(id);
|
|
214
|
+
assert.equal(resolved, true);
|
|
215
|
+
|
|
216
|
+
const afterResolve = await storage.readQuestions({ unresolvedOnly: true });
|
|
217
|
+
assert.ok(!afterResolve.some((x) => x.id === id), "resolved question must not appear in unresolvedOnly");
|
|
218
|
+
|
|
219
|
+
const allAfter = await storage.readQuestions();
|
|
220
|
+
const resolvedQ = allAfter.find((x) => x.id === id);
|
|
221
|
+
assert.ok(resolvedQ, "resolved question should still be readable without the filter");
|
|
222
|
+
assert.equal(resolvedQ!.resolved, true);
|
|
223
|
+
} finally {
|
|
224
|
+
await cleanup();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: public-surface enumeration.
|
|
3
|
+
*
|
|
4
|
+
* Phase A step 1: enumerate the public surface of storage.ts so the contract
|
|
5
|
+
* suite has an explicit inventory. This test pins the exported symbols the 51+
|
|
6
|
+
* importers depend on. When Phase B extracts seams, every entry here must still
|
|
7
|
+
* resolve — either on storage.ts or on the storage/* module that absorbed it.
|
|
8
|
+
*
|
|
9
|
+
* This is NOT a behavior test; it is a surface-stability guard. It documents
|
|
10
|
+
* what the MemoryStorage interface must cover.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
|
|
16
|
+
import * as storageModule from "../storage.js";
|
|
17
|
+
|
|
18
|
+
test("surface: StorageManager class is exported and constructible", () => {
|
|
19
|
+
assert.equal(typeof storageModule.StorageManager, "function");
|
|
20
|
+
assert.equal(storageModule.StorageManager.name, "StorageManager");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("surface: ContentHashIndex class is exported", () => {
|
|
24
|
+
assert.equal(typeof storageModule.ContentHashIndex, "function");
|
|
25
|
+
assert.equal(storageModule.ContentHashIndex.name, "ContentHashIndex");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("surface: entity helpers are exported (parseEntityFile, serializeEntityFile, normalizeEntityName)", () => {
|
|
29
|
+
assert.equal(typeof storageModule.parseEntityFile, "function");
|
|
30
|
+
assert.equal(typeof storageModule.serializeEntityFile, "function");
|
|
31
|
+
assert.equal(typeof storageModule.normalizeEntityName, "function");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("surface: StorageManager exposes the documented memory CRUD methods", () => {
|
|
35
|
+
const proto = storageModule.StorageManager.prototype;
|
|
36
|
+
const required = [
|
|
37
|
+
"writeMemory",
|
|
38
|
+
"readAllMemories",
|
|
39
|
+
"readMemoryByPath",
|
|
40
|
+
"getMemoryById",
|
|
41
|
+
"invalidateMemory",
|
|
42
|
+
"updateMemory",
|
|
43
|
+
"ensureDirectories",
|
|
44
|
+
"setVersioningConfig",
|
|
45
|
+
"isSecureStoreUnlocked",
|
|
46
|
+
"getMemoryStatusVersion",
|
|
47
|
+
];
|
|
48
|
+
for (const method of required) {
|
|
49
|
+
assert.equal(
|
|
50
|
+
typeof (proto as unknown as Record<string, unknown>)[method],
|
|
51
|
+
"function",
|
|
52
|
+
`StorageManager.prototype.${method} must be a function (documented public surface)`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("surface: StorageManager exposes the question-queue methods", () => {
|
|
58
|
+
const proto = storageModule.StorageManager.prototype;
|
|
59
|
+
assert.equal(typeof proto.writeQuestion, "function");
|
|
60
|
+
assert.equal(typeof proto.readQuestions, "function");
|
|
61
|
+
assert.equal(typeof proto.resolveQuestion, "function");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("surface: StorageManager exposes cache invalidation methods (rule 37)", () => {
|
|
65
|
+
const proto = storageModule.StorageManager.prototype;
|
|
66
|
+
assert.equal(typeof proto.invalidateAllMemoriesCacheForDir, "function");
|
|
67
|
+
assert.equal(typeof proto.invalidateMemoryCachesForTiers, "function");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("surface: StorageManager has a static clearAllStaticCaches for test isolation", () => {
|
|
71
|
+
assert.equal(
|
|
72
|
+
typeof storageModule.StorageManager.clearAllStaticCaches,
|
|
73
|
+
"function",
|
|
74
|
+
"clearAllStaticCaches is the test-isolation chokepoint",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("surface: ContentHashIndex exposes computeHash and normalizeContent (rule 23)", () => {
|
|
79
|
+
assert.equal(typeof storageModule.ContentHashIndex.computeHash, "function");
|
|
80
|
+
assert.equal(typeof storageModule.ContentHashIndex.normalizeContent, "function");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("surface: StorageManager.dir getter returns the base directory string", () => {
|
|
84
|
+
const desc = Object.getOwnPropertyDescriptor(
|
|
85
|
+
storageModule.StorageManager.prototype,
|
|
86
|
+
"dir",
|
|
87
|
+
);
|
|
88
|
+
assert.ok(desc, "dir must be a property on the prototype");
|
|
89
|
+
assert.equal(typeof desc?.get, "function", "dir must have a getter");
|
|
90
|
+
});
|