@remnic/core 9.3.703 → 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 +8 -8
- package/dist/access-boundary.js +27 -27
- package/dist/access-cli.js +57 -56
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +7 -7
- package/dist/access-http.js +32 -31
- package/dist/access-mcp.d.ts +7 -7
- package/dist/access-mcp.js +31 -30
- package/dist/access-operations-batch.d.ts +2 -0
- package/dist/access-operations-batch.js +120 -0
- package/dist/access-operations.d.ts +8 -7
- package/dist/access-operations.js +30 -29
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-COCzhgEL.d.ts → access-service-C8hc8N2i.d.ts} +4 -4
- package/dist/access-service.d.ts +7 -7
- package/dist/access-service.js +26 -26
- package/dist/access-surface-catalog.d.ts +7 -7
- package/dist/access-surface-catalog.js +57 -54
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/action-confidence.d.ts +3 -3
- package/dist/active-memory-bridge.d.ts +3 -3
- package/dist/active-recall.d.ts +3 -3
- package/dist/{auto-sync-3AAP25FV.js → auto-sync-24NNS3GU.js} +3 -3
- package/dist/behavior-learner.d.ts +3 -3
- package/dist/behavior-signals.d.ts +3 -3
- package/dist/bootstrap.d.ts +6 -6
- package/dist/briefing.d.ts +3 -3
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +3 -3
- package/dist/buffer.d.ts +3 -3
- package/dist/bulk-import/index.d.ts +4 -4
- package/dist/calibration.d.ts +3 -3
- package/dist/capabilities.d.ts +3 -3
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/{catalog-DxCzhjE6.d.ts → catalog-CAeU0e1C.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +3 -3
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +3 -3
- package/dist/causal-consolidation.js +9 -9
- 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-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-EOOVJK2U.js → chunk-2SYPOX45.js} +10 -10
- package/dist/{chunk-64XXOQRW.js → chunk-47LPHBIP.js} +2 -2
- package/dist/{chunk-QGJAGC2J.js → chunk-5DTWZ66X.js} +2 -2
- package/dist/chunk-5DTWZ66X.js.map +1 -0
- package/dist/{chunk-6VVP6NK7.js → chunk-6GJ2WQRK.js} +2 -2
- package/dist/{chunk-4NFVPDIL.js → chunk-74MYJSKB.js} +5 -5
- package/dist/{chunk-X5J3JZR3.js → chunk-773DPI6T.js} +6 -6
- package/dist/{chunk-FVI5B7DE.js → chunk-7ARKLLCV.js} +6 -6
- package/dist/{chunk-ZCEI242W.js → chunk-7L54YZUI.js} +411 -401
- package/dist/chunk-7L54YZUI.js.map +1 -0
- package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
- package/dist/{chunk-CI7RKSRE.js → chunk-AQEJKOKI.js} +2 -1
- package/dist/{chunk-DL6H3D7S.js → chunk-ARV3AUOM.js} +2 -2
- package/dist/{chunk-JTKFZMZ7.js → chunk-BKBSE3BU.js} +3 -3
- package/dist/{chunk-TGAHHCB6.js → chunk-BMKGG7GC.js} +2 -2
- package/dist/{chunk-GFIARMA7.js → chunk-C2EHWNP2.js} +13 -13
- 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-5VXVXJH6.js → chunk-GT4VBKM4.js} +59 -5
- package/dist/chunk-GT4VBKM4.js.map +1 -0
- package/dist/{chunk-KC6TCAWV.js → chunk-HEHPVBWA.js} +6 -6
- package/dist/{chunk-MDJURR27.js → chunk-HW5EF6LV.js} +2 -2
- package/dist/{chunk-BMFZLLNI.js → chunk-ICP53SKG.js} +2 -2
- package/dist/{chunk-YOI3ELXF.js → chunk-IF362TF6.js} +2 -2
- package/dist/{chunk-ABCGDW3A.js → chunk-IFR23TC5.js} +4 -4
- package/dist/{chunk-OXEAMU42.js → chunk-JHAYR2JY.js} +33 -33
- package/dist/{chunk-CZJ6QSKG.js → chunk-JZCKDTAS.js} +4 -4
- package/dist/{chunk-CZJ6QSKG.js.map → chunk-JZCKDTAS.js.map} +1 -1
- package/dist/{chunk-VPAFWWCL.js → chunk-K22IJO4R.js} +2 -2
- package/dist/{chunk-JMA4RYRN.js → chunk-KFU2SKDD.js} +2 -2
- package/dist/{chunk-VDX2J7OX.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-AMNLZ6SF.js → chunk-KZMMAVR2.js} +2 -2
- package/dist/{chunk-EZR35XHX.js → chunk-LY4FJLPT.js} +2 -2
- package/dist/{chunk-NUIJEGVD.js → chunk-M5MWSG4S.js} +7 -7
- package/dist/{chunk-Z7XEIAV4.js → chunk-MNH2OODZ.js} +2 -2
- package/dist/{chunk-MXEWQKM7.js → chunk-NFGJBEPT.js} +6 -6
- package/dist/{chunk-Q4O3ET6F.js → chunk-NUWZFSKC.js} +2 -2
- package/dist/{chunk-6TAETM63.js → chunk-OM6ETLLH.js} +2 -2
- package/dist/{chunk-QQW2LIEA.js → chunk-OMSTU233.js} +7 -7
- package/dist/{chunk-XGMCUY5P.js → chunk-S3WYRPSA.js} +64 -64
- package/dist/{chunk-QIMFOCSH.js → chunk-S5APSA3U.js} +2 -2
- package/dist/{chunk-IX72AAMZ.js → chunk-SY6WG6DB.js} +2 -2
- package/dist/{chunk-MBUM2Y3L.js → chunk-ULVM34NP.js} +5 -5
- package/dist/{chunk-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-7WWURLG6.js → chunk-XPOOY35N.js} +2 -2
- package/dist/{chunk-EYJD6KIO.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-BM4xQPp4.d.ts → cli-ChE0XlJY.d.ts} +6 -6
- package/dist/cli.d.ts +9 -9
- package/dist/cli.js +55 -54
- package/dist/compounding/engine.d.ts +3 -3
- package/dist/compounding/engine.js +5 -5
- package/dist/compounding/preference-consolidator.d.ts +3 -3
- package/dist/compression-optimizer.d.ts +3 -3
- package/dist/config.d.ts +3 -3
- package/dist/connectors/codex-materialize-runner.d.ts +3 -3
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +3 -3
- package/dist/connectors/index.d.ts +3 -3
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +3 -3
- package/dist/consolidation-undo.d.ts +3 -3
- package/dist/contradiction/index.d.ts +3 -3
- package/dist/conversation-index/backend.d.ts +3 -3
- package/dist/conversation-index/chunker.d.ts +3 -3
- package/dist/conversation-index/faiss-adapter.d.ts +3 -3
- package/dist/conversation-index/indexer.d.ts +3 -3
- package/dist/conversation-index/search.d.ts +3 -3
- package/dist/day-summary.d.ts +3 -3
- package/dist/delinearize.d.ts +3 -3
- package/dist/direct-answer-wiring.d.ts +3 -3
- package/dist/direct-answer.d.ts +3 -3
- package/dist/embedding-fallback.d.ts +3 -3
- package/dist/enrichment/index.d.ts +3 -3
- package/dist/entity-retrieval.d.ts +3 -3
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +3 -3
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/extraction-faithfulness.d.ts +3 -3
- package/dist/extraction-judge-telemetry.d.ts +3 -3
- package/dist/extraction-judge-training.d.ts +3 -3
- package/dist/extraction-judge.d.ts +3 -3
- package/dist/extraction.d.ts +3 -3
- package/dist/extraction.js +2 -2
- package/dist/fallback-llm.d.ts +3 -3
- 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/identity-continuity.d.ts +3 -3
- package/dist/importance.d.ts +3 -3
- package/dist/importers/index.d.ts +2 -2
- package/dist/index.d.ts +14 -29
- package/dist/index.js +100 -99
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +3 -3
- package/dist/lcm/archive.d.ts +1 -1
- package/dist/lcm/archive.js +2 -2
- package/dist/lcm/engine.d.ts +3 -3
- package/dist/lcm/engine.js +5 -5
- package/dist/lcm/index.d.ts +3 -3
- package/dist/lcm/index.js +7 -7
- package/dist/lcm/queue.d.ts +1 -1
- package/dist/lcm/recall.d.ts +1 -1
- package/dist/lcm/summarizer.d.ts +1 -1
- package/dist/lcm/summarizer.js +3 -3
- package/dist/lcm/tools.d.ts +3 -3
- package/dist/lifecycle.d.ts +3 -3
- package/dist/live-connectors-runner.d.ts +3 -3
- package/dist/local-llm.d.ts +3 -3
- package/dist/maintenance/memory-governance.d.ts +3 -3
- package/dist/maintenance/memory-governance.js +6 -6
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +7 -7
- package/dist/mcp-memory-inspector-app.d.ts +7 -7
- package/dist/memory-action-policy.d.ts +3 -3
- package/dist/memory-cache.d.ts +3 -3
- package/dist/memory-lifecycle-ledger-utils.d.ts +3 -3
- package/dist/memory-projection-store.d.ts +3 -3
- package/dist/memory-provenance.d.ts +3 -3
- package/dist/memory-worth-outcomes.d.ts +3 -3
- package/dist/{index-DJ9QWMw-.d.ts → message-parts/index.d.ts} +2 -1
- package/dist/message-parts/index.js +24 -0
- package/dist/message-parts/index.js.map +1 -0
- package/dist/models-json.d.ts +3 -3
- package/dist/namespaces/migrate.d.ts +4 -4
- package/dist/namespaces/migrate.js +10 -10
- package/dist/namespaces/principal.d.ts +3 -3
- package/dist/namespaces/search.d.ts +3 -3
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +4 -4
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +3 -3
- package/dist/objective-state-writers.d.ts +1 -1
- package/dist/objective-state-writers.js +2 -2
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +17 -17
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +7 -7
- package/dist/{orchestrator-C9CDWAm6.d.ts → orchestrator-m_PnhyHj.d.ts} +4 -4
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +44 -44
- package/dist/patterns-cli.d.ts +3 -3
- package/dist/{pipeline-BSTuml0d.d.ts → pipeline-DmqtvION.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +3 -3
- package/dist/provenance.d.ts +3 -3
- package/dist/qmd-recall-cache.d.ts +3 -3
- package/dist/qmd.d.ts +3 -3
- package/dist/recall-disclosure-escalation.d.ts +3 -3
- package/dist/recall-explain-renderer.d.ts +3 -3
- package/dist/recall-planner-llm.d.ts +3 -3
- package/dist/recall-state.d.ts +3 -3
- package/dist/recall-tag-filter.d.ts +3 -3
- package/dist/recall-xray-cli.d.ts +3 -3
- package/dist/recall-xray-renderer.d.ts +3 -3
- package/dist/recall-xray.d.ts +3 -3
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +3 -3
- package/dist/resume-bundles.js +1 -1
- package/dist/retrieval-agents.d.ts +3 -3
- package/dist/retrieval-tiers.d.ts +3 -3
- package/dist/routing/engine.d.ts +3 -3
- package/dist/routing/store.d.ts +3 -3
- package/dist/search/embed-helper.d.ts +3 -3
- package/dist/search/factory.d.ts +3 -3
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +3 -3
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.d.ts +3 -3
- package/dist/search/meilisearch-backend.d.ts +3 -3
- package/dist/search/noop-backend.d.ts +3 -3
- package/dist/search/orama-backend.d.ts +3 -3
- package/dist/search/port.d.ts +3 -3
- package/dist/search/remote-backend.d.ts +3 -3
- package/dist/{semantic-consolidation-DgFXyALl.d.ts → semantic-consolidation-DVBbq6Wr.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +4 -4
- package/dist/semantic-consolidation.js +7 -7
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +3 -3
- package/dist/semantic-rule-verifier.js +6 -6
- package/dist/session-observer-bands.d.ts +3 -3
- package/dist/session-observer-state.d.ts +3 -3
- package/dist/shared-context/manager.d.ts +3 -3
- package/dist/signal.d.ts +3 -3
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +3 -3
- package/dist/summary-snapshot.d.ts +3 -3
- package/dist/temporal-supersession.d.ts +3 -3
- package/dist/temporal-validity.d.ts +3 -3
- package/dist/threading.d.ts +3 -3
- package/dist/tier-migration.d.ts +3 -3
- package/dist/tier-routing.d.ts +3 -3
- package/dist/topics.d.ts +3 -3
- package/dist/transcript.d.ts +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/{types-MWKPZnM0.d.ts → types-CJrvHJjx.d.ts} +2 -2
- package/dist/{types-ByK7T3L6.d.ts → types-gb_SNtjR.d.ts} +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/utility-runtime.d.ts +3 -3
- package/dist/utils/path.d.ts +17 -0
- package/dist/utils/path.js +9 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/verified-recall.js +6 -6
- package/package.json +27 -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/cli/creation-ledger-commands.ts +346 -0
- package/src/cli/research-status-commands.ts +256 -0
- package/src/cli.ts +7 -516
- 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-5VXVXJH6.js.map +0 -1
- package/dist/chunk-QGJAGC2J.js.map +0 -1
- package/dist/chunk-ZCEI242W.js.map +0 -1
- package/dist/path-X2K5XCHL.js +0 -9
- /package/dist/{capsule-crypto-YO5QJ6L3.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/{path-X2K5XCHL.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-EOOVJK2U.js.map → chunk-2SYPOX45.js.map} +0 -0
- /package/dist/{chunk-64XXOQRW.js.map → chunk-47LPHBIP.js.map} +0 -0
- /package/dist/{chunk-6VVP6NK7.js.map → chunk-6GJ2WQRK.js.map} +0 -0
- /package/dist/{chunk-4NFVPDIL.js.map → chunk-74MYJSKB.js.map} +0 -0
- /package/dist/{chunk-X5J3JZR3.js.map → chunk-773DPI6T.js.map} +0 -0
- /package/dist/{chunk-FVI5B7DE.js.map → chunk-7ARKLLCV.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
- /package/dist/{chunk-CI7RKSRE.js.map → chunk-AQEJKOKI.js.map} +0 -0
- /package/dist/{chunk-DL6H3D7S.js.map → chunk-ARV3AUOM.js.map} +0 -0
- /package/dist/{chunk-JTKFZMZ7.js.map → chunk-BKBSE3BU.js.map} +0 -0
- /package/dist/{chunk-TGAHHCB6.js.map → chunk-BMKGG7GC.js.map} +0 -0
- /package/dist/{chunk-GFIARMA7.js.map → chunk-C2EHWNP2.js.map} +0 -0
- /package/dist/{chunk-VKJHM6PL.js.map → chunk-DGTDRIUO.js.map} +0 -0
- /package/dist/{chunk-KC6TCAWV.js.map → chunk-HEHPVBWA.js.map} +0 -0
- /package/dist/{chunk-MDJURR27.js.map → chunk-HW5EF6LV.js.map} +0 -0
- /package/dist/{chunk-BMFZLLNI.js.map → chunk-ICP53SKG.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-OXEAMU42.js.map → chunk-JHAYR2JY.js.map} +0 -0
- /package/dist/{chunk-VPAFWWCL.js.map → chunk-K22IJO4R.js.map} +0 -0
- /package/dist/{chunk-JMA4RYRN.js.map → chunk-KFU2SKDD.js.map} +0 -0
- /package/dist/{chunk-VDX2J7OX.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-AMNLZ6SF.js.map → chunk-KZMMAVR2.js.map} +0 -0
- /package/dist/{chunk-EZR35XHX.js.map → chunk-LY4FJLPT.js.map} +0 -0
- /package/dist/{chunk-NUIJEGVD.js.map → chunk-M5MWSG4S.js.map} +0 -0
- /package/dist/{chunk-Z7XEIAV4.js.map → chunk-MNH2OODZ.js.map} +0 -0
- /package/dist/{chunk-MXEWQKM7.js.map → chunk-NFGJBEPT.js.map} +0 -0
- /package/dist/{chunk-Q4O3ET6F.js.map → chunk-NUWZFSKC.js.map} +0 -0
- /package/dist/{chunk-6TAETM63.js.map → chunk-OM6ETLLH.js.map} +0 -0
- /package/dist/{chunk-QQW2LIEA.js.map → chunk-OMSTU233.js.map} +0 -0
- /package/dist/{chunk-XGMCUY5P.js.map → chunk-S3WYRPSA.js.map} +0 -0
- /package/dist/{chunk-QIMFOCSH.js.map → chunk-S5APSA3U.js.map} +0 -0
- /package/dist/{chunk-IX72AAMZ.js.map → chunk-SY6WG6DB.js.map} +0 -0
- /package/dist/{chunk-MBUM2Y3L.js.map → chunk-ULVM34NP.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-7WWURLG6.js.map → chunk-XPOOY35N.js.map} +0 -0
- /package/dist/{chunk-EYJD6KIO.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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: cache coherence across instances.
|
|
3
|
+
*
|
|
4
|
+
* Rule 37 (memory-cache): write via instance A, read via a FRESH instance B
|
|
5
|
+
* over the same dir → B sees the write. The module-level caches
|
|
6
|
+
* (readAllMemories in-flight, version sentinels) are keyed by baseDir and
|
|
7
|
+
* shared/static — this test pins that a write invalidates correctly so a
|
|
8
|
+
* second instance never serves stale data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { StorageManager } from "../storage.js";
|
|
15
|
+
import { resetStaticCaches } from "./harness.js";
|
|
16
|
+
|
|
17
|
+
test("cache-coherence: write via instance A, read via fresh instance B → B sees the write", async () => {
|
|
18
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
19
|
+
const os = await import("node:os");
|
|
20
|
+
const path = await import("node:path");
|
|
21
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-coherence-"));
|
|
22
|
+
try {
|
|
23
|
+
resetStaticCaches();
|
|
24
|
+
const smA = new StorageManager(dir);
|
|
25
|
+
await smA.ensureDirectories();
|
|
26
|
+
|
|
27
|
+
const id = await smA.writeMemory("fact", "written by A");
|
|
28
|
+
|
|
29
|
+
// Fresh instance over the same dir
|
|
30
|
+
const smB = new StorageManager(dir);
|
|
31
|
+
await smB.ensureDirectories();
|
|
32
|
+
const found = await smB.getMemoryById(id);
|
|
33
|
+
assert.ok(found, "instance B must see the memory written by instance A");
|
|
34
|
+
assert.equal(found!.content, "written by A");
|
|
35
|
+
} finally {
|
|
36
|
+
resetStaticCaches();
|
|
37
|
+
await rm(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("cache-coherence: delete via instance A is reflected in fresh instance B", async () => {
|
|
42
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
43
|
+
const os = await import("node:os");
|
|
44
|
+
const path = await import("node:path");
|
|
45
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-del-"));
|
|
46
|
+
try {
|
|
47
|
+
resetStaticCaches();
|
|
48
|
+
const smA = new StorageManager(dir);
|
|
49
|
+
await smA.ensureDirectories();
|
|
50
|
+
const id = await smA.writeMemory("fact", "to be deleted");
|
|
51
|
+
|
|
52
|
+
// Confirm B sees it
|
|
53
|
+
const smB = new StorageManager(dir);
|
|
54
|
+
await smB.ensureDirectories();
|
|
55
|
+
assert.ok(await smB.getMemoryById(id));
|
|
56
|
+
|
|
57
|
+
// A deletes
|
|
58
|
+
await smA.invalidateMemory(id);
|
|
59
|
+
|
|
60
|
+
// Fresh instance C must NOT see it
|
|
61
|
+
const smC = new StorageManager(dir);
|
|
62
|
+
await smC.ensureDirectories();
|
|
63
|
+
const found = await smC.getMemoryById(id);
|
|
64
|
+
assert.equal(found, null, "fresh instance must not see a deleted memory");
|
|
65
|
+
} finally {
|
|
66
|
+
resetStaticCaches();
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("cache-coherence: readAllMemories reflects writes across instances after invalidation", async () => {
|
|
72
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
73
|
+
const os = await import("node:os");
|
|
74
|
+
const path = await import("node:path");
|
|
75
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-cache-list-"));
|
|
76
|
+
try {
|
|
77
|
+
resetStaticCaches();
|
|
78
|
+
const smA = new StorageManager(dir);
|
|
79
|
+
await smA.ensureDirectories();
|
|
80
|
+
await smA.writeMemory("fact", "first");
|
|
81
|
+
await smA.writeMemory("decision", "second");
|
|
82
|
+
|
|
83
|
+
const smB = new StorageManager(dir);
|
|
84
|
+
await smB.ensureDirectories();
|
|
85
|
+
const all = await smB.readAllMemories();
|
|
86
|
+
assert.ok(all.length >= 2, "fresh instance must list all written memories");
|
|
87
|
+
|
|
88
|
+
// Write more via A
|
|
89
|
+
await smA.writeMemory("principle", "third");
|
|
90
|
+
|
|
91
|
+
const smC = new StorageManager(dir);
|
|
92
|
+
await smC.ensureDirectories();
|
|
93
|
+
const allAfter = await smC.readAllMemories();
|
|
94
|
+
assert.ok(allAfter.length >= 3, "fresh instance must see the additional write");
|
|
95
|
+
assert.ok(allAfter.some((m) => m.content === "third"));
|
|
96
|
+
} finally {
|
|
97
|
+
resetStaticCaches();
|
|
98
|
+
await rm(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: content hash identity.
|
|
3
|
+
*
|
|
4
|
+
* Rule 23: the write-side and dedup-side MUST hash the same `rawContent`,
|
|
5
|
+
* never the timestamped/cited form. `writeMemory` persists a `contentHash` on
|
|
6
|
+
* the frontmatter for facts; `hasFactContentHash` queries the dedup index.
|
|
7
|
+
* This test pins that the two sides agree so subsequent extractions of the
|
|
8
|
+
* same logical fact are deduped correctly even when citation timestamps differ.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { ContentHashIndex } from "../storage.js";
|
|
15
|
+
import { makeStorage } from "./harness.js";
|
|
16
|
+
|
|
17
|
+
test("content-hash: ContentHashIndex.computeHash is deterministic for the same input", () => {
|
|
18
|
+
const a = ContentHashIndex.computeHash("the sky is blue");
|
|
19
|
+
const b = ContentHashIndex.computeHash("the sky is blue");
|
|
20
|
+
assert.equal(a, b);
|
|
21
|
+
assert.notEqual(
|
|
22
|
+
ContentHashIndex.computeHash("the sky is blue"),
|
|
23
|
+
ContentHashIndex.computeHash("the sky is green"),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("content-hash: writeMemory for fact registers the hash so hasFactContentHash returns true", async () => {
|
|
28
|
+
const { storage, cleanup } = await makeStorage();
|
|
29
|
+
try {
|
|
30
|
+
const rawFact = "PostgreSQL is our primary database";
|
|
31
|
+
await storage.writeMemory("fact", rawFact);
|
|
32
|
+
|
|
33
|
+
const has = await storage.hasFactContentHash(rawFact);
|
|
34
|
+
assert.equal(has, true, "hasFactContentHash must return true for a just-written fact");
|
|
35
|
+
} finally {
|
|
36
|
+
await cleanup();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("content-hash: contentHashSource overrides the persisted body for dedup (rule 23)", async () => {
|
|
41
|
+
const { storage, cleanup } = await makeStorage();
|
|
42
|
+
try {
|
|
43
|
+
const rawFact = "We migrated to MySQL in March";
|
|
44
|
+
// Persist a citation-annotated body, but index the RAW fact text
|
|
45
|
+
await storage.writeMemory("fact", `${rawFact} [cited 2026-01-01]`, {
|
|
46
|
+
contentHashSource: rawFact,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Querying with the RAW fact (not the cited form) must hit
|
|
50
|
+
assert.equal(
|
|
51
|
+
await storage.hasFactContentHash(rawFact),
|
|
52
|
+
true,
|
|
53
|
+
"hasFactContentHash must match on the raw fact, not the cited body",
|
|
54
|
+
);
|
|
55
|
+
// The cited form should NOT be separately indexed (it has the citation suffix)
|
|
56
|
+
assert.equal(
|
|
57
|
+
await storage.hasFactContentHash(`${rawFact} [cited 2026-01-01]`),
|
|
58
|
+
false,
|
|
59
|
+
"the cited/timestamped form must not be the indexed hash",
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
await cleanup();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("content-hash: non-fact categories do not register a content hash", async () => {
|
|
67
|
+
const { storage, cleanup } = await makeStorage();
|
|
68
|
+
try {
|
|
69
|
+
await storage.writeMemory("decision", "we chose option B");
|
|
70
|
+
assert.equal(
|
|
71
|
+
await storage.hasFactContentHash("we chose option B"),
|
|
72
|
+
false,
|
|
73
|
+
"non-fact categories must not enter the fact dedup index",
|
|
74
|
+
);
|
|
75
|
+
} finally {
|
|
76
|
+
await cleanup();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("content-hash: normalizeContent produces stable text for equivalent inputs", () => {
|
|
81
|
+
const a = ContentHashIndex.normalizeContent(" Hello World ");
|
|
82
|
+
const b = ContentHashIndex.normalizeContent("Hello World");
|
|
83
|
+
assert.equal(a, b, "normalizeContent must collapse whitespace consistently");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("content-hash: the frontmatter contentHash matches computeHash of the raw content", async () => {
|
|
87
|
+
const { storage, cleanup } = await makeStorage();
|
|
88
|
+
try {
|
|
89
|
+
const rawFact = "deterministic hash check";
|
|
90
|
+
const id = await storage.writeMemory("fact", rawFact);
|
|
91
|
+
const memory = await storage.getMemoryById(id);
|
|
92
|
+
assert.ok(memory);
|
|
93
|
+
assert.ok(memory!.frontmatter.contentHash, "frontmatter must carry a contentHash for facts");
|
|
94
|
+
assert.equal(
|
|
95
|
+
memory!.frontmatter.contentHash,
|
|
96
|
+
ContentHashIndex.computeHash(rawFact),
|
|
97
|
+
"frontmatter contentHash must equal computeHash of the raw content",
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
await cleanup();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1533 — Phase A contract test: failure semantics.
|
|
3
|
+
*
|
|
4
|
+
* Rule 34: reads distinguish empty-dir from unreadable-dir — `{ok:false}` vs
|
|
5
|
+
* `[]`. Where the current surface already does this, we pin it; where it
|
|
6
|
+
* doesn't, we record the gap (for the silent-failures issue) rather than
|
|
7
|
+
* change behavior here.
|
|
8
|
+
*
|
|
9
|
+
* Also pins containment: a FILE where a directory is expected is skipped via
|
|
10
|
+
* lstat().isDirectory(), not existsSync() (rule 24).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import { StorageManager } from "../storage.js";
|
|
20
|
+
import { makeStorage } from "./harness.js";
|
|
21
|
+
|
|
22
|
+
test("failure-semantics: readAllMemories on a fresh (empty) dir returns [] not null", async () => {
|
|
23
|
+
const { storage, cleanup } = await makeStorage();
|
|
24
|
+
try {
|
|
25
|
+
const result = await storage.readAllMemories();
|
|
26
|
+
assert.ok(Array.isArray(result), "readAllMemories must return an array");
|
|
27
|
+
assert.equal(result.length, 0, "empty store must return an empty array");
|
|
28
|
+
} finally {
|
|
29
|
+
await cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("failure-semantics: getMemoryById on empty store returns null (not throw)", async () => {
|
|
34
|
+
const { storage, cleanup } = await makeStorage();
|
|
35
|
+
try {
|
|
36
|
+
const result = await storage.getMemoryById("anything");
|
|
37
|
+
assert.equal(result, null);
|
|
38
|
+
} finally {
|
|
39
|
+
await cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("failure-semantics: readMemoryByPath on a non-existent file returns null (not throw)", async () => {
|
|
44
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
45
|
+
try {
|
|
46
|
+
const result = await storage.readMemoryByPath(path.join(baseDir, "facts", "2026-01-01", "nope.md"));
|
|
47
|
+
assert.equal(result, null);
|
|
48
|
+
} finally {
|
|
49
|
+
await cleanup();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("failure-semantics: readMemoryByPath on a malformed markdown file returns null", async () => {
|
|
54
|
+
const { storage, baseDir, cleanup } = await makeStorage();
|
|
55
|
+
try {
|
|
56
|
+
const dir = path.join(baseDir, "facts", "2026-01-01");
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
const filePath = path.join(dir, "garbage.md");
|
|
59
|
+
await writeFile(filePath, "this has no frontmatter at all\njust random text", "utf-8");
|
|
60
|
+
|
|
61
|
+
const result = await storage.readMemoryByPath(filePath);
|
|
62
|
+
assert.equal(result, null, "malformed file (no frontmatter, not entity) must return null");
|
|
63
|
+
} finally {
|
|
64
|
+
await cleanup();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("failure-semantics: a FILE where a category directory is expected does not crash readAllMemories", async () => {
|
|
69
|
+
// Create a FILE named "facts" (not a directory) at the base dir, then scan.
|
|
70
|
+
// collectActiveMemoryPaths uses lstat().isDirectory() (not existsSync) so a
|
|
71
|
+
// file-as-directory entry is skipped without crashing (rule 24 containment).
|
|
72
|
+
// We do NOT call ensureDirectories — it would try to mkdir facts/<today> and
|
|
73
|
+
// fail on the file. The scan path must be robust independently of dir creation.
|
|
74
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-fail-file-as-dir-"));
|
|
75
|
+
try {
|
|
76
|
+
StorageManager.clearAllStaticCaches();
|
|
77
|
+
await writeFile(path.join(baseDir, "facts"), "I am a file not a directory", "utf-8");
|
|
78
|
+
const storage = new StorageManager(baseDir);
|
|
79
|
+
|
|
80
|
+
const result = await storage.readAllMemories();
|
|
81
|
+
assert.ok(Array.isArray(result));
|
|
82
|
+
assert.equal(result.length, 0, "a file-as-directory must not produce spurious memories");
|
|
83
|
+
} finally {
|
|
84
|
+
StorageManager.clearAllStaticCaches();
|
|
85
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("failure-semantics: invalidateMemory on a missing memory returns false (not throw)", async () => {
|
|
90
|
+
const { storage, cleanup } = await makeStorage();
|
|
91
|
+
try {
|
|
92
|
+
const result = await storage.invalidateMemory("totally-missing-id-99999");
|
|
93
|
+
assert.equal(result, false);
|
|
94
|
+
} finally {
|
|
95
|
+
await cleanup();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("failure-semantics: ensureDirectories is idempotent (safe to call multiple times)", async () => {
|
|
100
|
+
const { storage, cleanup } = await makeStorage();
|
|
101
|
+
try {
|
|
102
|
+
await storage.ensureDirectories();
|
|
103
|
+
await storage.ensureDirectories();
|
|
104
|
+
await storage.ensureDirectories();
|
|
105
|
+
} finally {
|
|
106
|
+
await cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
@@ -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
|
+
});
|