@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,623 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("node:crypto", () => ({
|
|
4
|
+
randomUUID: () => "instance-123",
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../../src/reference/catalog.js", async (importOriginal) => {
|
|
8
|
+
const mod = await importOriginal<typeof import("../../src/reference/catalog.js")>();
|
|
9
|
+
return {
|
|
10
|
+
...mod,
|
|
11
|
+
buildReferenceHarnessSpec: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
vi.mock("../../src/compiler/compile.js", () => ({
|
|
16
|
+
compileHarnessSpec: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../../src/planner/synthesize.js", () => ({
|
|
20
|
+
planWorkflowRequest: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("../../src/replanner/synthesize.js", () => ({
|
|
24
|
+
parseReplanRequest: vi.fn(),
|
|
25
|
+
replanWorkflowRequest: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { compileHarnessSpec } from "../../src/compiler/compile.js";
|
|
29
|
+
import { planWorkflowRequest } from "../../src/planner/synthesize.js";
|
|
30
|
+
import { parseReplanRequest, replanWorkflowRequest } from "../../src/replanner/synthesize.js";
|
|
31
|
+
import { buildReferenceHarnessSpec } from "../../src/reference/catalog.js";
|
|
32
|
+
import { createLassoCommands, clearCompiledHarnesses } from "../../src/pi/commands.js";
|
|
33
|
+
|
|
34
|
+
describe("Lasso pi commands", () => {
|
|
35
|
+
const prBundle = {
|
|
36
|
+
repoPath: "/tmp/repo",
|
|
37
|
+
sourceBranch: "feature/pr-change",
|
|
38
|
+
targetBranch: "main",
|
|
39
|
+
reviewInstructions: "Review carefully.",
|
|
40
|
+
verificationCommands: ['node -e "process.exit(0)"'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const patchRequest = {
|
|
44
|
+
workflow: "patch-validation",
|
|
45
|
+
input: {
|
|
46
|
+
repoPath: "/tmp/repo",
|
|
47
|
+
baselineRef: "HEAD",
|
|
48
|
+
candidateSource: { kind: "branch", value: "fix/bug-123" },
|
|
49
|
+
reproduceCommands: ["npm test -- failing.spec.ts"],
|
|
50
|
+
verificationCommands: ["npm test"],
|
|
51
|
+
reviewInstructions: "Approve only if the bug reproduces on baseline and all checks pass on the candidate.",
|
|
52
|
+
approvalRequired: true,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const replanInput = {
|
|
57
|
+
workflow: "patch-validation" as const,
|
|
58
|
+
originalRequest: patchRequest,
|
|
59
|
+
observedOutcome: {
|
|
60
|
+
terminalNodeId: "validated-fix" as const,
|
|
61
|
+
notes: ["prod hotfix"],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const prSpec = { name: "pr-review-merge", graph: { entryNodeId: "start", nodes: [], edges: [] } };
|
|
66
|
+
const patchSpec = { name: "patch-validation", graph: { entryNodeId: "run-baseline", nodes: [], edges: [] } };
|
|
67
|
+
const customSpec = {
|
|
68
|
+
name: "custom-echo",
|
|
69
|
+
graph: {
|
|
70
|
+
entryNodeId: "echo",
|
|
71
|
+
nodes: [
|
|
72
|
+
{
|
|
73
|
+
id: "echo",
|
|
74
|
+
kind: "tool" as const,
|
|
75
|
+
tool: "bash",
|
|
76
|
+
args: ["-lc", "echo hello"],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
edges: [],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const prCompiled = {
|
|
84
|
+
name: "pr-review-merge",
|
|
85
|
+
spec: prSpec,
|
|
86
|
+
cir: { name: "pr-review-merge", entryNodeId: "start", nodes: [], transitions: [] },
|
|
87
|
+
workflows: [],
|
|
88
|
+
register: vi.fn(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const patchCompiled = {
|
|
92
|
+
name: "patch-validation",
|
|
93
|
+
spec: patchSpec,
|
|
94
|
+
cir: { name: "patch-validation", entryNodeId: "run-baseline", nodes: [], transitions: [] },
|
|
95
|
+
workflows: [],
|
|
96
|
+
register: vi.fn(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const customCompiled = {
|
|
100
|
+
name: "custom-echo",
|
|
101
|
+
spec: customSpec,
|
|
102
|
+
cir: { name: "custom-echo", entryNodeId: "echo", nodes: [], transitions: [] },
|
|
103
|
+
workflows: [],
|
|
104
|
+
register: vi.fn(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
clearCompiledHarnesses();
|
|
109
|
+
vi.mocked(buildReferenceHarnessSpec).mockReset();
|
|
110
|
+
vi.mocked(compileHarnessSpec).mockReset();
|
|
111
|
+
vi.mocked(planWorkflowRequest).mockReset();
|
|
112
|
+
vi.mocked(parseReplanRequest).mockReset();
|
|
113
|
+
vi.mocked(replanWorkflowRequest).mockReset();
|
|
114
|
+
vi.mocked(buildReferenceHarnessSpec).mockReturnValue(prSpec as any);
|
|
115
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(prCompiled as any);
|
|
116
|
+
prCompiled.register.mockReset();
|
|
117
|
+
patchCompiled.register.mockReset();
|
|
118
|
+
customCompiled.register.mockReset();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("creates compile, run, inspect, plan, and replan commands", () => {
|
|
122
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
123
|
+
|
|
124
|
+
expect(commands.map(command => command.name)).toEqual([
|
|
125
|
+
"lasso:compile",
|
|
126
|
+
"lasso:run",
|
|
127
|
+
"lasso:inspect",
|
|
128
|
+
"lasso:plan",
|
|
129
|
+
"lasso:replan",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("compile command routes legacy raw LocalPrBundle to the pr-review-merge builder", async () => {
|
|
134
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
135
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
136
|
+
const ctx = createCommandContext();
|
|
137
|
+
|
|
138
|
+
await compileCommand?.handler(JSON.stringify(prBundle), ctx as any);
|
|
139
|
+
|
|
140
|
+
expect(buildReferenceHarnessSpec).toHaveBeenCalledWith({ workflow: "pr-review-merge", input: prBundle });
|
|
141
|
+
expect(compileHarnessSpec).toHaveBeenCalledWith(prSpec);
|
|
142
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Compiled `pr-review-merge`"), "info");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("compile command routes explicit patch-validation envelope to the patch-validation builder", async () => {
|
|
146
|
+
vi.mocked(buildReferenceHarnessSpec).mockReturnValue(patchSpec as any);
|
|
147
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(patchCompiled as any);
|
|
148
|
+
|
|
149
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
150
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
151
|
+
const ctx = createCommandContext();
|
|
152
|
+
|
|
153
|
+
await compileCommand?.handler(JSON.stringify(patchRequest), ctx as any);
|
|
154
|
+
|
|
155
|
+
expect(buildReferenceHarnessSpec).toHaveBeenCalledWith(patchRequest);
|
|
156
|
+
expect(compileHarnessSpec).toHaveBeenCalledWith(patchSpec);
|
|
157
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Compiled `patch-validation`"), "info");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("compile command compiles raw HarnessSpec JSON without going through the reference builder", async () => {
|
|
161
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(customCompiled as any);
|
|
162
|
+
|
|
163
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
164
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
165
|
+
const ctx = createCommandContext();
|
|
166
|
+
|
|
167
|
+
await compileCommand?.handler(JSON.stringify(customSpec), ctx as any);
|
|
168
|
+
|
|
169
|
+
expect(buildReferenceHarnessSpec).not.toHaveBeenCalled();
|
|
170
|
+
expect(compileHarnessSpec).toHaveBeenCalledWith(customSpec);
|
|
171
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Compiled `custom-echo`"), "info");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("run command compiles, registers, and starts the pr-review-merge workflow from a legacy bundle", async () => {
|
|
175
|
+
const registry = createMockRegistry();
|
|
176
|
+
const commands = createLassoCommands(registry as any);
|
|
177
|
+
const runCommand = commands.find(command => command.name === "lasso:run");
|
|
178
|
+
const ctx = createCommandContext();
|
|
179
|
+
|
|
180
|
+
await runCommand?.handler(JSON.stringify(prBundle), ctx as any);
|
|
181
|
+
|
|
182
|
+
expect(buildReferenceHarnessSpec).toHaveBeenCalledWith({ workflow: "pr-review-merge", input: prBundle });
|
|
183
|
+
expect(compileHarnessSpec).toHaveBeenCalledWith(prSpec);
|
|
184
|
+
expect(prCompiled.register).toHaveBeenCalledWith();
|
|
185
|
+
|
|
186
|
+
const startOrchestrationCall = vi.mocked(registry.client.startOrchestration).mock.calls[0];
|
|
187
|
+
expect(startOrchestrationCall[0]).toBe("instance-123");
|
|
188
|
+
expect(startOrchestrationCall[1]).toBe("pr-review-merge");
|
|
189
|
+
|
|
190
|
+
const adaptiveInput = startOrchestrationCall[2];
|
|
191
|
+
expect(adaptiveInput).toHaveProperty("input");
|
|
192
|
+
expect(adaptiveInput).toHaveProperty("__lassoAdaptiveRuntime");
|
|
193
|
+
expect(adaptiveInput.input).toEqual({});
|
|
194
|
+
expect(adaptiveInput.__lassoAdaptiveRuntime.currentRequest).toEqual({ workflow: "pr-review-merge", input: prBundle });
|
|
195
|
+
|
|
196
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Started `pr-review-merge`"), "info");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("run command compiles, registers, and starts a patch-validation workflow", async () => {
|
|
200
|
+
vi.mocked(buildReferenceHarnessSpec).mockReturnValue(patchSpec as any);
|
|
201
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(patchCompiled as any);
|
|
202
|
+
|
|
203
|
+
const registry = createMockRegistry();
|
|
204
|
+
const commands = createLassoCommands(registry as any);
|
|
205
|
+
const runCommand = commands.find(command => command.name === "lasso:run");
|
|
206
|
+
const ctx = createCommandContext();
|
|
207
|
+
|
|
208
|
+
await runCommand?.handler(JSON.stringify(patchRequest), ctx as any);
|
|
209
|
+
|
|
210
|
+
expect(buildReferenceHarnessSpec).toHaveBeenCalledWith(patchRequest);
|
|
211
|
+
expect(patchCompiled.register).toHaveBeenCalledWith();
|
|
212
|
+
|
|
213
|
+
const startOrchestrationCall = vi.mocked(registry.client.startOrchestration).mock.calls[0];
|
|
214
|
+
expect(startOrchestrationCall[0]).toBe("instance-123");
|
|
215
|
+
expect(startOrchestrationCall[1]).toBe("patch-validation");
|
|
216
|
+
|
|
217
|
+
const adaptiveInput = startOrchestrationCall[2];
|
|
218
|
+
expect(adaptiveInput).toHaveProperty("input");
|
|
219
|
+
expect(adaptiveInput).toHaveProperty("__lassoAdaptiveRuntime");
|
|
220
|
+
expect(adaptiveInput.input).toEqual({});
|
|
221
|
+
expect(adaptiveInput.__lassoAdaptiveRuntime.currentRequest).toEqual(patchRequest);
|
|
222
|
+
|
|
223
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Started `patch-validation`"), "info");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("run command starts a custom HarnessSpec with explicit runtime input", async () => {
|
|
227
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(customCompiled as any);
|
|
228
|
+
|
|
229
|
+
const registry = createMockRegistry();
|
|
230
|
+
const commands = createLassoCommands(registry as any);
|
|
231
|
+
const runCommand = commands.find(command => command.name === "lasso:run");
|
|
232
|
+
const ctx = createCommandContext();
|
|
233
|
+
const request = {
|
|
234
|
+
spec: customSpec,
|
|
235
|
+
input: { memberId: "12345", dryRun: true },
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
await runCommand?.handler(JSON.stringify(request), ctx as any);
|
|
239
|
+
|
|
240
|
+
expect(buildReferenceHarnessSpec).not.toHaveBeenCalled();
|
|
241
|
+
expect(compileHarnessSpec).toHaveBeenCalledWith(customSpec);
|
|
242
|
+
expect(customCompiled.register).toHaveBeenCalledWith();
|
|
243
|
+
expect(registry.client.startOrchestration).toHaveBeenCalledWith("instance-123", "custom-echo", request.input);
|
|
244
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Started `custom-echo`"), "info");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("run command defaults raw custom HarnessSpec input to an empty object", async () => {
|
|
248
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(customCompiled as any);
|
|
249
|
+
|
|
250
|
+
const registry = createMockRegistry();
|
|
251
|
+
const commands = createLassoCommands(registry as any);
|
|
252
|
+
const runCommand = commands.find(command => command.name === "lasso:run");
|
|
253
|
+
const ctx = createCommandContext();
|
|
254
|
+
|
|
255
|
+
await runCommand?.handler(JSON.stringify(customSpec), ctx as any);
|
|
256
|
+
|
|
257
|
+
expect(registry.client.startOrchestration).toHaveBeenCalledWith("instance-123", "custom-echo", {});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("inspect command prints the spec, cir, and workflow state", async () => {
|
|
261
|
+
const registry = createMockRegistry();
|
|
262
|
+
const commands = createLassoCommands(registry as any);
|
|
263
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
264
|
+
const inspectCommand = commands.find(command => command.name === "lasso:inspect");
|
|
265
|
+
const ctx = createCommandContext();
|
|
266
|
+
|
|
267
|
+
await compileCommand?.handler(JSON.stringify(prBundle), ctx as any);
|
|
268
|
+
await inspectCommand?.handler("pr-review-merge", ctx as any);
|
|
269
|
+
|
|
270
|
+
expect(registry.client.listAllInstances).toHaveBeenCalled();
|
|
271
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
272
|
+
expect.stringContaining("### Lasso Workflow `pr-review-merge`"),
|
|
273
|
+
"info",
|
|
274
|
+
);
|
|
275
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
276
|
+
expect.stringContaining('"name": "pr-review-merge"'),
|
|
277
|
+
"info",
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("inspect command prints a custom HarnessSpec after compiling it directly", async () => {
|
|
282
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(customCompiled as any);
|
|
283
|
+
|
|
284
|
+
const registry = createMockRegistry();
|
|
285
|
+
const commands = createLassoCommands(registry as any);
|
|
286
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
287
|
+
const inspectCommand = commands.find(command => command.name === "lasso:inspect");
|
|
288
|
+
const ctx = createCommandContext();
|
|
289
|
+
|
|
290
|
+
await compileCommand?.handler(JSON.stringify(customSpec), ctx as any);
|
|
291
|
+
await inspectCommand?.handler("custom-echo", ctx as any);
|
|
292
|
+
|
|
293
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
294
|
+
expect.stringContaining("### Lasso Workflow `custom-echo`"),
|
|
295
|
+
"info",
|
|
296
|
+
);
|
|
297
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
298
|
+
expect.stringContaining('"name": "custom-echo"'),
|
|
299
|
+
"info",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("inspect command displays adaptive lineage when available", async () => {
|
|
304
|
+
const adaptiveCompiled = {
|
|
305
|
+
...prCompiled,
|
|
306
|
+
adaptive: {
|
|
307
|
+
currentVersion: {
|
|
308
|
+
version: 3,
|
|
309
|
+
parentVersion: 2,
|
|
310
|
+
reason: "risk-escalation: verification failures detected",
|
|
311
|
+
spec: prSpec,
|
|
312
|
+
generatedAt: 1700000002000,
|
|
313
|
+
},
|
|
314
|
+
lineage: [
|
|
315
|
+
{
|
|
316
|
+
version: 1,
|
|
317
|
+
terminalNodeId: "merged",
|
|
318
|
+
outputs: {},
|
|
319
|
+
nodeResults: {},
|
|
320
|
+
failures: [],
|
|
321
|
+
metrics: { retries: 0, durationMs: 1500 },
|
|
322
|
+
trace: [],
|
|
323
|
+
completedAt: 1700000000000,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
version: 2,
|
|
327
|
+
terminalNodeId: "failed-verification",
|
|
328
|
+
outputs: {},
|
|
329
|
+
nodeResults: {},
|
|
330
|
+
failures: [
|
|
331
|
+
{
|
|
332
|
+
domainType: "lasso",
|
|
333
|
+
rootCause: "verification_failed",
|
|
334
|
+
nodeId: "run-checks",
|
|
335
|
+
message: "Tests failed on candidate branch",
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
metrics: { retries: 2, durationMs: 4500 },
|
|
339
|
+
trace: [],
|
|
340
|
+
completedAt: 1700000001000,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
vi.mocked(compileHarnessSpec).mockReturnValue(adaptiveCompiled as any);
|
|
347
|
+
|
|
348
|
+
const registry = createMockRegistry();
|
|
349
|
+
const commands = createLassoCommands(registry as any);
|
|
350
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
351
|
+
const inspectCommand = commands.find(command => command.name === "lasso:inspect");
|
|
352
|
+
const ctx = createCommandContext();
|
|
353
|
+
|
|
354
|
+
await compileCommand?.handler(JSON.stringify(prBundle), ctx as any);
|
|
355
|
+
await inspectCommand?.handler("pr-review-merge", ctx as any);
|
|
356
|
+
|
|
357
|
+
const notifyCalls = vi.mocked(ctx.ui.notify).mock.calls;
|
|
358
|
+
const output = notifyCalls[notifyCalls.length - 1][0] as string;
|
|
359
|
+
|
|
360
|
+
expect(output).toContain("#### Adaptive Lineage");
|
|
361
|
+
expect(output).toContain("Version: 3");
|
|
362
|
+
expect(output).toContain("Parent: 2");
|
|
363
|
+
expect(output).toContain("Reason: risk-escalation: verification failures detected");
|
|
364
|
+
expect(output).toContain("| # | Outcome | Duration | Failures | Needs Input |");
|
|
365
|
+
expect(output).toContain("| 1 | merged | 1.5s | 0 | no |");
|
|
366
|
+
expect(output).toContain("| 2 | failed-verification | 4.5s | 1 | no |");
|
|
367
|
+
expect(output).toContain("Status: evolving (version 3 of 5)");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("compile command reports malformed JSON cleanly", async () => {
|
|
371
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
372
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
373
|
+
const ctx = createCommandContext();
|
|
374
|
+
|
|
375
|
+
await compileCommand?.handler("{not-json", ctx as any);
|
|
376
|
+
|
|
377
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
378
|
+
"Invalid compile input. Expected workflow request JSON, HarnessSpec JSON, {spec|specPath,input?}, or an absolute spec path.",
|
|
379
|
+
"error",
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("compile command reports malformed patch-validation envelope cleanly", async () => {
|
|
384
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
385
|
+
const compileCommand = commands.find(command => command.name === "lasso:compile");
|
|
386
|
+
const ctx = createCommandContext();
|
|
387
|
+
|
|
388
|
+
await compileCommand?.handler(JSON.stringify({ workflow: "patch-validation", input: {} }), ctx as any);
|
|
389
|
+
|
|
390
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Invalid patch-validation input", "error");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("plan command renders a draft request without compiling or running anything", async () => {
|
|
394
|
+
vi.mocked(planWorkflowRequest).mockReturnValue({
|
|
395
|
+
status: "draft_request",
|
|
396
|
+
workflow: "patch-validation",
|
|
397
|
+
request: patchRequest as any,
|
|
398
|
+
rationale: ["Classified as patch-validation workflow", "Baseline: HEAD"],
|
|
399
|
+
warnings: ["approvalRequired not specified; defaulting to false"],
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
403
|
+
const planCommand = commands.find(command => command.name === "lasso:plan");
|
|
404
|
+
const ctx = createCommandContext();
|
|
405
|
+
const brief = "Validate the fix in /tmp/repo against HEAD";
|
|
406
|
+
|
|
407
|
+
await planCommand?.handler(brief, ctx as any);
|
|
408
|
+
|
|
409
|
+
expect(planWorkflowRequest).toHaveBeenCalledWith(brief);
|
|
410
|
+
expect(buildReferenceHarnessSpec).not.toHaveBeenCalled();
|
|
411
|
+
expect(compileHarnessSpec).not.toHaveBeenCalled();
|
|
412
|
+
|
|
413
|
+
const [message, level] = ctx.ui.notify.mock.calls[0];
|
|
414
|
+
expect(level).toBe("info");
|
|
415
|
+
expect(message).toContain("### Planner Draft `patch-validation`");
|
|
416
|
+
expect(message).toContain("#### Rationale");
|
|
417
|
+
expect(message).toContain("#### Warnings");
|
|
418
|
+
expect(message).toContain("```json");
|
|
419
|
+
expect(message).toContain('"workflow": "patch-validation"');
|
|
420
|
+
expect(message).toContain("/lasso:compile");
|
|
421
|
+
expect(message).toContain("/lasso:run");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("plan command renders clarification output without emitting partial JSON", async () => {
|
|
425
|
+
vi.mocked(planWorkflowRequest).mockReturnValue({
|
|
426
|
+
status: "needs_clarification",
|
|
427
|
+
candidateWorkflow: "pr-review-merge",
|
|
428
|
+
reasons: ["PR review/merge workflow requires: repoPath, verificationCommands"],
|
|
429
|
+
missingFields: ["repoPath", "verificationCommands"],
|
|
430
|
+
guidance: ["Provide repoPath", "Provide verificationCommands"],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
434
|
+
const planCommand = commands.find(command => command.name === "lasso:plan");
|
|
435
|
+
const ctx = createCommandContext();
|
|
436
|
+
|
|
437
|
+
await planCommand?.handler("Review and merge this branch", ctx as any);
|
|
438
|
+
|
|
439
|
+
expect(planWorkflowRequest).toHaveBeenCalledWith("Review and merge this branch");
|
|
440
|
+
expect(buildReferenceHarnessSpec).not.toHaveBeenCalled();
|
|
441
|
+
expect(compileHarnessSpec).not.toHaveBeenCalled();
|
|
442
|
+
|
|
443
|
+
const [message, level] = ctx.ui.notify.mock.calls[0];
|
|
444
|
+
expect(level).toBe("info");
|
|
445
|
+
expect(message).toContain("### Planner Needs Clarification");
|
|
446
|
+
expect(message).toContain("Likely workflow: `pr-review-merge`");
|
|
447
|
+
expect(message).toContain("#### Missing Fields");
|
|
448
|
+
expect(message).toContain("repoPath");
|
|
449
|
+
expect(message).toContain("verificationCommands");
|
|
450
|
+
expect(message).not.toContain("```json");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("plan command reports usage for an empty brief", async () => {
|
|
454
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
455
|
+
const planCommand = commands.find(command => command.name === "lasso:plan");
|
|
456
|
+
const ctx = createCommandContext();
|
|
457
|
+
|
|
458
|
+
await planCommand?.handler(" ", ctx as any);
|
|
459
|
+
|
|
460
|
+
expect(planWorkflowRequest).not.toHaveBeenCalled();
|
|
461
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /lasso:plan <freeform brief>", "error");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("plan command reports unexpected planner failures cleanly", async () => {
|
|
465
|
+
vi.mocked(planWorkflowRequest).mockImplementation(() => {
|
|
466
|
+
throw new Error("Internal planner error");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
470
|
+
const planCommand = commands.find(command => command.name === "lasso:plan");
|
|
471
|
+
const ctx = createCommandContext();
|
|
472
|
+
|
|
473
|
+
await planCommand?.handler("Review and merge this branch", ctx as any);
|
|
474
|
+
|
|
475
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Internal planner error", "error");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("replan command renders a revised draft request without compiling or running anything", async () => {
|
|
479
|
+
vi.mocked(parseReplanRequest).mockReturnValue(replanInput);
|
|
480
|
+
vi.mocked(replanWorkflowRequest).mockReturnValue({
|
|
481
|
+
status: "draft_request",
|
|
482
|
+
workflow: "patch-validation",
|
|
483
|
+
request: {
|
|
484
|
+
workflow: "patch-validation",
|
|
485
|
+
input: {
|
|
486
|
+
...patchRequest.input,
|
|
487
|
+
approvalRequired: true,
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
trigger: "risk-escalation",
|
|
491
|
+
riskLevel: "high",
|
|
492
|
+
rationale: ["Patch-file candidates are treated as high risk for adaptive replanning."],
|
|
493
|
+
warnings: [],
|
|
494
|
+
changes: ["approvalRequired: false -> true"],
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
498
|
+
const replanCommand = commands.find(command => command.name === "lasso:replan");
|
|
499
|
+
const ctx = createCommandContext();
|
|
500
|
+
const requestJson = JSON.stringify(replanInput);
|
|
501
|
+
|
|
502
|
+
await replanCommand?.handler(requestJson, ctx as any);
|
|
503
|
+
|
|
504
|
+
expect(parseReplanRequest).toHaveBeenCalledWith(requestJson);
|
|
505
|
+
expect(replanWorkflowRequest).toHaveBeenCalledWith(replanInput);
|
|
506
|
+
expect(buildReferenceHarnessSpec).not.toHaveBeenCalled();
|
|
507
|
+
expect(compileHarnessSpec).not.toHaveBeenCalled();
|
|
508
|
+
|
|
509
|
+
const [message, level] = ctx.ui.notify.mock.calls[0];
|
|
510
|
+
expect(level).toBe("info");
|
|
511
|
+
expect(message).toContain("### Replan Draft `patch-validation`");
|
|
512
|
+
expect(message).toContain("Trigger: `risk-escalation`");
|
|
513
|
+
expect(message).toContain("Risk level: `high`");
|
|
514
|
+
expect(message).toContain("#### Changes");
|
|
515
|
+
expect(message).toContain("approvalRequired: false -> true");
|
|
516
|
+
expect(message).toContain("```json");
|
|
517
|
+
expect(message).toContain('"workflow": "patch-validation"');
|
|
518
|
+
expect(message).toContain("/lasso:compile");
|
|
519
|
+
expect(message).toContain("/lasso:run");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("replan command renders operator-input guidance without emitting partial JSON", async () => {
|
|
523
|
+
vi.mocked(parseReplanRequest).mockReturnValue(replanInput);
|
|
524
|
+
vi.mocked(replanWorkflowRequest).mockReturnValue({
|
|
525
|
+
status: "needs_operator_input",
|
|
526
|
+
candidateWorkflow: "pr-review-merge",
|
|
527
|
+
riskLevel: "medium",
|
|
528
|
+
reasons: ["Verification failed before merge."],
|
|
529
|
+
missingFields: ["sourceBranch", "verificationCommands"],
|
|
530
|
+
guidance: ["Update the branch", "Review the verification commands"],
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
534
|
+
const replanCommand = commands.find(command => command.name === "lasso:replan");
|
|
535
|
+
const ctx = createCommandContext();
|
|
536
|
+
|
|
537
|
+
await replanCommand?.handler(JSON.stringify(replanInput), ctx as any);
|
|
538
|
+
|
|
539
|
+
const [message, level] = ctx.ui.notify.mock.calls[0];
|
|
540
|
+
expect(level).toBe("info");
|
|
541
|
+
expect(message).toContain("### Replan Needs Operator Input");
|
|
542
|
+
expect(message).toContain("Likely workflow: `pr-review-merge`");
|
|
543
|
+
expect(message).toContain("Risk level: `medium`");
|
|
544
|
+
expect(message).toContain("#### Missing Fields");
|
|
545
|
+
expect(message).not.toContain("```json");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("replan command renders stop output without emitting partial JSON", async () => {
|
|
549
|
+
vi.mocked(parseReplanRequest).mockReturnValue(replanInput);
|
|
550
|
+
vi.mocked(replanWorkflowRequest).mockReturnValue({
|
|
551
|
+
status: "stop",
|
|
552
|
+
workflow: "patch-validation",
|
|
553
|
+
riskLevel: "high",
|
|
554
|
+
reasons: ["The previous attempt was rejected by a human reviewer."],
|
|
555
|
+
guidance: ["Review the candidate manually before retrying."],
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
559
|
+
const replanCommand = commands.find(command => command.name === "lasso:replan");
|
|
560
|
+
const ctx = createCommandContext();
|
|
561
|
+
|
|
562
|
+
await replanCommand?.handler(JSON.stringify(replanInput), ctx as any);
|
|
563
|
+
|
|
564
|
+
const [message, level] = ctx.ui.notify.mock.calls[0];
|
|
565
|
+
expect(level).toBe("info");
|
|
566
|
+
expect(message).toContain("### Replan Stop");
|
|
567
|
+
expect(message).toContain("Workflow: `patch-validation`");
|
|
568
|
+
expect(message).toContain("Risk level: `high`");
|
|
569
|
+
expect(message).not.toContain("```json");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("replan command reports usage for an empty request", async () => {
|
|
573
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
574
|
+
const replanCommand = commands.find(command => command.name === "lasso:replan");
|
|
575
|
+
const ctx = createCommandContext();
|
|
576
|
+
|
|
577
|
+
await replanCommand?.handler(" ", ctx as any);
|
|
578
|
+
|
|
579
|
+
expect(parseReplanRequest).not.toHaveBeenCalled();
|
|
580
|
+
expect(replanWorkflowRequest).not.toHaveBeenCalled();
|
|
581
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /lasso:replan <replan request JSON>", "error");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("replan command reports malformed replan JSON cleanly", async () => {
|
|
585
|
+
vi.mocked(parseReplanRequest).mockImplementation(() => {
|
|
586
|
+
throw new Error("Invalid replan request JSON");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const commands = createLassoCommands(createMockRegistry() as any);
|
|
590
|
+
const replanCommand = commands.find(command => command.name === "lasso:replan");
|
|
591
|
+
const ctx = createCommandContext();
|
|
592
|
+
|
|
593
|
+
await replanCommand?.handler("{not-json", ctx as any);
|
|
594
|
+
|
|
595
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Invalid replan request JSON", "error");
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
function createMockRegistry() {
|
|
600
|
+
const client = {
|
|
601
|
+
startOrchestration: vi.fn().mockResolvedValue(undefined),
|
|
602
|
+
listAllInstances: vi.fn().mockResolvedValue([
|
|
603
|
+
{ instanceId: "instance-123", name: "pr-review-merge", status: "Running" },
|
|
604
|
+
]),
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
client,
|
|
609
|
+
getRuntime: () => ({
|
|
610
|
+
getClient: () => client,
|
|
611
|
+
isRunning: () => true,
|
|
612
|
+
}),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function createCommandContext() {
|
|
617
|
+
return {
|
|
618
|
+
pi: {},
|
|
619
|
+
ui: {
|
|
620
|
+
notify: vi.fn(),
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { classifyTemplate } from "../../src/planner/template-rules.js";
|
|
3
|
+
|
|
4
|
+
describe("classifyTemplate", () => {
|
|
5
|
+
describe("existing behavior", () => {
|
|
6
|
+
it("should return pr-review-merge for PR-related briefs", () => {
|
|
7
|
+
expect(classifyTemplate("Review the pull request")).toBe("pr-review-merge");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should return patch-validation for patch-related briefs", () => {
|
|
11
|
+
expect(classifyTemplate("Validate the patch against baseline")).toBe("patch-validation");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should return ambiguous for mixed signals", () => {
|
|
15
|
+
expect(classifyTemplate("Review this PR and validate the patch baseline")).toBe("ambiguous");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("custom workflow families", () => {
|
|
20
|
+
it("should return 'custom' for unrecognized briefs with a workflow hint", () => {
|
|
21
|
+
expect(classifyTemplate("Deploy to staging environment")).toBe("custom");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return 'custom' for completely generic briefs", () => {
|
|
25
|
+
expect(classifyTemplate("Run the data pipeline on the new dataset")).toBe("custom");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return 'custom' for briefs with no workflow indicators", () => {
|
|
29
|
+
expect(classifyTemplate("Do some stuff with the codebase")).toBe("custom");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|