@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,439 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { FailureRecord } from "../../src/failures/types.js";
|
|
3
|
+
import {
|
|
4
|
+
classifyFailureRecord,
|
|
5
|
+
isRetryableFailure,
|
|
6
|
+
classifyFailure,
|
|
7
|
+
type FailureSignature,
|
|
8
|
+
type FailureClass,
|
|
9
|
+
type FailureContext,
|
|
10
|
+
} from "../../src/failures/ontology.js";
|
|
11
|
+
import { mapReferenceFailure } from "../../src/failures/map-reference-failures.js";
|
|
12
|
+
|
|
13
|
+
describe("Failure ontology", () => {
|
|
14
|
+
describe("FailureRecord type", () => {
|
|
15
|
+
it("should represent a normalized failure with root cause", () => {
|
|
16
|
+
const failure: FailureRecord = {
|
|
17
|
+
domainType: "pr-review",
|
|
18
|
+
rootCause: "tool_timeout",
|
|
19
|
+
nodeId: "apply-patch",
|
|
20
|
+
message: "Git apply timed out after 30s",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(failure.domainType).toBe("pr-review");
|
|
24
|
+
expect(failure.rootCause).toBe("tool_timeout");
|
|
25
|
+
expect(failure.nodeId).toBe("apply-patch");
|
|
26
|
+
expect(failure.message).toBe("Git apply timed out after 30s");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should allow optional nodeId", () => {
|
|
30
|
+
const failure: FailureRecord = {
|
|
31
|
+
domainType: "generic",
|
|
32
|
+
rootCause: "unknown",
|
|
33
|
+
message: "Something went wrong",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(failure.nodeId).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should support all root cause types", () => {
|
|
40
|
+
const rootCauses: FailureRecord["rootCause"][] = [
|
|
41
|
+
"tool_timeout",
|
|
42
|
+
"auth_required",
|
|
43
|
+
"rate_limited",
|
|
44
|
+
"invalid_output",
|
|
45
|
+
"dependency_failure",
|
|
46
|
+
"verification_failed",
|
|
47
|
+
"human_block",
|
|
48
|
+
"unknown",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
rootCauses.forEach(rootCause => {
|
|
52
|
+
const failure: FailureRecord = {
|
|
53
|
+
domainType: "test",
|
|
54
|
+
rootCause,
|
|
55
|
+
message: `Test ${rootCause}`,
|
|
56
|
+
};
|
|
57
|
+
expect(failure.rootCause).toBe(rootCause);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("classifyFailureRecord", () => {
|
|
63
|
+
it("should identify retryable failures", () => {
|
|
64
|
+
const toolTimeout: FailureRecord = {
|
|
65
|
+
domainType: "test",
|
|
66
|
+
rootCause: "tool_timeout",
|
|
67
|
+
message: "Timeout",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = classifyFailureRecord(toolTimeout);
|
|
71
|
+
expect(result.retryable).toBe(true);
|
|
72
|
+
expect(result.category).toBe("transient");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should identify non-retryable failures", () => {
|
|
76
|
+
const authRequired: FailureRecord = {
|
|
77
|
+
domainType: "test",
|
|
78
|
+
rootCause: "auth_required",
|
|
79
|
+
message: "Authentication needed",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = classifyFailureRecord(authRequired);
|
|
83
|
+
expect(result.retryable).toBe(false);
|
|
84
|
+
expect(result.category).toBe("permanent");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should classify verification failures as permanent", () => {
|
|
88
|
+
const verificationFailed: FailureRecord = {
|
|
89
|
+
domainType: "test",
|
|
90
|
+
rootCause: "verification_failed",
|
|
91
|
+
message: "Verification check failed",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = classifyFailureRecord(verificationFailed);
|
|
95
|
+
expect(result.retryable).toBe(false);
|
|
96
|
+
expect(result.category).toBe("permanent");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should classify human blocks as permanent", () => {
|
|
100
|
+
const humanBlock: FailureRecord = {
|
|
101
|
+
domainType: "test",
|
|
102
|
+
rootCause: "human_block",
|
|
103
|
+
message: "Human intervention required",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = classifyFailureRecord(humanBlock);
|
|
107
|
+
expect(result.retryable).toBe(false);
|
|
108
|
+
expect(result.category).toBe("permanent");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should treat unknown as retryable", () => {
|
|
112
|
+
const unknown: FailureRecord = {
|
|
113
|
+
domainType: "test",
|
|
114
|
+
rootCause: "unknown",
|
|
115
|
+
message: "Unknown error",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = classifyFailureRecord(unknown);
|
|
119
|
+
expect(result.retryable).toBe(true);
|
|
120
|
+
expect(result.category).toBe("transient");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("isRetryableFailure", () => {
|
|
125
|
+
it("should return true for transient failures", () => {
|
|
126
|
+
const failure: FailureRecord = {
|
|
127
|
+
domainType: "test",
|
|
128
|
+
rootCause: "rate_limited",
|
|
129
|
+
message: "Rate limit hit",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
expect(isRetryableFailure(failure)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should return false for permanent failures", () => {
|
|
136
|
+
const failure: FailureRecord = {
|
|
137
|
+
domainType: "test",
|
|
138
|
+
rootCause: "auth_required",
|
|
139
|
+
message: "Auth needed",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
expect(isRetryableFailure(failure)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("mapReferenceFailure", () => {
|
|
147
|
+
it("should map workflow-specific failures to normalized failures", () => {
|
|
148
|
+
const result = mapReferenceFailure(
|
|
149
|
+
"pr-review",
|
|
150
|
+
"apply-failed",
|
|
151
|
+
"apply-patch",
|
|
152
|
+
"Patch conflicts with target branch",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(result.domainType).toBe("pr-review");
|
|
156
|
+
expect(result.rootCause).toBe("dependency_failure");
|
|
157
|
+
expect(result.nodeId).toBe("apply-patch");
|
|
158
|
+
expect(result.message).toBe("Patch conflicts with target branch");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should map candidate-failed to invalid_output", () => {
|
|
162
|
+
const result = mapReferenceFailure(
|
|
163
|
+
"pr-review",
|
|
164
|
+
"candidate-failed",
|
|
165
|
+
"generate-candidate",
|
|
166
|
+
"LLM produced invalid patch format",
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.domainType).toBe("pr-review");
|
|
170
|
+
expect(result.rootCause).toBe("invalid_output");
|
|
171
|
+
expect(result.nodeId).toBe("generate-candidate");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should map reject-human to human_block", () => {
|
|
175
|
+
const result = mapReferenceFailure(
|
|
176
|
+
"pr-review",
|
|
177
|
+
"reject-human",
|
|
178
|
+
"await-approval",
|
|
179
|
+
"Human rejected the proposed changes",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result.domainType).toBe("pr-review");
|
|
183
|
+
expect(result.rootCause).toBe("human_block");
|
|
184
|
+
expect(result.nodeId).toBe("await-approval");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should map unknown workflow failures to unknown root cause", () => {
|
|
188
|
+
const result = mapReferenceFailure(
|
|
189
|
+
"custom-workflow",
|
|
190
|
+
"unknown-error-type",
|
|
191
|
+
"some-node",
|
|
192
|
+
"Something unexpected",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result.domainType).toBe("custom-workflow");
|
|
196
|
+
expect(result.rootCause).toBe("unknown");
|
|
197
|
+
expect(result.nodeId).toBe("some-node");
|
|
198
|
+
expect(result.message).toBe("Something unexpected");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("classifyFailure (new ontology)", () => {
|
|
203
|
+
describe("auth failures", () => {
|
|
204
|
+
it("should classify 401 errors as auth", () => {
|
|
205
|
+
const result = classifyFailure(
|
|
206
|
+
new Error("Request failed with status code 401"),
|
|
207
|
+
);
|
|
208
|
+
expect(result.class).toBe("auth");
|
|
209
|
+
expect(result.confidence).toBeGreaterThan(0.5);
|
|
210
|
+
expect(result.retryable).toBe(false);
|
|
211
|
+
expect(result.requiresHumanIntervention).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should classify unauthorized messages as auth", () => {
|
|
215
|
+
const result = classifyFailure(
|
|
216
|
+
new Error("Unauthorized: invalid credentials"),
|
|
217
|
+
);
|
|
218
|
+
expect(result.class).toBe("auth");
|
|
219
|
+
expect(result.evidence.length).toBeGreaterThan(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should classify token expired as auth", () => {
|
|
223
|
+
const result = classifyFailure(
|
|
224
|
+
new Error("Token expired, please refresh"),
|
|
225
|
+
);
|
|
226
|
+
expect(result.class).toBe("auth");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("tool failures", () => {
|
|
231
|
+
it("should classify command not found as tool", () => {
|
|
232
|
+
const result = classifyFailure(
|
|
233
|
+
new Error("bash: git: command not found"),
|
|
234
|
+
);
|
|
235
|
+
expect(result.class).toBe("tool");
|
|
236
|
+
expect(result.retryable).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should classify non-zero exit codes as tool", () => {
|
|
240
|
+
const result = classifyFailure(
|
|
241
|
+
new Error("Process exited with code 1"),
|
|
242
|
+
);
|
|
243
|
+
expect(result.class).toBe("tool");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("resource failures", () => {
|
|
248
|
+
it("should classify disk full as resource", () => {
|
|
249
|
+
const result = classifyFailure(
|
|
250
|
+
new Error("No space left on device"),
|
|
251
|
+
);
|
|
252
|
+
expect(result.class).toBe("resource");
|
|
253
|
+
expect(result.retryable).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should classify OOM as resource", () => {
|
|
257
|
+
const result = classifyFailure(
|
|
258
|
+
new Error("JavaScript heap out of memory"),
|
|
259
|
+
);
|
|
260
|
+
expect(result.class).toBe("resource");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should classify rate limit as resource", () => {
|
|
264
|
+
const result = classifyFailure(
|
|
265
|
+
new Error("Rate limit exceeded: too many requests"),
|
|
266
|
+
);
|
|
267
|
+
expect(result.class).toBe("resource");
|
|
268
|
+
expect(result.retryable).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("semantic failures", () => {
|
|
273
|
+
it("should classify assertion failures as semantic", () => {
|
|
274
|
+
const result = classifyFailure(
|
|
275
|
+
new Error("Assertion failed: expected 5, got 3"),
|
|
276
|
+
);
|
|
277
|
+
expect(result.class).toBe("semantic");
|
|
278
|
+
expect(result.retryable).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should classify schema mismatches as semantic", () => {
|
|
282
|
+
const result = classifyFailure(
|
|
283
|
+
new Error("Schema validation error: missing field 'id'"),
|
|
284
|
+
);
|
|
285
|
+
expect(result.class).toBe("semantic");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("human failures", () => {
|
|
290
|
+
it("should classify rejected as human", () => {
|
|
291
|
+
const result = classifyFailure(
|
|
292
|
+
new Error("Human rejected the proposed changes"),
|
|
293
|
+
);
|
|
294
|
+
expect(result.class).toBe("human");
|
|
295
|
+
expect(result.requiresHumanIntervention).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should classify human timeout as human", () => {
|
|
299
|
+
const result = classifyFailure(
|
|
300
|
+
new Error("Timed out waiting for human approval"),
|
|
301
|
+
);
|
|
302
|
+
expect(result.class).toBe("human");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("environment-drift failures", () => {
|
|
307
|
+
it("should classify version mismatch as environment-drift", () => {
|
|
308
|
+
const result = classifyFailure(
|
|
309
|
+
new Error("Node.js version mismatch: expected 18, got 16"),
|
|
310
|
+
);
|
|
311
|
+
expect(result.class).toBe("environment-drift");
|
|
312
|
+
expect(result.retryable).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should classify missing dependency as environment-drift", () => {
|
|
316
|
+
const result = classifyFailure(
|
|
317
|
+
new Error("Cannot find module 'lodash'"),
|
|
318
|
+
);
|
|
319
|
+
expect(result.class).toBe("environment-drift");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("network failures", () => {
|
|
324
|
+
it("should classify connection timeout as network", () => {
|
|
325
|
+
const result = classifyFailure(
|
|
326
|
+
new Error("Connection timed out after 30000ms"),
|
|
327
|
+
);
|
|
328
|
+
expect(result.class).toBe("network");
|
|
329
|
+
expect(result.retryable).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should classify connection refused as network", () => {
|
|
333
|
+
const result = classifyFailure(
|
|
334
|
+
new Error("connect ECONNREFUSED 127.0.0.1:8080"),
|
|
335
|
+
);
|
|
336
|
+
expect(result.class).toBe("network");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should classify DNS failure as network", () => {
|
|
340
|
+
const result = classifyFailure(
|
|
341
|
+
new Error("getaddrinfo ENOTFOUND api.example.com"),
|
|
342
|
+
);
|
|
343
|
+
expect(result.class).toBe("network");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("unknown failures", () => {
|
|
348
|
+
it("should classify unrecognizable errors as unknown", () => {
|
|
349
|
+
const result = classifyFailure(
|
|
350
|
+
new Error("Something completely unexpected happened"),
|
|
351
|
+
);
|
|
352
|
+
expect(result.class).toBe("unknown");
|
|
353
|
+
expect(result.confidence).toBeLessThan(0.5);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("should handle non-Error objects", () => {
|
|
357
|
+
const result = classifyFailure("string error");
|
|
358
|
+
expect(result.class).toBe("unknown");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should handle null errors", () => {
|
|
362
|
+
const result = classifyFailure(null);
|
|
363
|
+
expect(result.class).toBe("unknown");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("confidence scores", () => {
|
|
368
|
+
it("should have high confidence for exact pattern matches", () => {
|
|
369
|
+
const result = classifyFailure(
|
|
370
|
+
new Error("401 Unauthorized"),
|
|
371
|
+
);
|
|
372
|
+
expect(result.confidence).toBeGreaterThanOrEqual(0.8);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should have lower confidence for partial matches", () => {
|
|
376
|
+
const result = classifyFailure(
|
|
377
|
+
new Error("maybe unauthorized? not sure"),
|
|
378
|
+
);
|
|
379
|
+
expect(result.confidence).toBeGreaterThan(0);
|
|
380
|
+
expect(result.confidence).toBeLessThanOrEqual(1);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("evidence extraction", () => {
|
|
385
|
+
it("should extract evidence from error message", () => {
|
|
386
|
+
const result = classifyFailure(
|
|
387
|
+
new Error("401 Unauthorized: token expired"),
|
|
388
|
+
);
|
|
389
|
+
expect(result.evidence.length).toBeGreaterThan(0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should include multiple evidence items for multi-pattern matches", () => {
|
|
393
|
+
const result = classifyFailure(
|
|
394
|
+
new Error("401 Unauthorized: authentication required, token expired"),
|
|
395
|
+
);
|
|
396
|
+
expect(result.evidence.length).toBeGreaterThanOrEqual(1);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("suggested recovery", () => {
|
|
401
|
+
it("should suggest recovery steps for auth failures", () => {
|
|
402
|
+
const result = classifyFailure(
|
|
403
|
+
new Error("401 Unauthorized"),
|
|
404
|
+
);
|
|
405
|
+
expect(result.suggestedRecovery.length).toBeGreaterThan(0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should suggest recovery steps for network failures", () => {
|
|
409
|
+
const result = classifyFailure(
|
|
410
|
+
new Error("Connection timed out"),
|
|
411
|
+
);
|
|
412
|
+
expect(result.suggestedRecovery.length).toBeGreaterThan(0);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("with context", () => {
|
|
417
|
+
it("should use context nodeId in evidence", () => {
|
|
418
|
+
const ctx: FailureContext = { nodeId: "fetch-data" };
|
|
419
|
+
const result = classifyFailure(
|
|
420
|
+
new Error("Connection refused"),
|
|
421
|
+
ctx,
|
|
422
|
+
);
|
|
423
|
+
expect(result.evidence.some(e => e.includes("fetch-data"))).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should classify with additional context information", () => {
|
|
427
|
+
const ctx: FailureContext = {
|
|
428
|
+
nodeId: "auth-check",
|
|
429
|
+
attemptNumber: 3,
|
|
430
|
+
};
|
|
431
|
+
const result = classifyFailure(
|
|
432
|
+
new Error("401 Unauthorized"),
|
|
433
|
+
ctx,
|
|
434
|
+
);
|
|
435
|
+
expect(result.class).toBe("auth");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|