@remnic/core 9.3.692 → 9.3.694
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 +6 -6
- package/dist/access-boundary.js +11 -9
- package/dist/access-cli.js +76 -23
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +14 -12
- package/dist/access-mcp.d.ts +14 -5
- package/dist/access-mcp.js +13 -11
- package/dist/access-operations.d.ts +54 -7
- package/dist/access-operations.js +42 -10
- package/dist/{access-service-DvApf9LZ.d.ts → access-service-Ceq4TdPO.d.ts} +180 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +10 -8
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +17 -0
- package/dist/access-surface-catalog.js.map +1 -1
- 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 +4 -4
- 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/capabilities.d.ts +1 -1
- package/dist/{catalog-BEWLjdOr.d.ts → catalog-Tkb991rg.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-ADUAF5EV.js → chunk-24FCDBJV.js} +2 -2
- package/dist/{chunk-V56S3A23.js → chunk-5A35EGHM.js} +1092 -29
- package/dist/chunk-5A35EGHM.js.map +1 -0
- package/dist/{chunk-3VVH6ZQW.js → chunk-5S53ZBPH.js} +2 -2
- package/dist/{chunk-DO7MDZMU.js → chunk-6C4WHMKV.js} +123 -7
- package/dist/chunk-6C4WHMKV.js.map +1 -0
- package/dist/{chunk-2D27ODNX.js → chunk-B55EAZNF.js} +2 -2
- package/dist/{chunk-W4MICQIZ.js → chunk-C5AOVGVC.js} +15 -15
- package/dist/chunk-CDOIDOTQ.js +381 -0
- package/dist/chunk-CDOIDOTQ.js.map +1 -0
- package/dist/{chunk-UOCLRM74.js → chunk-D7PQ7HZL.js} +2 -2
- package/dist/{chunk-HXNMDPHN.js → chunk-DT2ZGQT7.js} +2 -2
- package/dist/{chunk-QFG23CRE.js → chunk-EPVIBPVE.js} +2 -2
- package/dist/{chunk-O4ZIV62T.js → chunk-FCH766FX.js} +21 -19
- package/dist/chunk-FCH766FX.js.map +1 -0
- package/dist/{chunk-7MOTEVAA.js → chunk-HRUULBBV.js} +3 -1
- package/dist/chunk-HRUULBBV.js.map +1 -0
- package/dist/{chunk-U2OBZSGG.js → chunk-IZLNJYLX.js} +2 -2
- package/dist/{chunk-KYH53EFD.js → chunk-JIKBEDSB.js} +6 -6
- package/dist/{chunk-3BRA3N5A.js → chunk-KCRV3HVN.js} +2 -2
- package/dist/{chunk-AFWEUTMS.js → chunk-MOGFK2DU.js} +2 -2
- package/dist/{chunk-SAC5V6JT.js → chunk-MSUA4KAK.js} +2 -2
- package/dist/{chunk-VFVG4NS2.js → chunk-NXNUG7IA.js} +2 -2
- package/dist/{chunk-66HROLW5.js → chunk-P7IDNF7H.js} +2 -2
- package/dist/{chunk-FM7KLRKN.js → chunk-PILLH262.js} +2 -2
- package/dist/{chunk-UTUYAPLY.js → chunk-PNW4KKJX.js} +12 -3
- package/dist/{chunk-UTUYAPLY.js.map → chunk-PNW4KKJX.js.map} +1 -1
- package/dist/{chunk-2EN5WZQB.js → chunk-PQP6MFPL.js} +2 -2
- package/dist/chunk-PQP6MFPL.js.map +1 -0
- package/dist/{chunk-6C2BMUZA.js → chunk-QXWQ5V24.js} +2 -2
- package/dist/{chunk-SEK4XIDN.js → chunk-SJGTY2TW.js} +35 -9
- package/dist/chunk-SJGTY2TW.js.map +1 -0
- package/dist/{chunk-4IQNAOP3.js → chunk-SN5DFE34.js} +2 -2
- package/dist/{chunk-DQHZA7Y3.js → chunk-U62ZGWU7.js} +1 -1
- package/dist/chunk-U62ZGWU7.js.map +1 -0
- package/dist/{chunk-WIKNVFBS.js → chunk-UX5QTAFE.js} +2 -2
- package/dist/chunk-VQ7B2Q6M.js +112 -0
- package/dist/chunk-VQ7B2Q6M.js.map +1 -0
- package/dist/{chunk-MCV2DD6F.js → chunk-WVTNZUER.js} +2 -2
- package/dist/{chunk-ZLXGFAML.js → chunk-YI3KC2B5.js} +2 -2
- package/dist/{chunk-W4ELOUNX.js → chunk-YWYZPKK3.js} +2 -2
- package/dist/{chunk-NUZ3N445.js → chunk-YXZPKBFW.js} +3 -3
- package/dist/{chunk-GCOSQBQ2.js → chunk-ZKD3UVEO.js} +3 -3
- package/dist/{cli-BjsY9Le7.d.ts → cli-CxtZyBLl.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +28 -26
- package/dist/coding/optional-coding-graph.js +7 -104
- package/dist/coding/optional-coding-graph.js.map +1 -1
- 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 +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 +4 -4
- 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 +9 -9
- package/dist/index.js +37 -36
- 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 +5 -5
- 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 +2 -2
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-CNJCYjZ1.d.ts → orchestrator-CnbxRQs8.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +16 -16
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- 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/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 +28 -28
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-D5U0Mrdb.d.ts → semantic-consolidation-DXmtQ_4B.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-2NoBoRBM.d.ts → types-CEWBmjCH.d.ts} +14 -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 +2 -2
- package/src/access-boundary.ts +19 -1
- package/src/access-cli.ts +65 -2
- package/src/access-http.ts +47 -3
- package/src/access-mcp.ts +128 -2
- package/src/access-operations.ts +305 -0
- package/src/access-service.ts +137 -0
- package/src/access-surface-catalog.test.ts +1 -1
- package/src/access-surface-catalog.ts +17 -0
- package/src/cli.ts +2 -0
- package/src/coding/architecture-surfaces.test.ts +2 -0
- package/src/coding/codegraph-runtime.ts +773 -0
- package/src/coding/codegraph-surfaces.test.ts +880 -0
- package/src/coding/codegraph-surfaces.ts +771 -0
- package/src/coding/coding-knowledge-config.ts +7 -0
- package/src/coding/coding-namespace.ts +1 -1
- package/src/coding/decision-surfaces.test.ts +2 -0
- package/src/coding/session-delta-surfaces.test.ts +385 -0
- package/src/coding/session-delta-surfaces.ts +325 -0
- package/src/coding/session-delta.test.ts +411 -0
- package/src/coding/session-delta.ts +442 -0
- package/src/config.test.ts +2 -0
- package/src/types.ts +14 -0
- package/dist/chunk-2EN5WZQB.js.map +0 -1
- package/dist/chunk-7MOTEVAA.js.map +0 -1
- package/dist/chunk-DO7MDZMU.js.map +0 -1
- package/dist/chunk-DQHZA7Y3.js.map +0 -1
- package/dist/chunk-NEILAC3H.js +0 -153
- package/dist/chunk-NEILAC3H.js.map +0 -1
- package/dist/chunk-O4ZIV62T.js.map +0 -1
- package/dist/chunk-SEK4XIDN.js.map +0 -1
- package/dist/chunk-V56S3A23.js.map +0 -1
- /package/dist/{chunk-ADUAF5EV.js.map → chunk-24FCDBJV.js.map} +0 -0
- /package/dist/{chunk-3VVH6ZQW.js.map → chunk-5S53ZBPH.js.map} +0 -0
- /package/dist/{chunk-2D27ODNX.js.map → chunk-B55EAZNF.js.map} +0 -0
- /package/dist/{chunk-W4MICQIZ.js.map → chunk-C5AOVGVC.js.map} +0 -0
- /package/dist/{chunk-UOCLRM74.js.map → chunk-D7PQ7HZL.js.map} +0 -0
- /package/dist/{chunk-HXNMDPHN.js.map → chunk-DT2ZGQT7.js.map} +0 -0
- /package/dist/{chunk-QFG23CRE.js.map → chunk-EPVIBPVE.js.map} +0 -0
- /package/dist/{chunk-U2OBZSGG.js.map → chunk-IZLNJYLX.js.map} +0 -0
- /package/dist/{chunk-KYH53EFD.js.map → chunk-JIKBEDSB.js.map} +0 -0
- /package/dist/{chunk-3BRA3N5A.js.map → chunk-KCRV3HVN.js.map} +0 -0
- /package/dist/{chunk-AFWEUTMS.js.map → chunk-MOGFK2DU.js.map} +0 -0
- /package/dist/{chunk-SAC5V6JT.js.map → chunk-MSUA4KAK.js.map} +0 -0
- /package/dist/{chunk-VFVG4NS2.js.map → chunk-NXNUG7IA.js.map} +0 -0
- /package/dist/{chunk-66HROLW5.js.map → chunk-P7IDNF7H.js.map} +0 -0
- /package/dist/{chunk-FM7KLRKN.js.map → chunk-PILLH262.js.map} +0 -0
- /package/dist/{chunk-6C2BMUZA.js.map → chunk-QXWQ5V24.js.map} +0 -0
- /package/dist/{chunk-4IQNAOP3.js.map → chunk-SN5DFE34.js.map} +0 -0
- /package/dist/{chunk-WIKNVFBS.js.map → chunk-UX5QTAFE.js.map} +0 -0
- /package/dist/{chunk-MCV2DD6F.js.map → chunk-WVTNZUER.js.map} +0 -0
- /package/dist/{chunk-ZLXGFAML.js.map → chunk-YI3KC2B5.js.map} +0 -0
- /package/dist/{chunk-W4ELOUNX.js.map → chunk-YWYZPKK3.js.map} +0 -0
- /package/dist/{chunk-NUZ3N445.js.map → chunk-YXZPKBFW.js.map} +0 -0
- /package/dist/{chunk-GCOSQBQ2.js.map → chunk-ZKD3UVEO.js.map} +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the session-delta pure differ + state persistence
|
|
3
|
+
* (issue #1548 Track A PR 4).
|
|
4
|
+
*
|
|
5
|
+
* Contract under test (the issue's three-state matrix + edge cases):
|
|
6
|
+
* - first_run: no prior state → no delta AND nextState initialized.
|
|
7
|
+
* A first session must NOT claim "0 changes" (rule 34).
|
|
8
|
+
* - unchanged: prior head == current head → suppressed, not rendered.
|
|
9
|
+
* - changed: prior head is an ancestor of current → real delta with caps.
|
|
10
|
+
* - unreachable_head: prior head missing from repo (force-push/rebase) →
|
|
11
|
+
* tagged { ok:false, code:"unreachable_head" }, never a crash.
|
|
12
|
+
* - rule 27: slice(-n) guard against n === 0.
|
|
13
|
+
* - rule 25 / rule 54: state write is temp-file-then-rename; read-back round-trips.
|
|
14
|
+
* - parseLogOutput: handles multi-commit + multi-file + blank-line blocks.
|
|
15
|
+
*/
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
buildSummaryLine,
|
|
24
|
+
capCommits,
|
|
25
|
+
capFiles,
|
|
26
|
+
computeSessionDelta,
|
|
27
|
+
MAX_DELTA_COMMITS,
|
|
28
|
+
MAX_DELTA_FILES,
|
|
29
|
+
parseLogOutput,
|
|
30
|
+
readLastSeenState,
|
|
31
|
+
resolveSlice,
|
|
32
|
+
sessionDeltaStatePath,
|
|
33
|
+
writeLastSeenState,
|
|
34
|
+
type GitCommit,
|
|
35
|
+
type GitLogSlice,
|
|
36
|
+
type LastSeenState,
|
|
37
|
+
type SessionDeltaGitInvoker,
|
|
38
|
+
} from "./session-delta.js";
|
|
39
|
+
|
|
40
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Helpers
|
|
42
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function slice(head: string, commits: GitCommit[] = [], files: string[] = []): GitLogSlice {
|
|
45
|
+
return { commits, touchedFiles: files, currentHead: head };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PRIOR: LastSeenState = { head: "aaa111", at: "2026-06-01T10:00:00Z" };
|
|
49
|
+
|
|
50
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Three-state matrix
|
|
52
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
test("first_run: null prior state → no delta, nextState initialized", () => {
|
|
55
|
+
const result = computeSessionDelta(null, slice("bbb222"));
|
|
56
|
+
assert.equal(result.ok, true);
|
|
57
|
+
if (!result.ok || result.kind !== "first_run") {
|
|
58
|
+
assert.fail(`expected first_run, got ${JSON.stringify(result)}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
assert.equal(result.nextState.head, "bbb222");
|
|
62
|
+
assert.ok(result.nextState.at);
|
|
63
|
+
// The first_run shape carries NO delta field — callers must suppress.
|
|
64
|
+
assert.equal("delta" in result, false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("unchanged: prior head == current head → suppressed", () => {
|
|
68
|
+
const result = computeSessionDelta(PRIOR, slice("aaa111"));
|
|
69
|
+
assert.equal(result.ok, true);
|
|
70
|
+
if (!result.ok || result.kind !== "unchanged") {
|
|
71
|
+
assert.fail(`expected unchanged, got ${JSON.stringify(result)}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
assert.equal(result.nextState.head, "aaa111");
|
|
75
|
+
assert.equal("delta" in result, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("changed: real delta returned with commits + summary", () => {
|
|
79
|
+
const commits: GitCommit[] = [
|
|
80
|
+
{ sha: "bbb222", subject: "feat: add session delta" },
|
|
81
|
+
{ sha: "ccc333", subject: "fix: edge case" },
|
|
82
|
+
];
|
|
83
|
+
const result = computeSessionDelta(
|
|
84
|
+
PRIOR,
|
|
85
|
+
slice("ccc333", commits, ["src/a.ts", "src/b.ts"]),
|
|
86
|
+
);
|
|
87
|
+
assert.equal(result.ok, true);
|
|
88
|
+
if (!result.ok || result.kind !== "changed") {
|
|
89
|
+
assert.fail(`expected changed, got ${JSON.stringify(result)}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
assert.equal(result.delta.commits.length, 2);
|
|
93
|
+
assert.equal(result.delta.touchedFiles.length, 2);
|
|
94
|
+
assert.equal(result.nextState.head, "ccc333");
|
|
95
|
+
// Summary line is deterministic and locale-stable.
|
|
96
|
+
assert.match(result.delta.summaryLine, /^Since 2026-06-01: 2 commits, 2 files touched\.$/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// Edge cases — rule 34 (tagged failures, never silent)
|
|
101
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
test("unreachable_head: head changed but zero commits → tagged failure", () => {
|
|
104
|
+
const result = computeSessionDelta(PRIOR, slice("fff999", [], []));
|
|
105
|
+
assert.equal(result.ok, false);
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
assert.fail(`expected failure, got ${JSON.stringify(result)}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
assert.equal(result.code, "unreachable_head");
|
|
111
|
+
assert.match(result.detail, /aaa111/);
|
|
112
|
+
assert.match(result.detail, /fff999/);
|
|
113
|
+
// nextState still advances so the next call sees the new head.
|
|
114
|
+
assert.equal(result.nextState.head, "fff999");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("summary line uses singular form for 1 commit / 1 file", () => {
|
|
118
|
+
const line = buildSummaryLine(1, 1, "2026-06-01T10:00:00Z");
|
|
119
|
+
assert.equal(line, "Since 2026-06-01: 1 commit, 1 file touched.");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("summary line falls back to raw timestamp when malformed", () => {
|
|
123
|
+
const line = buildSummaryLine(3, 2, "not-a-date");
|
|
124
|
+
assert.match(line, /not-a-date/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Caps — rule 27 (slice(-n) guard against n === 0)
|
|
129
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
test("capCommits: keeps the most-recent N commits", () => {
|
|
132
|
+
const commits: GitCommit[] = Array.from({ length: 30 }, (_, i) => ({
|
|
133
|
+
sha: `sha${i}`,
|
|
134
|
+
subject: `subject ${i}`,
|
|
135
|
+
}));
|
|
136
|
+
const capped = capCommits(commits, MAX_DELTA_COMMITS);
|
|
137
|
+
assert.equal(capped.length, MAX_DELTA_COMMITS);
|
|
138
|
+
// Most-recent (tail) wins.
|
|
139
|
+
assert.equal(capped[capped.length - 1]!.sha, "sha29");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("capCommits: max=0 returns empty (rule 27 guard)", () => {
|
|
143
|
+
const commits: GitCommit[] = [{ sha: "x", subject: "y" }];
|
|
144
|
+
assert.equal(capCommits(commits, 0).length, 0);
|
|
145
|
+
assert.equal(capCommits(commits, -1).length, 0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("capFiles: truncates to N entries preserving order", () => {
|
|
149
|
+
const files = Array.from({ length: 60 }, (_, i) => `file${i}.ts`);
|
|
150
|
+
const capped = capFiles(files, MAX_DELTA_FILES);
|
|
151
|
+
assert.equal(capped.length, MAX_DELTA_FILES);
|
|
152
|
+
assert.equal(capped[0], "file0.ts");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("capFiles: max=0 returns empty (rule 27 guard)", () => {
|
|
156
|
+
assert.equal(capFiles(["a", "b"], 0).length, 0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("computeSessionDelta caps a large delta to MAX constants", () => {
|
|
160
|
+
const commits: GitCommit[] = Array.from({ length: 100 }, (_, i) => ({
|
|
161
|
+
sha: `c${i}`,
|
|
162
|
+
subject: `s ${i}`,
|
|
163
|
+
}));
|
|
164
|
+
const files = Array.from({ length: 200 }, (_, i) => `f${i}.ts`);
|
|
165
|
+
const result = computeSessionDelta(PRIOR, slice("head", commits, files));
|
|
166
|
+
if (!result.ok || result.kind !== "changed") {
|
|
167
|
+
assert.fail(`expected changed, got ${JSON.stringify(result)}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
assert.equal(result.delta.commits.length, MAX_DELTA_COMMITS);
|
|
171
|
+
assert.equal(result.delta.touchedFiles.length, MAX_DELTA_FILES);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
175
|
+
// State persistence — rule 25 (write after compute) + rule 54 (temp+rename)
|
|
176
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
test("state path: namespace sanitized into the filename", () => {
|
|
179
|
+
const p = sessionDeltaStatePath("/mem", "project-abc");
|
|
180
|
+
assert.equal(p, path.join("/mem", "state", "coding-knowledge", "project-abc.json"));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("state path: unsafe namespace characters sanitized defensively", () => {
|
|
184
|
+
const p = sessionDeltaStatePath("/mem", "../escape!!");
|
|
185
|
+
// No path traversal — the '..' is collapsed.
|
|
186
|
+
assert.ok(!p.includes("escape!!"));
|
|
187
|
+
assert.ok(!p.includes("../"));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("state path: empty namespace falls back to 'default'", () => {
|
|
191
|
+
const p = sessionDeltaStatePath("/mem", "");
|
|
192
|
+
assert.ok(p.endsWith("default.json"));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("readLastSeenState: absent file returns null, never throws", async () => {
|
|
196
|
+
const result = await readLastSeenState("/nonexistent/path/state.json");
|
|
197
|
+
assert.equal(result, null);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("writeLastSeenState + readLastSeenState round-trips", async () => {
|
|
201
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
202
|
+
try {
|
|
203
|
+
const statePath = path.join(dir, "state.json");
|
|
204
|
+
const state: LastSeenState = { head: "deadbeef", at: "2026-07-01T00:00:00Z" };
|
|
205
|
+
await writeLastSeenState(statePath, state);
|
|
206
|
+
const readBack = await readLastSeenState(statePath);
|
|
207
|
+
assert.deepEqual(readBack, state);
|
|
208
|
+
} finally {
|
|
209
|
+
await rm(dir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("writeLastSeenState: temp file cleaned up after rename (rule 54)", async () => {
|
|
214
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
215
|
+
try {
|
|
216
|
+
const statePath = path.join(dir, "state.json");
|
|
217
|
+
await writeLastSeenState(statePath, { head: "abc", at: "2026-07-01T00:00:00Z" });
|
|
218
|
+
const { readdir } = await import("node:fs/promises");
|
|
219
|
+
const entries = await readdir(dir);
|
|
220
|
+
// Only state.json should remain — no leftover .tmp-* file.
|
|
221
|
+
assert.deepEqual(entries, ["state.json"]);
|
|
222
|
+
} finally {
|
|
223
|
+
await rm(dir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("readLastSeenState: malformed JSON returns null, never throws", async () => {
|
|
228
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
229
|
+
try {
|
|
230
|
+
const statePath = path.join(dir, "state.json");
|
|
231
|
+
const { writeFile } = await import("node:fs/promises");
|
|
232
|
+
await writeFile(statePath, "{not json", "utf8");
|
|
233
|
+
const result = await readLastSeenState(statePath);
|
|
234
|
+
assert.equal(result, null);
|
|
235
|
+
} finally {
|
|
236
|
+
await rm(dir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("readLastSeenState: missing head or at returns null", async () => {
|
|
241
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
242
|
+
try {
|
|
243
|
+
const statePath = path.join(dir, "state.json");
|
|
244
|
+
const { writeFile } = await import("node:fs/promises");
|
|
245
|
+
await writeFile(statePath, JSON.stringify({ head: "abc" }), "utf8");
|
|
246
|
+
const result = await readLastSeenState(statePath);
|
|
247
|
+
assert.equal(result, null);
|
|
248
|
+
} finally {
|
|
249
|
+
await rm(dir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("writeLastSeenState: invalid state throws (rule 51)", async () => {
|
|
254
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
255
|
+
try {
|
|
256
|
+
const statePath = path.join(dir, "state.json");
|
|
257
|
+
await assert.rejects(
|
|
258
|
+
() => writeLastSeenState(statePath, { head: "", at: "2026-07-01T00:00:00Z" }),
|
|
259
|
+
/invalid state/,
|
|
260
|
+
);
|
|
261
|
+
} finally {
|
|
262
|
+
await rm(dir, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
267
|
+
// Git invoker + slice resolution
|
|
268
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
function makeInvoker(responses: Array<{ args: string[]; stdout: string; exitCode?: number }>): SessionDeltaGitInvoker {
|
|
271
|
+
return (_cwd: string, args: string[]) => {
|
|
272
|
+
const match = responses.find((r) => r.args.join(" ") === args.join(" "));
|
|
273
|
+
if (!match) return { stdout: "", exitCode: 1 };
|
|
274
|
+
return { stdout: match.stdout, exitCode: match.exitCode ?? 0 };
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
test("resolveSlice: first run (no prior head) returns empty slice", () => {
|
|
279
|
+
const invoker = makeInvoker([
|
|
280
|
+
{ args: ["rev-parse", "HEAD"], stdout: "newhead\n" },
|
|
281
|
+
]);
|
|
282
|
+
const result = resolveSlice("/repo", null, invoker);
|
|
283
|
+
assert.equal(result.ok, true);
|
|
284
|
+
if (!result.ok) return;
|
|
285
|
+
assert.equal(result.slice.commits.length, 0);
|
|
286
|
+
assert.equal(result.slice.currentHead, "newhead");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("resolveSlice: unchanged head short-circuits before the log call", () => {
|
|
290
|
+
let logCalled = false;
|
|
291
|
+
const invoker: SessionDeltaGitInvoker = (_cwd, args) => {
|
|
292
|
+
if (args[0] === "log") logCalled = true;
|
|
293
|
+
if (args[0] === "rev-parse") return { stdout: "samehead\n", exitCode: 0 };
|
|
294
|
+
return { stdout: "", exitCode: 0 };
|
|
295
|
+
};
|
|
296
|
+
const result = resolveSlice("/repo", "samehead", invoker);
|
|
297
|
+
assert.equal(result.ok, true);
|
|
298
|
+
assert.equal(logCalled, false, "git log must not be called when head is unchanged");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("resolveSlice: real delta with commits + files", () => {
|
|
302
|
+
const sep = "\x1f";
|
|
303
|
+
const logOutput = [
|
|
304
|
+
`aaa${sep}feat: first`,
|
|
305
|
+
"",
|
|
306
|
+
"src/a.ts",
|
|
307
|
+
"src/b.ts",
|
|
308
|
+
"",
|
|
309
|
+
`bbb${sep}fix: second`,
|
|
310
|
+
"",
|
|
311
|
+
"src/a.ts",
|
|
312
|
+
"",
|
|
313
|
+
].join("\n");
|
|
314
|
+
const invoker = makeInvoker([
|
|
315
|
+
{ args: ["rev-parse", "HEAD"], stdout: "bbb\n" },
|
|
316
|
+
{ args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "aaa..bbb"], stdout: logOutput },
|
|
317
|
+
]);
|
|
318
|
+
const result = resolveSlice("/repo", "aaa", invoker);
|
|
319
|
+
assert.equal(result.ok, true);
|
|
320
|
+
if (!result.ok) return;
|
|
321
|
+
assert.equal(result.slice.commits.length, 2);
|
|
322
|
+
assert.equal(result.slice.commits[0]!.sha, "aaa");
|
|
323
|
+
assert.equal(result.slice.commits[1]!.subject, "fix: second");
|
|
324
|
+
// De-duplicated + sorted touched files.
|
|
325
|
+
assert.deepEqual(result.slice.touchedFiles, ["src/a.ts", "src/b.ts"]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("resolveSlice: rev-parse failure → git_failed / no_head", () => {
|
|
329
|
+
const invoker = makeInvoker([
|
|
330
|
+
{ args: ["rev-parse", "HEAD"], stdout: "", exitCode: 128 },
|
|
331
|
+
]);
|
|
332
|
+
const result = resolveSlice("/repo", null, invoker);
|
|
333
|
+
assert.equal(result.ok, false);
|
|
334
|
+
if (result.ok) return;
|
|
335
|
+
assert.equal(result.code, "no_head");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("resolveSlice: git log exit 128 → empty slice (unreachable head) → empty slice (differ flags unreachable)", () => {
|
|
339
|
+
const invoker = makeInvoker([
|
|
340
|
+
{ args: ["rev-parse", "HEAD"], stdout: "newhead\n" },
|
|
341
|
+
{ args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "oldhead..newhead"], stdout: "", exitCode: 128 },
|
|
342
|
+
]);
|
|
343
|
+
const result = resolveSlice("/repo", "oldhead", invoker);
|
|
344
|
+
// Returns ok with empty slice; the differ converts this to unreachable_head.
|
|
345
|
+
assert.equal(result.ok, true);
|
|
346
|
+
if (!result.ok) return;
|
|
347
|
+
assert.equal(result.slice.commits.length, 0);
|
|
348
|
+
assert.equal(result.slice.currentHead, "newhead");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("resolveSlice: git log exit 127 (timeout/spawn) \u2192 git_failed, not unreachable", () => {
|
|
352
|
+
const invoker = makeInvoker([
|
|
353
|
+
{ args: ["rev-parse", "HEAD"], stdout: "newhead\n" },
|
|
354
|
+
{ args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "oldhead..newhead"], stdout: "", exitCode: 127 },
|
|
355
|
+
]);
|
|
356
|
+
const result = resolveSlice("/repo", "oldhead", invoker);
|
|
357
|
+
// Transient failure \u2192 git_failed, NOT empty-slice/unreachable.
|
|
358
|
+
// The state marker must NOT advance so the next call retries.
|
|
359
|
+
assert.equal(result.ok, false);
|
|
360
|
+
if (result.ok) return;
|
|
361
|
+
assert.equal(result.code, "git_failed");
|
|
362
|
+
assert.match(result.detail, /127/);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
366
|
+
// parseLogOutput — direct unit test
|
|
367
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
test("parseLogOutput: handles blank-line-separated blocks", () => {
|
|
370
|
+
const sep = "\x1f";
|
|
371
|
+
const stdout = [
|
|
372
|
+
`sha1${sep}subject one`,
|
|
373
|
+
"",
|
|
374
|
+
"file1.ts",
|
|
375
|
+
"",
|
|
376
|
+
`sha2${sep}subject two`,
|
|
377
|
+
"",
|
|
378
|
+
"file1.ts",
|
|
379
|
+
"file2.ts",
|
|
380
|
+
].join("\n");
|
|
381
|
+
const { commits, touchedFiles } = parseLogOutput(stdout, sep);
|
|
382
|
+
assert.equal(commits.length, 2);
|
|
383
|
+
assert.equal(commits[0]!.sha, "sha1");
|
|
384
|
+
assert.equal(commits[1]!.subject, "subject two");
|
|
385
|
+
// file1.ts deduplicated.
|
|
386
|
+
assert.equal(touchedFiles.length, 2);
|
|
387
|
+
assert.deepEqual(touchedFiles, ["file1.ts", "file2.ts"]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("parseLogOutput: empty input → empty results", () => {
|
|
391
|
+
const { commits, touchedFiles } = parseLogOutput("", "\x1f");
|
|
392
|
+
assert.equal(commits.length, 0);
|
|
393
|
+
assert.equal(touchedFiles.length, 0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
397
|
+
// Read-back from disk proves the file shape on disk (rule: verify on disk)
|
|
398
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
test("writeLastSeenState writes JSON with head + at keys", async () => {
|
|
401
|
+
const dir = await mkdtemp(path.join(tmpdir(), "session-delta-"));
|
|
402
|
+
try {
|
|
403
|
+
const statePath = path.join(dir, "state.json");
|
|
404
|
+
await writeLastSeenState(statePath, { head: "abc123", at: "2026-07-01T00:00:00Z" });
|
|
405
|
+
const raw = await readFile(statePath, "utf8");
|
|
406
|
+
const parsed = JSON.parse(raw);
|
|
407
|
+
assert.deepEqual(parsed, { head: "abc123", at: "2026-07-01T00:00:00Z" });
|
|
408
|
+
} finally {
|
|
409
|
+
await rm(dir, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
});
|