@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,784 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("pi-duroxide", () => ({
|
|
4
|
+
registerWorkflow: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
analyzeCompiledWorkflow,
|
|
9
|
+
applyCompilerSuggestions,
|
|
10
|
+
type CompilerAnalysis,
|
|
11
|
+
type CompilerSuggestion,
|
|
12
|
+
type CostEstimate,
|
|
13
|
+
} from "../../src/compiler/feedback.js";
|
|
14
|
+
import { compileHarnessSpec } from "../../src/compiler/compile.js";
|
|
15
|
+
import type { HarnessSpec } from "../../src/spec/types.js";
|
|
16
|
+
import type { HarnessMutation, MutationTrigger } from "../../src/mutation/types.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Test helper: create specs with various shapes
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
function createEmptySpec(): HarnessSpec {
|
|
23
|
+
return {
|
|
24
|
+
name: "empty",
|
|
25
|
+
graph: {
|
|
26
|
+
entryNodeId: "start",
|
|
27
|
+
nodes: [
|
|
28
|
+
{
|
|
29
|
+
id: "start",
|
|
30
|
+
kind: "tool",
|
|
31
|
+
tool: "echo",
|
|
32
|
+
args: ["hello"],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
edges: [],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createMultiLlmSpec(llmCount: number): HarnessSpec {
|
|
41
|
+
const nodes: HarnessSpec["graph"]["nodes"] = [];
|
|
42
|
+
const edges: HarnessSpec["graph"]["edges"] = [];
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < llmCount; i++) {
|
|
45
|
+
nodes.push({
|
|
46
|
+
id: `llm-${i}`,
|
|
47
|
+
kind: "llm",
|
|
48
|
+
provider: "anthropic",
|
|
49
|
+
model: "claude-sonnet",
|
|
50
|
+
prompt: `Prompt ${i}`,
|
|
51
|
+
});
|
|
52
|
+
if (i > 0) {
|
|
53
|
+
edges.push({ from: `llm-${i - 1}`, to: `llm-${i}` });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: "multi-llm",
|
|
59
|
+
graph: {
|
|
60
|
+
entryNodeId: "llm-0",
|
|
61
|
+
nodes,
|
|
62
|
+
edges,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createSpecWithRetry(): HarnessSpec {
|
|
68
|
+
return {
|
|
69
|
+
name: "with-retry",
|
|
70
|
+
graph: {
|
|
71
|
+
entryNodeId: "tool-a",
|
|
72
|
+
nodes: [
|
|
73
|
+
{
|
|
74
|
+
id: "tool-a",
|
|
75
|
+
kind: "tool",
|
|
76
|
+
tool: "npm",
|
|
77
|
+
args: ["test"],
|
|
78
|
+
retryPolicy: {
|
|
79
|
+
maxAttempts: 3,
|
|
80
|
+
backoff: "exponential",
|
|
81
|
+
initialDelay: 1,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
edges: [],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createSpecWithVerification(): HarnessSpec {
|
|
91
|
+
return {
|
|
92
|
+
name: "with-verification",
|
|
93
|
+
graph: {
|
|
94
|
+
entryNodeId: "tool-a",
|
|
95
|
+
nodes: [
|
|
96
|
+
{
|
|
97
|
+
id: "tool-a",
|
|
98
|
+
kind: "tool",
|
|
99
|
+
tool: "npm",
|
|
100
|
+
args: ["test"],
|
|
101
|
+
verificationPolicy: {
|
|
102
|
+
rules: [
|
|
103
|
+
{
|
|
104
|
+
kind: "tool",
|
|
105
|
+
checkNodeId: "verify-check",
|
|
106
|
+
onFail: "block",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "verify-check",
|
|
113
|
+
kind: "tool",
|
|
114
|
+
tool: "cat",
|
|
115
|
+
args: ["output.txt"],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
edges: [],
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createSpecWithHumanNode(): HarnessSpec {
|
|
124
|
+
return {
|
|
125
|
+
name: "with-human",
|
|
126
|
+
graph: {
|
|
127
|
+
entryNodeId: "tool-a",
|
|
128
|
+
nodes: [
|
|
129
|
+
{
|
|
130
|
+
id: "tool-a",
|
|
131
|
+
kind: "tool",
|
|
132
|
+
tool: "echo",
|
|
133
|
+
args: ["hello"],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "approve",
|
|
137
|
+
kind: "human",
|
|
138
|
+
prompt: "Approve?",
|
|
139
|
+
interactionType: "approval",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
edges: [{ from: "tool-a", to: "approve" }],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createAdjacentToolSpec(): HarnessSpec {
|
|
148
|
+
return {
|
|
149
|
+
name: "adjacent-tools",
|
|
150
|
+
graph: {
|
|
151
|
+
entryNodeId: "tool-a",
|
|
152
|
+
nodes: [
|
|
153
|
+
{
|
|
154
|
+
id: "tool-a",
|
|
155
|
+
kind: "tool",
|
|
156
|
+
tool: "echo",
|
|
157
|
+
args: ["a"],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "tool-b",
|
|
161
|
+
kind: "tool",
|
|
162
|
+
tool: "echo",
|
|
163
|
+
args: ["b"],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "tool-c",
|
|
167
|
+
kind: "tool",
|
|
168
|
+
tool: "echo",
|
|
169
|
+
args: ["c"],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
edges: [
|
|
173
|
+
{ from: "tool-a", to: "tool-b" },
|
|
174
|
+
{ from: "tool-b", to: "tool-c" },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createComplexSpec(): HarnessSpec {
|
|
181
|
+
return {
|
|
182
|
+
name: "complex",
|
|
183
|
+
graph: {
|
|
184
|
+
entryNodeId: "n1",
|
|
185
|
+
nodes: [
|
|
186
|
+
{ id: "n1", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p1" },
|
|
187
|
+
{ id: "n2", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p2" },
|
|
188
|
+
{ id: "n3", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p3" },
|
|
189
|
+
{ id: "n4", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p4" },
|
|
190
|
+
{ id: "n5", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p5" },
|
|
191
|
+
{ id: "n6", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p6" },
|
|
192
|
+
{ id: "n7", kind: "tool", tool: "echo", args: ["done"] },
|
|
193
|
+
],
|
|
194
|
+
edges: [
|
|
195
|
+
{ from: "n1", to: "n2" },
|
|
196
|
+
{ from: "n2", to: "n3" },
|
|
197
|
+
{ from: "n3", to: "n4" },
|
|
198
|
+
{ from: "n4", to: "n5" },
|
|
199
|
+
{ from: "n5", to: "n6" },
|
|
200
|
+
{ from: "n6", to: "n7" },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Cost Estimation Tests
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
describe("analyzeCompiledWorkflow — cost estimation", () => {
|
|
211
|
+
it("estimates cost for a single tool node", () => {
|
|
212
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
213
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
214
|
+
|
|
215
|
+
expect(analysis.cost.llmCallCount).toBe(0);
|
|
216
|
+
expect(analysis.cost.toolCallCount).toBe(1);
|
|
217
|
+
expect(analysis.cost.humanInteractionCount).toBe(0);
|
|
218
|
+
expect(analysis.cost.estimatedCostUsd).toBe(0);
|
|
219
|
+
expect(analysis.cost.estimatedDurationMs).toBeGreaterThan(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("estimates cost for multiple LLM nodes", () => {
|
|
223
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(3));
|
|
224
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
225
|
+
|
|
226
|
+
expect(analysis.cost.llmCallCount).toBe(3);
|
|
227
|
+
expect(analysis.cost.estimatedCostUsd).toBeGreaterThan(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("estimates cost for human nodes", () => {
|
|
231
|
+
const compiled = compileHarnessSpec(createSpecWithHumanNode());
|
|
232
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
233
|
+
|
|
234
|
+
expect(analysis.cost.humanInteractionCount).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("estimates total cost combining all node types", () => {
|
|
238
|
+
const spec: HarnessSpec = {
|
|
239
|
+
name: "mixed",
|
|
240
|
+
graph: {
|
|
241
|
+
entryNodeId: "llm-1",
|
|
242
|
+
nodes: [
|
|
243
|
+
{ id: "llm-1", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p1" },
|
|
244
|
+
{ id: "tool-1", kind: "tool", tool: "echo", args: ["hi"] },
|
|
245
|
+
{ id: "human-1", kind: "human", prompt: "OK?", interactionType: "approval" },
|
|
246
|
+
],
|
|
247
|
+
edges: [
|
|
248
|
+
{ from: "llm-1", to: "tool-1" },
|
|
249
|
+
{ from: "tool-1", to: "human-1" },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const compiled = compileHarnessSpec(spec);
|
|
254
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
255
|
+
|
|
256
|
+
expect(analysis.cost.llmCallCount).toBe(1);
|
|
257
|
+
expect(analysis.cost.toolCallCount).toBe(1);
|
|
258
|
+
expect(analysis.cost.humanInteractionCount).toBe(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("estimates duration based on tool count", () => {
|
|
262
|
+
const compiled = compileHarnessSpec(createAdjacentToolSpec());
|
|
263
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
264
|
+
|
|
265
|
+
// 3 tool nodes should have longer duration than 1 tool node
|
|
266
|
+
const singleToolAnalysis = analyzeCompiledWorkflow(compileHarnessSpec(createEmptySpec()));
|
|
267
|
+
expect(analysis.cost.estimatedDurationMs).toBeGreaterThan(singleToolAnalysis.cost.estimatedDurationMs);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Risk Assessment Tests
|
|
273
|
+
// ============================================================================
|
|
274
|
+
|
|
275
|
+
describe("analyzeCompiledWorkflow — risk assessment", () => {
|
|
276
|
+
it("identifies high cost risk when LLM count > 5", () => {
|
|
277
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(6));
|
|
278
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
279
|
+
|
|
280
|
+
const costRisk = analysis.risk.costRisk;
|
|
281
|
+
expect(costRisk.level).toBe("high");
|
|
282
|
+
expect(costRisk.factors.length).toBeGreaterThan(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("identifies low cost risk when LLM count <= 2", () => {
|
|
286
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(2));
|
|
287
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
288
|
+
|
|
289
|
+
expect(analysis.risk.costRisk.level).toBe("low");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("identifies high failure risk when no retry policies exist", () => {
|
|
293
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
294
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
295
|
+
|
|
296
|
+
expect(analysis.risk.failureRisk.level).toBe("high");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("identifies low failure risk when retry policies exist", () => {
|
|
300
|
+
const compiled = compileHarnessSpec(createSpecWithRetry());
|
|
301
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
302
|
+
|
|
303
|
+
expect(analysis.risk.failureRisk.level).toBe("low");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("identifies high quality risk when no verification exists", () => {
|
|
307
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
308
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
309
|
+
|
|
310
|
+
expect(analysis.risk.qualityRisk.level).toBe("high");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("identifies low quality risk when verification exists", () => {
|
|
314
|
+
const compiled = compileHarnessSpec(createSpecWithVerification());
|
|
315
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
316
|
+
|
|
317
|
+
expect(analysis.risk.qualityRisk.level).toBe("low");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("identifies high complexity risk for complex graphs", () => {
|
|
321
|
+
const compiled = compileHarnessSpec(createComplexSpec());
|
|
322
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
323
|
+
|
|
324
|
+
expect(analysis.risk.complexityRisk.level).toBe("high");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("identifies low complexity risk for simple graphs", () => {
|
|
328
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
329
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
330
|
+
|
|
331
|
+
expect(analysis.risk.complexityRisk.level).toBe("low");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("computes overall risk as the maximum of individual risks", () => {
|
|
335
|
+
// Complex spec with no retry → high failure risk → overall high
|
|
336
|
+
const compiled = compileHarnessSpec(createComplexSpec());
|
|
337
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
338
|
+
|
|
339
|
+
expect(analysis.risk.overallRisk).toBe("high");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("computes overall risk as low when all individual risks are low", () => {
|
|
343
|
+
const spec: HarnessSpec = {
|
|
344
|
+
name: "safe",
|
|
345
|
+
graph: {
|
|
346
|
+
entryNodeId: "tool-a",
|
|
347
|
+
nodes: [
|
|
348
|
+
{
|
|
349
|
+
id: "tool-a",
|
|
350
|
+
kind: "tool",
|
|
351
|
+
tool: "echo",
|
|
352
|
+
args: ["hello"],
|
|
353
|
+
retryPolicy: { maxAttempts: 2, backoff: "constant" },
|
|
354
|
+
verificationPolicy: {
|
|
355
|
+
rules: [
|
|
356
|
+
{ kind: "tool", checkNodeId: "v1", onFail: "block" },
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "v1",
|
|
362
|
+
kind: "tool",
|
|
363
|
+
tool: "echo",
|
|
364
|
+
args: ["verify"],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
edges: [],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
const compiled = compileHarnessSpec(spec);
|
|
371
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
372
|
+
|
|
373
|
+
expect(analysis.risk.overallRisk).toBe("low");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Mutation Generation Tests
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
describe("analyzeCompiledWorkflow — mutation generation", () => {
|
|
382
|
+
it("emits replace-node mutation with cost_high trigger when LLM count > 5", () => {
|
|
383
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(6));
|
|
384
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
385
|
+
|
|
386
|
+
const costMutations = analysis.mutations.filter(m => m.trigger === "cost_high");
|
|
387
|
+
expect(costMutations.length).toBeGreaterThan(0);
|
|
388
|
+
expect(costMutations[0].type).toBe("replace-node");
|
|
389
|
+
expect(costMutations[0].params.nodeId).toBeDefined();
|
|
390
|
+
expect(costMutations[0].description).toBeDefined();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("does not emit cost_high mutation when LLM count <= 5", () => {
|
|
394
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(3));
|
|
395
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
396
|
+
|
|
397
|
+
const costMutations = analysis.mutations.filter(m => m.trigger === "cost_high");
|
|
398
|
+
expect(costMutations.length).toBe(0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("emits modify-node mutation with retry_exhausted trigger when no retry policies exist", () => {
|
|
402
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
403
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
404
|
+
|
|
405
|
+
const retryMutations = analysis.mutations.filter(m => m.trigger === "retry_exhausted");
|
|
406
|
+
expect(retryMutations.length).toBeGreaterThan(0);
|
|
407
|
+
expect(retryMutations[0].type).toBe("modify-node");
|
|
408
|
+
expect(retryMutations[0].params.nodeId).toBeDefined();
|
|
409
|
+
expect(retryMutations[0].params.changes).toBeDefined();
|
|
410
|
+
expect(retryMutations[0].description).toBeDefined();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("does not emit retry_exhausted mutation when retry policies exist", () => {
|
|
414
|
+
const compiled = compileHarnessSpec(createSpecWithRetry());
|
|
415
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
416
|
+
|
|
417
|
+
const retryMutations = analysis.mutations.filter(m => m.trigger === "retry_exhausted");
|
|
418
|
+
expect(retryMutations.length).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("emits modify-node mutation for merge-nodes with loop_detected trigger", () => {
|
|
422
|
+
const compiled = compileHarnessSpec(createAdjacentToolSpec());
|
|
423
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
424
|
+
|
|
425
|
+
const mergeMutations = analysis.mutations.filter(m => m.trigger === "loop_detected");
|
|
426
|
+
expect(mergeMutations.length).toBeGreaterThan(0);
|
|
427
|
+
expect(mergeMutations[0].type).toBe("modify-node");
|
|
428
|
+
expect(mergeMutations[0].description).toBeDefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("does not emit merge mutation for non-adjacent same-tool nodes", () => {
|
|
432
|
+
const spec: HarnessSpec = {
|
|
433
|
+
name: "non-adjacent",
|
|
434
|
+
graph: {
|
|
435
|
+
entryNodeId: "tool-a",
|
|
436
|
+
nodes: [
|
|
437
|
+
{ id: "tool-a", kind: "tool", tool: "echo", args: ["a"] },
|
|
438
|
+
{ id: "llm-1", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p1" },
|
|
439
|
+
{ id: "tool-b", kind: "tool", tool: "echo", args: ["b"] },
|
|
440
|
+
],
|
|
441
|
+
edges: [
|
|
442
|
+
{ from: "tool-a", to: "llm-1" },
|
|
443
|
+
{ from: "llm-1", to: "tool-b" },
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
const compiled = compileHarnessSpec(spec);
|
|
448
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
449
|
+
|
|
450
|
+
const mergeMutations = analysis.mutations.filter(m => m.trigger === "loop_detected");
|
|
451
|
+
expect(mergeMutations.length).toBe(0);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("emits add-verification mutation with verification_failed trigger when no verification exists", () => {
|
|
455
|
+
const compiled = compileHarnessSpec(createEmptySpec());
|
|
456
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
457
|
+
|
|
458
|
+
const verificationMutations = analysis.mutations.filter(m => m.trigger === "verification_failed");
|
|
459
|
+
expect(verificationMutations.length).toBeGreaterThan(0);
|
|
460
|
+
expect(verificationMutations[0].type).toBe("add-verification");
|
|
461
|
+
expect(verificationMutations[0].params.nodeId).toBeDefined();
|
|
462
|
+
expect(verificationMutations[0].description).toBeDefined();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("does not emit verification_failed mutation when verification exists", () => {
|
|
466
|
+
const compiled = compileHarnessSpec(createSpecWithVerification());
|
|
467
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
468
|
+
|
|
469
|
+
const verificationMutations = analysis.mutations.filter(m => m.trigger === "verification_failed");
|
|
470
|
+
expect(verificationMutations.length).toBe(0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("generates multiple mutations for a complex workflow", () => {
|
|
474
|
+
const compiled = compileHarnessSpec(createComplexSpec());
|
|
475
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
476
|
+
|
|
477
|
+
expect(analysis.mutations.length).toBeGreaterThan(1);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("mutations carry both trigger and description for readability", () => {
|
|
481
|
+
const compiled = compileHarnessSpec(createComplexSpec());
|
|
482
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
483
|
+
|
|
484
|
+
for (const mutation of analysis.mutations) {
|
|
485
|
+
expect(mutation.trigger).toBeDefined();
|
|
486
|
+
expect(mutation.description).toBeDefined();
|
|
487
|
+
expect(typeof mutation.description).toBe("string");
|
|
488
|
+
expect(mutation.description!.length).toBeGreaterThan(0);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("emits replace-node mutations for each LLM node when cost_high", () => {
|
|
493
|
+
const compiled = compileHarnessSpec(createMultiLlmSpec(6));
|
|
494
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
495
|
+
|
|
496
|
+
const replaceMutations = analysis.mutations.filter(m => m.type === "replace-node");
|
|
497
|
+
expect(replaceMutations.length).toBe(6);
|
|
498
|
+
for (const m of replaceMutations) {
|
|
499
|
+
expect(m.params.nodeId).toMatch(/^llm-/);
|
|
500
|
+
expect(m.params.changes).toBeDefined();
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("keeps backward-compatible suggestions field", () => {
|
|
505
|
+
const compiled = compileHarnessSpec(createComplexSpec());
|
|
506
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
507
|
+
|
|
508
|
+
expect(analysis.suggestions).toBeDefined();
|
|
509
|
+
expect(Array.isArray(analysis.suggestions)).toBe(true);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ============================================================================
|
|
514
|
+
// applyCompilerSuggestions Tests (backward compatibility)
|
|
515
|
+
// ============================================================================
|
|
516
|
+
|
|
517
|
+
describe("applyCompilerSuggestions", () => {
|
|
518
|
+
it("returns unchanged spec when no suggestions", () => {
|
|
519
|
+
const spec = createEmptySpec();
|
|
520
|
+
const result = applyCompilerSuggestions(spec, []);
|
|
521
|
+
|
|
522
|
+
expect(result).toEqual(spec);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("adds retry policy for add-retry suggestion", () => {
|
|
526
|
+
const spec: HarnessSpec = {
|
|
527
|
+
name: "no-retry",
|
|
528
|
+
graph: {
|
|
529
|
+
entryNodeId: "tool-a",
|
|
530
|
+
nodes: [
|
|
531
|
+
{
|
|
532
|
+
id: "tool-a",
|
|
533
|
+
kind: "tool",
|
|
534
|
+
tool: "npm",
|
|
535
|
+
args: ["test"],
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
edges: [],
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
const suggestions: CompilerSuggestion[] = [
|
|
542
|
+
{ type: "add-retry", description: "Add retry to tool nodes", impact: "high" },
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
546
|
+
const toolNode = result.graph.nodes.find(n => n.id === "tool-a");
|
|
547
|
+
|
|
548
|
+
expect(toolNode?.retryPolicy).toBeDefined();
|
|
549
|
+
expect(toolNode?.retryPolicy?.maxAttempts).toBe(3);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("adds verification for add-verification suggestion", () => {
|
|
553
|
+
const spec: HarnessSpec = {
|
|
554
|
+
name: "no-verification",
|
|
555
|
+
graph: {
|
|
556
|
+
entryNodeId: "tool-a",
|
|
557
|
+
nodes: [
|
|
558
|
+
{
|
|
559
|
+
id: "tool-a",
|
|
560
|
+
kind: "tool",
|
|
561
|
+
tool: "npm",
|
|
562
|
+
args: ["test"],
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
edges: [],
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
const suggestions: CompilerSuggestion[] = [
|
|
569
|
+
{ type: "add-verification", description: "Add verification", impact: "high" },
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
573
|
+
const toolNode = result.graph.nodes.find(n => n.id === "tool-a");
|
|
574
|
+
|
|
575
|
+
expect(toolNode?.verificationPolicy).toBeDefined();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("applies multiple suggestions in sequence", () => {
|
|
579
|
+
const spec: HarnessSpec = {
|
|
580
|
+
name: "multi-suggestion",
|
|
581
|
+
graph: {
|
|
582
|
+
entryNodeId: "tool-a",
|
|
583
|
+
nodes: [
|
|
584
|
+
{
|
|
585
|
+
id: "tool-a",
|
|
586
|
+
kind: "tool",
|
|
587
|
+
tool: "npm",
|
|
588
|
+
args: ["test"],
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
edges: [],
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
const suggestions: CompilerSuggestion[] = [
|
|
595
|
+
{ type: "add-retry", description: "Add retry", impact: "high" },
|
|
596
|
+
{ type: "add-verification", description: "Add verification", impact: "medium" },
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
600
|
+
const toolNode = result.graph.nodes.find(n => n.id === "tool-a");
|
|
601
|
+
|
|
602
|
+
expect(toolNode?.retryPolicy).toBeDefined();
|
|
603
|
+
expect(toolNode?.verificationPolicy).toBeDefined();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("does not modify spec for reduce-llm suggestion (requires manual intervention)", () => {
|
|
607
|
+
const spec = createMultiLlmSpec(6);
|
|
608
|
+
const suggestions: CompilerSuggestion[] = [
|
|
609
|
+
{ type: "reduce-llm", description: "Reduce LLM calls", impact: "high" },
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
613
|
+
|
|
614
|
+
// reduce-llm should not auto-modify the spec
|
|
615
|
+
expect(result.graph.nodes.length).toBe(spec.graph.nodes.length);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("does not modify spec for simplify suggestion (no-op for now)", () => {
|
|
619
|
+
const spec = createEmptySpec();
|
|
620
|
+
const suggestions: CompilerSuggestion[] = [
|
|
621
|
+
{ type: "simplify", description: "Simplify workflow", impact: "low" },
|
|
622
|
+
];
|
|
623
|
+
|
|
624
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
625
|
+
|
|
626
|
+
expect(result).toEqual(spec);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("does not modify spec for merge-nodes suggestion (no-op for now)", () => {
|
|
630
|
+
const spec = createAdjacentToolSpec();
|
|
631
|
+
const suggestions: CompilerSuggestion[] = [
|
|
632
|
+
{ type: "merge-nodes", description: "Merge adjacent tool nodes", impact: "medium" },
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
636
|
+
|
|
637
|
+
expect(result.graph.nodes.length).toBe(spec.graph.nodes.length);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("adds retry to all tool and llm nodes, not just one", () => {
|
|
641
|
+
const spec: HarnessSpec = {
|
|
642
|
+
name: "multi-node",
|
|
643
|
+
graph: {
|
|
644
|
+
entryNodeId: "tool-a",
|
|
645
|
+
nodes: [
|
|
646
|
+
{ id: "tool-a", kind: "tool", tool: "echo", args: ["a"] },
|
|
647
|
+
{ id: "tool-b", kind: "tool", tool: "echo", args: ["b"] },
|
|
648
|
+
{ id: "llm-c", kind: "llm", provider: "anthropic", model: "claude-sonnet", prompt: "p" },
|
|
649
|
+
],
|
|
650
|
+
edges: [
|
|
651
|
+
{ from: "tool-a", to: "tool-b" },
|
|
652
|
+
{ from: "tool-b", to: "llm-c" },
|
|
653
|
+
],
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
const suggestions: CompilerSuggestion[] = [
|
|
657
|
+
{ type: "add-retry", description: "Add retry", impact: "high" },
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
const result = applyCompilerSuggestions(spec, suggestions);
|
|
661
|
+
|
|
662
|
+
expect(result.graph.nodes.find(n => n.id === "tool-a")?.retryPolicy).toBeDefined();
|
|
663
|
+
expect(result.graph.nodes.find(n => n.id === "tool-b")?.retryPolicy).toBeDefined();
|
|
664
|
+
expect(result.graph.nodes.find(n => n.id === "llm-c")?.retryPolicy).toBeDefined();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// ============================================================================
|
|
669
|
+
// Mutation-based feedback → mutateHarness integration
|
|
670
|
+
// ============================================================================
|
|
671
|
+
|
|
672
|
+
describe("feedback mutations → mutateHarness integration", () => {
|
|
673
|
+
it("retry_exhausted mutations can be applied via mutateHarness", async () => {
|
|
674
|
+
const { mutateHarness } = await import("../../src/mutation/engine.js");
|
|
675
|
+
const spec = createEmptySpec();
|
|
676
|
+
const compiled = compileHarnessSpec(spec);
|
|
677
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
678
|
+
|
|
679
|
+
const retryMutations = analysis.mutations.filter(m => m.trigger === "retry_exhausted");
|
|
680
|
+
expect(retryMutations.length).toBeGreaterThan(0);
|
|
681
|
+
|
|
682
|
+
const result = mutateHarness(spec, retryMutations);
|
|
683
|
+
const toolNode = result.spec.graph.nodes.find(n => n.id === "start");
|
|
684
|
+
expect(toolNode?.retryPolicy).toBeDefined();
|
|
685
|
+
expect(toolNode?.retryPolicy?.maxAttempts).toBe(3);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("verification_failed mutations can be applied via mutateHarness", async () => {
|
|
689
|
+
const { mutateHarness } = await import("../../src/mutation/engine.js");
|
|
690
|
+
const spec = createEmptySpec();
|
|
691
|
+
const compiled = compileHarnessSpec(spec);
|
|
692
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
693
|
+
|
|
694
|
+
const verificationMutations = analysis.mutations.filter(m => m.trigger === "verification_failed");
|
|
695
|
+
expect(verificationMutations.length).toBeGreaterThan(0);
|
|
696
|
+
|
|
697
|
+
const result = mutateHarness(spec, verificationMutations);
|
|
698
|
+
const toolNode = result.spec.graph.nodes.find(n => n.id === "start");
|
|
699
|
+
expect(toolNode?.verificationPolicy).toBeDefined();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("cost_high replace-node mutations can be applied via mutateHarness", async () => {
|
|
703
|
+
const { mutateHarness } = await import("../../src/mutation/engine.js");
|
|
704
|
+
const spec = createMultiLlmSpec(6);
|
|
705
|
+
const compiled = compileHarnessSpec(spec);
|
|
706
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
707
|
+
|
|
708
|
+
const costMutations = analysis.mutations.filter(m => m.trigger === "cost_high");
|
|
709
|
+
expect(costMutations.length).toBeGreaterThan(0);
|
|
710
|
+
|
|
711
|
+
const result = mutateHarness(spec, costMutations);
|
|
712
|
+
for (const node of result.spec.graph.nodes) {
|
|
713
|
+
if (node.kind === "llm") {
|
|
714
|
+
expect((node as any).model).toBe("gpt-4o-mini");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("all mutation types from feedback are valid HarnessMutation types", async () => {
|
|
720
|
+
const { mutateHarness } = await import("../../src/mutation/engine.js");
|
|
721
|
+
const spec = createComplexSpec();
|
|
722
|
+
const compiled = compileHarnessSpec(spec);
|
|
723
|
+
const analysis = analyzeCompiledWorkflow(compiled);
|
|
724
|
+
|
|
725
|
+
// Should not throw — all mutation types are valid
|
|
726
|
+
expect(() => mutateHarness(spec, analysis.mutations)).not.toThrow();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// ============================================================================
|
|
731
|
+
// Integration with Meta-Harness Tests
|
|
732
|
+
// ============================================================================
|
|
733
|
+
|
|
734
|
+
describe("Meta-Harness integration", () => {
|
|
735
|
+
it("includes compilerAnalysis in MetaHarnessResult", async () => {
|
|
736
|
+
const { DefaultMetaHarness } = await import("../../src/metaharness/engine.js");
|
|
737
|
+
const harness = new DefaultMetaHarness({});
|
|
738
|
+
|
|
739
|
+
const result = await harness.generateHarness("Run tests and verify the build passes");
|
|
740
|
+
|
|
741
|
+
expect(result.compilerAnalysis).toBeDefined();
|
|
742
|
+
expect(result.compilerAnalysis?.cost).toBeDefined();
|
|
743
|
+
expect(result.compilerAnalysis?.risk).toBeDefined();
|
|
744
|
+
expect(result.compilerAnalysis?.mutations).toBeDefined();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("applies high-risk mutations and recompiles", async () => {
|
|
748
|
+
const { DefaultMetaHarness } = await import("../../src/metaharness/engine.js");
|
|
749
|
+
const harness = new DefaultMetaHarness({});
|
|
750
|
+
|
|
751
|
+
const result = await harness.generateHarness("Run tests");
|
|
752
|
+
|
|
753
|
+
// After feedback loop, the spec should have retry policies applied
|
|
754
|
+
// if retry_exhausted mutations were found
|
|
755
|
+
if (result.compilerAnalysis?.mutations.some(m => m.trigger === "retry_exhausted")) {
|
|
756
|
+
const hasRetry = result.spec.graph.nodes.some(
|
|
757
|
+
n => n.kind === "tool" || n.kind === "llm"
|
|
758
|
+
) && result.spec.graph.nodes.some(
|
|
759
|
+
n => (n.kind === "tool" || n.kind === "llm") && n.retryPolicy
|
|
760
|
+
);
|
|
761
|
+
expect(hasRetry).toBe(true);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("includes compiler optimizations in result", async () => {
|
|
766
|
+
const { DefaultMetaHarness } = await import("../../src/metaharness/engine.js");
|
|
767
|
+
const harness = new DefaultMetaHarness({});
|
|
768
|
+
|
|
769
|
+
const result = await harness.generateHarness("Run tests");
|
|
770
|
+
|
|
771
|
+
expect(result.compilerOptimizations).toBeDefined();
|
|
772
|
+
expect(Array.isArray(result.compilerOptimizations)).toBe(true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("stores applied mutations in MetaHarnessResult", async () => {
|
|
776
|
+
const { DefaultMetaHarness } = await import("../../src/metaharness/engine.js");
|
|
777
|
+
const harness = new DefaultMetaHarness({});
|
|
778
|
+
|
|
779
|
+
const result = await harness.generateHarness("Run tests");
|
|
780
|
+
|
|
781
|
+
expect(result.appliedMutations).toBeDefined();
|
|
782
|
+
expect(Array.isArray(result.appliedMutations)).toBe(true);
|
|
783
|
+
});
|
|
784
|
+
});
|