recallnest 3.0.0 → 3.0.1
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/CHANGELOG.md +101 -0
- package/README.md +222 -254
- package/README_CN.md +206 -243
- package/package.json +3 -2
- package/src/capture-engine.ts +26 -4
- package/src/context-composer-stable.ts +5 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +18 -0
- package/src/ingest.ts +225 -17
- package/src/kg-extractor.ts +17 -0
- package/src/memory-output.ts +35 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/runtime-config.ts +9 -0
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +9 -1
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +67 -1
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { readConsistencyInterval as envReadConsistencyInterval } from "../env-config.js";
|
|
7
|
-
import { MemoryStore } from "../store.js";
|
|
8
|
-
|
|
9
|
-
const cleanupPaths: string[] = [];
|
|
10
|
-
const savedEnv = process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
while (cleanupPaths.length > 0) {
|
|
14
|
-
const target = cleanupPaths.pop();
|
|
15
|
-
if (target) {
|
|
16
|
-
rmSync(target, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
if (savedEnv === undefined) {
|
|
20
|
-
delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
|
|
21
|
-
} else {
|
|
22
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = savedEnv;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function tempDb(): string {
|
|
27
|
-
const dbPath = mkdtempSync(join(tmpdir(), "recallnest-read-consistency-"));
|
|
28
|
-
cleanupPaths.push(dbPath);
|
|
29
|
-
return dbPath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function entry(i: number) {
|
|
33
|
-
return {
|
|
34
|
-
text: `read-consistency memory ${i}`,
|
|
35
|
-
vector: [0.1 * i, 0.2 * i, 0.3 * i],
|
|
36
|
-
category: "fact" as const,
|
|
37
|
-
scope: "project:rci-test",
|
|
38
|
-
importance: 0.5,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// A writer always sees its own commits on its own handle; the staleness gap
|
|
43
|
-
// only exists ACROSS handles. Two independent MemoryStore instances against
|
|
44
|
-
// the same dbPath reproduce the cross-process topology (CLI ingest vs resident
|
|
45
|
-
// MCP/API/UI server) in-process: the writer deletes a row after the reader's
|
|
46
|
-
// handle has already opened the table.
|
|
47
|
-
describe("read consistency across independent connections", () => {
|
|
48
|
-
it("default (env unset) is strong consistency: reader sees the writer's delete", async () => {
|
|
49
|
-
delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
|
|
50
|
-
const dbPath = tempDb();
|
|
51
|
-
|
|
52
|
-
const writer = new MemoryStore({ dbPath, vectorDim: 3 });
|
|
53
|
-
const e1 = await writer.store(entry(1));
|
|
54
|
-
await writer.store(entry(2));
|
|
55
|
-
|
|
56
|
-
const reader = new MemoryStore({ dbPath, vectorDim: 3 });
|
|
57
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
|
|
58
|
-
|
|
59
|
-
await writer.delete(e1.id);
|
|
60
|
-
|
|
61
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(1);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("env 'off' restores the legacy pinned-handle behavior (negative control)", async () => {
|
|
65
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "off";
|
|
66
|
-
const dbPath = tempDb();
|
|
67
|
-
|
|
68
|
-
const writer = new MemoryStore({ dbPath, vectorDim: 3 });
|
|
69
|
-
const e1 = await writer.store(entry(1));
|
|
70
|
-
await writer.store(entry(2));
|
|
71
|
-
|
|
72
|
-
const reader = new MemoryStore({ dbPath, vectorDim: 3 });
|
|
73
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
|
|
74
|
-
|
|
75
|
-
await writer.delete(e1.id);
|
|
76
|
-
|
|
77
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("explicit config wins over env: interval 0 sees the delete despite env 'off'", async () => {
|
|
81
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "off";
|
|
82
|
-
const dbPath = tempDb();
|
|
83
|
-
|
|
84
|
-
const writer = new MemoryStore({ dbPath, vectorDim: 3 });
|
|
85
|
-
const e1 = await writer.store(entry(1));
|
|
86
|
-
await writer.store(entry(2));
|
|
87
|
-
|
|
88
|
-
const reader = new MemoryStore({ dbPath, vectorDim: 3, readConsistencyInterval: 0 });
|
|
89
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(2);
|
|
90
|
-
|
|
91
|
-
await writer.delete(e1.id);
|
|
92
|
-
|
|
93
|
-
expect((await reader.list(undefined, undefined, 20, 0)).length).toBe(1);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Cross-handle visibility behavior is pinned by the three tests above;
|
|
97
|
-
// what remains is the env parsing matrix of the resolver itself.
|
|
98
|
-
it("env resolver: unset/empty → 0, off/none → undefined, numbers pass, junk clamps to 0", () => {
|
|
99
|
-
delete process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL;
|
|
100
|
-
expect(envReadConsistencyInterval()).toBe(0);
|
|
101
|
-
|
|
102
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "";
|
|
103
|
-
expect(envReadConsistencyInterval()).toBe(0);
|
|
104
|
-
|
|
105
|
-
for (const legacy of ["off", "none"]) {
|
|
106
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = legacy;
|
|
107
|
-
expect(envReadConsistencyInterval()).toBeUndefined();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "7";
|
|
111
|
-
expect(envReadConsistencyInterval()).toBe(7);
|
|
112
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "1.5";
|
|
113
|
-
expect(envReadConsistencyInterval()).toBe(1.5);
|
|
114
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = "0";
|
|
115
|
-
expect(envReadConsistencyInterval()).toBe(0);
|
|
116
|
-
|
|
117
|
-
for (const junk of ["-3", "abc", "Infinity", "NaN"]) {
|
|
118
|
-
process.env.RECALLNEST_READ_CONSISTENCY_INTERVAL = junk;
|
|
119
|
-
expect(envReadConsistencyInterval()).toBe(0);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import * as lancedb from "@lancedb/lancedb";
|
|
6
|
-
|
|
7
|
-
import { MemoryStore } from "../store.js";
|
|
8
|
-
|
|
9
|
-
const DIM = 4;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Regression: the pre-2026-04-09 table schema has 8 columns (no language /
|
|
13
|
-
* fts_text). The old migration check sampled a row to detect missing columns,
|
|
14
|
-
* so a 0-row legacy table never migrated and EVERY write then failed with
|
|
15
|
-
* "Found field not in schema: language". Production hit exactly this: a table
|
|
16
|
-
* created one day before the columns existed, empty for 3 months.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
async function createLegacyTable(dbPath: string, withRow: boolean) {
|
|
20
|
-
const db = await lancedb.connect(dbPath);
|
|
21
|
-
const seed = [
|
|
22
|
-
{
|
|
23
|
-
id: "__seed__",
|
|
24
|
-
text: withRow ? "legacy row" : "",
|
|
25
|
-
vector: new Array(DIM).fill(0),
|
|
26
|
-
category: "other",
|
|
27
|
-
scope: "s",
|
|
28
|
-
importance: 0.5,
|
|
29
|
-
timestamp: 1,
|
|
30
|
-
metadata: "{}",
|
|
31
|
-
},
|
|
32
|
-
];
|
|
33
|
-
const t = await db.createTable("memories", seed);
|
|
34
|
-
if (!withRow) await t.delete('id = "__seed__"');
|
|
35
|
-
return t;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe("MemoryStore schema migration (empty legacy table)", () => {
|
|
39
|
-
let dir = "";
|
|
40
|
-
afterEach(async () => {
|
|
41
|
-
if (dir) await rm(dir, { recursive: true, force: true });
|
|
42
|
-
dir = "";
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("migrates an EMPTY legacy table so writes succeed", async () => {
|
|
46
|
-
dir = await mkdtemp(join(tmpdir(), "rn-mig-empty-"));
|
|
47
|
-
await createLegacyTable(dir, false);
|
|
48
|
-
|
|
49
|
-
const store = new MemoryStore({ dbPath: dir, vectorDim: DIM });
|
|
50
|
-
const entry = await store.store({
|
|
51
|
-
text: "first write after migration",
|
|
52
|
-
vector: new Array(DIM).fill(0.1),
|
|
53
|
-
category: "other",
|
|
54
|
-
scope: "s",
|
|
55
|
-
importance: 0.5,
|
|
56
|
-
});
|
|
57
|
-
expect(entry.id).toBeTruthy();
|
|
58
|
-
|
|
59
|
-
const db = await lancedb.connect(dir);
|
|
60
|
-
const t = await db.openTable("memories");
|
|
61
|
-
const fields = (await t.schema()).fields.map((f) => f.name);
|
|
62
|
-
expect(fields).toContain("language");
|
|
63
|
-
expect(fields).toContain("fts_text");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("migrates a NON-EMPTY legacy table and backfills defaults", async () => {
|
|
67
|
-
dir = await mkdtemp(join(tmpdir(), "rn-mig-full-"));
|
|
68
|
-
await createLegacyTable(dir, true);
|
|
69
|
-
|
|
70
|
-
const store = new MemoryStore({ dbPath: dir, vectorDim: DIM });
|
|
71
|
-
await store.store({
|
|
72
|
-
text: "second row",
|
|
73
|
-
vector: new Array(DIM).fill(0.2),
|
|
74
|
-
category: "other",
|
|
75
|
-
scope: "s",
|
|
76
|
-
importance: 0.5,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const db = await lancedb.connect(dir);
|
|
80
|
-
const t = await db.openTable("memories");
|
|
81
|
-
const rows = await t.query().toArray();
|
|
82
|
-
expect(rows).toHaveLength(2);
|
|
83
|
-
const legacy = rows.find((r) => r.id === "__seed__") as any;
|
|
84
|
-
expect(legacy.language).toBe("en");
|
|
85
|
-
expect(legacy.fts_text).toBe("legacy row");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { MemoryStore, classifyLegacyScope } from "../store.js";
|
|
7
|
-
|
|
8
|
-
const cleanupPaths: string[] = [];
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
while (cleanupPaths.length > 0) {
|
|
12
|
-
const target = cleanupPaths.pop();
|
|
13
|
-
if (target) {
|
|
14
|
-
rmSync(target, { recursive: true, force: true });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function createStore(): MemoryStore {
|
|
20
|
-
const dbPath = mkdtempSync(join(tmpdir(), "recallnest-scope-audit-"));
|
|
21
|
-
cleanupPaths.push(dbPath);
|
|
22
|
-
return new MemoryStore({
|
|
23
|
-
dbPath,
|
|
24
|
-
vectorDim: 3,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("classifyLegacyScope", () => {
|
|
29
|
-
it("flags missing, empty, and global scope values", () => {
|
|
30
|
-
expect(classifyLegacyScope(undefined)).toBe("missing");
|
|
31
|
-
expect(classifyLegacyScope(null)).toBe("missing");
|
|
32
|
-
expect(classifyLegacyScope("")).toBe("empty");
|
|
33
|
-
expect(classifyLegacyScope(" ")).toBe("empty");
|
|
34
|
-
expect(classifyLegacyScope("global")).toBe("global");
|
|
35
|
-
expect(classifyLegacyScope(" global ")).toBe("global");
|
|
36
|
-
expect(classifyLegacyScope("project:recallnest")).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("MemoryStore.auditLegacyScopes", () => {
|
|
41
|
-
it("reports global and empty scope rows without mixing in valid scoped data", async () => {
|
|
42
|
-
const store = createStore();
|
|
43
|
-
|
|
44
|
-
await store.store({
|
|
45
|
-
text: "Scoped memory should not appear in the audit.",
|
|
46
|
-
vector: [1, 0, 0],
|
|
47
|
-
category: "entities",
|
|
48
|
-
scope: "project:recallnest",
|
|
49
|
-
importance: 0.8,
|
|
50
|
-
metadata: "{}",
|
|
51
|
-
});
|
|
52
|
-
await store.store({
|
|
53
|
-
text: "Historical row that used the global fallback.",
|
|
54
|
-
vector: [0, 1, 0],
|
|
55
|
-
category: "events",
|
|
56
|
-
scope: "global",
|
|
57
|
-
importance: 0.5,
|
|
58
|
-
metadata: "{}",
|
|
59
|
-
});
|
|
60
|
-
await store.store({
|
|
61
|
-
text: "Historical row that was written without a real scope.",
|
|
62
|
-
vector: [0, 0, 1],
|
|
63
|
-
category: "events",
|
|
64
|
-
scope: " ",
|
|
65
|
-
importance: 0.4,
|
|
66
|
-
metadata: "{}",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const audit = await store.auditLegacyScopes(10);
|
|
70
|
-
|
|
71
|
-
expect(audit.totalCount).toBe(2);
|
|
72
|
-
expect(audit.counts).toEqual({
|
|
73
|
-
missing: 0,
|
|
74
|
-
empty: 1,
|
|
75
|
-
global: 1,
|
|
76
|
-
});
|
|
77
|
-
expect(audit.samples).toHaveLength(2);
|
|
78
|
-
expect(audit.samples.map((sample) => sample.kind).sort()).toEqual(["empty", "global"]);
|
|
79
|
-
expect(audit.samples.every((sample) => sample.scope !== "project:recallnest")).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { MemoryStore, deterministicId, type MemoryEntry } from "../store.js";
|
|
7
|
-
import { getWriteCount } from "../activity-counter.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* P0-2 回归:store()/storeBatch() 改走 upsert(delete+add 入全局 store-write 锁)后,
|
|
11
|
-
* 相同 deterministicId 的写入是幂等的 —— 同 (scope,text) 写两次只留一行,不再堆 dup-id。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const tmpDirs: string[] = [];
|
|
15
|
-
|
|
16
|
-
function makeStore(): MemoryStore {
|
|
17
|
-
const dir = mkdtempSync(join(tmpdir(), "recallnest-upsert-"));
|
|
18
|
-
tmpDirs.push(dir);
|
|
19
|
-
return new MemoryStore({ dbPath: dir, vectorDim: 3 });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
while (tmpDirs.length > 0) {
|
|
24
|
-
try {
|
|
25
|
-
rmSync(tmpDirs.pop()!, { recursive: true, force: true });
|
|
26
|
-
} catch {
|
|
27
|
-
/* ignore */
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("store P0-2 idempotency (upsert-backed store/storeBatch)", () => {
|
|
33
|
-
it("store() of the same (scope,text) twice yields one row, latest wins", async () => {
|
|
34
|
-
const store = makeStore();
|
|
35
|
-
const scope = "project:upsert-test";
|
|
36
|
-
await store.store({ text: "同一条内容", vector: [1, 0, 0], category: "entities", scope, importance: 0.7, metadata: "{}" });
|
|
37
|
-
await store.store({ text: "同一条内容", vector: [1, 0, 0], category: "entities", scope, importance: 0.9, metadata: "{}" });
|
|
38
|
-
|
|
39
|
-
const rows = await store.list([scope]);
|
|
40
|
-
const target = rows.filter((r) => r.text === "同一条内容");
|
|
41
|
-
expect(target.length).toBe(1);
|
|
42
|
-
expect(target[0].importance).toBeCloseTo(0.9, 5); // upsert overwrote the 0.7 row
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("upsert() with the same id twice overwrites (single row, latest content)", async () => {
|
|
46
|
-
const store = makeStore();
|
|
47
|
-
const scope = "project:upsert-test";
|
|
48
|
-
const id = deterministicId(scope, "anchor");
|
|
49
|
-
const base: MemoryEntry = {
|
|
50
|
-
id,
|
|
51
|
-
text: "v1",
|
|
52
|
-
vector: [0, 1, 0],
|
|
53
|
-
category: "entities",
|
|
54
|
-
scope,
|
|
55
|
-
importance: 0.5,
|
|
56
|
-
timestamp: Date.now(),
|
|
57
|
-
metadata: "{}",
|
|
58
|
-
language: "en",
|
|
59
|
-
fts_text: "v1",
|
|
60
|
-
};
|
|
61
|
-
await store.upsert(base);
|
|
62
|
-
await store.upsert({ ...base, text: "v2", fts_text: "v2", importance: 0.8 });
|
|
63
|
-
|
|
64
|
-
const rows = await store.list([scope]);
|
|
65
|
-
const byId = rows.filter((r) => r.id === id);
|
|
66
|
-
expect(byId.length).toBe(1);
|
|
67
|
-
expect(byId[0].text).toBe("v2");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("upsert failure leaves the prior row intact (mergeInsert is atomic — no delete+add loss)", async () => {
|
|
71
|
-
const store = makeStore(); // vectorDim 3
|
|
72
|
-
const scope = "project:upsert-test";
|
|
73
|
-
const id = deterministicId(scope, "atomic-anchor");
|
|
74
|
-
const good: MemoryEntry = {
|
|
75
|
-
id, text: "original", vector: [1, 0, 0], category: "entities", scope, importance: 0.5,
|
|
76
|
-
timestamp: Date.now(), metadata: "{}", language: "en", fts_text: "original",
|
|
77
|
-
};
|
|
78
|
-
await store.upsert(good);
|
|
79
|
-
|
|
80
|
-
// Inject a write failure. With the old delete-then-add path the delete would already
|
|
81
|
-
// have removed the row before add threw (row lost); the atomic mergeInsert touches
|
|
82
|
-
// nothing on failure, so the prior row must survive.
|
|
83
|
-
const table = (store as unknown as { table: { mergeInsert: unknown } }).table;
|
|
84
|
-
const orig = table.mergeInsert;
|
|
85
|
-
table.mergeInsert = () => {
|
|
86
|
-
throw new Error("simulated write failure");
|
|
87
|
-
};
|
|
88
|
-
try {
|
|
89
|
-
await expect(store.upsert({ ...good, text: "corrupt", fts_text: "corrupt" })).rejects.toThrow("simulated write failure");
|
|
90
|
-
} finally {
|
|
91
|
-
table.mergeInsert = orig;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const survivor = (await store.list([scope])).filter((r) => r.id === id);
|
|
95
|
-
expect(survivor.length).toBe(1);
|
|
96
|
-
expect(survivor[0].text).toBe("original"); // prior row preserved, not lost
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("storeBatch() honors a caller-supplied id (idempotent re-run)", async () => {
|
|
100
|
-
const store = makeStore();
|
|
101
|
-
const scope = "project:upsert-test";
|
|
102
|
-
const fixedId = deterministicId(scope, "batch-anchor");
|
|
103
|
-
const entry = {
|
|
104
|
-
id: fixedId,
|
|
105
|
-
text: "batch-a",
|
|
106
|
-
vector: [0, 0, 1] as number[],
|
|
107
|
-
category: "entities" as const,
|
|
108
|
-
scope,
|
|
109
|
-
importance: 0.6,
|
|
110
|
-
metadata: "{}",
|
|
111
|
-
};
|
|
112
|
-
await store.storeBatch([entry]);
|
|
113
|
-
await store.storeBatch([{ ...entry, text: "batch-a-updated", fts_text: "batch-a-updated" }]);
|
|
114
|
-
|
|
115
|
-
const rows = await store.list([scope]);
|
|
116
|
-
const byId = rows.filter((r) => r.id === fixedId);
|
|
117
|
-
expect(byId.length).toBe(1);
|
|
118
|
-
expect(byId[0].text).toBe("batch-a-updated");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("storeBatch() collapses in-batch duplicate ids (latest-wins) into one row", async () => {
|
|
122
|
-
const store = makeStore();
|
|
123
|
-
const scope = "project:upsert-test";
|
|
124
|
-
const stored = await store.storeBatch([
|
|
125
|
-
{ text: "dup-in-batch", vector: [1, 0, 0], category: "entities", scope, importance: 0.3, metadata: "{}" },
|
|
126
|
-
{ text: "dup-in-batch", vector: [1, 0, 0], category: "entities", scope, importance: 0.8, metadata: "{}" },
|
|
127
|
-
]);
|
|
128
|
-
expect(stored).toBe(1); // two same-(scope,text) entries collapse to one
|
|
129
|
-
const rows = await store.list([scope]);
|
|
130
|
-
const target = rows.filter((r) => r.text === "dup-in-batch");
|
|
131
|
-
expect(target.length).toBe(1);
|
|
132
|
-
expect(target[0].importance).toBeCloseTo(0.8, 5); // last occurrence wins
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// A store whose activity-stats sits in its own unique dir (dirname(dbPath)), so the
|
|
136
|
-
// per-scope counter assertions are isolated from other tests' shared tmproot writes.
|
|
137
|
-
function makeIsolatedStore(): { store: MemoryStore; statsPath: string } {
|
|
138
|
-
const dir = mkdtempSync(join(tmpdir(), "recallnest-count-"));
|
|
139
|
-
tmpDirs.push(dir);
|
|
140
|
-
const dbPath = join(dir, "lancedb");
|
|
141
|
-
return { store: new MemoryStore({ dbPath, vectorDim: 3 }), statsPath: join(dir, "activity-stats.json") };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
it("store() increments the per-scope write counter once each (no double-count via upsert)", async () => {
|
|
145
|
-
const { store, statsPath } = makeIsolatedStore();
|
|
146
|
-
const scope = "cc:project:count-a";
|
|
147
|
-
await store.store({ text: "m1", vector: [1, 0, 0], category: "entities", scope, importance: 0.5, metadata: "{}" });
|
|
148
|
-
await store.store({ text: "m2", vector: [0, 1, 0], category: "entities", scope, importance: 0.5, metadata: "{}" });
|
|
149
|
-
// store()→upsert() counts once per call; two stores ⇒ 2 (not 4).
|
|
150
|
-
expect(getWriteCount(scope, { statsPath })).toBe(2);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("storeBatch() counts per scope by number of entries in that scope", async () => {
|
|
154
|
-
const { store, statsPath } = makeIsolatedStore();
|
|
155
|
-
await store.storeBatch([
|
|
156
|
-
{ text: "a", vector: [1, 0, 0], category: "entities", scope: "cc:s1", importance: 0.5, metadata: "{}" },
|
|
157
|
-
{ text: "b", vector: [0, 1, 0], category: "entities", scope: "cc:s1", importance: 0.5, metadata: "{}" },
|
|
158
|
-
{ text: "c", vector: [0, 0, 1], category: "entities", scope: "cc:s2", importance: 0.5, metadata: "{}" },
|
|
159
|
-
]);
|
|
160
|
-
expect(getWriteCount("cc:s1", { statsPath })).toBe(2);
|
|
161
|
-
expect(getWriteCount("cc:s2", { statsPath })).toBe(1);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("storeBatch() with an in-batch duplicate counts the collapsed row once", async () => {
|
|
165
|
-
const { store, statsPath } = makeIsolatedStore();
|
|
166
|
-
await store.storeBatch([
|
|
167
|
-
{ text: "dup", vector: [1, 0, 0], category: "entities", scope: "cc:s3", importance: 0.3, metadata: "{}" },
|
|
168
|
-
{ text: "dup", vector: [1, 0, 0], category: "entities", scope: "cc:s3", importance: 0.8, metadata: "{}" },
|
|
169
|
-
]);
|
|
170
|
-
expect(getWriteCount("cc:s3", { statsPath })).toBe(1); // deduped to one row ⇒ counted once
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("storeBatch() without ids still stores distinct entries", async () => {
|
|
174
|
-
const store = makeStore();
|
|
175
|
-
const scope = "project:upsert-test";
|
|
176
|
-
const stored = await store.storeBatch([
|
|
177
|
-
{ text: "batch-x", vector: [1, 0, 0], category: "entities", scope, importance: 0.5, metadata: "{}" },
|
|
178
|
-
{ text: "batch-y", vector: [0, 1, 0], category: "entities", scope, importance: 0.5, metadata: "{}" },
|
|
179
|
-
]);
|
|
180
|
-
expect(stored).toBe(2);
|
|
181
|
-
const rows = await store.list([scope]);
|
|
182
|
-
expect(rows.filter((r) => r.text === "batch-x" || r.text === "batch-y").length).toBe(2);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe("update() vector fallback (2026-07-23 null-vector regression)", () => {
|
|
187
|
-
it("metadata-only update preserves the original vector on healthy rows", async () => {
|
|
188
|
-
const store = makeStore();
|
|
189
|
-
const scope = "project:nullvec-test";
|
|
190
|
-
const entry = await store.store({
|
|
191
|
-
text: "健康行", vector: [0.1, 0.2, 0.3], category: "events", scope, importance: 0.7, metadata: "{}",
|
|
192
|
-
});
|
|
193
|
-
const updated = await store.update(entry.id, { metadata: JSON.stringify({ touched: true }) });
|
|
194
|
-
expect(updated).not.toBeNull();
|
|
195
|
-
// 修复 row.vector null-guard 后,正常行的 vector 必须原样保留(回归保护)
|
|
196
|
-
const rows = await store.list([scope]);
|
|
197
|
-
expect(rows.length).toBe(1);
|
|
198
|
-
const meta = JSON.parse(rows[0].metadata || "{}") as Record<string, unknown>;
|
|
199
|
-
expect(meta.touched).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
buildSupersedeMetadata,
|
|
4
|
-
buildSupersedeMetadataForNew,
|
|
5
|
-
parseEvolution,
|
|
6
|
-
traceEvolution,
|
|
7
|
-
patchEvolution,
|
|
8
|
-
defaultEvolution,
|
|
9
|
-
} from "../memory-evolution.js";
|
|
10
|
-
|
|
11
|
-
describe("HP-1: Superseded chain enhancement", () => {
|
|
12
|
-
describe("bidirectional linking", () => {
|
|
13
|
-
it("buildSupersedeMetadata marks old → supersededBy", () => {
|
|
14
|
-
const oldMeta = patchEvolution("{}", defaultEvolution());
|
|
15
|
-
const result = buildSupersedeMetadata(oldMeta, "new-id");
|
|
16
|
-
const evo = parseEvolution(result);
|
|
17
|
-
expect(evo.status).toBe("superseded");
|
|
18
|
-
expect(evo.supersededBy).toBe("new-id");
|
|
19
|
-
expect(evo.validUntil).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("buildSupersedeMetadataForNew marks new → supersedes + note", () => {
|
|
23
|
-
const newMeta = patchEvolution("{}", defaultEvolution());
|
|
24
|
-
const result = buildSupersedeMetadataForNew(newMeta, "old-id", "updated preference");
|
|
25
|
-
const evo = parseEvolution(result);
|
|
26
|
-
expect(evo.status).toBe("active"); // new memory stays active
|
|
27
|
-
expect(evo.supersedes).toBe("old-id");
|
|
28
|
-
expect(evo.evolutionNote).toBe("updated preference");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("bidirectional: old.supersededBy == new.id AND new.supersedes == old.id", () => {
|
|
32
|
-
const oldMeta = patchEvolution("{}", defaultEvolution());
|
|
33
|
-
const newMeta = patchEvolution("{}", defaultEvolution());
|
|
34
|
-
|
|
35
|
-
const updatedOld = buildSupersedeMetadata(oldMeta, "new-id");
|
|
36
|
-
const updatedNew = buildSupersedeMetadataForNew(newMeta, "old-id");
|
|
37
|
-
|
|
38
|
-
const oldEvo = parseEvolution(updatedOld);
|
|
39
|
-
const newEvo = parseEvolution(updatedNew);
|
|
40
|
-
|
|
41
|
-
expect(oldEvo.supersededBy).toBe("new-id");
|
|
42
|
-
expect(newEvo.supersedes).toBe("old-id");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("traceEvolution", () => {
|
|
47
|
-
it("traces a 3-node chain", async () => {
|
|
48
|
-
// v1 → v2 → v3
|
|
49
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {
|
|
50
|
-
v1: {
|
|
51
|
-
metadata: patchEvolution("{}", {
|
|
52
|
-
status: "superseded",
|
|
53
|
-
supersededBy: "v2",
|
|
54
|
-
validFrom: 1000,
|
|
55
|
-
validUntil: 2000,
|
|
56
|
-
}),
|
|
57
|
-
timestamp: 1000,
|
|
58
|
-
},
|
|
59
|
-
v2: {
|
|
60
|
-
metadata: patchEvolution("{}", {
|
|
61
|
-
status: "superseded",
|
|
62
|
-
supersedes: "v1",
|
|
63
|
-
supersededBy: "v3",
|
|
64
|
-
evolutionNote: "added detail",
|
|
65
|
-
validFrom: 2000,
|
|
66
|
-
validUntil: 3000,
|
|
67
|
-
}),
|
|
68
|
-
timestamp: 2000,
|
|
69
|
-
},
|
|
70
|
-
v3: {
|
|
71
|
-
metadata: patchEvolution("{}", {
|
|
72
|
-
status: "active",
|
|
73
|
-
supersedes: "v2",
|
|
74
|
-
evolutionNote: "corrected error",
|
|
75
|
-
validFrom: 3000,
|
|
76
|
-
}),
|
|
77
|
-
timestamp: 3000,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
82
|
-
|
|
83
|
-
// Trace from v2 (middle)
|
|
84
|
-
const trace = await traceEvolution("v2", getEntry);
|
|
85
|
-
expect(trace.length).toBe(3);
|
|
86
|
-
expect(trace[0].id).toBe("v1");
|
|
87
|
-
expect(trace[0].direction).toBe("predecessor");
|
|
88
|
-
expect(trace[1].id).toBe("v2");
|
|
89
|
-
expect(trace[1].direction).toBe("self");
|
|
90
|
-
expect(trace[2].id).toBe("v3");
|
|
91
|
-
expect(trace[2].direction).toBe("successor");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("handles single node (no chain)", async () => {
|
|
95
|
-
const getEntry = async (id: string) => {
|
|
96
|
-
if (id === "solo") return { metadata: patchEvolution("{}", defaultEvolution()), timestamp: 1000 };
|
|
97
|
-
return null;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const trace = await traceEvolution("solo", getEntry);
|
|
101
|
-
expect(trace.length).toBe(1);
|
|
102
|
-
expect(trace[0].direction).toBe("self");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("respects maxDepth", async () => {
|
|
106
|
-
// Create a long chain: v0 → v1 → v2 → ... → v20
|
|
107
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {};
|
|
108
|
-
for (let i = 0; i <= 20; i++) {
|
|
109
|
-
entries[`v${i}`] = {
|
|
110
|
-
metadata: patchEvolution("{}", {
|
|
111
|
-
status: i < 20 ? "superseded" : "active",
|
|
112
|
-
supersedes: i > 0 ? `v${i - 1}` : null,
|
|
113
|
-
supersededBy: i < 20 ? `v${i + 1}` : null,
|
|
114
|
-
validFrom: i * 1000,
|
|
115
|
-
}),
|
|
116
|
-
timestamp: i * 1000,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
121
|
-
const trace = await traceEvolution("v10", getEntry, 3);
|
|
122
|
-
// maxDepth=3: at most 3 predecessors + self + 3 successors = 7
|
|
123
|
-
expect(trace.length).toBeLessThanOrEqual(7);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("handles missing entries gracefully", async () => {
|
|
127
|
-
const entries: Record<string, { metadata: string; timestamp: number }> = {
|
|
128
|
-
v1: {
|
|
129
|
-
metadata: patchEvolution("{}", {
|
|
130
|
-
status: "active",
|
|
131
|
-
supersedes: "missing",
|
|
132
|
-
validFrom: 1000,
|
|
133
|
-
}),
|
|
134
|
-
timestamp: 1000,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
const getEntry = async (id: string) => entries[id] ?? null;
|
|
138
|
-
const trace = await traceEvolution("v1", getEntry);
|
|
139
|
-
expect(trace.length).toBe(1); // only self, missing predecessor stops chain
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("backward compatibility", () => {
|
|
144
|
-
it("parseEvolution defaults new fields to null", () => {
|
|
145
|
-
// Old metadata without supersedes/evolutionNote
|
|
146
|
-
const oldMeta = JSON.stringify({
|
|
147
|
-
evolution: { status: "active", version: 1, accessCount: 0 },
|
|
148
|
-
});
|
|
149
|
-
const evo = parseEvolution(oldMeta);
|
|
150
|
-
expect(evo.supersedes).toBeNull();
|
|
151
|
-
expect(evo.evolutionNote).toBeNull();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
});
|