@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,300 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
suggestRecovery,
|
|
4
|
+
type RecoveryPlan,
|
|
5
|
+
type FailureSignature,
|
|
6
|
+
type FailureContext,
|
|
7
|
+
} from "../../src/failures/recovery.js";
|
|
8
|
+
|
|
9
|
+
function makeSignature(
|
|
10
|
+
cls: FailureSignature["class"],
|
|
11
|
+
overrides?: Partial<FailureSignature>,
|
|
12
|
+
): FailureSignature {
|
|
13
|
+
return {
|
|
14
|
+
class: cls,
|
|
15
|
+
confidence: 0.9,
|
|
16
|
+
evidence: ["test evidence"],
|
|
17
|
+
suggestedRecovery: [],
|
|
18
|
+
retryable: true,
|
|
19
|
+
requiresHumanIntervention: false,
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Recovery plans", () => {
|
|
25
|
+
describe("suggestRecovery", () => {
|
|
26
|
+
describe("auth failure recovery", () => {
|
|
27
|
+
it("should generate recovery plan for auth failures", () => {
|
|
28
|
+
const signature = makeSignature("auth", {
|
|
29
|
+
retryable: false,
|
|
30
|
+
requiresHumanIntervention: true,
|
|
31
|
+
});
|
|
32
|
+
const plan = suggestRecovery(signature);
|
|
33
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
34
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should include credential refresh step", () => {
|
|
38
|
+
const signature = makeSignature("auth", {
|
|
39
|
+
evidence: ["token expired"],
|
|
40
|
+
});
|
|
41
|
+
const plan = suggestRecovery(signature);
|
|
42
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
43
|
+
expect(stepText).toMatch(/refresh|renew|credential|token|auth/i);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should have low estimated success rate for auth failures", () => {
|
|
47
|
+
const signature = makeSignature("auth", {
|
|
48
|
+
requiresHumanIntervention: true,
|
|
49
|
+
});
|
|
50
|
+
const plan = suggestRecovery(signature);
|
|
51
|
+
expect(plan.estimatedSuccessRate).toBeLessThan(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("tool failure recovery", () => {
|
|
56
|
+
it("should generate recovery plan for tool failures", () => {
|
|
57
|
+
const signature = makeSignature("tool", {
|
|
58
|
+
retryable: false,
|
|
59
|
+
});
|
|
60
|
+
const plan = suggestRecovery(signature);
|
|
61
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should include tool installation step for command not found", () => {
|
|
65
|
+
const signature = makeSignature("tool", {
|
|
66
|
+
evidence: ["command not found"],
|
|
67
|
+
});
|
|
68
|
+
const plan = suggestRecovery(signature);
|
|
69
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
70
|
+
expect(stepText).toMatch(/install|availability|check/i);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should not require human approval for tool failures", () => {
|
|
74
|
+
const signature = makeSignature("tool");
|
|
75
|
+
const plan = suggestRecovery(signature);
|
|
76
|
+
expect(plan.requiresHumanApproval).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("resource failure recovery", () => {
|
|
81
|
+
it("should generate recovery plan for resource failures", () => {
|
|
82
|
+
const signature = makeSignature("resource", {
|
|
83
|
+
retryable: false,
|
|
84
|
+
});
|
|
85
|
+
const plan = suggestRecovery(signature);
|
|
86
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should include cleanup step for disk full", () => {
|
|
90
|
+
const signature = makeSignature("resource", {
|
|
91
|
+
evidence: ["disk full"],
|
|
92
|
+
});
|
|
93
|
+
const plan = suggestRecovery(signature);
|
|
94
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
95
|
+
expect(stepText).toMatch(/cleanup|free|space|disk/i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should require human approval for OOM", () => {
|
|
99
|
+
const signature = makeSignature("resource", {
|
|
100
|
+
evidence: ["out of memory"],
|
|
101
|
+
});
|
|
102
|
+
const plan = suggestRecovery(signature);
|
|
103
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("semantic failure recovery", () => {
|
|
108
|
+
it("should generate recovery plan for semantic failures", () => {
|
|
109
|
+
const signature = makeSignature("semantic", {
|
|
110
|
+
retryable: false,
|
|
111
|
+
});
|
|
112
|
+
const plan = suggestRecovery(signature);
|
|
113
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should include validation step", () => {
|
|
117
|
+
const signature = makeSignature("semantic", {
|
|
118
|
+
evidence: ["schema mismatch"],
|
|
119
|
+
});
|
|
120
|
+
const plan = suggestRecovery(signature);
|
|
121
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
122
|
+
expect(stepText).toMatch(/validat|schema|output|format/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should require human approval for semantic failures", () => {
|
|
126
|
+
const signature = makeSignature("semantic");
|
|
127
|
+
const plan = suggestRecovery(signature);
|
|
128
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("human failure recovery", () => {
|
|
133
|
+
it("should generate recovery plan for human failures", () => {
|
|
134
|
+
const signature = makeSignature("human", {
|
|
135
|
+
requiresHumanIntervention: true,
|
|
136
|
+
});
|
|
137
|
+
const plan = suggestRecovery(signature);
|
|
138
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
139
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should include escalation step for rejected", () => {
|
|
143
|
+
const signature = makeSignature("human", {
|
|
144
|
+
evidence: ["rejected"],
|
|
145
|
+
});
|
|
146
|
+
const plan = suggestRecovery(signature);
|
|
147
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
148
|
+
expect(stepText).toMatch(/escalat|review|reconsider|human/i);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("environment-drift failure recovery", () => {
|
|
153
|
+
it("should generate recovery plan for environment-drift failures", () => {
|
|
154
|
+
const signature = makeSignature("environment-drift", {
|
|
155
|
+
retryable: false,
|
|
156
|
+
});
|
|
157
|
+
const plan = suggestRecovery(signature);
|
|
158
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should include environment sync step", () => {
|
|
162
|
+
const signature = makeSignature("environment-drift", {
|
|
163
|
+
evidence: ["version mismatch"],
|
|
164
|
+
});
|
|
165
|
+
const plan = suggestRecovery(signature);
|
|
166
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
167
|
+
expect(stepText).toMatch(/sync|install|update|dependenc|version/i);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should require human approval for environment-drift", () => {
|
|
171
|
+
const signature = makeSignature("environment-drift");
|
|
172
|
+
const plan = suggestRecovery(signature);
|
|
173
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("network failure recovery", () => {
|
|
178
|
+
it("should generate recovery plan for network failures", () => {
|
|
179
|
+
const signature = makeSignature("network", {
|
|
180
|
+
retryable: true,
|
|
181
|
+
});
|
|
182
|
+
const plan = suggestRecovery(signature);
|
|
183
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should include retry with backoff step", () => {
|
|
187
|
+
const signature = makeSignature("network", {
|
|
188
|
+
evidence: ["timeout"],
|
|
189
|
+
});
|
|
190
|
+
const plan = suggestRecovery(signature);
|
|
191
|
+
const stepText = plan.steps.map(s => s.action).join(" ").toLowerCase();
|
|
192
|
+
expect(stepText).toMatch(/retry|backoff|wait/i);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should not require human approval for retryable network failures", () => {
|
|
196
|
+
const signature = makeSignature("network", {
|
|
197
|
+
retryable: true,
|
|
198
|
+
});
|
|
199
|
+
const plan = suggestRecovery(signature);
|
|
200
|
+
expect(plan.requiresHumanApproval).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("unknown failure recovery", () => {
|
|
205
|
+
it("should generate recovery plan for unknown failures", () => {
|
|
206
|
+
const signature = makeSignature("unknown");
|
|
207
|
+
const plan = suggestRecovery(signature);
|
|
208
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should require human approval for unknown failures", () => {
|
|
212
|
+
const signature = makeSignature("unknown");
|
|
213
|
+
const plan = suggestRecovery(signature);
|
|
214
|
+
expect(plan.requiresHumanApproval).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should have low estimated success rate", () => {
|
|
218
|
+
const signature = makeSignature("unknown", {
|
|
219
|
+
confidence: 0.1,
|
|
220
|
+
});
|
|
221
|
+
const plan = suggestRecovery(signature);
|
|
222
|
+
expect(plan.estimatedSuccessRate).toBeLessThan(0.5);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("with context", () => {
|
|
227
|
+
it("should incorporate context into recovery steps", () => {
|
|
228
|
+
const signature = makeSignature("network", {
|
|
229
|
+
evidence: ["timeout"],
|
|
230
|
+
});
|
|
231
|
+
const ctx: FailureContext = { nodeId: "api-call" };
|
|
232
|
+
const plan = suggestRecovery(signature, ctx);
|
|
233
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should adjust recovery based on attempt number", () => {
|
|
237
|
+
const signature = makeSignature("network", {
|
|
238
|
+
retryable: true,
|
|
239
|
+
});
|
|
240
|
+
const ctx: FailureContext = { attemptNumber: 5 };
|
|
241
|
+
const plan = suggestRecovery(signature, ctx);
|
|
242
|
+
expect(plan.steps.length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("recovery step structure", () => {
|
|
247
|
+
it("should have description for each step", () => {
|
|
248
|
+
const signature = makeSignature("tool");
|
|
249
|
+
const plan = suggestRecovery(signature);
|
|
250
|
+
plan.steps.forEach(step => {
|
|
251
|
+
expect(step.description).toBeDefined();
|
|
252
|
+
expect(typeof step.description).toBe("string");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should have action for each step", () => {
|
|
257
|
+
const signature = makeSignature("auth");
|
|
258
|
+
const plan = suggestRecovery(signature);
|
|
259
|
+
plan.steps.forEach(step => {
|
|
260
|
+
expect(step.action).toBeDefined();
|
|
261
|
+
expect(typeof step.action).toBe("string");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("estimated success rate", () => {
|
|
267
|
+
it("should be between 0 and 1", () => {
|
|
268
|
+
const classes: FailureSignature["class"][] = [
|
|
269
|
+
"auth",
|
|
270
|
+
"tool",
|
|
271
|
+
"resource",
|
|
272
|
+
"semantic",
|
|
273
|
+
"human",
|
|
274
|
+
"environment-drift",
|
|
275
|
+
"network",
|
|
276
|
+
"unknown",
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
classes.forEach(cls => {
|
|
280
|
+
const signature = makeSignature(cls);
|
|
281
|
+
const plan = suggestRecovery(signature);
|
|
282
|
+
expect(plan.estimatedSuccessRate).toBeGreaterThanOrEqual(0);
|
|
283
|
+
expect(plan.estimatedSuccessRate).toBeLessThanOrEqual(1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should be higher for retryable failures", () => {
|
|
288
|
+
const retryableSig = makeSignature("network", { retryable: true });
|
|
289
|
+
const nonRetryableSig = makeSignature("semantic", { retryable: false });
|
|
290
|
+
|
|
291
|
+
const retryablePlan = suggestRecovery(retryableSig);
|
|
292
|
+
const nonRetryablePlan = suggestRecovery(nonRetryableSig);
|
|
293
|
+
|
|
294
|
+
expect(retryablePlan.estimatedSuccessRate).toBeGreaterThan(
|
|
295
|
+
nonRetryablePlan.estimatedSuccessRate,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { LocalPrBundle } from "../../src/reference/types.js";
|
|
6
|
+
|
|
7
|
+
export interface FixtureRepoOptions {
|
|
8
|
+
reviewInstructions?: string;
|
|
9
|
+
verificationCommands?: string[];
|
|
10
|
+
createMergeConflict?: boolean;
|
|
11
|
+
mergeConflictMode?: "both_modified" | "both_added";
|
|
12
|
+
createPostMergeFailureMarker?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FixtureRepo {
|
|
16
|
+
bundle: LocalPrBundle;
|
|
17
|
+
cleanup: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createFixtureRepo(options: FixtureRepoOptions = {}): FixtureRepo {
|
|
21
|
+
const repoPath = mkdtempSync(join(tmpdir(), "lasso-pr-"));
|
|
22
|
+
const targetBranch = "main";
|
|
23
|
+
const sourceBranch = "feature/pr-change";
|
|
24
|
+
|
|
25
|
+
runGit(repoPath, ["init", "-q"]);
|
|
26
|
+
runGit(repoPath, ["checkout", "-b", targetBranch]);
|
|
27
|
+
runGit(repoPath, ["config", "user.name", "Lasso Test"]);
|
|
28
|
+
runGit(repoPath, ["config", "user.email", "lasso@example.com"]);
|
|
29
|
+
|
|
30
|
+
writeFileSync(join(repoPath, "app.txt"), "base\n");
|
|
31
|
+
runGit(repoPath, ["add", "app.txt"]);
|
|
32
|
+
runGit(repoPath, ["commit", "-qm", "Initial commit"]);
|
|
33
|
+
|
|
34
|
+
runGit(repoPath, ["checkout", "-b", sourceBranch]);
|
|
35
|
+
if (options.createMergeConflict && options.mergeConflictMode === "both_added") {
|
|
36
|
+
writeFileSync(join(repoPath, "conflict.txt"), "feature branch version\n");
|
|
37
|
+
} else {
|
|
38
|
+
writeFileSync(join(repoPath, "app.txt"), options.createMergeConflict ? "feature branch change\n" : "base\nfeature change\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.createPostMergeFailureMarker) {
|
|
42
|
+
writeFileSync(join(repoPath, ".lasso-post-merge-fail"), "retry\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const filesToAdd = [
|
|
46
|
+
...(options.createMergeConflict && options.mergeConflictMode === "both_added" ? ["conflict.txt"] : ["app.txt"]),
|
|
47
|
+
...(options.createPostMergeFailureMarker ? [".lasso-post-merge-fail"] : []),
|
|
48
|
+
];
|
|
49
|
+
runGit(repoPath, ["add", ...filesToAdd]);
|
|
50
|
+
runGit(repoPath, ["commit", "-qm", "Feature change"]);
|
|
51
|
+
|
|
52
|
+
if (options.createMergeConflict) {
|
|
53
|
+
runGit(repoPath, ["checkout", targetBranch]);
|
|
54
|
+
if (options.mergeConflictMode === "both_added") {
|
|
55
|
+
writeFileSync(join(repoPath, "conflict.txt"), "main branch version\n");
|
|
56
|
+
runGit(repoPath, ["add", "conflict.txt"]);
|
|
57
|
+
} else {
|
|
58
|
+
writeFileSync(join(repoPath, "app.txt"), "main branch change\n");
|
|
59
|
+
runGit(repoPath, ["add", "app.txt"]);
|
|
60
|
+
}
|
|
61
|
+
runGit(repoPath, ["commit", "-qm", "Conflicting change on main"]);
|
|
62
|
+
runGit(repoPath, ["checkout", sourceBranch]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
bundle: {
|
|
67
|
+
repoPath,
|
|
68
|
+
sourceBranch,
|
|
69
|
+
targetBranch,
|
|
70
|
+
reviewInstructions: options.reviewInstructions ?? "Approve only if verification passes and the diff looks safe.",
|
|
71
|
+
verificationCommands: options.verificationCommands ?? ['node -e "process.exit(0)"'],
|
|
72
|
+
},
|
|
73
|
+
cleanup: () => {
|
|
74
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runGit(cwd: string, args: string[]): string {
|
|
80
|
+
return execFileSync("git", args, {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
}).trim();
|
|
84
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { LocalPatchValidationBundle } from "../../src/reference/types.js";
|
|
6
|
+
|
|
7
|
+
export interface PatchValidationFixtureOptions {
|
|
8
|
+
reviewInstructions?: string;
|
|
9
|
+
/**
|
|
10
|
+
* When true, add a human approval gate to the bundle and spec.
|
|
11
|
+
* Defaults to false.
|
|
12
|
+
*/
|
|
13
|
+
approvalRequired?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The baseline reproduce command already passes (exits 0), meaning the bug is
|
|
16
|
+
* not present. Routes the workflow to the `not-reproduced` terminal.
|
|
17
|
+
*/
|
|
18
|
+
baselineAlwaysPasses?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Produces a branch or patch-file candidate that cannot be applied cleanly.
|
|
21
|
+
* Routes to the `apply-failed` terminal.
|
|
22
|
+
* When combined with `candidateKind: "patchFile"` the patch file is malformed;
|
|
23
|
+
* when combined with `candidateKind: "branch"` the branch simply does not exist.
|
|
24
|
+
*/
|
|
25
|
+
applyFailure?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The fix is applied successfully but the reproduce command still fails after
|
|
28
|
+
* the candidate is applied. Routes to the `candidate-failed` terminal.
|
|
29
|
+
*/
|
|
30
|
+
fixDoesNotFixBug?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The fix resolves the reproduce command but the broader verification commands
|
|
33
|
+
* fail after the candidate is applied. Routes to the `candidate-failed` terminal.
|
|
34
|
+
*/
|
|
35
|
+
verificationFailure?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Which candidate source kind the bundle should expose.
|
|
38
|
+
* Defaults to "branch".
|
|
39
|
+
*/
|
|
40
|
+
candidateKind?: "branch" | "patchFile";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PatchValidationFixture {
|
|
44
|
+
bundle: LocalPatchValidationBundle;
|
|
45
|
+
/** Absolute path to the generated patch file (only populated for patchFile fixtures). */
|
|
46
|
+
patchFilePath: string;
|
|
47
|
+
cleanup: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BASELINE_REF = "main";
|
|
51
|
+
const FIX_BRANCH = "fix/bug";
|
|
52
|
+
|
|
53
|
+
export function createPatchValidationFixture(options: PatchValidationFixtureOptions = {}): PatchValidationFixture {
|
|
54
|
+
const repoPath = mkdtempSync(join(tmpdir(), "lasso-pv-"));
|
|
55
|
+
const candidateKind = options.candidateKind ?? "branch";
|
|
56
|
+
|
|
57
|
+
runGit(repoPath, ["init", "-q"]);
|
|
58
|
+
runGit(repoPath, ["checkout", "-b", BASELINE_REF]);
|
|
59
|
+
runGit(repoPath, ["config", "user.name", "Lasso Test"]);
|
|
60
|
+
runGit(repoPath, ["config", "user.email", "lasso@example.com"]);
|
|
61
|
+
|
|
62
|
+
// reproduce.sh: exits 1 on baseline (bug present) unless baselineAlwaysPasses
|
|
63
|
+
const baselineReproduceContent = options.baselineAlwaysPasses ? "exit 0\n" : "exit 1\n";
|
|
64
|
+
writeFileSync(join(repoPath, "reproduce.sh"), baselineReproduceContent);
|
|
65
|
+
// verify.sh: always passes on baseline
|
|
66
|
+
writeFileSync(join(repoPath, "verify.sh"), "exit 0\n");
|
|
67
|
+
runGit(repoPath, ["add", "reproduce.sh", "verify.sh"]);
|
|
68
|
+
runGit(repoPath, ["commit", "-qm", "Initial commit with known bug"]);
|
|
69
|
+
|
|
70
|
+
let patchFilePath: string;
|
|
71
|
+
|
|
72
|
+
if (options.baselineAlwaysPasses) {
|
|
73
|
+
// Workflow routes to not-reproduced before attempting to apply the candidate,
|
|
74
|
+
// so the candidate source is never exercised. Use a dummy patch path.
|
|
75
|
+
patchFilePath = join(repoPath, "unused.patch");
|
|
76
|
+
writeFileSync(patchFilePath, "");
|
|
77
|
+
} else if (options.applyFailure) {
|
|
78
|
+
// Do not create a real fix branch. For the patchFile case write a malformed patch.
|
|
79
|
+
patchFilePath = join(repoPath, "bad.patch");
|
|
80
|
+
writeFileSync(patchFilePath, "this is not a valid patch\n");
|
|
81
|
+
} else {
|
|
82
|
+
runGit(repoPath, ["checkout", "-b", FIX_BRANCH]);
|
|
83
|
+
|
|
84
|
+
if (options.fixDoesNotFixBug) {
|
|
85
|
+
// Cosmetic change only — bug still present after applying
|
|
86
|
+
writeFileSync(join(repoPath, "reproduce.sh"), "# attempted fix\nexit 1\n");
|
|
87
|
+
runGit(repoPath, ["add", "reproduce.sh"]);
|
|
88
|
+
} else if (options.verificationFailure) {
|
|
89
|
+
// Bug is fixed but verification breaks
|
|
90
|
+
writeFileSync(join(repoPath, "reproduce.sh"), "exit 0\n");
|
|
91
|
+
writeFileSync(join(repoPath, "verify.sh"), "exit 1\n");
|
|
92
|
+
runGit(repoPath, ["add", "reproduce.sh", "verify.sh"]);
|
|
93
|
+
} else {
|
|
94
|
+
// Clean fix
|
|
95
|
+
writeFileSync(join(repoPath, "reproduce.sh"), "exit 0\n");
|
|
96
|
+
runGit(repoPath, ["add", "reproduce.sh"]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
runGit(repoPath, ["commit", "-qm", "Fix: resolve the bug"]);
|
|
100
|
+
|
|
101
|
+
// Generate a patch file from the baseline to the fix branch.
|
|
102
|
+
// NOTE: Do NOT trim the output — git apply requires the trailing newline.
|
|
103
|
+
const patchContent = execFileSync("git", ["diff", `${BASELINE_REF}..${FIX_BRANCH}`], {
|
|
104
|
+
cwd: repoPath,
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
});
|
|
107
|
+
patchFilePath = join(repoPath, "fix.patch");
|
|
108
|
+
writeFileSync(patchFilePath, patchContent);
|
|
109
|
+
|
|
110
|
+
// Return to baseline so the workflow starts clean
|
|
111
|
+
runGit(repoPath, ["checkout", BASELINE_REF]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const candidateSource =
|
|
115
|
+
candidateKind === "patchFile"
|
|
116
|
+
? ({ kind: "patchFile", value: patchFilePath } as const)
|
|
117
|
+
: options.applyFailure
|
|
118
|
+
? ({ kind: "branch", value: "nonexistent-branch" } as const)
|
|
119
|
+
: ({ kind: "branch", value: FIX_BRANCH } as const);
|
|
120
|
+
|
|
121
|
+
const bundle: LocalPatchValidationBundle = {
|
|
122
|
+
repoPath,
|
|
123
|
+
baselineRef: BASELINE_REF,
|
|
124
|
+
candidateSource,
|
|
125
|
+
reproduceCommands: ["bash reproduce.sh"],
|
|
126
|
+
verificationCommands: ["bash verify.sh"],
|
|
127
|
+
reviewInstructions:
|
|
128
|
+
options.reviewInstructions ?? "Approve only if the fix is clean and regression-free.",
|
|
129
|
+
approvalRequired: options.approvalRequired ?? false,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
bundle,
|
|
134
|
+
patchFilePath,
|
|
135
|
+
cleanup: () => rmSync(repoPath, { recursive: true, force: true }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runGit(cwd: string, args: string[]): string {
|
|
140
|
+
return execFileSync("git", args, {
|
|
141
|
+
cwd,
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
}).trim();
|
|
144
|
+
}
|