@remnic/core 9.3.653 → 9.3.655
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 +24 -24
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +17 -17
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +16 -16
- package/dist/access-schema.d.ts +12 -12
- package/dist/{access-service-CdJFd3_b.d.ts → access-service-BEJvriUt.d.ts} +11 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +15 -15
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-GI45G4BK.js → chunk-2RCGZ67B.js} +4 -4
- package/dist/{chunk-BEMWL2FZ.js → chunk-54LOUIBE.js} +2 -2
- package/dist/{chunk-E3J6O6N7.js → chunk-55ZMNKMQ.js} +20 -9
- package/dist/{chunk-E3J6O6N7.js.map → chunk-55ZMNKMQ.js.map} +1 -1
- package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
- package/dist/{chunk-SPMZZUEJ.js → chunk-5QD3QD76.js} +2684 -401
- package/dist/chunk-5QD3QD76.js.map +1 -0
- package/dist/{chunk-WLGE6KEO.js → chunk-67G4T7KI.js} +3 -3
- package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
- package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
- package/dist/chunk-6IMKOIZ6.js.map +1 -0
- package/dist/{chunk-KJDKZVF3.js → chunk-A3Y37UWI.js} +3 -3
- package/dist/{chunk-CFOCZPIQ.js → chunk-BGKXTVNG.js} +2 -2
- package/dist/{chunk-QQHIQ7JD.js → chunk-COVZLGMR.js} +87 -18
- package/dist/chunk-COVZLGMR.js.map +1 -0
- package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
- package/dist/chunk-EKQMQQ3U.js.map +1 -0
- package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
- package/dist/chunk-GKKAXVAJ.js.map +1 -0
- package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
- package/dist/{chunk-EHQLDFSH.js → chunk-IQ53ZSXV.js} +2 -2
- package/dist/{chunk-C63WC454.js → chunk-KOI765XP.js} +125 -1
- package/dist/chunk-KOI765XP.js.map +1 -0
- package/dist/{chunk-IVYSVAC6.js → chunk-KZZ4YAEC.js} +2 -2
- package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
- package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
- package/dist/{chunk-JF7SFXTG.js → chunk-NCSJKK23.js} +2 -2
- package/dist/{chunk-XMN6MMTU.js → chunk-NRBGRZW4.js} +2 -2
- package/dist/{chunk-NOBL7OUP.js → chunk-OKW6F5S5.js} +12 -5
- package/dist/{chunk-NOBL7OUP.js.map → chunk-OKW6F5S5.js.map} +1 -1
- package/dist/{chunk-BNFRL6QW.js → chunk-PTMJ2FH2.js} +2 -2
- package/dist/{chunk-KWM33SPU.js → chunk-PVE7KSQP.js} +2 -2
- package/dist/{chunk-EW52H5EM.js → chunk-QDVQ4AN2.js} +12 -5
- package/dist/chunk-QDVQ4AN2.js.map +1 -0
- package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
- package/dist/chunk-QRSKPI62.js.map +1 -0
- package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
- package/dist/{chunk-C43KEWEV.js → chunk-TDZSSJV4.js} +1 -1
- package/dist/chunk-TDZSSJV4.js.map +1 -0
- package/dist/{chunk-Y7NWBBHV.js → chunk-TEO46GMM.js} +2 -2
- package/dist/{chunk-AJE7FJVE.js → chunk-UCEABZZN.js} +2 -2
- package/dist/{chunk-IENGGY2C.js → chunk-UCEDY5M7.js} +2 -2
- package/dist/{chunk-PRQXUSQV.js → chunk-UYNFWZWG.js} +2 -2
- package/dist/{chunk-V4UDXYGG.js → chunk-WDTUYOLS.js} +2 -2
- package/dist/{chunk-RZOBQ23O.js → chunk-XOFXKASO.js} +2 -2
- package/dist/chunk-XRKQOQLY.js +212 -0
- package/dist/chunk-XRKQOQLY.js.map +1 -0
- package/dist/{chunk-WTI35CVJ.js → chunk-YYN3LIYA.js} +5 -5
- package/dist/{cli-DDo7Qgs-.d.ts → cli-BGahB_d3.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +29 -29
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +19 -1
- package/dist/contradiction/index.js +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +37 -35
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +15 -4
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +52 -3
- package/dist/namespaces/storage.js +9 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +14 -14
- package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-BgzZlWxH.d.ts} +500 -3
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +20 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +5 -1
- package/dist/qmd.js +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.js} +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +6 -6
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +17 -1
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-Z8d_uMq8.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-D8yUmSik.d.ts → types-2OPlQWJG.d.ts} +23 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.ts +7 -0
- package/src/access-mcp.ts +7 -0
- package/src/access-service.ts +12 -0
- package/src/cli.ts +104 -0
- package/src/config.test.ts +109 -0
- package/src/config.ts +164 -0
- package/src/contradiction/contradiction.test.ts +284 -0
- package/src/contradiction/resolution.ts +151 -4
- package/src/explicit-capture.ts +31 -10
- package/src/index.ts +10 -0
- package/src/maintenance/namespace-planner.test.ts +1120 -0
- package/src/maintenance/namespace-planner.ts +893 -0
- package/src/namespaces/catalog.test.ts +3356 -0
- package/src/namespaces/catalog.ts +2123 -0
- package/src/namespaces/search.test.ts +130 -2
- package/src/namespaces/search.ts +71 -10
- package/src/namespaces/storage.ts +210 -30
- package/src/orchestrator-flush.test.ts +720 -0
- package/src/orchestrator.ts +881 -239
- package/src/qmd-client.test.ts +59 -0
- package/src/qmd.ts +124 -84
- package/src/search/port.ts +16 -0
- package/src/types.ts +23 -0
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-EW52H5EM.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-JVRPJ7D4.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-PYWNNF2I.js.map +0 -1
- package/dist/chunk-QQHIQ7JD.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-SPMZZUEJ.js.map +0 -1
- /package/dist/{chunk-GI45G4BK.js.map → chunk-2RCGZ67B.js.map} +0 -0
- /package/dist/{chunk-BEMWL2FZ.js.map → chunk-54LOUIBE.js.map} +0 -0
- /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
- /package/dist/{chunk-WLGE6KEO.js.map → chunk-67G4T7KI.js.map} +0 -0
- /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
- /package/dist/{chunk-KJDKZVF3.js.map → chunk-A3Y37UWI.js.map} +0 -0
- /package/dist/{chunk-CFOCZPIQ.js.map → chunk-BGKXTVNG.js.map} +0 -0
- /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
- /package/dist/{chunk-EHQLDFSH.js.map → chunk-IQ53ZSXV.js.map} +0 -0
- /package/dist/{chunk-IVYSVAC6.js.map → chunk-KZZ4YAEC.js.map} +0 -0
- /package/dist/{chunk-JF7SFXTG.js.map → chunk-NCSJKK23.js.map} +0 -0
- /package/dist/{chunk-XMN6MMTU.js.map → chunk-NRBGRZW4.js.map} +0 -0
- /package/dist/{chunk-BNFRL6QW.js.map → chunk-PTMJ2FH2.js.map} +0 -0
- /package/dist/{chunk-KWM33SPU.js.map → chunk-PVE7KSQP.js.map} +0 -0
- /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
- /package/dist/{chunk-Y7NWBBHV.js.map → chunk-TEO46GMM.js.map} +0 -0
- /package/dist/{chunk-AJE7FJVE.js.map → chunk-UCEABZZN.js.map} +0 -0
- /package/dist/{chunk-IENGGY2C.js.map → chunk-UCEDY5M7.js.map} +0 -0
- /package/dist/{chunk-PRQXUSQV.js.map → chunk-UYNFWZWG.js.map} +0 -0
- /package/dist/{chunk-V4UDXYGG.js.map → chunk-WDTUYOLS.js.map} +0 -0
- /package/dist/{chunk-RZOBQ23O.js.map → chunk-XOFXKASO.js.map} +0 -0
- /package/dist/{chunk-WTI35CVJ.js.map → chunk-YYN3LIYA.js.map} +0 -0
- /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
6
|
import {
|
|
4
7
|
BulkImportBatchPartialFailureError,
|
|
5
8
|
Orchestrator,
|
|
@@ -7,6 +10,8 @@ import {
|
|
|
7
10
|
import { parseConfig } from "./config.js";
|
|
8
11
|
import type { BufferTurn } from "./types.js";
|
|
9
12
|
import type { ImportTurn } from "./bulk-import/types.js";
|
|
13
|
+
import { namespaceIdentityToken } from "./namespaces/identity.js";
|
|
14
|
+
import { readNamespaceMaintenanceStatuses } from "./maintenance/namespace-planner.js";
|
|
10
15
|
|
|
11
16
|
function makeTurn(sessionKey: string, content: string): BufferTurn {
|
|
12
17
|
return {
|
|
@@ -1767,3 +1772,718 @@ test("runExtraction still clears the session buffer after persistence even if re
|
|
|
1767
1772
|
"persisted reset flushes must still clear the session buffer even when the reset timeout aborts after persistence",
|
|
1768
1773
|
);
|
|
1769
1774
|
});
|
|
1775
|
+
|
|
1776
|
+
// ── NGnei (codex P2): runQmdMaintenance must cover CATALOGED dynamic namespaces,
|
|
1777
|
+
// not only the configured set. An extraction writing to a coding-scoped/dynamic
|
|
1778
|
+
// namespace is made discoverable via the catalog; if maintenance embeds only
|
|
1779
|
+
// configuredNamespaces(), that namespace's QMD collection stays stale. We stub the
|
|
1780
|
+
// orchestrator internals and assert update/embed receive the UNION of configured +
|
|
1781
|
+
// cataloged namespaces.
|
|
1782
|
+
test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)", async () => {
|
|
1783
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1784
|
+
const updateArgs: string[] = [];
|
|
1785
|
+
const updateCalls: Array<{ namespaces: string[]; strict: boolean | undefined }> = [];
|
|
1786
|
+
const embedArgs: string[] = [];
|
|
1787
|
+
const embedCalls: string[][] = [];
|
|
1788
|
+
const memoryDir = path.join(os.tmpdir(), "remnic-qmd-maintenance-ngnei");
|
|
1789
|
+
const dynamicNamespace = "project-origin-dynamic";
|
|
1790
|
+
const dynamicStorageDir = path.join(
|
|
1791
|
+
memoryDir,
|
|
1792
|
+
"namespaces",
|
|
1793
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
1794
|
+
);
|
|
1795
|
+
await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
|
|
1796
|
+
|
|
1797
|
+
orchestrator.config = {
|
|
1798
|
+
memoryDir,
|
|
1799
|
+
namespacesEnabled: true,
|
|
1800
|
+
defaultNamespace: "default",
|
|
1801
|
+
sharedNamespace: "shared",
|
|
1802
|
+
namespacePolicies: [],
|
|
1803
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
1804
|
+
qmdAutoEmbedEnabled: true,
|
|
1805
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1806
|
+
};
|
|
1807
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1808
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1809
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1810
|
+
orchestrator.namespaceCatalog = {
|
|
1811
|
+
enabled: true,
|
|
1812
|
+
async listNamespaces() {
|
|
1813
|
+
return [
|
|
1814
|
+
{ namespace: "default" },
|
|
1815
|
+
{
|
|
1816
|
+
namespace: dynamicNamespace,
|
|
1817
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
1818
|
+
kind: "project",
|
|
1819
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
1820
|
+
storageDir: dynamicStorageDir,
|
|
1821
|
+
discoveredBy: "write",
|
|
1822
|
+
}, // dynamic, NOT configured
|
|
1823
|
+
];
|
|
1824
|
+
},
|
|
1825
|
+
};
|
|
1826
|
+
orchestrator.namespaceSearchRouter = {
|
|
1827
|
+
async updateNamespacesDetailed(ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
|
|
1828
|
+
updateCalls.push({ namespaces: [...ns], strict: options?.strict });
|
|
1829
|
+
updateArgs.push(...ns);
|
|
1830
|
+
return { backendCount: ns.length, eligibleNamespaces: ns };
|
|
1831
|
+
},
|
|
1832
|
+
async embedNamespaces(ns: string[]) {
|
|
1833
|
+
embedCalls.push([...ns]);
|
|
1834
|
+
embedArgs.push(...ns);
|
|
1835
|
+
},
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
await orchestrator.runQmdMaintenance();
|
|
1839
|
+
|
|
1840
|
+
assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
|
|
1841
|
+
assert.equal(updateCalls.length, 1, "global QMD maintenance must batch selected namespaces into one update call");
|
|
1842
|
+
assert.equal(updateCalls[0]?.strict, true, "recurring QMD maintenance must use strict update semantics");
|
|
1843
|
+
assert.ok(
|
|
1844
|
+
updateArgs.includes(dynamicNamespace),
|
|
1845
|
+
"QMD update must cover the cataloged dynamic namespace, not just configured ones",
|
|
1846
|
+
);
|
|
1847
|
+
assert.ok(
|
|
1848
|
+
updateArgs.includes("default") && updateArgs.includes("shared"),
|
|
1849
|
+
"configured namespaces remain covered",
|
|
1850
|
+
);
|
|
1851
|
+
assert.ok(
|
|
1852
|
+
embedArgs.includes(dynamicNamespace),
|
|
1853
|
+
"QMD embed must cover the cataloged dynamic namespace",
|
|
1854
|
+
);
|
|
1855
|
+
assert.equal(embedCalls.length, 1, "QMD embed must batch all selected namespaces into one router call");
|
|
1856
|
+
assert.deepEqual(new Set(embedCalls[0]), new Set(["default", "shared", dynamicNamespace]));
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
test("runQmdMaintenance tracks namespace embed cadence across budget rotation", async () => {
|
|
1860
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1861
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-namespace-embed-cadence-"));
|
|
1862
|
+
const updateCalls: string[][] = [];
|
|
1863
|
+
const embedCalls: string[][] = [];
|
|
1864
|
+
|
|
1865
|
+
try {
|
|
1866
|
+
orchestrator.config = {
|
|
1867
|
+
memoryDir,
|
|
1868
|
+
namespacesEnabled: true,
|
|
1869
|
+
defaultNamespace: "default",
|
|
1870
|
+
sharedNamespace: "shared",
|
|
1871
|
+
namespacePolicies: [{ name: "project-a" }, { name: "project-b" }],
|
|
1872
|
+
maintenanceMaxNamespacesPerCycle: 3,
|
|
1873
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
1874
|
+
qmdAutoEmbedEnabled: true,
|
|
1875
|
+
qmdEmbedMinIntervalMs: 60_000,
|
|
1876
|
+
};
|
|
1877
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1878
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1879
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1880
|
+
orchestrator.lastQmdEmbedAtMsByNamespace = new Map();
|
|
1881
|
+
orchestrator.namespaceCatalog = {
|
|
1882
|
+
enabled: false,
|
|
1883
|
+
async listNamespaces() {
|
|
1884
|
+
throw new Error("catalog disabled - must not be read");
|
|
1885
|
+
},
|
|
1886
|
+
};
|
|
1887
|
+
orchestrator.namespaceSearchRouter = {
|
|
1888
|
+
async updateNamespacesDetailed(ns: string[]) {
|
|
1889
|
+
updateCalls.push([...ns]);
|
|
1890
|
+
return { backendCount: ns.length, eligibleNamespaces: ns };
|
|
1891
|
+
},
|
|
1892
|
+
async embedNamespaces(ns: string[]) {
|
|
1893
|
+
embedCalls.push([...ns]);
|
|
1894
|
+
},
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
await orchestrator.runQmdMaintenance();
|
|
1898
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1899
|
+
await orchestrator.runQmdMaintenance();
|
|
1900
|
+
|
|
1901
|
+
assert.deepEqual(updateCalls, [
|
|
1902
|
+
["default", "shared", "project-a"],
|
|
1903
|
+
["default", "shared", "project-b"],
|
|
1904
|
+
]);
|
|
1905
|
+
assert.deepEqual(
|
|
1906
|
+
embedCalls,
|
|
1907
|
+
[["default", "shared", "project-a"], ["project-b"]],
|
|
1908
|
+
"a global embed timestamp must not suppress embeddings for newly budgeted namespaces",
|
|
1909
|
+
);
|
|
1910
|
+
} finally {
|
|
1911
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is unsafe", async () => {
|
|
1916
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1917
|
+
const updateArgs: string[] = [];
|
|
1918
|
+
const updateCalls: string[][] = [];
|
|
1919
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-root-"));
|
|
1920
|
+
const outsideDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-target-"));
|
|
1921
|
+
try {
|
|
1922
|
+
const dynamicNamespace = "project-origin-symlinked";
|
|
1923
|
+
const liveLegacyRoot = path.join(memoryDir, "namespaces", dynamicNamespace);
|
|
1924
|
+
const catalogSafeRoot = path.join(
|
|
1925
|
+
memoryDir,
|
|
1926
|
+
"namespaces",
|
|
1927
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
1928
|
+
);
|
|
1929
|
+
await mkdir(path.dirname(liveLegacyRoot), { recursive: true });
|
|
1930
|
+
await symlink(outsideDir, liveLegacyRoot, "dir");
|
|
1931
|
+
|
|
1932
|
+
orchestrator.config = {
|
|
1933
|
+
memoryDir,
|
|
1934
|
+
namespacesEnabled: true,
|
|
1935
|
+
defaultNamespace: "default",
|
|
1936
|
+
sharedNamespace: "shared",
|
|
1937
|
+
namespacePolicies: [],
|
|
1938
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
1939
|
+
qmdAutoEmbedEnabled: false,
|
|
1940
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1941
|
+
};
|
|
1942
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1943
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1944
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1945
|
+
orchestrator.namespaceCatalog = {
|
|
1946
|
+
enabled: true,
|
|
1947
|
+
async listNamespaces() {
|
|
1948
|
+
return [
|
|
1949
|
+
{
|
|
1950
|
+
namespace: dynamicNamespace,
|
|
1951
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
1952
|
+
kind: "project",
|
|
1953
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
1954
|
+
storageDir: catalogSafeRoot,
|
|
1955
|
+
discoveredBy: "write",
|
|
1956
|
+
},
|
|
1957
|
+
];
|
|
1958
|
+
},
|
|
1959
|
+
};
|
|
1960
|
+
orchestrator.namespaceSearchRouter = {
|
|
1961
|
+
async updateNamespacesDetailed(ns: string[]) {
|
|
1962
|
+
updateCalls.push([...ns]);
|
|
1963
|
+
updateArgs.push(...ns);
|
|
1964
|
+
return { backendCount: ns.length, eligibleNamespaces: ns };
|
|
1965
|
+
},
|
|
1966
|
+
async embedNamespaces() {},
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1969
|
+
await orchestrator.runQmdMaintenance();
|
|
1970
|
+
|
|
1971
|
+
assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
|
|
1972
|
+
assert.equal(updateCalls.length, 1, "global QMD maintenance must update once for the locked namespace set");
|
|
1973
|
+
assert.deepEqual(
|
|
1974
|
+
[...updateArgs].sort(),
|
|
1975
|
+
["default", "shared"],
|
|
1976
|
+
"cataloged dynamic namespaces are skipped when the live router root differs from the catalog-sanitized root",
|
|
1977
|
+
);
|
|
1978
|
+
} finally {
|
|
1979
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1980
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
test("runQmdMaintenance treats zero namespace updates as failed maintenance", async () => {
|
|
1985
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1986
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-zero-update-"));
|
|
1987
|
+
let markMaintenanceCalls = 0;
|
|
1988
|
+
let embedCalls = 0;
|
|
1989
|
+
try {
|
|
1990
|
+
orchestrator.config = {
|
|
1991
|
+
memoryDir,
|
|
1992
|
+
namespacesEnabled: true,
|
|
1993
|
+
defaultNamespace: "default",
|
|
1994
|
+
sharedNamespace: "shared",
|
|
1995
|
+
namespacePolicies: [],
|
|
1996
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
1997
|
+
qmdAutoEmbedEnabled: false,
|
|
1998
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1999
|
+
};
|
|
2000
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2001
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2002
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2003
|
+
orchestrator.namespaceCatalog = {
|
|
2004
|
+
enabled: true,
|
|
2005
|
+
async listNamespaces() {
|
|
2006
|
+
return [{ namespace: "default" }];
|
|
2007
|
+
},
|
|
2008
|
+
async markMaintenance() {
|
|
2009
|
+
markMaintenanceCalls += 1;
|
|
2010
|
+
},
|
|
2011
|
+
};
|
|
2012
|
+
orchestrator.namespaceSearchRouter = {
|
|
2013
|
+
async updateNamespacesDetailed() {
|
|
2014
|
+
return { backendCount: 0, eligibleNamespaces: [] };
|
|
2015
|
+
},
|
|
2016
|
+
async embedNamespaces() {
|
|
2017
|
+
embedCalls += 1;
|
|
2018
|
+
},
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
await orchestrator.runQmdMaintenance();
|
|
2022
|
+
|
|
2023
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2024
|
+
assert.ok(
|
|
2025
|
+
statuses.some((status) => status.namespace === "default" && status.state === "failed"),
|
|
2026
|
+
"zero updates should be recorded as failed maintenance, not a successful run",
|
|
2027
|
+
);
|
|
2028
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2029
|
+
assert.equal(embedCalls, 0);
|
|
2030
|
+
assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2031
|
+
} finally {
|
|
2032
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
test("runQmdMaintenance treats partial namespace update eligibility as failed maintenance", async () => {
|
|
2037
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2038
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-partial-update-"));
|
|
2039
|
+
let markMaintenanceCalls = 0;
|
|
2040
|
+
let embedCalls = 0;
|
|
2041
|
+
try {
|
|
2042
|
+
orchestrator.config = {
|
|
2043
|
+
memoryDir,
|
|
2044
|
+
namespacesEnabled: true,
|
|
2045
|
+
defaultNamespace: "default",
|
|
2046
|
+
sharedNamespace: "shared",
|
|
2047
|
+
namespacePolicies: [],
|
|
2048
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2049
|
+
qmdAutoEmbedEnabled: false,
|
|
2050
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2051
|
+
};
|
|
2052
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2053
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2054
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2055
|
+
orchestrator.namespaceCatalog = {
|
|
2056
|
+
enabled: true,
|
|
2057
|
+
async listNamespaces() {
|
|
2058
|
+
return [{ namespace: "default" }];
|
|
2059
|
+
},
|
|
2060
|
+
async markMaintenance() {
|
|
2061
|
+
markMaintenanceCalls += 1;
|
|
2062
|
+
},
|
|
2063
|
+
};
|
|
2064
|
+
orchestrator.namespaceSearchRouter = {
|
|
2065
|
+
async updateNamespacesDetailed(ns: string[]) {
|
|
2066
|
+
assert.ok(ns.includes("default") && ns.includes("shared"));
|
|
2067
|
+
return { backendCount: 1, eligibleNamespaces: ["default"] };
|
|
2068
|
+
},
|
|
2069
|
+
async embedNamespaces() {
|
|
2070
|
+
embedCalls += 1;
|
|
2071
|
+
},
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
await orchestrator.runQmdMaintenance();
|
|
2075
|
+
|
|
2076
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2077
|
+
assert.ok(
|
|
2078
|
+
statuses.some((status) => status.namespace === "default" && status.state === "failed"),
|
|
2079
|
+
"partial update eligibility should not be recorded as successful maintenance",
|
|
2080
|
+
);
|
|
2081
|
+
assert.ok(
|
|
2082
|
+
statuses.some((status) => status.namespace === "shared" && status.state === "failed"),
|
|
2083
|
+
"ineligible selected namespaces should not be rotated as maintained",
|
|
2084
|
+
);
|
|
2085
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2086
|
+
assert.equal(embedCalls, 0);
|
|
2087
|
+
assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2088
|
+
} finally {
|
|
2089
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
test("runQmdMaintenance treats namespace embed errors as failed maintenance", async () => {
|
|
2094
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2095
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-embed-failure-"));
|
|
2096
|
+
let markMaintenanceCalls = 0;
|
|
2097
|
+
try {
|
|
2098
|
+
orchestrator.config = {
|
|
2099
|
+
memoryDir,
|
|
2100
|
+
namespacesEnabled: true,
|
|
2101
|
+
defaultNamespace: "default",
|
|
2102
|
+
sharedNamespace: "shared",
|
|
2103
|
+
namespacePolicies: [],
|
|
2104
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2105
|
+
qmdAutoEmbedEnabled: true,
|
|
2106
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2107
|
+
};
|
|
2108
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2109
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2110
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2111
|
+
orchestrator.namespaceCatalog = {
|
|
2112
|
+
enabled: true,
|
|
2113
|
+
async listNamespaces() {
|
|
2114
|
+
return [{ namespace: "default" }];
|
|
2115
|
+
},
|
|
2116
|
+
async markMaintenance() {
|
|
2117
|
+
markMaintenanceCalls += 1;
|
|
2118
|
+
},
|
|
2119
|
+
};
|
|
2120
|
+
orchestrator.namespaceSearchRouter = {
|
|
2121
|
+
async updateNamespacesDetailed(ns: string[]) {
|
|
2122
|
+
return { backendCount: 1, eligibleNamespaces: ns };
|
|
2123
|
+
},
|
|
2124
|
+
async embedNamespaces() {
|
|
2125
|
+
throw Object.assign(new Error("embed failed"), { code: "EQMD" });
|
|
2126
|
+
},
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
await orchestrator.runQmdMaintenance();
|
|
2130
|
+
|
|
2131
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2132
|
+
assert.ok(
|
|
2133
|
+
statuses.some((status) => status.namespace === "default" && status.state === "failed"),
|
|
2134
|
+
"embed failures should not be recorded as successful namespace maintenance",
|
|
2135
|
+
);
|
|
2136
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2137
|
+
assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2138
|
+
} finally {
|
|
2139
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
test("runQmdMaintenance records QMD min-interval throttles as skipped maintenance", async () => {
|
|
2144
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2145
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-throttled-update-"));
|
|
2146
|
+
let markMaintenanceCalls = 0;
|
|
2147
|
+
let embedCalls = 0;
|
|
2148
|
+
let updateCalls = 0;
|
|
2149
|
+
try {
|
|
2150
|
+
orchestrator.config = {
|
|
2151
|
+
memoryDir,
|
|
2152
|
+
namespacesEnabled: true,
|
|
2153
|
+
defaultNamespace: "default",
|
|
2154
|
+
sharedNamespace: "shared",
|
|
2155
|
+
namespacePolicies: [],
|
|
2156
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2157
|
+
qmdAutoEmbedEnabled: false,
|
|
2158
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2159
|
+
};
|
|
2160
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2161
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2162
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2163
|
+
orchestrator.namespaceCatalog = {
|
|
2164
|
+
enabled: true,
|
|
2165
|
+
async listNamespaces() {
|
|
2166
|
+
return [{ namespace: "default" }];
|
|
2167
|
+
},
|
|
2168
|
+
async markMaintenance() {
|
|
2169
|
+
markMaintenanceCalls += 1;
|
|
2170
|
+
},
|
|
2171
|
+
};
|
|
2172
|
+
orchestrator.namespaceSearchRouter = {
|
|
2173
|
+
async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
|
|
2174
|
+
updateCalls += 1;
|
|
2175
|
+
assert.equal(options?.strict, true, "recurring maintenance must use strict QMD updates");
|
|
2176
|
+
throw new Error("QMD update skipped by global min-interval gate");
|
|
2177
|
+
},
|
|
2178
|
+
async embedNamespaces() {
|
|
2179
|
+
embedCalls += 1;
|
|
2180
|
+
},
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
await orchestrator.runQmdMaintenance();
|
|
2184
|
+
|
|
2185
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2186
|
+
assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
|
|
2187
|
+
assert.ok(
|
|
2188
|
+
statuses.some(
|
|
2189
|
+
(status) =>
|
|
2190
|
+
status.namespace === "default" &&
|
|
2191
|
+
status.state === "skipped" &&
|
|
2192
|
+
status.reason === "throttled",
|
|
2193
|
+
),
|
|
2194
|
+
"QMD min-interval throttles should be recorded as skipped maintenance",
|
|
2195
|
+
);
|
|
2196
|
+
assert.ok(
|
|
2197
|
+
statuses.some(
|
|
2198
|
+
(status) =>
|
|
2199
|
+
status.namespace === "shared" &&
|
|
2200
|
+
status.state === "skipped" &&
|
|
2201
|
+
status.reason === "throttled",
|
|
2202
|
+
),
|
|
2203
|
+
"every selected namespace should receive the throttled skip status",
|
|
2204
|
+
);
|
|
2205
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2206
|
+
assert.equal(embedCalls, 0);
|
|
2207
|
+
assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2208
|
+
} finally {
|
|
2209
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
test("runQmdMaintenance still embeds when due update is throttled", async () => {
|
|
2214
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2215
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-throttled-update-embed-"));
|
|
2216
|
+
let markMaintenanceCalls = 0;
|
|
2217
|
+
let updateCalls = 0;
|
|
2218
|
+
const embedCalls: string[][] = [];
|
|
2219
|
+
try {
|
|
2220
|
+
orchestrator.config = {
|
|
2221
|
+
memoryDir,
|
|
2222
|
+
namespacesEnabled: true,
|
|
2223
|
+
defaultNamespace: "default",
|
|
2224
|
+
sharedNamespace: "shared",
|
|
2225
|
+
namespacePolicies: [],
|
|
2226
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2227
|
+
qmdAutoEmbedEnabled: true,
|
|
2228
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2229
|
+
};
|
|
2230
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2231
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2232
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2233
|
+
orchestrator.namespaceCatalog = {
|
|
2234
|
+
enabled: true,
|
|
2235
|
+
async listNamespaces() {
|
|
2236
|
+
return [{ namespace: "default" }];
|
|
2237
|
+
},
|
|
2238
|
+
async markMaintenance() {
|
|
2239
|
+
markMaintenanceCalls += 1;
|
|
2240
|
+
},
|
|
2241
|
+
};
|
|
2242
|
+
orchestrator.namespaceSearchRouter = {
|
|
2243
|
+
async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
|
|
2244
|
+
updateCalls += 1;
|
|
2245
|
+
assert.equal(options?.strict, true, "recurring maintenance must use strict QMD updates");
|
|
2246
|
+
throw new Error("QMD update skipped by global min-interval gate");
|
|
2247
|
+
},
|
|
2248
|
+
async embedNamespaces(ns: string[], options?: { strict?: boolean }) {
|
|
2249
|
+
assert.equal(options?.strict, true, "due embed retries must surface embed failures");
|
|
2250
|
+
embedCalls.push([...ns]);
|
|
2251
|
+
},
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
await orchestrator.runQmdMaintenance();
|
|
2255
|
+
|
|
2256
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2257
|
+
assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
|
|
2258
|
+
assert.deepEqual(embedCalls, [["default", "shared"]]);
|
|
2259
|
+
assert.ok(
|
|
2260
|
+
statuses.every((status) => status.state === "skipped" && status.reason === "throttled"),
|
|
2261
|
+
"a throttled update should still be recorded as skipped after the due embed retry",
|
|
2262
|
+
);
|
|
2263
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2264
|
+
assert.notEqual(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2265
|
+
} finally {
|
|
2266
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
test("runQmdMaintenance treats strict namespace update errors as failed maintenance", async () => {
|
|
2271
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2272
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-update-failure-"));
|
|
2273
|
+
let markMaintenanceCalls = 0;
|
|
2274
|
+
let embedCalls = 0;
|
|
2275
|
+
let updateCalls = 0;
|
|
2276
|
+
try {
|
|
2277
|
+
orchestrator.config = {
|
|
2278
|
+
memoryDir,
|
|
2279
|
+
namespacesEnabled: true,
|
|
2280
|
+
defaultNamespace: "default",
|
|
2281
|
+
sharedNamespace: "shared",
|
|
2282
|
+
namespacePolicies: [],
|
|
2283
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2284
|
+
qmdAutoEmbedEnabled: true,
|
|
2285
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2286
|
+
};
|
|
2287
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2288
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2289
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2290
|
+
orchestrator.namespaceCatalog = {
|
|
2291
|
+
enabled: true,
|
|
2292
|
+
async listNamespaces() {
|
|
2293
|
+
return [{ namespace: "default" }];
|
|
2294
|
+
},
|
|
2295
|
+
async markMaintenance() {
|
|
2296
|
+
markMaintenanceCalls += 1;
|
|
2297
|
+
},
|
|
2298
|
+
};
|
|
2299
|
+
orchestrator.namespaceSearchRouter = {
|
|
2300
|
+
async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
|
|
2301
|
+
updateCalls += 1;
|
|
2302
|
+
assert.equal(options?.strict, true, "recurring maintenance must require strict update failure propagation");
|
|
2303
|
+
throw new Error("qmd exploded");
|
|
2304
|
+
},
|
|
2305
|
+
async embedNamespaces() {
|
|
2306
|
+
embedCalls += 1;
|
|
2307
|
+
},
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
await orchestrator.runQmdMaintenance();
|
|
2311
|
+
|
|
2312
|
+
const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
|
|
2313
|
+
assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
|
|
2314
|
+
assert.ok(
|
|
2315
|
+
statuses.some((status) => status.namespace === "default" && status.state === "failed"),
|
|
2316
|
+
"strict update errors should be recorded as failed maintenance",
|
|
2317
|
+
);
|
|
2318
|
+
assert.equal(markMaintenanceCalls, 0);
|
|
2319
|
+
assert.equal(embedCalls, 0);
|
|
2320
|
+
assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
|
|
2321
|
+
} finally {
|
|
2322
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
// NGnei fallback: when the catalog is disabled, maintenance covers exactly the
|
|
2327
|
+
// configured set (no catalog read), and a catalog read failure degrades to the
|
|
2328
|
+
// configured set rather than breaking maintenance.
|
|
2329
|
+
test("runQmdMaintenance falls back to configured namespaces when the catalog is disabled (NGnei)", async () => {
|
|
2330
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2331
|
+
const updateArgs: string[] = [];
|
|
2332
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-disabled-catalog-"));
|
|
2333
|
+
|
|
2334
|
+
try {
|
|
2335
|
+
orchestrator.config = {
|
|
2336
|
+
memoryDir,
|
|
2337
|
+
namespacesEnabled: true,
|
|
2338
|
+
defaultNamespace: "default",
|
|
2339
|
+
sharedNamespace: "shared",
|
|
2340
|
+
namespacePolicies: [],
|
|
2341
|
+
maintenanceNamespaceLockStaleMs: 100,
|
|
2342
|
+
qmdAutoEmbedEnabled: false,
|
|
2343
|
+
qmdEmbedMinIntervalMs: 0,
|
|
2344
|
+
};
|
|
2345
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
2346
|
+
orchestrator.qmdMaintenancePending = true;
|
|
2347
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
2348
|
+
orchestrator.namespaceCatalog = {
|
|
2349
|
+
enabled: false,
|
|
2350
|
+
async listNamespaces() {
|
|
2351
|
+
throw new Error("catalog disabled — must not be read");
|
|
2352
|
+
},
|
|
2353
|
+
};
|
|
2354
|
+
orchestrator.namespaceSearchRouter = {
|
|
2355
|
+
async updateNamespacesDetailed(ns: string[]) {
|
|
2356
|
+
updateArgs.push(...ns);
|
|
2357
|
+
return { backendCount: ns.length, eligibleNamespaces: ns };
|
|
2358
|
+
},
|
|
2359
|
+
async embedNamespaces() {},
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
await orchestrator.runQmdMaintenance();
|
|
2363
|
+
|
|
2364
|
+
assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
|
|
2365
|
+
assert.deepEqual(
|
|
2366
|
+
[...updateArgs].sort(),
|
|
2367
|
+
["default", "shared"],
|
|
2368
|
+
"a disabled catalog covers exactly the configured set",
|
|
2369
|
+
);
|
|
2370
|
+
} finally {
|
|
2371
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
// ── NHZEV (codex P2): the QMD STARTUP sync in deferredInitialize() must cover
|
|
2376
|
+
// cataloged dynamic namespaces too, not only configuredNamespaces(). A dynamic
|
|
2377
|
+
// namespace written before a daemon restart exists ONLY in the persisted catalog;
|
|
2378
|
+
// if the boot-time "sync current disk state" pass embeds only the configured set,
|
|
2379
|
+
// that namespace's QMD collection stays stale after restart. We drive
|
|
2380
|
+
// deferredInitialize() with stubbed internals and abort the signal right after the
|
|
2381
|
+
// sync (the next `if (signal.aborted) return;` bails before warmup), then assert the
|
|
2382
|
+
// startup updateNamespaces() received the UNION of configured + cataloged namespaces.
|
|
2383
|
+
test("deferredInitialize startup sync covers cataloged dynamic namespaces (NHZEV)", async () => {
|
|
2384
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2385
|
+
let updateArg: string[] | undefined;
|
|
2386
|
+
const abortController = new AbortController();
|
|
2387
|
+
const memoryDir = path.join(os.tmpdir(), "remnic-startup-maintenance-nhzev");
|
|
2388
|
+
const dynamicNamespace = "project-origin-dynamic";
|
|
2389
|
+
const dynamicStorageDir = path.join(
|
|
2390
|
+
memoryDir,
|
|
2391
|
+
"namespaces",
|
|
2392
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
2393
|
+
);
|
|
2394
|
+
await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
|
|
2395
|
+
|
|
2396
|
+
orchestrator.config = {
|
|
2397
|
+
memoryDir,
|
|
2398
|
+
namespacesEnabled: true,
|
|
2399
|
+
defaultNamespace: "default",
|
|
2400
|
+
sharedNamespace: "shared",
|
|
2401
|
+
namespacePolicies: [],
|
|
2402
|
+
qmdMaintenanceEnabled: true,
|
|
2403
|
+
maintenanceMaxNamespacesPerCycle: 2,
|
|
2404
|
+
};
|
|
2405
|
+
orchestrator.qmd = {
|
|
2406
|
+
isAvailable: () => true,
|
|
2407
|
+
async update() {},
|
|
2408
|
+
};
|
|
2409
|
+
orchestrator.namespaceCatalog = {
|
|
2410
|
+
enabled: true,
|
|
2411
|
+
async listNamespaces() {
|
|
2412
|
+
return [
|
|
2413
|
+
{ namespace: "default" },
|
|
2414
|
+
{
|
|
2415
|
+
namespace: dynamicNamespace,
|
|
2416
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
2417
|
+
kind: "project",
|
|
2418
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
2419
|
+
storageDir: dynamicStorageDir,
|
|
2420
|
+
discoveredBy: "write",
|
|
2421
|
+
}, // dynamic, catalog-ONLY, NOT configured
|
|
2422
|
+
];
|
|
2423
|
+
},
|
|
2424
|
+
};
|
|
2425
|
+
orchestrator.namespaceSearchRouter = {
|
|
2426
|
+
async updateNamespaces(ns: string[]) {
|
|
2427
|
+
updateArg = ns;
|
|
2428
|
+
// Abort AFTER the startup sync records its arg so deferredInitialize bails
|
|
2429
|
+
// at the next `if (signal.aborted) return;` before warmup/caches run.
|
|
2430
|
+
abortController.abort();
|
|
2431
|
+
return ns.length;
|
|
2432
|
+
},
|
|
2433
|
+
};
|
|
2434
|
+
|
|
2435
|
+
await orchestrator.deferredInitialize(abortController.signal);
|
|
2436
|
+
|
|
2437
|
+
assert.ok(updateArg, "startup updateNamespaces must be called");
|
|
2438
|
+
assert.ok(
|
|
2439
|
+
updateArg!.includes(dynamicNamespace),
|
|
2440
|
+
"startup sync must cover the cataloged dynamic namespace even when it exceeds the recurring maintenance cycle budget",
|
|
2441
|
+
);
|
|
2442
|
+
assert.ok(
|
|
2443
|
+
updateArg!.includes("default") && updateArg!.includes("shared"),
|
|
2444
|
+
"configured namespaces remain covered at startup",
|
|
2445
|
+
);
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
// NHZEV fallback: a catalog read failure during startup sync must degrade to the
|
|
2449
|
+
// configured set rather than breaking deferredInitialize — same failure-tolerance
|
|
2450
|
+
// contract as runQmdMaintenance (maintenanceNamespaces swallows the read error).
|
|
2451
|
+
test("deferredInitialize startup sync falls back to configured set on catalog read failure (NHZEV)", async () => {
|
|
2452
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2453
|
+
let updateArg: string[] | undefined;
|
|
2454
|
+
const abortController = new AbortController();
|
|
2455
|
+
|
|
2456
|
+
orchestrator.config = {
|
|
2457
|
+
namespacesEnabled: true,
|
|
2458
|
+
defaultNamespace: "default",
|
|
2459
|
+
sharedNamespace: "shared",
|
|
2460
|
+
namespacePolicies: [],
|
|
2461
|
+
qmdMaintenanceEnabled: true,
|
|
2462
|
+
};
|
|
2463
|
+
orchestrator.qmd = {
|
|
2464
|
+
isAvailable: () => true,
|
|
2465
|
+
async update() {},
|
|
2466
|
+
};
|
|
2467
|
+
orchestrator.namespaceCatalog = {
|
|
2468
|
+
enabled: true,
|
|
2469
|
+
async listNamespaces() {
|
|
2470
|
+
throw new Error("catalog read failed");
|
|
2471
|
+
},
|
|
2472
|
+
};
|
|
2473
|
+
orchestrator.namespaceSearchRouter = {
|
|
2474
|
+
async updateNamespaces(ns: string[]) {
|
|
2475
|
+
updateArg = ns;
|
|
2476
|
+
abortController.abort();
|
|
2477
|
+
return ns.length;
|
|
2478
|
+
},
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
await orchestrator.deferredInitialize(abortController.signal);
|
|
2482
|
+
|
|
2483
|
+
assert.ok(updateArg, "startup updateNamespaces must be called");
|
|
2484
|
+
assert.deepEqual(
|
|
2485
|
+
[...updateArg!].sort(),
|
|
2486
|
+
["default", "shared"],
|
|
2487
|
+
"a catalog read failure degrades startup sync to the configured set",
|
|
2488
|
+
);
|
|
2489
|
+
});
|