@davidorex/pi-workflows 0.2.0 → 0.3.1
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/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +178 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +171 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +63 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +136 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +738 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +457 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +476 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +107 -0
- package/skills/pi-workflows/SKILL.md +185 -0
- package/skills/pi-workflows/references/bundled-resources.md +87 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +94 -0
- package/workflows/create-handoff.workflow.yaml +66 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +81 -0
- package/workflows/plan-from-requirements.workflow.yaml +59 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
package/src/output.test.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { persistStepOutput } from "./output.ts";
|
|
7
|
-
|
|
8
|
-
describe("persistStepOutput", () => {
|
|
9
|
-
it("writes structured output as JSON", (t) => {
|
|
10
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
11
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
12
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
13
|
-
|
|
14
|
-
const result = persistStepOutput(tmpDir, "analyze", { findings: ["a", "b"] });
|
|
15
|
-
assert.ok(result);
|
|
16
|
-
assert.ok(result.endsWith("analyze.json"));
|
|
17
|
-
const content = JSON.parse(fs.readFileSync(result, "utf-8"));
|
|
18
|
-
assert.deepStrictEqual(content, { findings: ["a", "b"] });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("wraps string output as JSON", (t) => {
|
|
22
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
23
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
24
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
25
|
-
|
|
26
|
-
const result = persistStepOutput(tmpDir, "explore", undefined, "Found patterns");
|
|
27
|
-
assert.ok(result);
|
|
28
|
-
assert.ok(result.endsWith("explore.json"));
|
|
29
|
-
const content = JSON.parse(fs.readFileSync(result, "utf-8"));
|
|
30
|
-
assert.deepStrictEqual(content, { text: "Found patterns" });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("prefers structured output over textOutput", (t) => {
|
|
34
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
35
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
36
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
37
|
-
|
|
38
|
-
const result = persistStepOutput(tmpDir, "step1", { key: "val" }, "text fallback");
|
|
39
|
-
assert.ok(result);
|
|
40
|
-
const content = JSON.parse(fs.readFileSync(result, "utf-8"));
|
|
41
|
-
assert.deepStrictEqual(content, { key: "val" });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("wraps string output field as JSON", (t) => {
|
|
45
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
46
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
47
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
48
|
-
|
|
49
|
-
const result = persistStepOutput(tmpDir, "step1", "plain string");
|
|
50
|
-
assert.ok(result);
|
|
51
|
-
assert.ok(result.endsWith(".json"));
|
|
52
|
-
const content = JSON.parse(fs.readFileSync(result, "utf-8"));
|
|
53
|
-
assert.deepStrictEqual(content, { text: "plain string" });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns undefined when nothing to write", (t) => {
|
|
57
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
58
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
59
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
60
|
-
assert.strictEqual(persistStepOutput(tmpDir, "empty", undefined, undefined), undefined);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("returns undefined for empty string", (t) => {
|
|
64
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
65
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
66
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
67
|
-
assert.strictEqual(persistStepOutput(tmpDir, "empty", undefined, ""), undefined);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("writes array output as JSON", (t) => {
|
|
71
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
72
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
73
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
74
|
-
|
|
75
|
-
const result = persistStepOutput(tmpDir, "items", ["a", "b", "c"]);
|
|
76
|
-
assert.ok(result);
|
|
77
|
-
assert.ok(result.endsWith(".json"));
|
|
78
|
-
assert.deepStrictEqual(JSON.parse(fs.readFileSync(result, "utf-8")), ["a", "b", "c"]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("all outputs are JSON regardless of input type", (t) => {
|
|
82
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
83
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
84
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
85
|
-
|
|
86
|
-
const r1 = persistStepOutput(tmpDir, "s1", { a: 1 });
|
|
87
|
-
const r2 = persistStepOutput(tmpDir, "s2", "hello");
|
|
88
|
-
const r3 = persistStepOutput(tmpDir, "s3", undefined, "world");
|
|
89
|
-
assert.ok(r1!.endsWith(".json"));
|
|
90
|
-
assert.ok(r2!.endsWith(".json"));
|
|
91
|
-
assert.ok(r3!.endsWith(".json"));
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("creates outputs directory if missing", (t) => {
|
|
95
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
96
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
97
|
-
// Don't create outputs/ — persistStepOutput should handle it
|
|
98
|
-
|
|
99
|
-
const result = persistStepOutput(tmpDir, "auto", { data: true });
|
|
100
|
-
assert.ok(result);
|
|
101
|
-
assert.ok(fs.existsSync(result));
|
|
102
|
-
assert.deepStrictEqual(JSON.parse(fs.readFileSync(result, "utf-8")), { data: true });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("writes to author-declared output path", (t) => {
|
|
106
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
107
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
108
|
-
|
|
109
|
-
const customPath = path.join(tmpDir, "reports", "analysis", "structure.json");
|
|
110
|
-
const result = persistStepOutput(tmpDir, "step1", { key: "val" }, undefined, customPath);
|
|
111
|
-
assert.strictEqual(result, customPath);
|
|
112
|
-
assert.ok(fs.existsSync(customPath));
|
|
113
|
-
assert.deepStrictEqual(JSON.parse(fs.readFileSync(customPath, "utf-8")), { key: "val" });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("creates parent directories for author-declared path", (t) => {
|
|
117
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
118
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
119
|
-
|
|
120
|
-
const deepPath = path.join(tmpDir, "a", "b", "c", "result.json");
|
|
121
|
-
const result = persistStepOutput(tmpDir, "step1", ["x"], undefined, deepPath);
|
|
122
|
-
assert.strictEqual(result, deepPath);
|
|
123
|
-
assert.deepStrictEqual(JSON.parse(fs.readFileSync(deepPath, "utf-8")), ["x"]);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("defaults to run dir when no output path declared", (t) => {
|
|
127
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-"));
|
|
128
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
129
|
-
|
|
130
|
-
const result = persistStepOutput(tmpDir, "my-step", { a: 1 });
|
|
131
|
-
assert.ok(result);
|
|
132
|
-
assert.strictEqual(result, path.join(tmpDir, "outputs", "my-step.json"));
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("persistStepOutput error handling", () => {
|
|
137
|
-
it("returns undefined when directory creation fails", (t) => {
|
|
138
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
|
|
139
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
140
|
-
|
|
141
|
-
// Create a file where the outputs directory should be — mkdirSync will fail with ENOTDIR
|
|
142
|
-
const blocker = path.join(tmpDir, "outputs");
|
|
143
|
-
fs.writeFileSync(blocker, "not a directory");
|
|
144
|
-
|
|
145
|
-
const result = persistStepOutput(tmpDir, "step1", { data: true });
|
|
146
|
-
assert.strictEqual(result, undefined);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("returns undefined when write to author-declared path fails", (t) => {
|
|
150
|
-
// Use /dev/null/impossible on unix — a known-bad path
|
|
151
|
-
const result = persistStepOutput(
|
|
152
|
-
"/tmp", "step1", { data: true }, undefined,
|
|
153
|
-
"/dev/null/impossible/path/output.json",
|
|
154
|
-
);
|
|
155
|
-
assert.strictEqual(result, undefined);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("returns undefined when file write fails due to unwritable directory", (t) => {
|
|
159
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
|
|
160
|
-
t.after(() => {
|
|
161
|
-
// Restore permissions for cleanup
|
|
162
|
-
try { fs.chmodSync(path.join(tmpDir, "outputs"), 0o755); } catch { /* ignore */ }
|
|
163
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Create outputs dir then make it read-only
|
|
167
|
-
const outputsDir = path.join(tmpDir, "outputs");
|
|
168
|
-
fs.mkdirSync(outputsDir, { recursive: true });
|
|
169
|
-
fs.chmodSync(outputsDir, 0o444);
|
|
170
|
-
|
|
171
|
-
const result = persistStepOutput(tmpDir, "step1", { data: true });
|
|
172
|
-
assert.strictEqual(result, undefined);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("writes warning to stderr on failure", (t) => {
|
|
176
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
|
|
177
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
178
|
-
|
|
179
|
-
// Create a file where outputs dir should be
|
|
180
|
-
fs.writeFileSync(path.join(tmpDir, "outputs"), "blocker");
|
|
181
|
-
|
|
182
|
-
// Capture stderr
|
|
183
|
-
const originalWrite = process.stderr.write;
|
|
184
|
-
let captured = "";
|
|
185
|
-
process.stderr.write = ((chunk: string) => {
|
|
186
|
-
captured += chunk;
|
|
187
|
-
return true;
|
|
188
|
-
}) as typeof process.stderr.write;
|
|
189
|
-
t.after(() => { process.stderr.write = originalWrite; });
|
|
190
|
-
|
|
191
|
-
persistStepOutput(tmpDir, "step1", { data: true });
|
|
192
|
-
assert.ok(captured.includes("step1"));
|
|
193
|
-
assert.ok(captured.includes("[pi-workflows]"));
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("returns undefined for string output when directory creation fails", (t) => {
|
|
197
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-output-err-"));
|
|
198
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
199
|
-
|
|
200
|
-
// Create a file where the outputs directory should be
|
|
201
|
-
fs.writeFileSync(path.join(tmpDir, "outputs"), "not a directory");
|
|
202
|
-
|
|
203
|
-
const result = persistStepOutput(tmpDir, "step1", undefined, "some text");
|
|
204
|
-
assert.strictEqual(result, undefined);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("does not throw on write failure — existing tests still pass", () => {
|
|
208
|
-
// Verify that all the existing happy-path tests still work unchanged.
|
|
209
|
-
// This test is a meta-check — if output.test.ts runs without error,
|
|
210
|
-
// the non-throwing behavior is confirmed alongside existing behavior.
|
|
211
|
-
assert.ok(true);
|
|
212
|
-
});
|
|
213
|
-
});
|
package/src/output.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step output persistence.
|
|
3
|
-
*
|
|
4
|
-
* Every step that produces output writes it as JSON to disk.
|
|
5
|
-
* The workflow author controls the output path via the step spec's
|
|
6
|
-
* output.path field. When no path is declared, defaults to
|
|
7
|
-
* outputs/<stepName>.json in the run directory.
|
|
8
|
-
*
|
|
9
|
-
* JSON is the intermediate representation between steps — structured,
|
|
10
|
-
* validated, consumable by templates, filterable by transforms, mergeable
|
|
11
|
-
* across parallel runs. Text is rendered FROM JSON via templates. It's
|
|
12
|
-
* never the data format.
|
|
13
|
-
*/
|
|
14
|
-
import fs from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Persist a step's output as JSON to disk.
|
|
19
|
-
*
|
|
20
|
-
* @param runDir - workflow run directory (default location for outputs)
|
|
21
|
-
* @param stepName - step name (used for default filename)
|
|
22
|
-
* @param output - structured output (object/array) or string
|
|
23
|
-
* @param textOutput - fallback text output if output is undefined
|
|
24
|
-
* @param outputPath - author-declared output path (from step spec output.path, already resolved)
|
|
25
|
-
* @returns absolute path to the written JSON file, or undefined if nothing to write
|
|
26
|
-
*/
|
|
27
|
-
export function persistStepOutput(
|
|
28
|
-
runDir: string,
|
|
29
|
-
stepName: string,
|
|
30
|
-
output: unknown,
|
|
31
|
-
textOutput?: string,
|
|
32
|
-
outputPath?: string,
|
|
33
|
-
): string | undefined {
|
|
34
|
-
// Author-declared path takes priority; default to run dir
|
|
35
|
-
const filePath = outputPath ?? path.join(runDir, "outputs", `${stepName}.json`);
|
|
36
|
-
const dir = path.dirname(filePath);
|
|
37
|
-
|
|
38
|
-
// Structured output — the primary case
|
|
39
|
-
if (output !== undefined && output !== null && typeof output === "object") {
|
|
40
|
-
try {
|
|
41
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
-
fs.writeFileSync(filePath, JSON.stringify(output, null, 2), "utf-8");
|
|
43
|
-
return filePath;
|
|
44
|
-
} catch (err) {
|
|
45
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
-
process.stderr.write(
|
|
47
|
-
`[pi-workflows] Warning: failed to persist output for step '${stepName}' to ${filePath}: ${msg}\n`,
|
|
48
|
-
);
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// String output — wrap in JSON to maintain uniform contract
|
|
54
|
-
const text = typeof output === "string" ? output : textOutput;
|
|
55
|
-
if (text) {
|
|
56
|
-
try {
|
|
57
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
58
|
-
fs.writeFileSync(filePath, JSON.stringify({ text }, null, 2), "utf-8");
|
|
59
|
-
return filePath;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
-
process.stderr.write(
|
|
63
|
-
`[pi-workflows] Warning: failed to persist output for step '${stepName}' to ${filePath}: ${msg}\n`,
|
|
64
|
-
);
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { buildExecutionPlan } from "./dag.ts";
|
|
4
|
-
import { parseWorkflowSpec } from "./workflow-spec.ts";
|
|
5
|
-
import { executeWorkflow } from "./workflow-executor.ts";
|
|
6
|
-
import type { WorkflowSpec } from "./types.ts";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
|
|
11
|
-
// Mock ExtensionContext and ExtensionAPI for testing
|
|
12
|
-
function mockCtx(cwd: string) {
|
|
13
|
-
return {
|
|
14
|
-
cwd,
|
|
15
|
-
hasUI: false,
|
|
16
|
-
ui: {
|
|
17
|
-
setWidget: () => {},
|
|
18
|
-
notify: () => {},
|
|
19
|
-
setStatus: () => {},
|
|
20
|
-
},
|
|
21
|
-
} as any;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function mockPi() {
|
|
25
|
-
const messages: any[] = [];
|
|
26
|
-
return {
|
|
27
|
-
sendMessage: (msg: any, opts: any) => messages.push({ msg, opts }),
|
|
28
|
-
_messages: messages,
|
|
29
|
-
} as any;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mockOptions(tmpDir?: string) {
|
|
33
|
-
const cwd = tmpDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "wf-parallel-"));
|
|
34
|
-
return {
|
|
35
|
-
ctx: mockCtx(cwd),
|
|
36
|
-
pi: mockPi(),
|
|
37
|
-
loadAgent: (name: string) => ({ name }),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function makeSpec(overrides: Partial<WorkflowSpec> & { steps: WorkflowSpec["steps"] }): WorkflowSpec {
|
|
42
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-parallel-"));
|
|
43
|
-
return {
|
|
44
|
-
name: "test-parallel",
|
|
45
|
-
description: "test",
|
|
46
|
-
source: "project" as const,
|
|
47
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe("parallel integration — DAG analysis", () => {
|
|
53
|
-
it("DAG-inferred parallel workflow produces correct execution plan", () => {
|
|
54
|
-
const yaml = fs.readFileSync(
|
|
55
|
-
path.resolve(import.meta.dirname, "../workflows/parallel-analysis.workflow.yaml"),
|
|
56
|
-
"utf-8",
|
|
57
|
-
);
|
|
58
|
-
const spec = parseWorkflowSpec(yaml, "parallel-analysis.workflow.yaml", "project");
|
|
59
|
-
const plan = buildExecutionPlan(spec);
|
|
60
|
-
|
|
61
|
-
// Layer 0: explore (no deps)
|
|
62
|
-
assert.deepStrictEqual(plan[0].steps, ["explore"]);
|
|
63
|
-
|
|
64
|
-
// Layer 1: three analyzers (all depend only on explore)
|
|
65
|
-
assert.strictEqual(plan[1].steps.length, 3);
|
|
66
|
-
assert.ok(plan[1].steps.includes("analyze-structure"));
|
|
67
|
-
assert.ok(plan[1].steps.includes("analyze-quality"));
|
|
68
|
-
assert.ok(plan[1].steps.includes("analyze-patterns"));
|
|
69
|
-
|
|
70
|
-
// Layer 2: synthesize (depends on all three analyzers)
|
|
71
|
-
assert.deepStrictEqual(plan[2].steps, ["synthesize"]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("explicit parallel workflow parses correctly", () => {
|
|
75
|
-
const yaml = fs.readFileSync(
|
|
76
|
-
path.resolve(import.meta.dirname, "../workflows/parallel-explicit.workflow.yaml"),
|
|
77
|
-
"utf-8",
|
|
78
|
-
);
|
|
79
|
-
const spec = parseWorkflowSpec(yaml, "parallel-explicit.workflow.yaml", "project");
|
|
80
|
-
|
|
81
|
-
assert.ok(spec.steps.analyzers);
|
|
82
|
-
assert.ok(spec.steps.analyzers.parallel);
|
|
83
|
-
assert.ok(spec.steps.analyzers.parallel!.structure);
|
|
84
|
-
assert.ok(spec.steps.analyzers.parallel!.quality);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("parallel integration — execution", () => {
|
|
89
|
-
it("parallel gate steps complete faster than sequential", async () => {
|
|
90
|
-
// Two 2-second sleeps in parallel should take ~2s, not ~4s.
|
|
91
|
-
// Steps need explicit ${{ steps.X }} deps to be parallelized
|
|
92
|
-
// (conservative plan adds implicit sequential deps for steps without them).
|
|
93
|
-
const spec = makeSpec({
|
|
94
|
-
name: "timing-test",
|
|
95
|
-
steps: {
|
|
96
|
-
source: {
|
|
97
|
-
transform: { mapping: { ready: true } },
|
|
98
|
-
},
|
|
99
|
-
a: {
|
|
100
|
-
gate: { check: "sleep 2 && echo done-a" },
|
|
101
|
-
input: { trigger: "${{ steps.source.output.ready }}" },
|
|
102
|
-
},
|
|
103
|
-
b: {
|
|
104
|
-
gate: { check: "sleep 2 && echo done-b" },
|
|
105
|
-
input: { trigger: "${{ steps.source.output.ready }}" },
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const startTime = Date.now();
|
|
111
|
-
const result = await executeWorkflow(spec, {}, mockOptions());
|
|
112
|
-
const elapsed = Date.now() - startTime;
|
|
113
|
-
|
|
114
|
-
assert.strictEqual(result.status, "completed");
|
|
115
|
-
// If parallel: ~2s. If sequential: ~4s. Allow margin.
|
|
116
|
-
assert.ok(elapsed < 3500, `Expected parallel execution (~2s) but took ${elapsed}ms`);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("timeout kills a slow gate step", async () => {
|
|
120
|
-
const spec = makeSpec({
|
|
121
|
-
name: "timeout-test",
|
|
122
|
-
steps: {
|
|
123
|
-
slow: {
|
|
124
|
-
gate: { check: "sleep 60 && echo done" },
|
|
125
|
-
timeout: { seconds: 2 },
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const startTime = Date.now();
|
|
131
|
-
const result = await executeWorkflow(spec, {}, mockOptions());
|
|
132
|
-
const elapsed = Date.now() - startTime;
|
|
133
|
-
|
|
134
|
-
// Gate check failed because process was killed by timeout
|
|
135
|
-
assert.strictEqual(result.status, "failed");
|
|
136
|
-
assert.ok(elapsed < 10000, `Timeout should have killed step quickly, took ${elapsed}ms`);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("parallel step failure cancels siblings", async () => {
|
|
140
|
-
// fast_fail and slow_success both depend on source → parallel layer.
|
|
141
|
-
// fast_fail exits immediately with error → slow_success should be cancelled.
|
|
142
|
-
const spec = makeSpec({
|
|
143
|
-
name: "cancel-test",
|
|
144
|
-
steps: {
|
|
145
|
-
source: {
|
|
146
|
-
transform: { mapping: { ready: true } },
|
|
147
|
-
},
|
|
148
|
-
fast_fail: {
|
|
149
|
-
gate: { check: "exit 1", onFail: "fail" },
|
|
150
|
-
input: { trigger: "${{ steps.source.output.ready }}" },
|
|
151
|
-
},
|
|
152
|
-
slow_success: {
|
|
153
|
-
gate: { check: "sleep 10 && echo done" },
|
|
154
|
-
input: { trigger: "${{ steps.source.output.ready }}" },
|
|
155
|
-
},
|
|
156
|
-
after: {
|
|
157
|
-
transform: {
|
|
158
|
-
mapping: {
|
|
159
|
-
a: "${{ steps.fast_fail.output }}",
|
|
160
|
-
b: "${{ steps.slow_success.output }}",
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const startTime = Date.now();
|
|
168
|
-
const result = await executeWorkflow(spec, {}, mockOptions());
|
|
169
|
-
const elapsed = Date.now() - startTime;
|
|
170
|
-
|
|
171
|
-
assert.strictEqual(result.status, "failed");
|
|
172
|
-
// Should not have waited 10s for slow_success
|
|
173
|
-
assert.ok(elapsed < 5000, `Should have cancelled quickly, took ${elapsed}ms`);
|
|
174
|
-
});
|
|
175
|
-
});
|
package/src/resume.test.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for checkpoint/resume — proves the full cycle
|
|
3
|
-
* using mock dispatch (no real subprocesses).
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it } from "node:test";
|
|
6
|
-
import assert from "node:assert";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import { executeWorkflow, requestPause } from "./workflow-executor.ts";
|
|
11
|
-
import { readState } from "./state.ts";
|
|
12
|
-
import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
|
|
13
|
-
import type { StepResult, StepUsage } from "./types.ts";
|
|
14
|
-
|
|
15
|
-
const zeroUsage: StepUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
16
|
-
|
|
17
|
-
function mockResult(stepName: string, text: string): StepResult {
|
|
18
|
-
return {
|
|
19
|
-
step: stepName,
|
|
20
|
-
agent: "mock",
|
|
21
|
-
status: "completed" as const,
|
|
22
|
-
textOutput: text,
|
|
23
|
-
usage: { ...zeroUsage },
|
|
24
|
-
durationMs: 100,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("resume: crash recovery", () => {
|
|
29
|
-
it("skips completed steps when resuming after failure", async (t) => {
|
|
30
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
|
|
31
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
32
|
-
|
|
33
|
-
const dispatched: string[] = [];
|
|
34
|
-
|
|
35
|
-
const spec = makeSpec({
|
|
36
|
-
steps: {
|
|
37
|
-
step1: { agent: "a" },
|
|
38
|
-
step2: { agent: "b" },
|
|
39
|
-
step3: { agent: "c" },
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// First run: step1 succeeds, step2 fails
|
|
44
|
-
const failDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
|
|
45
|
-
dispatched.push(opts.stepName);
|
|
46
|
-
if (opts.stepName === "step2") {
|
|
47
|
-
return {
|
|
48
|
-
...mockResult(opts.stepName, ""),
|
|
49
|
-
status: "failed" as const,
|
|
50
|
-
error: "Simulated failure",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return mockResult(opts.stepName, `output-${opts.stepName}`);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const result1 = await executeWorkflow(spec, {}, {
|
|
57
|
-
ctx: mockCtx(tmpDir),
|
|
58
|
-
pi: mockPi(),
|
|
59
|
-
loadAgent: () => ({ name: "mock" }),
|
|
60
|
-
dispatchFn: failDispatch,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
assert.strictEqual(result1.status, "failed");
|
|
64
|
-
assert.ok(dispatched.includes("step1"));
|
|
65
|
-
assert.ok(dispatched.includes("step2"));
|
|
66
|
-
|
|
67
|
-
// Verify state on disk
|
|
68
|
-
const savedState = readState(result1.runDir)!;
|
|
69
|
-
assert.ok(savedState, "state should be persisted");
|
|
70
|
-
assert.strictEqual(savedState.steps.step1.status, "completed");
|
|
71
|
-
assert.strictEqual(savedState.steps.step2.status, "failed");
|
|
72
|
-
|
|
73
|
-
// Second run: resume — step1 NOT dispatched, step2 re-runs successfully, step3 runs
|
|
74
|
-
dispatched.length = 0;
|
|
75
|
-
const successDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
|
|
76
|
-
dispatched.push(opts.stepName);
|
|
77
|
-
return mockResult(opts.stepName, `output-${opts.stepName}`);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const result2 = await executeWorkflow(spec, {}, {
|
|
81
|
-
ctx: mockCtx(tmpDir),
|
|
82
|
-
pi: mockPi(),
|
|
83
|
-
loadAgent: () => ({ name: "mock" }),
|
|
84
|
-
dispatchFn: successDispatch,
|
|
85
|
-
resume: {
|
|
86
|
-
runId: result1.runId,
|
|
87
|
-
runDir: result1.runDir,
|
|
88
|
-
state: savedState,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
assert.strictEqual(result2.status, "completed");
|
|
93
|
-
assert.ok(!dispatched.includes("step1"), "step1 should be skipped on resume");
|
|
94
|
-
assert.ok(dispatched.includes("step2"), "step2 should re-run");
|
|
95
|
-
assert.ok(dispatched.includes("step3"), "step3 should run");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe("resume: pause step", () => {
|
|
100
|
-
it("pauses at a pause step and resumes from the next step", async (t) => {
|
|
101
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
|
|
102
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
103
|
-
|
|
104
|
-
const dispatched: string[] = [];
|
|
105
|
-
const mockDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
|
|
106
|
-
dispatched.push(opts.stepName);
|
|
107
|
-
return mockResult(opts.stepName, `output-${opts.stepName}`);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const spec = makeSpec({
|
|
111
|
-
steps: {
|
|
112
|
-
step1: { agent: "a" },
|
|
113
|
-
checkpoint: { pause: "Review step1 results" },
|
|
114
|
-
step2: { agent: "b" },
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Run 1: should pause after step1 + checkpoint
|
|
119
|
-
const result1 = await executeWorkflow(spec, {}, {
|
|
120
|
-
ctx: mockCtx(tmpDir),
|
|
121
|
-
pi: mockPi(),
|
|
122
|
-
loadAgent: () => ({ name: "mock" }),
|
|
123
|
-
dispatchFn: mockDispatch,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
assert.strictEqual(result1.status, "paused");
|
|
127
|
-
assert.ok(dispatched.includes("step1"));
|
|
128
|
-
assert.ok(!dispatched.includes("step2"), "step2 should not run before resume");
|
|
129
|
-
|
|
130
|
-
// Verify state on disk
|
|
131
|
-
const savedState = readState(result1.runDir)!;
|
|
132
|
-
assert.ok(savedState, "state should be persisted");
|
|
133
|
-
assert.strictEqual(savedState.status, "paused");
|
|
134
|
-
assert.strictEqual(savedState.steps.step1.status, "completed");
|
|
135
|
-
assert.strictEqual(savedState.steps.checkpoint.status, "completed");
|
|
136
|
-
|
|
137
|
-
// Run 2: resume — step1 and checkpoint skipped, step2 runs
|
|
138
|
-
dispatched.length = 0;
|
|
139
|
-
const result2 = await executeWorkflow(spec, {}, {
|
|
140
|
-
ctx: mockCtx(tmpDir),
|
|
141
|
-
pi: mockPi(),
|
|
142
|
-
loadAgent: () => ({ name: "mock" }),
|
|
143
|
-
dispatchFn: mockDispatch,
|
|
144
|
-
resume: {
|
|
145
|
-
runId: result1.runId,
|
|
146
|
-
runDir: result1.runDir,
|
|
147
|
-
state: savedState,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
assert.strictEqual(result2.status, "completed");
|
|
152
|
-
assert.ok(!dispatched.includes("step1"), "step1 should be skipped");
|
|
153
|
-
assert.ok(!dispatched.includes("checkpoint"), "checkpoint should be skipped");
|
|
154
|
-
assert.ok(dispatched.includes("step2"), "step2 should run");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("resume: keybinding-initiated pause", () => {
|
|
159
|
-
it("pauses between steps when requestPause is called", async (t) => {
|
|
160
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resume-"));
|
|
161
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
162
|
-
|
|
163
|
-
const dispatched: string[] = [];
|
|
164
|
-
const mockDispatch = async (_step: any, _agent: any, _prompt: string, opts: any) => {
|
|
165
|
-
dispatched.push(opts.stepName);
|
|
166
|
-
// After step1 dispatch, request pause
|
|
167
|
-
if (opts.stepName === "step1") {
|
|
168
|
-
requestPause();
|
|
169
|
-
}
|
|
170
|
-
return mockResult(opts.stepName, `output-${opts.stepName}`);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const spec = makeSpec({
|
|
174
|
-
steps: {
|
|
175
|
-
step1: { agent: "a" },
|
|
176
|
-
step2: { agent: "b" },
|
|
177
|
-
step3: { agent: "c" },
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const result = await executeWorkflow(spec, {}, {
|
|
182
|
-
ctx: mockCtx(tmpDir),
|
|
183
|
-
pi: mockPi(),
|
|
184
|
-
loadAgent: () => ({ name: "mock" }),
|
|
185
|
-
dispatchFn: mockDispatch,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
assert.strictEqual(result.status, "paused");
|
|
189
|
-
assert.ok(dispatched.includes("step1"));
|
|
190
|
-
assert.ok(!dispatched.includes("step2"), "step2 should not run — paused after step1");
|
|
191
|
-
});
|
|
192
|
-
});
|