recallnest 2.2.1 → 2.5.4
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 +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3: Temporal Validity Windows — tests for eventTime, validUntil,
|
|
3
|
+
* retrieval filtering, and auto-GC decay acceleration.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
defaultEvolution,
|
|
9
|
+
parseEvolution,
|
|
10
|
+
patchEvolution,
|
|
11
|
+
type EvolutionMetadata,
|
|
12
|
+
} from "../memory-evolution.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Schema: eventTime field
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
describe("F3: eventTime in EvolutionMetadata", () => {
|
|
19
|
+
test("defaultEvolution includes eventTime: null", () => {
|
|
20
|
+
const evo = defaultEvolution(1000);
|
|
21
|
+
expect(evo.eventTime).toBeNull();
|
|
22
|
+
expect(evo.validFrom).toBe(1000);
|
|
23
|
+
expect(evo.validUntil).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("parseEvolution extracts eventTime from metadata", () => {
|
|
27
|
+
const metadata = JSON.stringify({
|
|
28
|
+
evolution: {
|
|
29
|
+
status: "active",
|
|
30
|
+
version: 1,
|
|
31
|
+
accessCount: 0,
|
|
32
|
+
lastAccessedAt: null,
|
|
33
|
+
supersededBy: null,
|
|
34
|
+
supersedes: null,
|
|
35
|
+
evolutionNote: null,
|
|
36
|
+
consolidatedInto: null,
|
|
37
|
+
contributedToPattern: null,
|
|
38
|
+
sourceMemories: [],
|
|
39
|
+
validFrom: 1000,
|
|
40
|
+
validUntil: null,
|
|
41
|
+
eventTime: 5000,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const evo = parseEvolution(metadata);
|
|
45
|
+
expect(evo.eventTime).toBe(5000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("parseEvolution defaults eventTime to null when missing", () => {
|
|
49
|
+
const metadata = JSON.stringify({
|
|
50
|
+
evolution: {
|
|
51
|
+
status: "active",
|
|
52
|
+
version: 1,
|
|
53
|
+
validFrom: 1000,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const evo = parseEvolution(metadata);
|
|
57
|
+
expect(evo.eventTime).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("patchEvolution can set eventTime", () => {
|
|
61
|
+
const metadata = JSON.stringify({ evolution: defaultEvolution(1000) });
|
|
62
|
+
const patched = patchEvolution(metadata, { eventTime: 9999 });
|
|
63
|
+
const evo = parseEvolution(patched);
|
|
64
|
+
expect(evo.eventTime).toBe(9999);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("patchEvolution can set validUntil", () => {
|
|
68
|
+
const metadata = JSON.stringify({ evolution: defaultEvolution(1000) });
|
|
69
|
+
const patched = patchEvolution(metadata, { validUntil: 2000 });
|
|
70
|
+
const evo = parseEvolution(patched);
|
|
71
|
+
expect(evo.validUntil).toBe(2000);
|
|
72
|
+
// Other fields unchanged
|
|
73
|
+
expect(evo.validFrom).toBe(1000);
|
|
74
|
+
expect(evo.status).toBe("active");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Retrieval: filterByValidity (unit-level via import)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
// We test the filterByValidity logic indirectly through the exported
|
|
83
|
+
// parseEvolution + the contract: expired memories get score * 0.2.
|
|
84
|
+
// Direct integration tested via full retriever in separate test files.
|
|
85
|
+
|
|
86
|
+
describe("F3: validity window logic", () => {
|
|
87
|
+
function makeEvo(overrides: Partial<EvolutionMetadata> = {}): EvolutionMetadata {
|
|
88
|
+
return { ...defaultEvolution(1000), ...overrides };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test("memory with validUntil in the future is not expired", () => {
|
|
92
|
+
const now = 5000;
|
|
93
|
+
const evo = makeEvo({ validUntil: 10000 });
|
|
94
|
+
// validUntil > now → not expired
|
|
95
|
+
expect(evo.validUntil).toBeGreaterThan(now);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("memory with validUntil in the past is expired", () => {
|
|
99
|
+
const now = 5000;
|
|
100
|
+
const evo = makeEvo({ validUntil: 3000 });
|
|
101
|
+
// validUntil < now → expired
|
|
102
|
+
expect(evo.validUntil).toBeLessThan(now);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("memory without validUntil never expires", () => {
|
|
106
|
+
const evo = makeEvo({ validUntil: null });
|
|
107
|
+
expect(evo.validUntil).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("point-in-time query: memory valid in range", () => {
|
|
111
|
+
const evo = makeEvo({ validFrom: 1000, validUntil: 5000 });
|
|
112
|
+
const queryTime = 3000;
|
|
113
|
+
expect(evo.validFrom).toBeLessThanOrEqual(queryTime);
|
|
114
|
+
expect(evo.validUntil).toBeGreaterThanOrEqual(queryTime);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("point-in-time query: memory not yet valid", () => {
|
|
118
|
+
const evo = makeEvo({ validFrom: 5000, validUntil: 10000 });
|
|
119
|
+
const queryTime = 3000;
|
|
120
|
+
expect(evo.validFrom).toBeGreaterThan(queryTime);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("point-in-time query: memory already expired at query time", () => {
|
|
124
|
+
const evo = makeEvo({ validFrom: 1000, validUntil: 3000 });
|
|
125
|
+
const queryTime = 5000;
|
|
126
|
+
expect(evo.validUntil).toBeLessThan(queryTime);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Auto-GC: decay acceleration for expired memories
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
describe("F3: GC decay acceleration", () => {
|
|
135
|
+
test("expired memory decay score is halved", () => {
|
|
136
|
+
// Simulate the GC logic: if validUntil < now, decayScore *= 0.5
|
|
137
|
+
const baseDecayScore = 0.30;
|
|
138
|
+
const now = 10000;
|
|
139
|
+
const validUntil = 5000; // expired
|
|
140
|
+
const adjusted = validUntil < now ? baseDecayScore * 0.5 : baseDecayScore;
|
|
141
|
+
expect(adjusted).toBe(0.15);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("non-expired memory decay score unchanged", () => {
|
|
145
|
+
const baseDecayScore = 0.30;
|
|
146
|
+
const now = 10000;
|
|
147
|
+
const validUntil = 20000; // still valid
|
|
148
|
+
const adjusted = validUntil < now ? baseDecayScore * 0.5 : baseDecayScore;
|
|
149
|
+
expect(adjusted).toBe(0.30);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("memory without validUntil — decay score unchanged", () => {
|
|
153
|
+
const baseDecayScore = 0.30;
|
|
154
|
+
const validUntil = null;
|
|
155
|
+
const adjusted = (validUntil != null && validUntil < Date.now())
|
|
156
|
+
? baseDecayScore * 0.5
|
|
157
|
+
: baseDecayScore;
|
|
158
|
+
expect(adjusted).toBe(0.30);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildTaskHintTerms,
|
|
5
|
+
extractTerms,
|
|
6
|
+
looksLikeContinuityTask,
|
|
7
|
+
looksLikeRecallOnlyTask,
|
|
8
|
+
taskCueCoverage,
|
|
9
|
+
} from "../term-registry.js";
|
|
10
|
+
|
|
11
|
+
describe("term registry", () => {
|
|
12
|
+
it("extracts compact Chinese and latin cue terms", () => {
|
|
13
|
+
expect(extractTerms("继续 RecallNest continuity bridge 适配")).toEqual([
|
|
14
|
+
"继续",
|
|
15
|
+
"recallnest",
|
|
16
|
+
"continuity",
|
|
17
|
+
"bridge",
|
|
18
|
+
"适配",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("can keep later terms when a wider extraction window is requested", () => {
|
|
23
|
+
expect(
|
|
24
|
+
extractTerms(
|
|
25
|
+
"继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation",
|
|
26
|
+
24,
|
|
27
|
+
),
|
|
28
|
+
).toEqual(expect.arrayContaining([
|
|
29
|
+
"profile",
|
|
30
|
+
"forwarding",
|
|
31
|
+
"runner",
|
|
32
|
+
"isolation",
|
|
33
|
+
]));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("builds task hints for writing prompts across languages", () => {
|
|
37
|
+
expect(buildTaskHintTerms("continue my AI writing project")).toEqual([
|
|
38
|
+
"写作",
|
|
39
|
+
"文章",
|
|
40
|
+
"语气",
|
|
41
|
+
"风格",
|
|
42
|
+
"口语化",
|
|
43
|
+
"不端着",
|
|
44
|
+
"ai",
|
|
45
|
+
"公众号",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("treats recall-only wording differently from writing actions", () => {
|
|
50
|
+
expect(looksLikeRecallOnlyTask("你还记得我之前的偏好吗")).toBe(true);
|
|
51
|
+
expect(looksLikeRecallOnlyTask("不要让我重复前情,继续写文章")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("tracks workflow cue coverage from pattern text", () => {
|
|
55
|
+
expect(
|
|
56
|
+
taskCueCoverage(
|
|
57
|
+
"patterns",
|
|
58
|
+
"Workflow pattern: Cross-window continuity handoff Tools: resume_context, checkpoint_session, search_memory",
|
|
59
|
+
),
|
|
60
|
+
).toEqual(["search_memory", "resume_context", "checkpoint"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("treats named RecallNest and memory-layer continues as continuity tasks", () => {
|
|
64
|
+
expect(looksLikeContinuityTask("回到 RecallNest 继续做")).toBe(true);
|
|
65
|
+
expect(looksLikeContinuityTask("接着弄 RecallNest 那个 memory layer")).toBe(true);
|
|
66
|
+
expect(looksLikeContinuityTask("把记忆层这条线先续上")).toBe(true);
|
|
67
|
+
expect(looksLikeContinuityTask("把之前那套 recall 管线先捡起来")).toBe(true);
|
|
68
|
+
expect(looksLikeContinuityTask("pick up where we left off on RecallNest")).toBe(true);
|
|
69
|
+
expect(looksLikeContinuityTask("回到 A2A 继续做")).toBe(false);
|
|
70
|
+
// Status queries are NOT continuation tasks — entity+cases is sufficient
|
|
71
|
+
expect(looksLikeContinuityTask("RecallNest 现在啥情况")).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("builds RecallNest continuity hints for memory-layer shorthand", () => {
|
|
75
|
+
expect(buildTaskHintTerms("把刚才那个 memory layer 接回去")).toEqual(expect.arrayContaining([
|
|
76
|
+
"recallnest",
|
|
77
|
+
"checkpoint_session",
|
|
78
|
+
"resume_context",
|
|
79
|
+
"store_memory",
|
|
80
|
+
]));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("builds RecallNest continuity hints for colloquial memory shorthand", () => {
|
|
84
|
+
expect(buildTaskHintTerms("把那条跨窗口记忆的活接着弄完")).toEqual(expect.arrayContaining([
|
|
85
|
+
"recallnest",
|
|
86
|
+
"checkpoint_session",
|
|
87
|
+
"resume_context",
|
|
88
|
+
]));
|
|
89
|
+
expect(buildTaskHintTerms("把之前那套 recall 管线先捡起来")).toEqual(expect.arrayContaining([
|
|
90
|
+
"recallnest",
|
|
91
|
+
"checkpoint_session",
|
|
92
|
+
"resume_context",
|
|
93
|
+
]));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("treats 记忆项目 as continuity task and builds RecallNest hints", () => {
|
|
97
|
+
expect(looksLikeContinuityTask("那个记忆项目,之前做到哪了")).toBe(true);
|
|
98
|
+
expect(looksLikeContinuityTask("继续记忆项目")).toBe(true);
|
|
99
|
+
expect(buildTaskHintTerms("那个记忆项目,之前做到哪了")).toEqual(expect.arrayContaining([
|
|
100
|
+
"recallnest",
|
|
101
|
+
"resume_context",
|
|
102
|
+
]));
|
|
103
|
+
expect(buildTaskHintTerms("continue the memory project")).toEqual(expect.arrayContaining([
|
|
104
|
+
"recallnest",
|
|
105
|
+
]));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("treats 跨终端记忆/记忆功能/记忆服务 shorthand as continuity cue", () => {
|
|
109
|
+
// "还搞吗" is a status question, not a continuation verb — correct to return false
|
|
110
|
+
expect(looksLikeContinuityTask("那个跨终端记忆还搞吗")).toBe(false);
|
|
111
|
+
expect(looksLikeContinuityTask("继续那个跨终端记忆")).toBe(true);
|
|
112
|
+
expect(looksLikeContinuityTask("上次做的那个记忆功能")).toBe(true);
|
|
113
|
+
expect(looksLikeContinuityTask("之前那个 MCP 记忆服务")).toBe(true);
|
|
114
|
+
expect(buildTaskHintTerms("那个跨终端记忆还搞吗")).toEqual(expect.arrayContaining([
|
|
115
|
+
"recallnest",
|
|
116
|
+
]));
|
|
117
|
+
expect(buildTaskHintTerms("上次做的记忆功能")).toEqual(expect.arrayContaining([
|
|
118
|
+
"recallnest",
|
|
119
|
+
]));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { registerAdvancedTools } from "../mcp-tools-advanced.js";
|
|
4
|
+
import { registerGovernanceTools } from "../mcp-tools-governance.js";
|
|
5
|
+
import { runToolSafely } from "../error-taxonomy.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* P1-7 第二阶段回归:验证「主操作错误」与「C 类逻辑失败」经 registerTool 的
|
|
9
|
+
* runToolSafely 包装后带 isError + reason_code,而不是被 handler 自吞成「看起来成功」的文本。
|
|
10
|
+
*
|
|
11
|
+
* 手法:用注入式 registerTool 捕获真实 handler,并复刻生产 registerTool 的 runToolSafely
|
|
12
|
+
* 包装,使测试看到与生产一致的 isError 行为(无需起 MCP server)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface ToolResult {
|
|
16
|
+
content: { type: string; text: string }[];
|
|
17
|
+
isError?: boolean;
|
|
18
|
+
}
|
|
19
|
+
type CapturedHandler = (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
20
|
+
|
|
21
|
+
function captureTools(
|
|
22
|
+
register: (deps: any) => void,
|
|
23
|
+
getComponents: () => unknown,
|
|
24
|
+
): Map<string, CapturedHandler> {
|
|
25
|
+
const handlers = new Map<string, CapturedHandler>();
|
|
26
|
+
const deps = {
|
|
27
|
+
registerTool: (
|
|
28
|
+
name: string,
|
|
29
|
+
_desc: string,
|
|
30
|
+
_schema: unknown,
|
|
31
|
+
handler: (...a: unknown[]) => unknown,
|
|
32
|
+
) => {
|
|
33
|
+
// 复刻 mcp-server.ts registerTool 的兜底:把 handler 交给 runToolSafely。
|
|
34
|
+
handlers.set(name, (args) => runToolSafely(name, async () => handler(args)) as Promise<ToolResult>);
|
|
35
|
+
},
|
|
36
|
+
getComponents,
|
|
37
|
+
conflictStore: {},
|
|
38
|
+
workflowObservationStore: {},
|
|
39
|
+
getKGExtractor: () => null,
|
|
40
|
+
getKGStore: () => null,
|
|
41
|
+
};
|
|
42
|
+
register(deps as any);
|
|
43
|
+
return handlers;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("memory_drill_down error surfacing (P1-7)", () => {
|
|
47
|
+
it("surfaces a store error as isError with store_error reason (no swallow)", async () => {
|
|
48
|
+
const handlers = captureTools(registerAdvancedTools, () => ({
|
|
49
|
+
store: {
|
|
50
|
+
getById: async () => {
|
|
51
|
+
throw new Error("lancedb table read failed");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
const drill = handlers.get("memory_drill_down");
|
|
56
|
+
expect(drill).toBeDefined();
|
|
57
|
+
const r = await drill!({ id: "abcd1234", level: "full" });
|
|
58
|
+
expect(r.isError).toBe(true);
|
|
59
|
+
expect(r.content[0].text).toContain("store_error");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("still returns plain not-found text (read-only, no isError)", async () => {
|
|
63
|
+
const handlers = captureTools(registerAdvancedTools, () => ({
|
|
64
|
+
store: { getById: async () => null },
|
|
65
|
+
}));
|
|
66
|
+
const r = await handlers.get("memory_drill_down")!({ id: "abcd1234", level: "full" });
|
|
67
|
+
expect(r.isError).toBeUndefined();
|
|
68
|
+
expect(r.content[0].text).toContain("No memory found");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("forget_memory error surfacing (P1-7 C 类)", () => {
|
|
73
|
+
it("throws logic failure into the taxonomy layer (isError) not fake-success text", async () => {
|
|
74
|
+
const handlers = captureTools(registerAdvancedTools, () => ({
|
|
75
|
+
// store.get → null ⇒ forgetMemory 早返回 success:false(不触碰 kg/audit)
|
|
76
|
+
store: { get: async () => null },
|
|
77
|
+
}));
|
|
78
|
+
const r = await handlers.get("forget_memory")!({ memoryId: "abcd1234", confirm: false });
|
|
79
|
+
expect(r.isError).toBe(true);
|
|
80
|
+
expect(r.content[0].text).toContain("not found");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("manage_alias error surfacing (P1-7 C 类)", () => {
|
|
85
|
+
it("throws invalid_input on missing add params (not returned as success text)", async () => {
|
|
86
|
+
const handlers = captureTools(registerGovernanceTools, () => ({}));
|
|
87
|
+
const r = await handlers.get("manage_alias")!({ action: "add" });
|
|
88
|
+
expect(r.isError).toBe(true);
|
|
89
|
+
expect(r.content[0].text).toContain("invalid_input");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws invalid_input on missing remove trigger", async () => {
|
|
93
|
+
const handlers = captureTools(registerGovernanceTools, () => ({}));
|
|
94
|
+
const r = await handlers.get("manage_alias")!({ action: "remove" });
|
|
95
|
+
expect(r.isError).toBe(true);
|
|
96
|
+
expect(r.content[0].text).toContain("invalid_input");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { compressToolOutput } from "../tool-output-compressor.js";
|
|
3
|
+
|
|
4
|
+
describe("compressToolOutput", () => {
|
|
5
|
+
// ========================================================================
|
|
6
|
+
// Base64 replacement
|
|
7
|
+
// ========================================================================
|
|
8
|
+
|
|
9
|
+
it("replaces large base64 blocks with size placeholder", () => {
|
|
10
|
+
const base64 = "A".repeat(2000);
|
|
11
|
+
const text = `Here is a screenshot:\n${base64}\nEnd of screenshot.`;
|
|
12
|
+
const result = compressToolOutput(text);
|
|
13
|
+
expect(result).toContain("[image:");
|
|
14
|
+
expect(result).toContain("base64");
|
|
15
|
+
expect(result).not.toContain("AAAA");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("preserves short base64-like strings", () => {
|
|
19
|
+
const text = "The hash is abc123==";
|
|
20
|
+
expect(compressToolOutput(text)).toBe(text);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ========================================================================
|
|
24
|
+
// Git output compression
|
|
25
|
+
// ========================================================================
|
|
26
|
+
|
|
27
|
+
it("compresses git push boilerplate", () => {
|
|
28
|
+
const text = `$ git push origin main
|
|
29
|
+
Enumerating objects: 5, done.
|
|
30
|
+
Counting objects: 100% (5/5), done.
|
|
31
|
+
Delta compression using up to 8 threads
|
|
32
|
+
Compressing objects: 100% (3/3), done.
|
|
33
|
+
Writing objects: 100% (3/3), 450 bytes | 450.00 KiB/s, done.
|
|
34
|
+
Total 3 (delta 2), reused 0 (delta 0)
|
|
35
|
+
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
|
|
36
|
+
To github.com:user/repo.git
|
|
37
|
+
abc1234..def5678 main -> main
|
|
38
|
+
|
|
39
|
+
Next I'll check the status.`;
|
|
40
|
+
|
|
41
|
+
const result = compressToolOutput(text);
|
|
42
|
+
expect(result).toContain("[git: ok");
|
|
43
|
+
expect(result).toContain("main");
|
|
44
|
+
expect(result).not.toContain("Enumerating objects");
|
|
45
|
+
expect(result).toContain("Next I'll check the status");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("preserves git push failures", () => {
|
|
49
|
+
const text = `$ git push
|
|
50
|
+
Enumerating objects: 5, done.
|
|
51
|
+
error: failed to push some refs to 'origin'
|
|
52
|
+
hint: Updates were rejected because the tip of your branch is behind -> main`;
|
|
53
|
+
|
|
54
|
+
const result = compressToolOutput(text);
|
|
55
|
+
expect(result).toContain("error");
|
|
56
|
+
expect(result).toContain("rejected");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ========================================================================
|
|
60
|
+
// Test output compression
|
|
61
|
+
// ========================================================================
|
|
62
|
+
|
|
63
|
+
it("compresses passing test summary", () => {
|
|
64
|
+
const text = `$ bun test
|
|
65
|
+
running 15 tests
|
|
66
|
+
test a ... ok
|
|
67
|
+
test b ... ok
|
|
68
|
+
test c ... ok
|
|
69
|
+
test result: ok. 15 passed; 0 failed; 0 ignored
|
|
70
|
+
|
|
71
|
+
All tests passed.`;
|
|
72
|
+
|
|
73
|
+
const result = compressToolOutput(text);
|
|
74
|
+
expect(result.length).toBeLessThanOrEqual(text.length);
|
|
75
|
+
expect(result).toContain("All tests passed");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("preserves failing test output", () => {
|
|
79
|
+
const text = `$ npm test
|
|
80
|
+
Tests: 2 failed, 8 passed
|
|
81
|
+
FAIL src/main.test.ts
|
|
82
|
+
✕ should handle edge case`;
|
|
83
|
+
|
|
84
|
+
const result = compressToolOutput(text);
|
|
85
|
+
expect(result).toContain("failed");
|
|
86
|
+
expect(result).toContain("FAIL");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ========================================================================
|
|
90
|
+
// Safety: user/AI content untouched
|
|
91
|
+
// ========================================================================
|
|
92
|
+
|
|
93
|
+
it("never modifies plain conversation text", () => {
|
|
94
|
+
const text = "User: How do I fix this git push error?\n\nAssistant: You need to pull first.";
|
|
95
|
+
expect(compressToolOutput(text)).toBe(text);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("preserves AI reasoning about tool results", () => {
|
|
99
|
+
const text = "The test result shows 15 passed, which means our fix works.";
|
|
100
|
+
expect(compressToolOutput(text)).toBe(text);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ========================================================================
|
|
104
|
+
// Edge cases
|
|
105
|
+
// ========================================================================
|
|
106
|
+
|
|
107
|
+
it("handles empty input", () => {
|
|
108
|
+
expect(compressToolOutput("")).toBe("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("handles short input", () => {
|
|
112
|
+
expect(compressToolOutput("hi")).toBe("hi");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("handles text without tool outputs", () => {
|
|
116
|
+
const text = "This is a normal conversation.\nNo tool outputs here.";
|
|
117
|
+
expect(compressToolOutput(text)).toBe(text);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ========================================================================
|
|
121
|
+
// Large output truncation
|
|
122
|
+
// ========================================================================
|
|
123
|
+
|
|
124
|
+
it("truncates large unmatched tool output", () => {
|
|
125
|
+
// Use content with spaces/special chars to avoid base64 regex match
|
|
126
|
+
const largeOutput = Array.from({ length: 150 }, (_, i) => `line ${i}: some detailed output here`).join("\n");
|
|
127
|
+
const text = `$ some-command\n${largeOutput}\n\nNext step.`;
|
|
128
|
+
const result = compressToolOutput(text);
|
|
129
|
+
expect(result).toContain("[...");
|
|
130
|
+
expect(result).toContain("chars truncated");
|
|
131
|
+
expect(result.length).toBeLessThan(text.length);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ========================================================================
|
|
135
|
+
// 回归:工具输出块位于字符串结尾(原 \z 锚点 bug)
|
|
136
|
+
// 旧实现 lookahead 末项是 \z(JS 无此锚点 → 当字面字符 z):
|
|
137
|
+
// - 块结尾无字面 z 时整块漏匹配 → 末尾大输出逃过截断
|
|
138
|
+
// - 块含字面 z 时惰性匹配在第一个 z 处误截断
|
|
139
|
+
// 修复为 (?![\s\S])(字符串绝对结尾)后两种都应正确处理。
|
|
140
|
+
// ========================================================================
|
|
141
|
+
|
|
142
|
+
it("truncates a large tool output block that ends the string (no trailing marker)", () => {
|
|
143
|
+
// 末尾块、无字母 z、后面没有下一个命令/空行大写/## → 旧 \z 实现整块漏匹配
|
|
144
|
+
const largeOutput = Array.from({ length: 150 }, (_, i) => `line ${i}: detailed output here`).join("\n");
|
|
145
|
+
const text = `$ some-command\n${largeOutput}`;
|
|
146
|
+
const result = compressToolOutput(text);
|
|
147
|
+
expect(result).toContain("chars truncated");
|
|
148
|
+
expect(result.length).toBeLessThan(text.length);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does not cut a trailing tool output block at a literal 'z'", () => {
|
|
152
|
+
// 末尾大块含字母 z → 旧 \z 实现在第一个 z 处截断匹配,导致块太短逃过截断
|
|
153
|
+
const largeOutput = Array.from({ length: 150 }, (_, i) => `line ${i}: zebra output zone`).join("\n");
|
|
154
|
+
const text = `$ some-command\n${largeOutput}`;
|
|
155
|
+
const result = compressToolOutput(text);
|
|
156
|
+
expect(result).toContain("chars truncated");
|
|
157
|
+
// 截断尾部应保留块真正的结尾,而不是停在某个 z 之前
|
|
158
|
+
expect(result).toContain("zone");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { detectTopicTag, extractTopicTag, injectTopicTag } from "../topic-tag.js";
|
|
3
|
+
|
|
4
|
+
describe("detectTopicTag", () => {
|
|
5
|
+
it("detects auth-related content", () => {
|
|
6
|
+
expect(detectTopicTag("Implement JWT authentication with OAuth2 flow")).toBe("auth");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("detects deploy-related content", () => {
|
|
10
|
+
expect(detectTopicTag("Set up CI/CD pipeline with GitHub Actions for production deployment")).toBe("deploy");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("detects infra-related content", () => {
|
|
14
|
+
expect(detectTopicTag("Configure Docker containers and Kubernetes cluster")).toBe("infra");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects testing-related content", () => {
|
|
18
|
+
expect(detectTopicTag("Write unit tests with bun test and add coverage for the new module")).toBe("testing");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("detects database-related content", () => {
|
|
22
|
+
expect(detectTopicTag("Run SQL migration to add new columns to postgres users table")).toBe("database");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("detects api-related content", () => {
|
|
26
|
+
expect(detectTopicTag("Design REST API endpoints for the new webhook integration")).toBe("api");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("detects ui-related content", () => {
|
|
30
|
+
expect(detectTopicTag("Build React component with Tailwind CSS for the dashboard layout")).toBe("ui");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("detects performance-related content", () => {
|
|
34
|
+
expect(detectTopicTag("Profile cache hit rates and optimize latency bottleneck")).toBe("perf");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("detects security-related content", () => {
|
|
38
|
+
expect(detectTopicTag("Fix XSS vulnerability and add CSRF protection")).toBe("security");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("detects memory-related content", () => {
|
|
42
|
+
expect(detectTopicTag("Improve recall retrieval with better embedding vectors for LanceDB")).toBe("memory");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("detects mcp-related content", () => {
|
|
46
|
+
expect(detectTopicTag("Register new MCP tool in the Model Context Protocol server")).toBe("mcp");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("detects llm-related content", () => {
|
|
50
|
+
expect(detectTopicTag("Optimize prompt engineering for better token efficiency with Claude")).toBe("llm");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns undefined for generic text", () => {
|
|
54
|
+
expect(detectTopicTag("Had a great day today")).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns undefined for empty text", () => {
|
|
58
|
+
expect(detectTopicTag("")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("picks strongest match when multiple topics present", () => {
|
|
62
|
+
const tag = detectTopicTag("Deploy the authentication service to production using Docker");
|
|
63
|
+
expect(tag).toBeDefined();
|
|
64
|
+
// Multiple topics present; strongest wins
|
|
65
|
+
expect(["auth", "deploy", "infra"]).toContain(tag);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles Chinese text with English tech terms", () => {
|
|
69
|
+
expect(detectTopicTag("配置 Docker 容器编排和 Kubernetes 集群")).toBe("infra");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("truncates very long text for efficiency", () => {
|
|
73
|
+
const longText = "Fix authentication bug. ".repeat(500);
|
|
74
|
+
expect(detectTopicTag(longText)).toBe("auth");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("extractTopicTag", () => {
|
|
79
|
+
it("extracts topicTag from metadata JSON", () => {
|
|
80
|
+
expect(extractTopicTag('{"topicTag":"auth","source":"manual"}')).toBe("auth");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns undefined when no topicTag", () => {
|
|
84
|
+
expect(extractTopicTag('{"source":"manual"}')).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns undefined for invalid JSON", () => {
|
|
88
|
+
expect(extractTopicTag("not json")).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns undefined for undefined/null metadata", () => {
|
|
92
|
+
expect(extractTopicTag(undefined)).toBeUndefined();
|
|
93
|
+
expect(extractTopicTag("")).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("injectTopicTag", () => {
|
|
98
|
+
it("injects topicTag into metadata JSON", () => {
|
|
99
|
+
const result = injectTopicTag('{"source":"manual"}', "deploy");
|
|
100
|
+
const parsed = JSON.parse(result);
|
|
101
|
+
expect(parsed.topicTag).toBe("deploy");
|
|
102
|
+
expect(parsed.source).toBe("manual");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("overwrites existing topicTag", () => {
|
|
106
|
+
const result = injectTopicTag('{"topicTag":"old","source":"manual"}', "new");
|
|
107
|
+
expect(JSON.parse(result).topicTag).toBe("new");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns original on invalid JSON", () => {
|
|
111
|
+
expect(injectTopicTag("broken", "tag")).toBe("broken");
|
|
112
|
+
});
|
|
113
|
+
});
|