@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,215 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parallelHarnesses } from "../../src/composition/parallel.js";
|
|
3
|
+
import type { HarnessSpec, TaskNode } from "../../src/spec/types.js";
|
|
4
|
+
|
|
5
|
+
function makeSpec(name: string, nodes: TaskNode[], edges: { from: string; to: string }[] = []): HarnessSpec {
|
|
6
|
+
return {
|
|
7
|
+
name,
|
|
8
|
+
graph: {
|
|
9
|
+
entryNodeId: nodes[0]?.id ?? "entry",
|
|
10
|
+
nodes,
|
|
11
|
+
edges,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("parallelHarnesses", () => {
|
|
17
|
+
describe("basic parallel execution", () => {
|
|
18
|
+
it("merges two harnesses in parallel", () => {
|
|
19
|
+
const harnesses: HarnessSpec[] = [
|
|
20
|
+
makeSpec("alpha", [
|
|
21
|
+
{ id: "a1", kind: "tool", tool: "echo", args: ["alpha"] },
|
|
22
|
+
]),
|
|
23
|
+
makeSpec("beta", [
|
|
24
|
+
{ id: "b1", kind: "tool", tool: "echo", args: ["beta"] },
|
|
25
|
+
]),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const result = parallelHarnesses(harnesses);
|
|
29
|
+
|
|
30
|
+
expect(result.stageCount).toBe(2);
|
|
31
|
+
expect(result.totalNodes).toBeGreaterThanOrEqual(3);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("creates a merge node to synchronize parallel branches", () => {
|
|
35
|
+
const harnesses: HarnessSpec[] = [
|
|
36
|
+
makeSpec("alpha", [
|
|
37
|
+
{ id: "a1", kind: "tool", tool: "echo", args: ["a"] },
|
|
38
|
+
]),
|
|
39
|
+
makeSpec("beta", [
|
|
40
|
+
{ id: "b1", kind: "tool", tool: "echo", args: ["b"] },
|
|
41
|
+
]),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const result = parallelHarnesses(harnesses);
|
|
45
|
+
|
|
46
|
+
const mergeNode = result.combinedSpec.graph.nodes.find((n) => n.kind === "merge");
|
|
47
|
+
expect(mergeNode).toBeDefined();
|
|
48
|
+
expect(mergeNode!.kind).toBe("merge");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("merge node waits for all branch terminal nodes", () => {
|
|
52
|
+
const harnesses: HarnessSpec[] = [
|
|
53
|
+
makeSpec("alpha", [
|
|
54
|
+
{ id: "a1", kind: "tool", tool: "echo", args: ["a"] },
|
|
55
|
+
]),
|
|
56
|
+
makeSpec("beta", [
|
|
57
|
+
{ id: "b1", kind: "tool", tool: "echo", args: ["b"] },
|
|
58
|
+
]),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const result = parallelHarnesses(harnesses);
|
|
62
|
+
|
|
63
|
+
const mergeNode = result.combinedSpec.graph.nodes.find(
|
|
64
|
+
(n) => n.kind === "merge",
|
|
65
|
+
) as any;
|
|
66
|
+
expect(mergeNode).toBeDefined();
|
|
67
|
+
expect(mergeNode.waitFor).toContain("alpha:a1");
|
|
68
|
+
expect(mergeNode.waitFor).toContain("beta:b1");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("node ID prefixing", () => {
|
|
73
|
+
it("prefixes node IDs with harness name to avoid collisions", () => {
|
|
74
|
+
const harnesses: HarnessSpec[] = [
|
|
75
|
+
makeSpec("h1", [
|
|
76
|
+
{ id: "step", kind: "tool", tool: "a", args: [] },
|
|
77
|
+
]),
|
|
78
|
+
makeSpec("h2", [
|
|
79
|
+
{ id: "step", kind: "tool", tool: "b", args: [] },
|
|
80
|
+
]),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const result = parallelHarnesses(harnesses);
|
|
84
|
+
|
|
85
|
+
const nodeIds = result.combinedSpec.graph.nodes.map((n) => n.id);
|
|
86
|
+
expect(nodeIds).toContain("h1:step");
|
|
87
|
+
expect(nodeIds).toContain("h2:step");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("produces all unique node IDs", () => {
|
|
91
|
+
const harnesses: HarnessSpec[] = [
|
|
92
|
+
makeSpec("h1", [
|
|
93
|
+
{ id: "x", kind: "tool", tool: "a", args: [] },
|
|
94
|
+
{ id: "y", kind: "tool", tool: "b", args: [] },
|
|
95
|
+
]),
|
|
96
|
+
makeSpec("h2", [
|
|
97
|
+
{ id: "x", kind: "tool", tool: "c", args: [] },
|
|
98
|
+
{ id: "y", kind: "tool", tool: "d", args: [] },
|
|
99
|
+
]),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const result = parallelHarnesses(harnesses);
|
|
103
|
+
|
|
104
|
+
const nodeIds = result.combinedSpec.graph.nodes.map((n) => n.id);
|
|
105
|
+
const uniqueIds = new Set(nodeIds);
|
|
106
|
+
expect(uniqueIds.size).toBe(nodeIds.length);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("edge rewiring", () => {
|
|
111
|
+
it("rewires internal edges with prefixed IDs", () => {
|
|
112
|
+
const harnesses: HarnessSpec[] = [
|
|
113
|
+
makeSpec("h1", [
|
|
114
|
+
{ id: "first", kind: "tool", tool: "a", args: [] },
|
|
115
|
+
{ id: "second", kind: "tool", tool: "b", args: [] },
|
|
116
|
+
], [{ from: "first", to: "second" }]),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const result = parallelHarnesses(harnesses);
|
|
120
|
+
|
|
121
|
+
const edge = result.combinedSpec.graph.edges.find(
|
|
122
|
+
(e) => e.from === "h1:first" && e.to === "h1:second",
|
|
123
|
+
);
|
|
124
|
+
expect(edge).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("connects each branch terminal node to merge node", () => {
|
|
128
|
+
const harnesses: HarnessSpec[] = [
|
|
129
|
+
makeSpec("h1", [
|
|
130
|
+
{ id: "a1", kind: "tool", tool: "a", args: [] },
|
|
131
|
+
]),
|
|
132
|
+
makeSpec("h2", [
|
|
133
|
+
{ id: "b1", kind: "tool", tool: "b", args: [] },
|
|
134
|
+
]),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const result = parallelHarnesses(harnesses);
|
|
138
|
+
|
|
139
|
+
const mergeNode = result.combinedSpec.graph.nodes.find(
|
|
140
|
+
(n) => n.kind === "merge",
|
|
141
|
+
)!;
|
|
142
|
+
const mergeNodeId = mergeNode.id;
|
|
143
|
+
|
|
144
|
+
const edgesToMerge = result.combinedSpec.graph.edges.filter(
|
|
145
|
+
(e) => e.to === mergeNodeId,
|
|
146
|
+
);
|
|
147
|
+
expect(edgesToMerge.length).toBe(2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("multi-harness parallel", () => {
|
|
152
|
+
it("merges three harnesses in parallel", () => {
|
|
153
|
+
const harnesses: HarnessSpec[] = [
|
|
154
|
+
makeSpec("h1", [{ id: "a", kind: "tool", tool: "a", args: [] }]),
|
|
155
|
+
makeSpec("h2", [{ id: "b", kind: "tool", tool: "b", args: [] }]),
|
|
156
|
+
makeSpec("h3", [{ id: "c", kind: "tool", tool: "c", args: [] }]),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const result = parallelHarnesses(harnesses);
|
|
160
|
+
|
|
161
|
+
expect(result.stageCount).toBe(3);
|
|
162
|
+
const mergeNode = result.combinedSpec.graph.nodes.find(
|
|
163
|
+
(n) => n.kind === "merge",
|
|
164
|
+
) as any;
|
|
165
|
+
expect(mergeNode.waitFor.length).toBe(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("entry point connects to all harness entry nodes", () => {
|
|
169
|
+
const harnesses: HarnessSpec[] = [
|
|
170
|
+
makeSpec("h1", [{ id: "a", kind: "tool", tool: "a", args: [] }]),
|
|
171
|
+
makeSpec("h2", [{ id: "b", kind: "tool", tool: "b", args: [] }]),
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const result = parallelHarnesses(harnesses);
|
|
175
|
+
|
|
176
|
+
const entryId = result.combinedSpec.graph.entryNodeId;
|
|
177
|
+
const entryNode = result.combinedSpec.graph.nodes.find((n) => n.id === entryId);
|
|
178
|
+
expect(entryNode).toBeDefined();
|
|
179
|
+
|
|
180
|
+
const edgesFromEntry = result.combinedSpec.graph.edges.filter(
|
|
181
|
+
(e) => e.from === entryId,
|
|
182
|
+
);
|
|
183
|
+
expect(edgesFromEntry.length).toBe(2);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("composition result", () => {
|
|
188
|
+
it("calculates total nodes including merge and entry", () => {
|
|
189
|
+
const harnesses: HarnessSpec[] = [
|
|
190
|
+
makeSpec("h1", [{ id: "a", kind: "tool", tool: "a", args: [] }]),
|
|
191
|
+
makeSpec("h2", [{ id: "b", kind: "tool", tool: "b", args: [] }]),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const result = parallelHarnesses(harnesses);
|
|
195
|
+
|
|
196
|
+
expect(result.totalNodes).toBe(4);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("estimates duration", () => {
|
|
200
|
+
const harnesses: HarnessSpec[] = [
|
|
201
|
+
makeSpec("h1", [{ id: "a", kind: "tool", tool: "a", args: [] }]),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const result = parallelHarnesses(harnesses);
|
|
205
|
+
|
|
206
|
+
expect(result.estimatedDurationMs).toBeGreaterThan(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("empty harnesses", () => {
|
|
211
|
+
it("throws on empty array", () => {
|
|
212
|
+
expect(() => parallelHarnesses([])).toThrow();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyzeEnvironment } from "../../src/environment/analyzer.js";
|
|
3
|
+
import type { EnvironmentModel, ToolCapability, Constraint } from "../../src/environment/types.js";
|
|
4
|
+
|
|
5
|
+
function createMockModel(overrides: Partial<EnvironmentModel> = {}): EnvironmentModel {
|
|
6
|
+
return {
|
|
7
|
+
tools: [
|
|
8
|
+
{ name: "bash", version: "5.0.0", available: true },
|
|
9
|
+
{ name: "git", version: "2.30.0", available: true },
|
|
10
|
+
{ name: "node", version: "18.0.0", available: true },
|
|
11
|
+
],
|
|
12
|
+
resources: [
|
|
13
|
+
{ name: "disk", type: "disk", available: true, limit: "500GB", usage: "200GB" },
|
|
14
|
+
],
|
|
15
|
+
constraints: [],
|
|
16
|
+
authState: [],
|
|
17
|
+
externalSystems: [],
|
|
18
|
+
discoveredAt: Date.now(),
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("analyzeEnvironment", () => {
|
|
24
|
+
describe("tool matching", () => {
|
|
25
|
+
it("matches all available required tools", () => {
|
|
26
|
+
const model = createMockModel();
|
|
27
|
+
|
|
28
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
29
|
+
|
|
30
|
+
expect(result.matchedTools).toHaveLength(2);
|
|
31
|
+
expect(result.missingTools).toHaveLength(0);
|
|
32
|
+
expect(result.matchedTools.map(t => t.name)).toContain("bash");
|
|
33
|
+
expect(result.matchedTools.map(t => t.name)).toContain("git");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("identifies missing required tools", () => {
|
|
37
|
+
const model = createMockModel();
|
|
38
|
+
|
|
39
|
+
const result = analyzeEnvironment(model, ["bash", "nonexistent-tool"]);
|
|
40
|
+
|
|
41
|
+
expect(result.matchedTools).toHaveLength(1);
|
|
42
|
+
expect(result.missingTools).toHaveLength(1);
|
|
43
|
+
expect(result.missingTools).toContain("nonexistent-tool");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles empty required tools list", () => {
|
|
47
|
+
const model = createMockModel();
|
|
48
|
+
|
|
49
|
+
const result = analyzeEnvironment(model, []);
|
|
50
|
+
|
|
51
|
+
expect(result.matchedTools).toHaveLength(0);
|
|
52
|
+
expect(result.missingTools).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("handles undefined required tools", () => {
|
|
56
|
+
const model = createMockModel();
|
|
57
|
+
|
|
58
|
+
const result = analyzeEnvironment(model);
|
|
59
|
+
|
|
60
|
+
expect(result.matchedTools).toHaveLength(0);
|
|
61
|
+
expect(result.missingTools).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("identifies multiple missing tools", () => {
|
|
65
|
+
const model = createMockModel({
|
|
66
|
+
tools: [
|
|
67
|
+
{ name: "bash", available: true },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = analyzeEnvironment(model, ["bash", "git", "node", "docker"]);
|
|
72
|
+
|
|
73
|
+
expect(result.missingTools).toHaveLength(3);
|
|
74
|
+
expect(result.missingTools).toContain("git");
|
|
75
|
+
expect(result.missingTools).toContain("node");
|
|
76
|
+
expect(result.missingTools).toContain("docker");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("readiness score", () => {
|
|
81
|
+
it("computes 100 when all tools available", () => {
|
|
82
|
+
const model = createMockModel();
|
|
83
|
+
|
|
84
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
85
|
+
|
|
86
|
+
expect(result.readinessScore).toBe(100);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("computes 0 when no tools available", () => {
|
|
90
|
+
const model = createMockModel({
|
|
91
|
+
tools: [
|
|
92
|
+
{ name: "bash", available: false },
|
|
93
|
+
{ name: "git", available: false },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
98
|
+
|
|
99
|
+
expect(result.readinessScore).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("computes 50 when half tools available", () => {
|
|
103
|
+
const model = createMockModel({
|
|
104
|
+
tools: [
|
|
105
|
+
{ name: "bash", available: true },
|
|
106
|
+
{ name: "git", available: false },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
111
|
+
|
|
112
|
+
expect(result.readinessScore).toBe(50);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("reduces score for high-risk constraints", () => {
|
|
116
|
+
const model = createMockModel({
|
|
117
|
+
tools: [
|
|
118
|
+
{ name: "bash", available: true },
|
|
119
|
+
{ name: "git", available: true },
|
|
120
|
+
],
|
|
121
|
+
constraints: [
|
|
122
|
+
{ type: "auth", description: "GitHub not authenticated", severity: "high" },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
127
|
+
|
|
128
|
+
expect(result.readinessScore).toBeLessThan(100);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("constraint analysis", () => {
|
|
133
|
+
it("flags high-risk constraints", () => {
|
|
134
|
+
const highRisk: Constraint = {
|
|
135
|
+
type: "auth",
|
|
136
|
+
description: "GitHub not authenticated",
|
|
137
|
+
severity: "high",
|
|
138
|
+
};
|
|
139
|
+
const model = createMockModel({
|
|
140
|
+
constraints: [
|
|
141
|
+
highRisk,
|
|
142
|
+
{ type: "network", description: "Slow network", severity: "low" },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = analyzeEnvironment(model, ["bash"]);
|
|
147
|
+
|
|
148
|
+
expect(result.highRiskConstraints).toHaveLength(1);
|
|
149
|
+
expect(result.highRiskConstraints[0]).toBe(highRisk);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns empty highRiskConstraints when none exist", () => {
|
|
153
|
+
const model = createMockModel();
|
|
154
|
+
|
|
155
|
+
const result = analyzeEnvironment(model, ["bash"]);
|
|
156
|
+
|
|
157
|
+
expect(result.highRiskConstraints).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("includes medium severity constraints in preparatory steps", () => {
|
|
161
|
+
const model = createMockModel({
|
|
162
|
+
constraints: [
|
|
163
|
+
{ type: "network", description: "Rate limit approaching", severity: "medium" },
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = analyzeEnvironment(model, ["bash"]);
|
|
168
|
+
|
|
169
|
+
expect(result.preparatorySteps.some(s => s.includes("Rate limit"))).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("preparatory steps", () => {
|
|
174
|
+
it("suggests steps for missing tools", () => {
|
|
175
|
+
const model = createMockModel({
|
|
176
|
+
tools: [{ name: "bash", available: true }],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = analyzeEnvironment(model, ["bash", "docker"]);
|
|
180
|
+
|
|
181
|
+
expect(result.preparatorySteps.some(s => s.includes("docker"))).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("suggests steps for high-risk constraints", () => {
|
|
185
|
+
const model = createMockModel({
|
|
186
|
+
constraints: [
|
|
187
|
+
{ type: "auth", description: "GitHub not authenticated", severity: "high" },
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = analyzeEnvironment(model, ["bash"]);
|
|
192
|
+
|
|
193
|
+
expect(result.preparatorySteps.some(s => s.includes("GitHub"))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("returns empty preparatory steps when environment is ready", () => {
|
|
197
|
+
const model = createMockModel();
|
|
198
|
+
|
|
199
|
+
const result = analyzeEnvironment(model, ["bash", "git"]);
|
|
200
|
+
|
|
201
|
+
expect(result.preparatorySteps).toHaveLength(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { discoverEnvironment } from "../../src/environment/discovery.js";
|
|
3
|
+
import type { DiscoveryOptions } from "../../src/environment/types.js";
|
|
4
|
+
|
|
5
|
+
describe("discoverEnvironment", () => {
|
|
6
|
+
describe("tool detection", () => {
|
|
7
|
+
it("detects available tools (bash should be available)", async () => {
|
|
8
|
+
const model = await discoverEnvironment();
|
|
9
|
+
|
|
10
|
+
const bashTool = model.tools.find(t => t.name === "bash");
|
|
11
|
+
expect(bashTool).toBeDefined();
|
|
12
|
+
expect(bashTool!.available).toBe(true);
|
|
13
|
+
expect(bashTool!.version).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("detects git as available", async () => {
|
|
17
|
+
const model = await discoverEnvironment();
|
|
18
|
+
|
|
19
|
+
const gitTool = model.tools.find(t => t.name === "git");
|
|
20
|
+
expect(gitTool).toBeDefined();
|
|
21
|
+
expect(gitTool!.available).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("detects node as available", async () => {
|
|
25
|
+
const model = await discoverEnvironment();
|
|
26
|
+
|
|
27
|
+
const nodeTool = model.tools.find(t => t.name === "node");
|
|
28
|
+
expect(nodeTool).toBeDefined();
|
|
29
|
+
expect(nodeTool!.available).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("reports missing tools as unavailable", async () => {
|
|
33
|
+
const options: DiscoveryOptions = {
|
|
34
|
+
tools: ["bash", "nonexistent-tool-xyz-123"],
|
|
35
|
+
};
|
|
36
|
+
const model = await discoverEnvironment(undefined, options);
|
|
37
|
+
|
|
38
|
+
const missingTool = model.tools.find(t => t.name === "nonexistent-tool-xyz-123");
|
|
39
|
+
expect(missingTool).toBeDefined();
|
|
40
|
+
expect(missingTool!.available).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("uses default tool set when no tools specified", async () => {
|
|
44
|
+
const model = await discoverEnvironment();
|
|
45
|
+
|
|
46
|
+
const defaultTools = ["bash", "git", "node"];
|
|
47
|
+
for (const toolName of defaultTools) {
|
|
48
|
+
const tool = model.tools.find(t => t.name === toolName);
|
|
49
|
+
expect(tool).toBeDefined();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("discovers custom tools list", async () => {
|
|
54
|
+
const options: DiscoveryOptions = {
|
|
55
|
+
tools: ["bash", "node"],
|
|
56
|
+
};
|
|
57
|
+
const model = await discoverEnvironment(undefined, options);
|
|
58
|
+
|
|
59
|
+
expect(model.tools).toHaveLength(2);
|
|
60
|
+
expect(model.tools.map(t => t.name)).toContain("bash");
|
|
61
|
+
expect(model.tools.map(t => t.name)).toContain("node");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("resource detection", () => {
|
|
66
|
+
it("detects disk resource", async () => {
|
|
67
|
+
const model = await discoverEnvironment();
|
|
68
|
+
|
|
69
|
+
const diskResource = model.resources.find(r => r.type === "disk");
|
|
70
|
+
expect(diskResource).toBeDefined();
|
|
71
|
+
expect(diskResource!.available).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("repo state detection", () => {
|
|
76
|
+
it("detects repo state when repoPath provided", async () => {
|
|
77
|
+
const model = await discoverEnvironment(process.cwd());
|
|
78
|
+
|
|
79
|
+
expect(model.repoState).toBeDefined();
|
|
80
|
+
expect(model.repoState!.path).toBe(process.cwd());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not set repoState when no repoPath provided", async () => {
|
|
84
|
+
const model = await discoverEnvironment();
|
|
85
|
+
|
|
86
|
+
expect(model.repoState).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("detects branch name in a git repo", async () => {
|
|
90
|
+
const model = await discoverEnvironment(process.cwd());
|
|
91
|
+
|
|
92
|
+
expect(model.repoState!.branch).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("detects remotes in a git repo", async () => {
|
|
96
|
+
const model = await discoverEnvironment(process.cwd());
|
|
97
|
+
|
|
98
|
+
expect(Array.isArray(model.repoState!.remotes)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("external system probing", () => {
|
|
103
|
+
it("probes external systems when specified", async () => {
|
|
104
|
+
const options: DiscoveryOptions = {
|
|
105
|
+
externalSystems: ["example.com"],
|
|
106
|
+
networkTimeoutMS: 2000,
|
|
107
|
+
};
|
|
108
|
+
const model = await discoverEnvironment(undefined, options);
|
|
109
|
+
|
|
110
|
+
const exampleSystem = model.externalSystems.find(s => s.name === "example.com");
|
|
111
|
+
expect(exampleSystem).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("reports unreachable systems", async () => {
|
|
115
|
+
const options: DiscoveryOptions = {
|
|
116
|
+
externalSystems: ["192.0.2.1"],
|
|
117
|
+
networkTimeoutMS: 500,
|
|
118
|
+
};
|
|
119
|
+
const model = await discoverEnvironment(undefined, options);
|
|
120
|
+
|
|
121
|
+
const unreachableSystem = model.externalSystems.find(s => s.name === "192.0.2.1");
|
|
122
|
+
expect(unreachableSystem).toBeDefined();
|
|
123
|
+
expect(unreachableSystem!.reachable).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("model metadata", () => {
|
|
128
|
+
it("sets discoveredAt timestamp", async () => {
|
|
129
|
+
const before = Date.now();
|
|
130
|
+
const model = await discoverEnvironment();
|
|
131
|
+
const after = Date.now();
|
|
132
|
+
|
|
133
|
+
expect(model.discoveredAt).toBeGreaterThanOrEqual(before);
|
|
134
|
+
expect(model.discoveredAt).toBeLessThanOrEqual(after);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("includes auth state array", async () => {
|
|
138
|
+
const model = await discoverEnvironment();
|
|
139
|
+
|
|
140
|
+
expect(Array.isArray(model.authState)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("includes constraints array", async () => {
|
|
144
|
+
const model = await discoverEnvironment();
|
|
145
|
+
|
|
146
|
+
expect(Array.isArray(model.constraints)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|