@mhingston5/lasso 0.1.0
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/README.md +707 -0
- package/docs/agent-wrangling.png +0 -0
- package/package.json +26 -0
- package/src/capabilities/matcher.ts +25 -0
- package/src/capabilities/registry.ts +103 -0
- package/src/capabilities/types.ts +15 -0
- package/src/cir/lower.ts +253 -0
- package/src/cir/optimize.ts +251 -0
- package/src/cir/types.ts +131 -0
- package/src/cir/validate.ts +265 -0
- package/src/compiler/compile.ts +601 -0
- package/src/compiler/feedback.ts +471 -0
- package/src/compiler/runtime-helpers.ts +455 -0
- package/src/composition/chain.ts +58 -0
- package/src/composition/conditional.ts +76 -0
- package/src/composition/parallel.ts +75 -0
- package/src/composition/types.ts +105 -0
- package/src/environment/analyzer.ts +56 -0
- package/src/environment/discovery.ts +179 -0
- package/src/environment/types.ts +68 -0
- package/src/failures/classifiers.ts +134 -0
- package/src/failures/generator.ts +421 -0
- package/src/failures/map-reference-failures.ts +23 -0
- package/src/failures/ontology.ts +210 -0
- package/src/failures/recovery.ts +214 -0
- package/src/failures/types.ts +14 -0
- package/src/index.ts +67 -0
- package/src/memory/advisor.ts +132 -0
- package/src/memory/extractor.ts +166 -0
- package/src/memory/store.ts +107 -0
- package/src/memory/types.ts +53 -0
- package/src/metaharness/engine.ts +256 -0
- package/src/metaharness/predictor.ts +168 -0
- package/src/metaharness/types.ts +40 -0
- package/src/mutation/derive.ts +308 -0
- package/src/mutation/diff.ts +52 -0
- package/src/mutation/engine.ts +256 -0
- package/src/mutation/types.ts +84 -0
- package/src/pi/command-input.ts +209 -0
- package/src/pi/commands.ts +351 -0
- package/src/pi/extension.ts +16 -0
- package/src/planner/synthesize.ts +83 -0
- package/src/planner/template-rules.ts +183 -0
- package/src/planner/types.ts +42 -0
- package/src/reference/catalog.ts +128 -0
- package/src/reference/patch-validation-strategies.ts +170 -0
- package/src/reference/patch-validation.ts +174 -0
- package/src/reference/pr-review-merge.ts +155 -0
- package/src/reference/strategies.ts +126 -0
- package/src/reference/types.ts +33 -0
- package/src/replanner/risk-rules.ts +161 -0
- package/src/replanner/runtime.ts +308 -0
- package/src/replanner/synthesize.ts +619 -0
- package/src/replanner/types.ts +73 -0
- package/src/spec/schema.ts +254 -0
- package/src/spec/types.ts +319 -0
- package/src/spec/validate.ts +296 -0
- package/src/state/snapshots.ts +43 -0
- package/src/state/types.ts +12 -0
- package/src/synthesis/graph-builder.ts +267 -0
- package/src/synthesis/harness-builder.ts +113 -0
- package/src/synthesis/intent-ir.ts +63 -0
- package/src/synthesis/policy-builder.ts +320 -0
- package/src/synthesis/risk-analyzer.ts +182 -0
- package/src/synthesis/skill-parser.ts +441 -0
- package/src/verification/engine.ts +230 -0
- package/src/versioning/file-store.ts +103 -0
- package/src/versioning/history.ts +43 -0
- package/src/versioning/store.ts +16 -0
- package/src/versioning/types.ts +31 -0
- package/test/capabilities/matcher.test.ts +67 -0
- package/test/capabilities/registry.test.ts +136 -0
- package/test/capabilities/synthesis.test.ts +264 -0
- package/test/cir/lower.test.ts +417 -0
- package/test/cir/optimize.test.ts +266 -0
- package/test/cir/validate.test.ts +368 -0
- package/test/compiler/adaptive-runtime.test.ts +157 -0
- package/test/compiler/compile.test.ts +1198 -0
- package/test/compiler/feedback.test.ts +784 -0
- package/test/compiler/guardrails.test.ts +191 -0
- package/test/compiler/trace.test.ts +404 -0
- package/test/composition/chain.test.ts +328 -0
- package/test/composition/conditional.test.ts +241 -0
- package/test/composition/parallel.test.ts +215 -0
- package/test/environment/analyzer.test.ts +204 -0
- package/test/environment/discovery.test.ts +149 -0
- package/test/failures/classifiers.test.ts +287 -0
- package/test/failures/generator.test.ts +203 -0
- package/test/failures/ontology.test.ts +439 -0
- package/test/failures/recovery.test.ts +300 -0
- package/test/helpers/createFixtureRepo.ts +84 -0
- package/test/helpers/createPatchValidationFixture.ts +144 -0
- package/test/helpers/runCompiledWorkflow.ts +208 -0
- package/test/memory/advisor.test.ts +332 -0
- package/test/memory/extractor.test.ts +295 -0
- package/test/memory/store.test.ts +244 -0
- package/test/metaharness/engine.test.ts +575 -0
- package/test/metaharness/predictor.test.ts +436 -0
- package/test/mutation/derive-failure.test.ts +209 -0
- package/test/mutation/engine.test.ts +622 -0
- package/test/package-smoke.test.ts +29 -0
- package/test/pi/command-input.test.ts +153 -0
- package/test/pi/commands.test.ts +623 -0
- package/test/planner/classify-template.test.ts +32 -0
- package/test/planner/synthesize.test.ts +901 -0
- package/test/reference/PatchValidation.failures.test.ts +137 -0
- package/test/reference/PatchValidation.test.ts +326 -0
- package/test/reference/PrReviewMerge.failures.test.ts +121 -0
- package/test/reference/PrReviewMerge.test.ts +55 -0
- package/test/reference/catalog-open.test.ts +70 -0
- package/test/replanner/runtime.test.ts +207 -0
- package/test/replanner/synthesize.test.ts +303 -0
- package/test/spec/validate.test.ts +1056 -0
- package/test/state/snapshots.test.ts +264 -0
- package/test/synthesis/custom-workflow.test.ts +264 -0
- package/test/synthesis/graph-builder.test.ts +370 -0
- package/test/synthesis/harness-builder.test.ts +128 -0
- package/test/synthesis/policy-builder.test.ts +149 -0
- package/test/synthesis/risk-analyzer.test.ts +230 -0
- package/test/synthesis/skill-parser.test.ts +796 -0
- package/test/verification/engine.test.ts +509 -0
- package/test/versioning/history.test.ts +144 -0
- package/test/versioning/store.test.ts +254 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractPatternsFromTrace } from "../../src/memory/extractor.js";
|
|
3
|
+
import type { HarnessExecutionTrace } from "../../src/versioning/types.js";
|
|
4
|
+
import type { HarnessSpec } from "../../src/spec/types.js";
|
|
5
|
+
import type { ExecutionTraceEntry } from "../../src/compiler/runtime-helpers.js";
|
|
6
|
+
|
|
7
|
+
function makeSpec(overrides?: Partial<HarnessSpec>): HarnessSpec {
|
|
8
|
+
return {
|
|
9
|
+
name: "test-harness",
|
|
10
|
+
graph: {
|
|
11
|
+
entryNodeId: "auth-check",
|
|
12
|
+
nodes: [
|
|
13
|
+
{ id: "auth-check", kind: "tool" as const, tool: "bash", args: ["echo", "auth"] },
|
|
14
|
+
{ id: "deploy", kind: "tool" as const, tool: "bash", args: ["echo", "deploy"] },
|
|
15
|
+
{ id: "verify", kind: "tool" as const, tool: "bash", args: ["echo", "verify"] },
|
|
16
|
+
],
|
|
17
|
+
edges: [
|
|
18
|
+
{ from: "auth-check", to: "deploy" },
|
|
19
|
+
{ from: "deploy", to: "verify" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeTraceEntry(
|
|
27
|
+
nodeId: string,
|
|
28
|
+
phase: ExecutionTraceEntry["phase"],
|
|
29
|
+
overrides?: Partial<ExecutionTraceEntry>,
|
|
30
|
+
): ExecutionTraceEntry {
|
|
31
|
+
return {
|
|
32
|
+
nodeId,
|
|
33
|
+
source: { specNodeId: nodeId, specNodeKind: "tool", specPath: `graph.nodes[0]` },
|
|
34
|
+
phase,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeTrace(entries: ExecutionTraceEntry[], overrides?: Partial<HarnessExecutionTrace>): HarnessExecutionTrace {
|
|
40
|
+
const failureCount = entries.filter(e => e.phase === "failure").length;
|
|
41
|
+
return {
|
|
42
|
+
entries,
|
|
43
|
+
totalDurationMs: entries.length * 100,
|
|
44
|
+
nodeCount: new Set(entries.map(e => e.nodeId)).size,
|
|
45
|
+
failureCount,
|
|
46
|
+
startTimeMs: 1000,
|
|
47
|
+
endTimeMs: 1000 + entries.length * 100,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("memory/extractPatternsFromTrace", () => {
|
|
53
|
+
describe("successful pattern detection", () => {
|
|
54
|
+
it("should identify nodes that always succeed", () => {
|
|
55
|
+
const trace = makeTrace([
|
|
56
|
+
makeTraceEntry("auth-check", "enter"),
|
|
57
|
+
makeTraceEntry("auth-check", "success"),
|
|
58
|
+
makeTraceEntry("deploy", "enter"),
|
|
59
|
+
makeTraceEntry("deploy", "success"),
|
|
60
|
+
]);
|
|
61
|
+
const spec = makeSpec();
|
|
62
|
+
|
|
63
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
64
|
+
|
|
65
|
+
expect(successful.some(p => p.includes("auth-check"))).toBe(true);
|
|
66
|
+
expect(successful.some(p => p.includes("deploy"))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should identify ordering patterns (A before B succeeds)", () => {
|
|
70
|
+
const trace = makeTrace([
|
|
71
|
+
makeTraceEntry("auth-check", "enter"),
|
|
72
|
+
makeTraceEntry("auth-check", "success"),
|
|
73
|
+
makeTraceEntry("deploy", "enter"),
|
|
74
|
+
makeTraceEntry("deploy", "success"),
|
|
75
|
+
]);
|
|
76
|
+
const spec = makeSpec();
|
|
77
|
+
|
|
78
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
79
|
+
|
|
80
|
+
expect(successful.some(p => p.includes("auth-check") && p.includes("deploy"))).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should identify verification-after-deploy pattern", () => {
|
|
84
|
+
const trace = makeTrace([
|
|
85
|
+
makeTraceEntry("auth-check", "enter"),
|
|
86
|
+
makeTraceEntry("auth-check", "success"),
|
|
87
|
+
makeTraceEntry("deploy", "enter"),
|
|
88
|
+
makeTraceEntry("deploy", "success"),
|
|
89
|
+
makeTraceEntry("verify", "enter"),
|
|
90
|
+
makeTraceEntry("verify", "success"),
|
|
91
|
+
]);
|
|
92
|
+
const spec = makeSpec();
|
|
93
|
+
|
|
94
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
95
|
+
|
|
96
|
+
expect(successful.some(p => p.includes("deploy") && p.includes("verify"))).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("failed pattern detection", () => {
|
|
101
|
+
it("should identify nodes that fail", () => {
|
|
102
|
+
const trace = makeTrace([
|
|
103
|
+
makeTraceEntry("deploy", "enter"),
|
|
104
|
+
makeTraceEntry("deploy", "failure", { details: { message: "auth required" } }),
|
|
105
|
+
]);
|
|
106
|
+
const spec = makeSpec();
|
|
107
|
+
|
|
108
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
109
|
+
|
|
110
|
+
expect(failed.some(p => p.includes("deploy"))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should identify A-before-B-fails pattern", () => {
|
|
114
|
+
const trace = makeTrace([
|
|
115
|
+
makeTraceEntry("auth-check", "enter"),
|
|
116
|
+
makeTraceEntry("auth-check", "success"),
|
|
117
|
+
makeTraceEntry("deploy", "enter"),
|
|
118
|
+
makeTraceEntry("deploy", "failure", { details: { message: "connection refused" } }),
|
|
119
|
+
]);
|
|
120
|
+
const spec = makeSpec();
|
|
121
|
+
|
|
122
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
123
|
+
|
|
124
|
+
expect(failed.some(p => p.includes("auth-check") && p.includes("deploy") && p.includes("fails"))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should identify auth-check-prevents-failure when auth-check succeeds before deploy", () => {
|
|
128
|
+
const trace = makeTrace([
|
|
129
|
+
makeTraceEntry("auth-check", "enter"),
|
|
130
|
+
makeTraceEntry("auth-check", "success"),
|
|
131
|
+
makeTraceEntry("deploy", "enter"),
|
|
132
|
+
makeTraceEntry("deploy", "success"),
|
|
133
|
+
]);
|
|
134
|
+
const spec = makeSpec();
|
|
135
|
+
|
|
136
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
137
|
+
|
|
138
|
+
expect(successful.some(p => p.includes("auth-check") && p.includes("before"))).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should identify retry patterns", () => {
|
|
142
|
+
const trace = makeTrace([
|
|
143
|
+
makeTraceEntry("deploy", "enter"),
|
|
144
|
+
makeTraceEntry("deploy", "failure"),
|
|
145
|
+
makeTraceEntry("deploy", "retry"),
|
|
146
|
+
makeTraceEntry("deploy", "success"),
|
|
147
|
+
]);
|
|
148
|
+
const spec = makeSpec();
|
|
149
|
+
|
|
150
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
151
|
+
|
|
152
|
+
expect(successful.some(p => p.includes("retry"))).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should identify verification-fail patterns", () => {
|
|
156
|
+
const trace = makeTrace([
|
|
157
|
+
makeTraceEntry("deploy", "enter"),
|
|
158
|
+
makeTraceEntry("deploy", "success"),
|
|
159
|
+
makeTraceEntry("verify", "enter"),
|
|
160
|
+
makeTraceEntry("verify", "verification-fail"),
|
|
161
|
+
]);
|
|
162
|
+
const spec = makeSpec();
|
|
163
|
+
|
|
164
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
165
|
+
|
|
166
|
+
expect(failed.some(p => p.includes("verify"))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("edge cases", () => {
|
|
171
|
+
it("should return empty arrays for empty trace", () => {
|
|
172
|
+
const trace = makeTrace([]);
|
|
173
|
+
const spec = makeSpec();
|
|
174
|
+
|
|
175
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
176
|
+
|
|
177
|
+
expect(successful).toEqual([]);
|
|
178
|
+
expect(failed).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should handle trace with only enter phases", () => {
|
|
182
|
+
const trace = makeTrace([
|
|
183
|
+
makeTraceEntry("auth-check", "enter"),
|
|
184
|
+
makeTraceEntry("deploy", "enter"),
|
|
185
|
+
]);
|
|
186
|
+
const spec = makeSpec();
|
|
187
|
+
|
|
188
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
189
|
+
|
|
190
|
+
expect(successful).toEqual([]);
|
|
191
|
+
expect(failed).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle multiple failures of same node", () => {
|
|
195
|
+
const trace = makeTrace([
|
|
196
|
+
makeTraceEntry("deploy", "enter"),
|
|
197
|
+
makeTraceEntry("deploy", "failure"),
|
|
198
|
+
makeTraceEntry("deploy", "retry"),
|
|
199
|
+
makeTraceEntry("deploy", "failure"),
|
|
200
|
+
]);
|
|
201
|
+
const spec = makeSpec();
|
|
202
|
+
|
|
203
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
204
|
+
|
|
205
|
+
expect(failed.some(p => p.includes("deploy"))).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should handle condition branching patterns", () => {
|
|
209
|
+
const trace = makeTrace([
|
|
210
|
+
makeTraceEntry("check", "enter"),
|
|
211
|
+
makeTraceEntry("check", "success"),
|
|
212
|
+
makeTraceEntry("check", "condition-true"),
|
|
213
|
+
makeTraceEntry("branch-a", "enter"),
|
|
214
|
+
makeTraceEntry("branch-a", "success"),
|
|
215
|
+
]);
|
|
216
|
+
const spec = makeSpec({
|
|
217
|
+
name: "conditional-harness",
|
|
218
|
+
graph: {
|
|
219
|
+
entryNodeId: "check",
|
|
220
|
+
nodes: [
|
|
221
|
+
{ id: "check", kind: "condition" as const, condition: "true", thenNodeId: "branch-a", elseNodeId: "branch-b" },
|
|
222
|
+
{ id: "branch-a", kind: "tool" as const, tool: "bash", args: ["echo", "a"] },
|
|
223
|
+
{ id: "branch-b", kind: "tool" as const, tool: "bash", args: ["echo", "b"] },
|
|
224
|
+
],
|
|
225
|
+
edges: [],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
230
|
+
|
|
231
|
+
expect(successful.some(p => p.includes("check") && p.includes("branch-a"))).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should handle merge patterns", () => {
|
|
235
|
+
const trace = makeTrace([
|
|
236
|
+
makeTraceEntry("branch-a", "enter"),
|
|
237
|
+
makeTraceEntry("branch-a", "success"),
|
|
238
|
+
makeTraceEntry("branch-b", "enter"),
|
|
239
|
+
makeTraceEntry("branch-b", "success"),
|
|
240
|
+
makeTraceEntry("merge", "enter"),
|
|
241
|
+
makeTraceEntry("merge", "merge"),
|
|
242
|
+
makeTraceEntry("merge", "success"),
|
|
243
|
+
]);
|
|
244
|
+
const spec = makeSpec({
|
|
245
|
+
name: "merge-harness",
|
|
246
|
+
graph: {
|
|
247
|
+
entryNodeId: "branch-a",
|
|
248
|
+
nodes: [
|
|
249
|
+
{ id: "branch-a", kind: "tool" as const, tool: "bash", args: ["echo", "a"] },
|
|
250
|
+
{ id: "branch-b", kind: "tool" as const, tool: "bash", args: ["echo", "b"] },
|
|
251
|
+
{ id: "merge", kind: "merge" as const, waitFor: ["branch-a", "branch-b"] },
|
|
252
|
+
],
|
|
253
|
+
edges: [
|
|
254
|
+
{ from: "branch-a", to: "merge" },
|
|
255
|
+
{ from: "branch-b", to: "merge" },
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
261
|
+
|
|
262
|
+
expect(successful.some(p => p.includes("merge"))).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("pattern naming conventions", () => {
|
|
267
|
+
it("should use kebab-case for pattern names", () => {
|
|
268
|
+
const trace = makeTrace([
|
|
269
|
+
makeTraceEntry("auth-check", "enter"),
|
|
270
|
+
makeTraceEntry("auth-check", "success"),
|
|
271
|
+
makeTraceEntry("deploy", "enter"),
|
|
272
|
+
makeTraceEntry("deploy", "success"),
|
|
273
|
+
]);
|
|
274
|
+
const spec = makeSpec();
|
|
275
|
+
|
|
276
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
277
|
+
|
|
278
|
+
for (const pattern of successful) {
|
|
279
|
+
expect(pattern).toMatch(/^[a-z0-9-]+$/);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should include node IDs in pattern names", () => {
|
|
284
|
+
const trace = makeTrace([
|
|
285
|
+
makeTraceEntry("auth-check", "enter"),
|
|
286
|
+
makeTraceEntry("auth-check", "success"),
|
|
287
|
+
]);
|
|
288
|
+
const spec = makeSpec();
|
|
289
|
+
|
|
290
|
+
const { successful, failed } = extractPatternsFromTrace(trace, spec);
|
|
291
|
+
|
|
292
|
+
expect(successful.some(p => p.includes("auth-check"))).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { FileMemoryStore } from "../../src/memory/store.js";
|
|
6
|
+
import type { HarnessMemory, MemoryUpdate, MemoryQuery } from "../../src/memory/types.js";
|
|
7
|
+
|
|
8
|
+
function makeMemory(taskId: string, overrides?: Partial<HarnessMemory>): HarnessMemory {
|
|
9
|
+
return {
|
|
10
|
+
taskId,
|
|
11
|
+
taskEmbedding: `hash-${taskId}`,
|
|
12
|
+
successfulPatterns: ["auth-check-before-deploy"],
|
|
13
|
+
failedPatterns: ["deploy-without-auth"],
|
|
14
|
+
mutationHistory: [],
|
|
15
|
+
effectivenessScore: 0.7,
|
|
16
|
+
lastUpdated: Date.now(),
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("memory/FileMemoryStore", () => {
|
|
22
|
+
let storeDir: string;
|
|
23
|
+
let store: FileMemoryStore;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
storeDir = await mkdtemp(join(tmpdir(), "memory-test-"));
|
|
27
|
+
store = new FileMemoryStore(storeDir);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await rm(storeDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("saveMemory / getMemory", () => {
|
|
35
|
+
it("should save and retrieve a memory", async () => {
|
|
36
|
+
const memory = makeMemory("task-1");
|
|
37
|
+
|
|
38
|
+
await store.saveMemory(memory);
|
|
39
|
+
const retrieved = await store.getMemory("task-1");
|
|
40
|
+
|
|
41
|
+
expect(retrieved).toEqual(memory);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return null for a missing memory", async () => {
|
|
45
|
+
const result = await store.getMemory("nonexistent");
|
|
46
|
+
expect(result).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should deep clone on save (mutations do not affect store)", async () => {
|
|
50
|
+
const memory = makeMemory("task-mutate");
|
|
51
|
+
await store.saveMemory(memory);
|
|
52
|
+
|
|
53
|
+
memory.successfulPatterns.push("mutated-pattern");
|
|
54
|
+
|
|
55
|
+
const retrieved = await store.getMemory("task-mutate");
|
|
56
|
+
expect(retrieved!.successfulPatterns).not.toContain("mutated-pattern");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle memory without taskEmbedding", async () => {
|
|
60
|
+
const memory = makeMemory("task-no-embedding", { taskEmbedding: undefined });
|
|
61
|
+
|
|
62
|
+
await store.saveMemory(memory);
|
|
63
|
+
const retrieved = await store.getMemory("task-no-embedding");
|
|
64
|
+
|
|
65
|
+
expect(retrieved).toEqual(memory);
|
|
66
|
+
expect(retrieved!.taskEmbedding).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("updateMemory", () => {
|
|
71
|
+
it("should add a successful pattern", async () => {
|
|
72
|
+
const memory = makeMemory("task-update");
|
|
73
|
+
await store.saveMemory(memory);
|
|
74
|
+
|
|
75
|
+
const update: MemoryUpdate = { successfulPattern: "retry-before-fail" };
|
|
76
|
+
const updated = await store.updateMemory("task-update", update);
|
|
77
|
+
|
|
78
|
+
expect(updated.successfulPatterns).toContain("retry-before-fail");
|
|
79
|
+
expect(updated.successfulPatterns).toContain("auth-check-before-deploy");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should add a failed pattern", async () => {
|
|
83
|
+
const memory = makeMemory("task-update");
|
|
84
|
+
await store.saveMemory(memory);
|
|
85
|
+
|
|
86
|
+
const update: MemoryUpdate = { failedPattern: "skip-validation" };
|
|
87
|
+
const updated = await store.updateMemory("task-update", update);
|
|
88
|
+
|
|
89
|
+
expect(updated.failedPatterns).toContain("skip-validation");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should add a mutation record", async () => {
|
|
93
|
+
const memory = makeMemory("task-update");
|
|
94
|
+
await store.saveMemory(memory);
|
|
95
|
+
|
|
96
|
+
const mutationRecord = {
|
|
97
|
+
mutation: "add-node:auth-check",
|
|
98
|
+
triggeredBy: "auth-failure",
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
outcome: "improved" as const,
|
|
101
|
+
};
|
|
102
|
+
const update: MemoryUpdate = { mutation: mutationRecord };
|
|
103
|
+
const updated = await store.updateMemory("task-update", update);
|
|
104
|
+
|
|
105
|
+
expect(updated.mutationHistory).toHaveLength(1);
|
|
106
|
+
expect(updated.mutationHistory[0]).toEqual(mutationRecord);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should adjust effectiveness score with delta", async () => {
|
|
110
|
+
const memory = makeMemory("task-update", { effectivenessScore: 0.5 });
|
|
111
|
+
await store.saveMemory(memory);
|
|
112
|
+
|
|
113
|
+
const update: MemoryUpdate = { effectivenessDelta: 0.1 };
|
|
114
|
+
const updated = await store.updateMemory("task-update", update);
|
|
115
|
+
|
|
116
|
+
expect(updated.effectivenessScore).toBeCloseTo(0.6);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should clamp effectiveness score to 1.0", async () => {
|
|
120
|
+
const memory = makeMemory("task-update", { effectivenessScore: 0.95 });
|
|
121
|
+
await store.saveMemory(memory);
|
|
122
|
+
|
|
123
|
+
const update: MemoryUpdate = { effectivenessDelta: 0.1 };
|
|
124
|
+
const updated = await store.updateMemory("task-update", update);
|
|
125
|
+
|
|
126
|
+
expect(updated.effectivenessScore).toBe(1.0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should clamp effectiveness score to 0.0", async () => {
|
|
130
|
+
const memory = makeMemory("task-update", { effectivenessScore: 0.05 });
|
|
131
|
+
await store.saveMemory(memory);
|
|
132
|
+
|
|
133
|
+
const update: MemoryUpdate = { effectivenessDelta: -0.1 };
|
|
134
|
+
const updated = await store.updateMemory("task-update", update);
|
|
135
|
+
|
|
136
|
+
expect(updated.effectivenessScore).toBe(0.0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should throw if memory does not exist", async () => {
|
|
140
|
+
const update: MemoryUpdate = { successfulPattern: "new-pattern" };
|
|
141
|
+
await expect(store.updateMemory("nonexistent", update)).rejects.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should apply multiple updates atomically", async () => {
|
|
145
|
+
const memory = makeMemory("task-update", { effectivenessScore: 0.5 });
|
|
146
|
+
await store.saveMemory(memory);
|
|
147
|
+
|
|
148
|
+
const update: MemoryUpdate = {
|
|
149
|
+
successfulPattern: "new-success",
|
|
150
|
+
effectivenessDelta: 0.2,
|
|
151
|
+
};
|
|
152
|
+
const updated = await store.updateMemory("task-update", update);
|
|
153
|
+
|
|
154
|
+
expect(updated.successfulPatterns).toContain("new-success");
|
|
155
|
+
expect(updated.effectivenessScore).toBeCloseTo(0.7);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("searchMemories", () => {
|
|
160
|
+
beforeEach(async () => {
|
|
161
|
+
const memories = [
|
|
162
|
+
makeMemory("task-alpha", { taskEmbedding: "hash-alpha", effectivenessScore: 0.8 }),
|
|
163
|
+
makeMemory("task-beta", { taskEmbedding: "hash-beta", effectivenessScore: 0.3 }),
|
|
164
|
+
makeMemory("task-gamma", { taskEmbedding: "hash-alpha-similar", effectivenessScore: 0.9 }),
|
|
165
|
+
];
|
|
166
|
+
for (const m of memories) {
|
|
167
|
+
await store.saveMemory(m);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should find memories by task signature prefix match", async () => {
|
|
172
|
+
const query: MemoryQuery = { taskSignature: "hash-alpha" };
|
|
173
|
+
const results = await store.searchMemories(query);
|
|
174
|
+
|
|
175
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
176
|
+
expect(results.some(r => r.taskEmbedding === "hash-alpha")).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should filter by minimum effectiveness", async () => {
|
|
180
|
+
const query: MemoryQuery = { minEffectiveness: 0.5 };
|
|
181
|
+
const results = await store.searchMemories(query);
|
|
182
|
+
|
|
183
|
+
expect(results.every(r => r.effectivenessScore >= 0.5)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should apply limit", async () => {
|
|
187
|
+
const query: MemoryQuery = { limit: 1 };
|
|
188
|
+
const results = await store.searchMemories(query);
|
|
189
|
+
|
|
190
|
+
expect(results.length).toBeLessThanOrEqual(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should combine filters", async () => {
|
|
194
|
+
const query: MemoryQuery = {
|
|
195
|
+
taskSignature: "hash-alpha",
|
|
196
|
+
minEffectiveness: 0.5,
|
|
197
|
+
limit: 10,
|
|
198
|
+
};
|
|
199
|
+
const results = await store.searchMemories(query);
|
|
200
|
+
|
|
201
|
+
expect(results.every(r => r.effectivenessScore >= 0.5)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should return empty when no memories match", async () => {
|
|
205
|
+
const query: MemoryQuery = { taskSignature: "nonexistent-signature" };
|
|
206
|
+
const results = await store.searchMemories(query);
|
|
207
|
+
|
|
208
|
+
expect(results).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should return all memories when query is empty", async () => {
|
|
212
|
+
const results = await store.searchMemories({});
|
|
213
|
+
expect(results.length).toBe(3);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("round-trip integrity", () => {
|
|
218
|
+
it("should preserve all fields through save → retrieve", async () => {
|
|
219
|
+
const mutationRecord = {
|
|
220
|
+
mutation: "add-node:auth-check",
|
|
221
|
+
triggeredBy: "auth-failure",
|
|
222
|
+
timestamp: 1234567890,
|
|
223
|
+
outcome: "improved" as const,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const memory: HarnessMemory = {
|
|
227
|
+
taskId: "roundtrip",
|
|
228
|
+
taskEmbedding: "hash-roundtrip",
|
|
229
|
+
successfulPatterns: ["pattern-a", "pattern-b"],
|
|
230
|
+
failedPatterns: ["pattern-c"],
|
|
231
|
+
mutationHistory: [mutationRecord],
|
|
232
|
+
effectivenessScore: 0.85,
|
|
233
|
+
lastUpdated: 9876543210,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
await store.saveMemory(memory);
|
|
237
|
+
const retrieved = await store.getMemory("roundtrip");
|
|
238
|
+
|
|
239
|
+
expect(retrieved).toEqual(memory);
|
|
240
|
+
expect(retrieved!.mutationHistory).toHaveLength(1);
|
|
241
|
+
expect(retrieved!.mutationHistory[0].outcome).toBe("improved");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|