gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.97e9e30
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/dist/resources/extensions/gsd/auto/loop.js +80 -0
- package/dist/resources/extensions/gsd/auto/phases.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
- package/dist/resources/extensions/gsd/auto.js +28 -1
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +91 -0
- package/src/resources/extensions/gsd/auto/phases.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto.ts +31 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → PXrI5DoWsm7rwAVnEU2rD}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* e2e-workflow-pipeline-integration.test.ts — End-to-end integration test
|
|
3
|
+
* proving the assembled workflow engine pipeline works.
|
|
4
|
+
*
|
|
5
|
+
* Exercises every engine feature in a single multi-step workflow:
|
|
6
|
+
* - Dependency-ordered dispatch
|
|
7
|
+
* - Parameter substitution ({{target}})
|
|
8
|
+
* - Content-heuristic verification (minSize)
|
|
9
|
+
* - Shell-command verification (test -f)
|
|
10
|
+
* - Context injection via context_from
|
|
11
|
+
* - Iterate/fan-out expansion
|
|
12
|
+
* - Dashboard metadata (step N/M)
|
|
13
|
+
* - Completion detection (isComplete: true)
|
|
14
|
+
*
|
|
15
|
+
* Operates at the engine level (CustomWorkflowEngine + CustomExecutionPolicy
|
|
16
|
+
* + real temp directories) — NOT through autoLoop() — to avoid the
|
|
17
|
+
* timing-dependent resolveAgentEnd pattern that causes flakiness.
|
|
18
|
+
*
|
|
19
|
+
* Follows the pattern from iterate-engine-integration.test.ts:
|
|
20
|
+
* real temp dirs via mkdtempSync, dispatch()/reconcile() helpers, afterEach cleanup.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, afterEach } from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import {
|
|
26
|
+
mkdtempSync,
|
|
27
|
+
rmSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
existsSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { tmpdir } from "node:os";
|
|
35
|
+
import { stringify, parse } from "yaml";
|
|
36
|
+
|
|
37
|
+
import { CustomWorkflowEngine } from "../custom-workflow-engine.ts";
|
|
38
|
+
import { CustomExecutionPolicy } from "../custom-execution-policy.ts";
|
|
39
|
+
import { createRun, listRuns } from "../run-manager.ts";
|
|
40
|
+
import { readGraph, writeGraph } from "../graph.ts";
|
|
41
|
+
import { validateDefinition } from "../definition-loader.ts";
|
|
42
|
+
|
|
43
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const tmpDirs: string[] = [];
|
|
46
|
+
|
|
47
|
+
function makeTmpDir(): string {
|
|
48
|
+
const dir = mkdtempSync(join(tmpdir(), "e2e-pipeline-"));
|
|
49
|
+
tmpDirs.push(dir);
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
for (const d of tmpDirs) {
|
|
55
|
+
try { rmSync(d, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
|
|
56
|
+
}
|
|
57
|
+
tmpDirs.length = 0;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Drive deriveState → resolveDispatch. */
|
|
61
|
+
async function dispatch(engine: CustomWorkflowEngine) {
|
|
62
|
+
const state = await engine.deriveState("/unused");
|
|
63
|
+
return { state, result: engine.resolveDispatch(state, { basePath: "/unused" }) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Drive deriveState → reconcile for a given unitId. */
|
|
67
|
+
async function reconcile(engine: CustomWorkflowEngine, unitId: string) {
|
|
68
|
+
const state = await engine.deriveState("/unused");
|
|
69
|
+
return engine.reconcile(state, {
|
|
70
|
+
unitType: "custom-step",
|
|
71
|
+
unitId,
|
|
72
|
+
startedAt: Date.now() - 1000,
|
|
73
|
+
finishedAt: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── The multi-feature YAML definition (snake_case for loadDefinition) ───
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 4-step workflow definition exercising every engine feature:
|
|
81
|
+
*
|
|
82
|
+
* gather → scan (iterate) → analyze (context_from scan) → report (context_from analyze)
|
|
83
|
+
*
|
|
84
|
+
* Note: The scan step prompt uses a literal string instead of {{item}} in the
|
|
85
|
+
* definition YAML because substituteParams() checks for unresolved {{key}}
|
|
86
|
+
* placeholders. After createRun, we patch GRAPH.yaml to add the {{item}}
|
|
87
|
+
* placeholder so iterate expansion produces item-specific prompts.
|
|
88
|
+
*/
|
|
89
|
+
const E2E_DEFINITION_YAML = `
|
|
90
|
+
version: 1
|
|
91
|
+
name: e2e-pipeline
|
|
92
|
+
description: End-to-end integration test workflow
|
|
93
|
+
params:
|
|
94
|
+
target: default-target
|
|
95
|
+
steps:
|
|
96
|
+
- id: gather
|
|
97
|
+
name: Gather Information
|
|
98
|
+
prompt: "Gather information about {{target}} and produce a bullet list of findings"
|
|
99
|
+
requires: []
|
|
100
|
+
produces:
|
|
101
|
+
- output/gather-results.md
|
|
102
|
+
verify:
|
|
103
|
+
policy: content-heuristic
|
|
104
|
+
minSize: 10
|
|
105
|
+
- id: scan
|
|
106
|
+
name: Scan Items
|
|
107
|
+
prompt: "Scan item: ITEM_PLACEHOLDER"
|
|
108
|
+
requires:
|
|
109
|
+
- gather
|
|
110
|
+
produces:
|
|
111
|
+
- output/scan-result.txt
|
|
112
|
+
verify:
|
|
113
|
+
policy: shell-command
|
|
114
|
+
command: "test -f output/scan-result.txt"
|
|
115
|
+
iterate:
|
|
116
|
+
source: output/gather-results.md
|
|
117
|
+
pattern: "^- (.+)$"
|
|
118
|
+
- id: analyze
|
|
119
|
+
name: Analyze Results
|
|
120
|
+
prompt: "Analyze all scan results and produce a summary"
|
|
121
|
+
requires:
|
|
122
|
+
- scan
|
|
123
|
+
produces:
|
|
124
|
+
- output/analysis.md
|
|
125
|
+
context_from:
|
|
126
|
+
- scan
|
|
127
|
+
verify:
|
|
128
|
+
policy: content-heuristic
|
|
129
|
+
minSize: 5
|
|
130
|
+
- id: report
|
|
131
|
+
name: Final Report
|
|
132
|
+
prompt: "Write final report for {{target}}"
|
|
133
|
+
requires:
|
|
134
|
+
- analyze
|
|
135
|
+
produces:
|
|
136
|
+
- output/report.md
|
|
137
|
+
context_from:
|
|
138
|
+
- analyze
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a temp project directory with the e2e-pipeline definition YAML,
|
|
143
|
+
* call createRun with param overrides, and patch GRAPH.yaml so the scan
|
|
144
|
+
* step's prompt contains {{item}} for iterate expansion.
|
|
145
|
+
*/
|
|
146
|
+
function setupProject(overrides?: Record<string, string>): {
|
|
147
|
+
basePath: string;
|
|
148
|
+
runDir: string;
|
|
149
|
+
} {
|
|
150
|
+
const basePath = makeTmpDir();
|
|
151
|
+
const defsDir = join(basePath, ".gsd", "workflow-defs");
|
|
152
|
+
mkdirSync(defsDir, { recursive: true });
|
|
153
|
+
writeFileSync(join(defsDir, "e2e-pipeline.yaml"), E2E_DEFINITION_YAML, "utf-8");
|
|
154
|
+
|
|
155
|
+
const runDir = createRun(basePath, "e2e-pipeline", overrides);
|
|
156
|
+
|
|
157
|
+
// Patch GRAPH.yaml: replace the scan step's placeholder with {{item}}
|
|
158
|
+
// so iterate expansion produces item-specific prompts. This works around
|
|
159
|
+
// substituteParams() rejecting unresolved {{item}} in the definition.
|
|
160
|
+
const graph = readGraph(runDir);
|
|
161
|
+
const scanStep = graph.steps.find((s) => s.id === "scan");
|
|
162
|
+
if (scanStep) {
|
|
163
|
+
scanStep.prompt = "Scan item: {{item}}";
|
|
164
|
+
writeGraph(runDir, graph);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { basePath, runDir };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Tests ───────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe("e2e-workflow-pipeline", () => {
|
|
173
|
+
it("drives the full engine pipeline: create → dispatch → verify → complete", async () => {
|
|
174
|
+
// ── 1. Create run with param overrides ────────────────────────────
|
|
175
|
+
const { basePath, runDir } = setupProject({ target: "my-project" });
|
|
176
|
+
|
|
177
|
+
// Verify run directory structure
|
|
178
|
+
assert.ok(existsSync(join(runDir, "DEFINITION.yaml")), "DEFINITION.yaml should exist");
|
|
179
|
+
assert.ok(existsSync(join(runDir, "GRAPH.yaml")), "GRAPH.yaml should exist");
|
|
180
|
+
assert.ok(existsSync(join(runDir, "PARAMS.json")), "PARAMS.json should exist");
|
|
181
|
+
|
|
182
|
+
// Verify PARAMS.json has the override
|
|
183
|
+
const params = JSON.parse(readFileSync(join(runDir, "PARAMS.json"), "utf-8"));
|
|
184
|
+
assert.deepStrictEqual(params, { target: "my-project" });
|
|
185
|
+
|
|
186
|
+
// Verify the frozen DEFINITION.yaml has substituted params in non-iterate steps
|
|
187
|
+
const frozenDef = readFileSync(join(runDir, "DEFINITION.yaml"), "utf-8");
|
|
188
|
+
assert.ok(
|
|
189
|
+
frozenDef.includes("my-project"),
|
|
190
|
+
"Frozen definition should have substituted 'my-project' for {{target}}",
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Instantiate engine and policy
|
|
194
|
+
const engine = new CustomWorkflowEngine(runDir);
|
|
195
|
+
const policy = new CustomExecutionPolicy(runDir);
|
|
196
|
+
|
|
197
|
+
// Verify initial graph has 4 steps all pending
|
|
198
|
+
const initialGraph = readGraph(runDir);
|
|
199
|
+
assert.equal(initialGraph.steps.length, 4, "Initial graph should have 4 steps");
|
|
200
|
+
assert.ok(
|
|
201
|
+
initialGraph.steps.every((s) => s.status === "pending"),
|
|
202
|
+
"All steps should start as pending",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Verify initial state is not complete
|
|
206
|
+
let state = await engine.deriveState("/unused");
|
|
207
|
+
assert.equal(state.isComplete, false, "Workflow should not be complete initially");
|
|
208
|
+
|
|
209
|
+
// Dashboard metadata: 0/4 initially
|
|
210
|
+
let meta = engine.getDisplayMetadata(state);
|
|
211
|
+
assert.equal(meta.stepCount!.completed, 0);
|
|
212
|
+
assert.equal(meta.stepCount!.total, 4);
|
|
213
|
+
assert.equal(meta.progressSummary, "Step 0/4");
|
|
214
|
+
|
|
215
|
+
// ── 2. Step 1: gather ─────────────────────────────────────────────
|
|
216
|
+
const { result: r1 } = await dispatch(engine);
|
|
217
|
+
const d1 = await r1;
|
|
218
|
+
assert.equal(d1.action, "dispatch", "Should dispatch gather step");
|
|
219
|
+
if (d1.action !== "dispatch") throw new Error("unreachable");
|
|
220
|
+
|
|
221
|
+
assert.equal(d1.step.unitId, "e2e-pipeline/gather");
|
|
222
|
+
assert.ok(
|
|
223
|
+
d1.step.prompt.includes("my-project"),
|
|
224
|
+
`Gather prompt should contain substituted param "my-project", got: "${d1.step.prompt}"`,
|
|
225
|
+
);
|
|
226
|
+
assert.ok(
|
|
227
|
+
!d1.step.prompt.includes("default-target"),
|
|
228
|
+
"Gather prompt should NOT contain default param value",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Simulate agent work: write the gather artifact with bullet items for iterate
|
|
232
|
+
const outputDir = join(runDir, "output");
|
|
233
|
+
mkdirSync(outputDir, { recursive: true });
|
|
234
|
+
writeFileSync(
|
|
235
|
+
join(runDir, "output/gather-results.md"),
|
|
236
|
+
"# Findings for my-project\n\n- security-audit\n- performance-review\n- code-quality\n",
|
|
237
|
+
"utf-8",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Reconcile gather
|
|
241
|
+
await reconcile(engine, "e2e-pipeline/gather");
|
|
242
|
+
|
|
243
|
+
// Verify gather: content-heuristic (minSize: 10) should pass
|
|
244
|
+
const gatherVerify = await policy.verify("custom-step", "e2e-pipeline/gather", {
|
|
245
|
+
basePath: "/unused",
|
|
246
|
+
});
|
|
247
|
+
assert.equal(
|
|
248
|
+
gatherVerify,
|
|
249
|
+
"continue",
|
|
250
|
+
"Gather verification (content-heuristic) should pass",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Dashboard after gather: 1 completed (gather), total still 4
|
|
254
|
+
state = await engine.deriveState("/unused");
|
|
255
|
+
meta = engine.getDisplayMetadata(state);
|
|
256
|
+
assert.equal(meta.stepCount!.completed, 1);
|
|
257
|
+
assert.equal(meta.progressSummary, "Step 1/4");
|
|
258
|
+
assert.equal(state.isComplete, false);
|
|
259
|
+
|
|
260
|
+
// ── 3. Step 2: scan with iterate ──────────────────────────────────
|
|
261
|
+
// Dispatch should trigger iterate expansion from gather-results.md
|
|
262
|
+
const { result: r2 } = await dispatch(engine);
|
|
263
|
+
const d2 = await r2;
|
|
264
|
+
assert.equal(d2.action, "dispatch", "Should dispatch first scan instance");
|
|
265
|
+
if (d2.action !== "dispatch") throw new Error("unreachable");
|
|
266
|
+
|
|
267
|
+
// First instance should be scan--001 for "security-audit"
|
|
268
|
+
assert.equal(d2.step.unitId, "e2e-pipeline/scan--001");
|
|
269
|
+
assert.ok(
|
|
270
|
+
d2.step.prompt.includes("security-audit"),
|
|
271
|
+
`First scan instance prompt should contain "security-audit", got: "${d2.step.prompt}"`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Verify graph expanded: parent "scan" is "expanded", 3 instances exist
|
|
275
|
+
let graph = readGraph(runDir);
|
|
276
|
+
const scanParent = graph.steps.find((s) => s.id === "scan");
|
|
277
|
+
assert.ok(scanParent, "Parent scan step should exist");
|
|
278
|
+
assert.equal(scanParent.status, "expanded", "Parent scan should be expanded");
|
|
279
|
+
|
|
280
|
+
const scanInstances = graph.steps.filter((s) => s.parentStepId === "scan");
|
|
281
|
+
assert.equal(scanInstances.length, 3, "Should have 3 scan instances");
|
|
282
|
+
assert.equal(scanInstances[0].id, "scan--001");
|
|
283
|
+
assert.equal(scanInstances[1].id, "scan--002");
|
|
284
|
+
assert.equal(scanInstances[2].id, "scan--003");
|
|
285
|
+
|
|
286
|
+
// Verify iterate prompts contain item-specific content
|
|
287
|
+
assert.ok(scanInstances[0].prompt.includes("security-audit"));
|
|
288
|
+
assert.ok(scanInstances[1].prompt.includes("performance-review"));
|
|
289
|
+
assert.ok(scanInstances[2].prompt.includes("code-quality"));
|
|
290
|
+
|
|
291
|
+
// Verify dependency rewriting: analyze should now depend on scan--001, scan--002, scan--003
|
|
292
|
+
const analyzeStep = graph.steps.find((s) => s.id === "analyze");
|
|
293
|
+
assert.ok(analyzeStep);
|
|
294
|
+
assert.deepStrictEqual(
|
|
295
|
+
analyzeStep.dependsOn.sort(),
|
|
296
|
+
["scan--001", "scan--002", "scan--003"],
|
|
297
|
+
"Analyze should depend on all scan instances after expansion",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Graph step count increased: 4 original + 3 instances = 7 (parent stays as "expanded")
|
|
301
|
+
assert.equal(graph.steps.length, 7, "Graph should have 7 steps after expansion");
|
|
302
|
+
|
|
303
|
+
// Dashboard after expansion: total now includes instance steps
|
|
304
|
+
state = await engine.deriveState("/unused");
|
|
305
|
+
meta = engine.getDisplayMetadata(state);
|
|
306
|
+
// completed: gather(1), expanded steps don't count as "complete" in getDisplayMetadata
|
|
307
|
+
assert.equal(meta.stepCount!.completed, 1, "Only gather should be complete");
|
|
308
|
+
|
|
309
|
+
// Write scan artifact (same path for all instances since the verify command checks run-dir-relative path)
|
|
310
|
+
writeFileSync(join(runDir, "output/scan-result.txt"), "scan output data", "utf-8");
|
|
311
|
+
|
|
312
|
+
// Complete scan--001, dispatch scan--002
|
|
313
|
+
await reconcile(engine, "e2e-pipeline/scan--001");
|
|
314
|
+
|
|
315
|
+
// Verify analyze is still blocked (not all scan instances complete)
|
|
316
|
+
const { result: r3a } = await dispatch(engine);
|
|
317
|
+
const d3a = await r3a;
|
|
318
|
+
assert.equal(d3a.action, "dispatch");
|
|
319
|
+
if (d3a.action !== "dispatch") throw new Error("unreachable");
|
|
320
|
+
assert.equal(
|
|
321
|
+
d3a.step.unitId,
|
|
322
|
+
"e2e-pipeline/scan--002",
|
|
323
|
+
"Should dispatch scan--002 (analyze still blocked)",
|
|
324
|
+
);
|
|
325
|
+
assert.ok(d3a.step.prompt.includes("performance-review"));
|
|
326
|
+
|
|
327
|
+
// Complete scan--002, dispatch scan--003
|
|
328
|
+
await reconcile(engine, "e2e-pipeline/scan--002");
|
|
329
|
+
const { result: r3b } = await dispatch(engine);
|
|
330
|
+
const d3b = await r3b;
|
|
331
|
+
assert.equal(d3b.action, "dispatch");
|
|
332
|
+
if (d3b.action !== "dispatch") throw new Error("unreachable");
|
|
333
|
+
assert.equal(d3b.step.unitId, "e2e-pipeline/scan--003");
|
|
334
|
+
assert.ok(d3b.step.prompt.includes("code-quality"));
|
|
335
|
+
|
|
336
|
+
// Complete scan--003 — now analyze should be unblocked
|
|
337
|
+
await reconcile(engine, "e2e-pipeline/scan--003");
|
|
338
|
+
|
|
339
|
+
// Dashboard after all scan instances: 4 complete (gather + 3 instances)
|
|
340
|
+
state = await engine.deriveState("/unused");
|
|
341
|
+
meta = engine.getDisplayMetadata(state);
|
|
342
|
+
assert.equal(meta.stepCount!.completed, 4, "gather + 3 scan instances should be complete");
|
|
343
|
+
assert.equal(state.isComplete, false);
|
|
344
|
+
|
|
345
|
+
// ── 4. Step 3: analyze (with context_from scan) ───────────────────
|
|
346
|
+
const { result: r4 } = await dispatch(engine);
|
|
347
|
+
const d4 = await r4;
|
|
348
|
+
assert.equal(d4.action, "dispatch", "Should dispatch analyze step");
|
|
349
|
+
if (d4.action !== "dispatch") throw new Error("unreachable");
|
|
350
|
+
|
|
351
|
+
assert.equal(d4.step.unitId, "e2e-pipeline/analyze");
|
|
352
|
+
|
|
353
|
+
// Context injection: the analyze prompt should include content from scan's produces
|
|
354
|
+
// scan produces output/scan-result.txt and context_from references "scan"
|
|
355
|
+
assert.ok(
|
|
356
|
+
d4.step.prompt.includes("scan output data"),
|
|
357
|
+
`Analyze prompt should include injected context from scan artifact, got: "${d4.step.prompt.slice(0, 200)}"`,
|
|
358
|
+
);
|
|
359
|
+
assert.ok(
|
|
360
|
+
d4.step.prompt.includes("Analyze all scan results"),
|
|
361
|
+
"Analyze prompt should still contain the original prompt text",
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Write analyze artifact
|
|
365
|
+
writeFileSync(
|
|
366
|
+
join(runDir, "output/analysis.md"),
|
|
367
|
+
"# Analysis Summary\n\nAll scans completed successfully with findings.\n",
|
|
368
|
+
"utf-8",
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
await reconcile(engine, "e2e-pipeline/analyze");
|
|
372
|
+
|
|
373
|
+
// Verify analyze: content-heuristic (minSize: 5) should pass
|
|
374
|
+
const analyzeVerify = await policy.verify("custom-step", "e2e-pipeline/analyze", {
|
|
375
|
+
basePath: "/unused",
|
|
376
|
+
});
|
|
377
|
+
assert.equal(
|
|
378
|
+
analyzeVerify,
|
|
379
|
+
"continue",
|
|
380
|
+
"Analyze verification (content-heuristic) should pass",
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Dashboard after analyze: 5 complete
|
|
384
|
+
state = await engine.deriveState("/unused");
|
|
385
|
+
meta = engine.getDisplayMetadata(state);
|
|
386
|
+
assert.equal(meta.stepCount!.completed, 5);
|
|
387
|
+
assert.equal(state.isComplete, false, "Should not be complete yet (report remaining)");
|
|
388
|
+
|
|
389
|
+
// ── 5. Step 4: report (with context_from analyze + param) ─────────
|
|
390
|
+
const { result: r5 } = await dispatch(engine);
|
|
391
|
+
const d5 = await r5;
|
|
392
|
+
assert.equal(d5.action, "dispatch", "Should dispatch report step");
|
|
393
|
+
if (d5.action !== "dispatch") throw new Error("unreachable");
|
|
394
|
+
|
|
395
|
+
assert.equal(d5.step.unitId, "e2e-pipeline/report");
|
|
396
|
+
|
|
397
|
+
// Context injection: report prompt should include content from analyze's produces
|
|
398
|
+
assert.ok(
|
|
399
|
+
d5.step.prompt.includes("Analysis Summary"),
|
|
400
|
+
`Report prompt should include injected context from analyze artifact, got: "${d5.step.prompt.slice(0, 200)}"`,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Parameter substitution: report prompt should contain "my-project"
|
|
404
|
+
assert.ok(
|
|
405
|
+
d5.step.prompt.includes("my-project"),
|
|
406
|
+
`Report prompt should contain substituted param "my-project", got: "${d5.step.prompt}"`,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Write report artifact
|
|
410
|
+
writeFileSync(
|
|
411
|
+
join(runDir, "output/report.md"),
|
|
412
|
+
"# Final Report for my-project\n\nComprehensive findings documented.\n",
|
|
413
|
+
"utf-8",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
await reconcile(engine, "e2e-pipeline/report");
|
|
417
|
+
|
|
418
|
+
// ── 6. Completion ─────────────────────────────────────────────────
|
|
419
|
+
state = await engine.deriveState("/unused");
|
|
420
|
+
assert.equal(state.isComplete, true, "Workflow should be complete after all steps");
|
|
421
|
+
assert.equal(state.phase, "complete");
|
|
422
|
+
|
|
423
|
+
// Dashboard: all steps complete
|
|
424
|
+
meta = engine.getDisplayMetadata(state);
|
|
425
|
+
assert.equal(meta.stepCount!.completed, 6, "All 6 dispatchable steps should be complete");
|
|
426
|
+
assert.equal(meta.currentPhase, "complete");
|
|
427
|
+
|
|
428
|
+
// Dispatch should return stop
|
|
429
|
+
const { result: rFinal } = await dispatch(engine);
|
|
430
|
+
const dFinal = await rFinal;
|
|
431
|
+
assert.equal(dFinal.action, "stop");
|
|
432
|
+
if (dFinal.action === "stop") {
|
|
433
|
+
assert.equal(dFinal.reason, "All steps complete");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Verify shell-command policy works on the scan step (parent, not instance)
|
|
437
|
+
const shellVerify = await policy.verify("custom-step", "e2e-pipeline/scan", {
|
|
438
|
+
basePath: "/unused",
|
|
439
|
+
});
|
|
440
|
+
assert.equal(
|
|
441
|
+
shellVerify,
|
|
442
|
+
"continue",
|
|
443
|
+
"Shell-command verification (test -f output/scan-result.txt) should pass",
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("createRun + listRuns integration", () => {
|
|
448
|
+
it("created run appears in listRuns with correct metadata", () => {
|
|
449
|
+
const { basePath, runDir } = setupProject({ target: "list-test" });
|
|
450
|
+
|
|
451
|
+
const runs = listRuns(basePath, "e2e-pipeline");
|
|
452
|
+
assert.ok(runs.length >= 1, "Should list at least one run");
|
|
453
|
+
|
|
454
|
+
const thisRun = runs.find((r) => r.runDir === runDir);
|
|
455
|
+
assert.ok(thisRun, "Created run should appear in listRuns");
|
|
456
|
+
assert.equal(thisRun.name, "e2e-pipeline");
|
|
457
|
+
assert.equal(thisRun.status, "pending", "New run should have pending status");
|
|
458
|
+
assert.equal(thisRun.steps.total, 4, "Should have 4 steps");
|
|
459
|
+
assert.equal(thisRun.steps.completed, 0);
|
|
460
|
+
assert.equal(thisRun.steps.pending, 4);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe("validateDefinition accepts the e2e definition", () => {
|
|
465
|
+
it("validates the e2e-pipeline YAML as valid V1 schema", () => {
|
|
466
|
+
const parsed = parse(E2E_DEFINITION_YAML);
|
|
467
|
+
const { valid, errors } = validateDefinition(parsed);
|
|
468
|
+
assert.equal(
|
|
469
|
+
valid,
|
|
470
|
+
true,
|
|
471
|
+
`Definition should be valid but got errors: ${errors.join(", ")}`,
|
|
472
|
+
);
|
|
473
|
+
assert.deepStrictEqual(errors, []);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|