@davidorex/pi-workflows 0.2.0 → 0.3.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/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 +172 -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 +675 -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 +156 -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 +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -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 +164 -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 +726 -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 +438 -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 +464 -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 +84 -0
- package/skills/pi-workflows/SKILL.md +247 -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 +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -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 +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -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
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for post-step block validation with filesystem diff.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it } from "node:test";
|
|
5
|
-
import assert from "node:assert";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import { snapshotBlockFiles, validateChangedBlocks } from "@davidorex/pi-project/src/block-validation.ts";
|
|
10
|
-
import { executeWorkflow } from "./workflow-executor.ts";
|
|
11
|
-
import type { WorkflowSpec } from "./types.ts";
|
|
12
|
-
import { mockCtx, mockPi } from "./test-helpers.ts";
|
|
13
|
-
|
|
14
|
-
// ── Unit tests for snapshotBlockFiles / validateChangedBlocks ──
|
|
15
|
-
|
|
16
|
-
describe("snapshotBlockFiles", () => {
|
|
17
|
-
it("returns empty map when .project/ does not exist", () => {
|
|
18
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-snap-"));
|
|
19
|
-
const result = snapshotBlockFiles(tmpDir);
|
|
20
|
-
assert.strictEqual(result.size, 0);
|
|
21
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("snapshots .json files in .project/", () => {
|
|
25
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-snap-"));
|
|
26
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
27
|
-
fs.mkdirSync(wfDir, { recursive: true });
|
|
28
|
-
fs.writeFileSync(path.join(wfDir, "gaps.json"), "{}");
|
|
29
|
-
fs.writeFileSync(path.join(wfDir, "decisions.json"), "[]");
|
|
30
|
-
fs.writeFileSync(path.join(wfDir, "readme.txt"), "not json"); // should be ignored
|
|
31
|
-
|
|
32
|
-
const result = snapshotBlockFiles(tmpDir);
|
|
33
|
-
assert.strictEqual(result.size, 2);
|
|
34
|
-
assert.ok(result.has(path.join(wfDir, "gaps.json")));
|
|
35
|
-
assert.ok(result.has(path.join(wfDir, "decisions.json")));
|
|
36
|
-
assert.ok(!result.has(path.join(wfDir, "readme.txt")));
|
|
37
|
-
|
|
38
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("validateChangedBlocks", () => {
|
|
43
|
-
it("does nothing when no files changed", () => {
|
|
44
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
|
|
45
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
46
|
-
fs.mkdirSync(wfDir, { recursive: true });
|
|
47
|
-
fs.writeFileSync(path.join(wfDir, "gaps.json"), "{}");
|
|
48
|
-
|
|
49
|
-
const before = snapshotBlockFiles(tmpDir);
|
|
50
|
-
// No changes — should not throw
|
|
51
|
-
assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
|
|
52
|
-
|
|
53
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("skips changed files with no matching schema", () => {
|
|
57
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
|
|
58
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
59
|
-
fs.mkdirSync(wfDir, { recursive: true });
|
|
60
|
-
|
|
61
|
-
const before = snapshotBlockFiles(tmpDir);
|
|
62
|
-
|
|
63
|
-
// Create a new file with no schema
|
|
64
|
-
fs.writeFileSync(path.join(wfDir, "no-schema.json"), '{"data": true}');
|
|
65
|
-
|
|
66
|
-
// Should not throw — no schema to validate against
|
|
67
|
-
assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
|
|
68
|
-
|
|
69
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("validates changed file against its schema — passes", () => {
|
|
73
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
|
|
74
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
75
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
76
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
77
|
-
|
|
78
|
-
// Create schema
|
|
79
|
-
fs.writeFileSync(path.join(schemasDir, "test-block.schema.json"), JSON.stringify({
|
|
80
|
-
type: "object",
|
|
81
|
-
required: ["name"],
|
|
82
|
-
properties: { name: { type: "string" } },
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
const before = snapshotBlockFiles(tmpDir);
|
|
86
|
-
|
|
87
|
-
// Create a valid block file
|
|
88
|
-
fs.writeFileSync(path.join(wfDir, "test-block.json"), JSON.stringify({ name: "valid" }));
|
|
89
|
-
|
|
90
|
-
assert.doesNotThrow(() => validateChangedBlocks(tmpDir, before));
|
|
91
|
-
|
|
92
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("throws on validation failure", () => {
|
|
96
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
|
|
97
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
98
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
99
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
100
|
-
|
|
101
|
-
// Create schema requiring name as string
|
|
102
|
-
fs.writeFileSync(path.join(schemasDir, "test-block.schema.json"), JSON.stringify({
|
|
103
|
-
type: "object",
|
|
104
|
-
required: ["name"],
|
|
105
|
-
properties: { name: { type: "string" } },
|
|
106
|
-
}));
|
|
107
|
-
|
|
108
|
-
const before = snapshotBlockFiles(tmpDir);
|
|
109
|
-
|
|
110
|
-
// Create an invalid block file (name should be string, not number)
|
|
111
|
-
fs.writeFileSync(path.join(wfDir, "test-block.json"), JSON.stringify({ name: 123 }));
|
|
112
|
-
|
|
113
|
-
assert.throws(
|
|
114
|
-
() => validateChangedBlocks(tmpDir, before),
|
|
115
|
-
(err: unknown) => err instanceof Error && err.message.includes("Block validation failed"),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("detects modified files (not just new files)", () => {
|
|
122
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-val-"));
|
|
123
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
124
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
125
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
126
|
-
|
|
127
|
-
// Create schema
|
|
128
|
-
fs.writeFileSync(path.join(schemasDir, "data.schema.json"), JSON.stringify({
|
|
129
|
-
type: "object",
|
|
130
|
-
required: ["value"],
|
|
131
|
-
properties: { value: { type: "number" } },
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
// Create valid initial file
|
|
135
|
-
const blockPath = path.join(wfDir, "data.json");
|
|
136
|
-
fs.writeFileSync(blockPath, JSON.stringify({ value: 42 }));
|
|
137
|
-
|
|
138
|
-
const before = snapshotBlockFiles(tmpDir);
|
|
139
|
-
|
|
140
|
-
// Modify the file to be invalid (need to ensure mtime changes)
|
|
141
|
-
// Use a tiny delay or touch the file
|
|
142
|
-
const newContent = JSON.stringify({ value: "not a number" });
|
|
143
|
-
// Force mtime change by writing with a slight delay
|
|
144
|
-
const origMtime = fs.statSync(blockPath).mtimeMs;
|
|
145
|
-
fs.writeFileSync(blockPath, newContent);
|
|
146
|
-
// If mtime didn't change (same millisecond), force it
|
|
147
|
-
const newMtime = fs.statSync(blockPath).mtimeMs;
|
|
148
|
-
if (newMtime === origMtime) {
|
|
149
|
-
fs.utimesSync(blockPath, new Date(), new Date(Date.now() + 1000));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
assert.throws(
|
|
153
|
-
() => validateChangedBlocks(tmpDir, before),
|
|
154
|
-
(err: unknown) => err instanceof Error && err.message.includes("Block validation failed"),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// ── Integration tests: block validation in workflow executor ──
|
|
162
|
-
|
|
163
|
-
describe("post-step block validation in executor", () => {
|
|
164
|
-
it("step that does not modify .project/ passes normally", async () => {
|
|
165
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
|
|
166
|
-
const spec: WorkflowSpec = {
|
|
167
|
-
name: "bv-no-change",
|
|
168
|
-
description: "step without block changes",
|
|
169
|
-
steps: {
|
|
170
|
-
compute: {
|
|
171
|
-
transform: { mapping: { result: 42 } },
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
source: "project",
|
|
175
|
-
filePath: path.join(tmpDir, "test.project.yaml"),
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const result = await executeWorkflow(spec, {}, {
|
|
179
|
-
ctx: mockCtx(tmpDir),
|
|
180
|
-
pi: mockPi(),
|
|
181
|
-
loadAgent: () => ({ name: "default" }),
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
assert.strictEqual(result.status, "completed");
|
|
185
|
-
assert.strictEqual(result.steps.compute.status, "completed");
|
|
186
|
-
|
|
187
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("step that writes valid data to a .project/ block passes", async () => {
|
|
191
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
|
|
192
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
193
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
194
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
195
|
-
|
|
196
|
-
// Create schema
|
|
197
|
-
fs.writeFileSync(path.join(schemasDir, "result.schema.json"), JSON.stringify({
|
|
198
|
-
type: "object",
|
|
199
|
-
required: ["status"],
|
|
200
|
-
properties: { status: { type: "string" } },
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
// Use a command step that writes a valid block file
|
|
204
|
-
const spec: WorkflowSpec = {
|
|
205
|
-
name: "bv-valid-write",
|
|
206
|
-
description: "step writes valid block",
|
|
207
|
-
steps: {
|
|
208
|
-
writeBlock: {
|
|
209
|
-
command: `echo '{"status": "ok"}' > ${path.join(wfDir, "result.json")}`,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
source: "project",
|
|
213
|
-
filePath: path.join(tmpDir, "test.project.yaml"),
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const result = await executeWorkflow(spec, {}, {
|
|
217
|
-
ctx: mockCtx(tmpDir),
|
|
218
|
-
pi: mockPi(),
|
|
219
|
-
loadAgent: () => ({ name: "default" }),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
assert.strictEqual(result.status, "completed");
|
|
223
|
-
assert.strictEqual(result.steps.writeBlock.status, "completed");
|
|
224
|
-
|
|
225
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("step that writes invalid data to a .project/ block fails", async () => {
|
|
229
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
|
|
230
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
231
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
232
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
233
|
-
|
|
234
|
-
// Create schema requiring status as string
|
|
235
|
-
fs.writeFileSync(path.join(schemasDir, "result.schema.json"), JSON.stringify({
|
|
236
|
-
type: "object",
|
|
237
|
-
required: ["status"],
|
|
238
|
-
properties: { status: { type: "string" } },
|
|
239
|
-
}));
|
|
240
|
-
|
|
241
|
-
// Command step writes invalid data (status is number, not string)
|
|
242
|
-
const spec: WorkflowSpec = {
|
|
243
|
-
name: "bv-invalid-write",
|
|
244
|
-
description: "step writes invalid block",
|
|
245
|
-
steps: {
|
|
246
|
-
writeBlock: {
|
|
247
|
-
command: `echo '{"status": 123}' > ${path.join(wfDir, "result.json")}`,
|
|
248
|
-
},
|
|
249
|
-
shouldNotRun: {
|
|
250
|
-
transform: { mapping: { ran: true } },
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
source: "project",
|
|
254
|
-
filePath: path.join(tmpDir, "test.project.yaml"),
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const result = await executeWorkflow(spec, {}, {
|
|
258
|
-
ctx: mockCtx(tmpDir),
|
|
259
|
-
pi: mockPi(),
|
|
260
|
-
loadAgent: () => ({ name: "default" }),
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
assert.strictEqual(result.status, "failed");
|
|
264
|
-
assert.strictEqual(result.steps.writeBlock.status, "failed");
|
|
265
|
-
assert.ok(result.steps.writeBlock.error?.includes("Block validation failed"));
|
|
266
|
-
// Fail-fast: next step should not run
|
|
267
|
-
assert.ok(!result.steps.shouldNotRun);
|
|
268
|
-
|
|
269
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("changed .project/ file with no matching schema is skipped", async () => {
|
|
273
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
|
|
274
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
275
|
-
fs.mkdirSync(wfDir, { recursive: true });
|
|
276
|
-
// No schemas directory at all
|
|
277
|
-
|
|
278
|
-
// Command writes a JSON file with no schema
|
|
279
|
-
const spec: WorkflowSpec = {
|
|
280
|
-
name: "bv-no-schema",
|
|
281
|
-
description: "step writes block with no schema",
|
|
282
|
-
steps: {
|
|
283
|
-
writeBlock: {
|
|
284
|
-
command: `echo '{"anything": "goes"}' > ${path.join(wfDir, "custom.json")}`,
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
source: "project",
|
|
288
|
-
filePath: path.join(tmpDir, "test.project.yaml"),
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const result = await executeWorkflow(spec, {}, {
|
|
292
|
-
ctx: mockCtx(tmpDir),
|
|
293
|
-
pi: mockPi(),
|
|
294
|
-
loadAgent: () => ({ name: "default" }),
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
assert.strictEqual(result.status, "completed");
|
|
298
|
-
assert.strictEqual(result.steps.writeBlock.status, "completed");
|
|
299
|
-
|
|
300
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("validates multiple changed block files in one step", async () => {
|
|
304
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bv-exec-"));
|
|
305
|
-
const wfDir = path.join(tmpDir, ".project");
|
|
306
|
-
const schemasDir = path.join(wfDir, "schemas");
|
|
307
|
-
fs.mkdirSync(schemasDir, { recursive: true });
|
|
308
|
-
|
|
309
|
-
// Schema for file-a: requires name as string
|
|
310
|
-
fs.writeFileSync(path.join(schemasDir, "file-a.schema.json"), JSON.stringify({
|
|
311
|
-
type: "object",
|
|
312
|
-
required: ["name"],
|
|
313
|
-
properties: { name: { type: "string" } },
|
|
314
|
-
}));
|
|
315
|
-
// Schema for file-b: requires count as number
|
|
316
|
-
fs.writeFileSync(path.join(schemasDir, "file-b.schema.json"), JSON.stringify({
|
|
317
|
-
type: "object",
|
|
318
|
-
required: ["count"],
|
|
319
|
-
properties: { count: { type: "number" } },
|
|
320
|
-
}));
|
|
321
|
-
|
|
322
|
-
// Write valid file-a, invalid file-b
|
|
323
|
-
const cmd = [
|
|
324
|
-
`echo '{"name": "ok"}' > ${path.join(wfDir, "file-a.json")}`,
|
|
325
|
-
`echo '{"count": "not-a-number"}' > ${path.join(wfDir, "file-b.json")}`,
|
|
326
|
-
].join(" && ");
|
|
327
|
-
|
|
328
|
-
const spec: WorkflowSpec = {
|
|
329
|
-
name: "bv-multi",
|
|
330
|
-
description: "step writes multiple blocks",
|
|
331
|
-
steps: {
|
|
332
|
-
writeBlocks: { command: cmd },
|
|
333
|
-
},
|
|
334
|
-
source: "project",
|
|
335
|
-
filePath: path.join(tmpDir, "test.project.yaml"),
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const result = await executeWorkflow(spec, {}, {
|
|
339
|
-
ctx: mockCtx(tmpDir),
|
|
340
|
-
pi: mockPi(),
|
|
341
|
-
loadAgent: () => ({ name: "default" }),
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
assert.strictEqual(result.status, "failed");
|
|
345
|
-
assert.strictEqual(result.steps.writeBlocks.status, "failed");
|
|
346
|
-
assert.ok(result.steps.writeBlocks.error?.includes("file-b.json"));
|
|
347
|
-
|
|
348
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
349
|
-
});
|
|
350
|
-
});
|
package/src/checkpoint.test.ts
DELETED
|
@@ -1,237 +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 { findIncompleteRun, validateResumeCompatibility, computeResumePoint, formatIncompleteRun } from "./checkpoint.ts";
|
|
7
|
-
import { writeState } from "./state.ts";
|
|
8
|
-
import type { ExecutionState } from "./types.ts";
|
|
9
|
-
import { makeSpec } from "./test-helpers.ts";
|
|
10
|
-
|
|
11
|
-
describe("findIncompleteRun", () => {
|
|
12
|
-
it("returns null when no runs directory exists", () => {
|
|
13
|
-
const result = findIncompleteRun("/nonexistent", "test-workflow");
|
|
14
|
-
assert.strictEqual(result, null);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("returns null when all runs are completed", (t) => {
|
|
18
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
19
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
20
|
-
|
|
21
|
-
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
22
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
23
|
-
writeState(runDir, { input: {}, steps: {}, status: "completed" });
|
|
24
|
-
|
|
25
|
-
const result = findIncompleteRun(tmpDir, "test");
|
|
26
|
-
assert.strictEqual(result, null);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("finds a run with status 'running' (interrupted)", (t) => {
|
|
30
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
31
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
32
|
-
|
|
33
|
-
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
34
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
35
|
-
|
|
36
|
-
const state: ExecutionState = {
|
|
37
|
-
input: { path: "/src" },
|
|
38
|
-
steps: {
|
|
39
|
-
explore: {
|
|
40
|
-
step: "explore", agent: "explorer", status: "completed",
|
|
41
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 1 },
|
|
42
|
-
durationMs: 1000, textOutput: "found stuff",
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
status: "running",
|
|
46
|
-
};
|
|
47
|
-
writeState(runDir, state);
|
|
48
|
-
|
|
49
|
-
const result = findIncompleteRun(tmpDir, "test");
|
|
50
|
-
assert.ok(result);
|
|
51
|
-
assert.strictEqual(result.runId, "test-20260314-120000-abcd");
|
|
52
|
-
assert.deepStrictEqual(result.completedSteps, ["explore"]);
|
|
53
|
-
assert.strictEqual(result.failedStep, undefined);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("finds a run with status 'failed'", (t) => {
|
|
57
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
58
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
59
|
-
|
|
60
|
-
const runDir = path.join(tmpDir, ".workflows", "runs", "test", "runs", "test-20260314-120000-abcd");
|
|
61
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
62
|
-
|
|
63
|
-
const state: ExecutionState = {
|
|
64
|
-
input: {},
|
|
65
|
-
steps: {
|
|
66
|
-
step1: {
|
|
67
|
-
step: "step1", agent: "a", status: "completed",
|
|
68
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
69
|
-
durationMs: 0,
|
|
70
|
-
},
|
|
71
|
-
step2: {
|
|
72
|
-
step: "step2", agent: "b", status: "failed",
|
|
73
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
74
|
-
durationMs: 0, error: "timeout",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
status: "failed",
|
|
78
|
-
};
|
|
79
|
-
writeState(runDir, state);
|
|
80
|
-
|
|
81
|
-
const result = findIncompleteRun(tmpDir, "test");
|
|
82
|
-
assert.ok(result);
|
|
83
|
-
assert.deepStrictEqual(result.completedSteps, ["step1"]);
|
|
84
|
-
assert.strictEqual(result.failedStep, "step2");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("returns most recent incomplete run", (t) => {
|
|
88
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-ckpt-"));
|
|
89
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
90
|
-
|
|
91
|
-
const runsBase = path.join(tmpDir, ".workflows", "runs", "test", "runs");
|
|
92
|
-
|
|
93
|
-
// Older run (completed)
|
|
94
|
-
const oldDir = path.join(runsBase, "test-20260314-100000-aaaa");
|
|
95
|
-
fs.mkdirSync(oldDir, { recursive: true });
|
|
96
|
-
writeState(oldDir, { input: {}, steps: {}, status: "completed" });
|
|
97
|
-
|
|
98
|
-
// Newer run (failed)
|
|
99
|
-
const newDir = path.join(runsBase, "test-20260314-120000-bbbb");
|
|
100
|
-
fs.mkdirSync(newDir, { recursive: true });
|
|
101
|
-
writeState(newDir, { input: {}, steps: {}, status: "failed" });
|
|
102
|
-
|
|
103
|
-
const result = findIncompleteRun(tmpDir, "test");
|
|
104
|
-
assert.ok(result);
|
|
105
|
-
assert.strictEqual(result.runId, "test-20260314-120000-bbbb");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("validateResumeCompatibility", () => {
|
|
110
|
-
it("returns null when compatible", () => {
|
|
111
|
-
const state: ExecutionState = {
|
|
112
|
-
input: {},
|
|
113
|
-
steps: {
|
|
114
|
-
step1: {
|
|
115
|
-
step: "step1", agent: "a", status: "completed",
|
|
116
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
117
|
-
durationMs: 0,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
status: "failed",
|
|
121
|
-
specVersion: "1",
|
|
122
|
-
};
|
|
123
|
-
const spec = makeSpec({ version: "1", steps: { step1: { agent: "a" }, step2: { agent: "b" } } });
|
|
124
|
-
assert.strictEqual(validateResumeCompatibility(state, spec), null);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("rejects version mismatch", () => {
|
|
128
|
-
const state: ExecutionState = {
|
|
129
|
-
input: {},
|
|
130
|
-
steps: {},
|
|
131
|
-
status: "failed",
|
|
132
|
-
specVersion: "1",
|
|
133
|
-
};
|
|
134
|
-
const spec = makeSpec({ version: "2", steps: { step1: { agent: "a" } } });
|
|
135
|
-
const msg = validateResumeCompatibility(state, spec);
|
|
136
|
-
assert.ok(msg?.includes("version"));
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("rejects when completed step no longer exists in spec", () => {
|
|
140
|
-
const state: ExecutionState = {
|
|
141
|
-
input: {},
|
|
142
|
-
steps: {
|
|
143
|
-
removed_step: {
|
|
144
|
-
step: "removed_step", agent: "a", status: "completed",
|
|
145
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
146
|
-
durationMs: 0,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
status: "failed",
|
|
150
|
-
};
|
|
151
|
-
const spec = makeSpec({ steps: { different_step: { agent: "a" } } });
|
|
152
|
-
const msg = validateResumeCompatibility(state, spec);
|
|
153
|
-
assert.ok(msg?.includes("removed_step"));
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe("computeResumePoint", () => {
|
|
158
|
-
it("returns first layer with pending steps", () => {
|
|
159
|
-
const plan = [
|
|
160
|
-
{ steps: ["a"] },
|
|
161
|
-
{ steps: ["b", "c"] },
|
|
162
|
-
{ steps: ["d"] },
|
|
163
|
-
];
|
|
164
|
-
const completed = new Set(["a"]);
|
|
165
|
-
const result = computeResumePoint(plan, completed);
|
|
166
|
-
assert.ok(result);
|
|
167
|
-
assert.strictEqual(result.resumeLayerIndex, 1);
|
|
168
|
-
assert.deepStrictEqual(result.pendingStepsInLayer, ["b", "c"]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("returns only pending steps in a partially completed layer", () => {
|
|
172
|
-
const plan = [
|
|
173
|
-
{ steps: ["a"] },
|
|
174
|
-
{ steps: ["b", "c", "d"] },
|
|
175
|
-
{ steps: ["e"] },
|
|
176
|
-
];
|
|
177
|
-
const completed = new Set(["a", "b", "d"]);
|
|
178
|
-
const result = computeResumePoint(plan, completed);
|
|
179
|
-
assert.ok(result);
|
|
180
|
-
assert.strictEqual(result.resumeLayerIndex, 1);
|
|
181
|
-
assert.deepStrictEqual(result.pendingStepsInLayer, ["c"]);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("returns null when all steps are completed", () => {
|
|
185
|
-
const plan = [
|
|
186
|
-
{ steps: ["a"] },
|
|
187
|
-
{ steps: ["b"] },
|
|
188
|
-
];
|
|
189
|
-
const completed = new Set(["a", "b"]);
|
|
190
|
-
const result = computeResumePoint(plan, completed);
|
|
191
|
-
assert.strictEqual(result, null);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("handles first layer being the resume point", () => {
|
|
195
|
-
const plan = [
|
|
196
|
-
{ steps: ["a", "b"] },
|
|
197
|
-
{ steps: ["c"] },
|
|
198
|
-
];
|
|
199
|
-
const completed = new Set<string>();
|
|
200
|
-
const result = computeResumePoint(plan, completed);
|
|
201
|
-
assert.ok(result);
|
|
202
|
-
assert.strictEqual(result.resumeLayerIndex, 0);
|
|
203
|
-
assert.deepStrictEqual(result.pendingStepsInLayer, ["a", "b"]);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("formatIncompleteRun", () => {
|
|
208
|
-
it("formats an interrupted run", () => {
|
|
209
|
-
const run = {
|
|
210
|
-
runId: "test-20260314-120000-abcd",
|
|
211
|
-
runDir: "/tmp/runs/test-20260314-120000-abcd",
|
|
212
|
-
state: { input: {}, steps: {}, status: "running" as const },
|
|
213
|
-
completedSteps: ["explore", "analyze"],
|
|
214
|
-
updatedAt: "2026-03-14T12:00:00.000Z",
|
|
215
|
-
};
|
|
216
|
-
const spec = makeSpec({ steps: { explore: { agent: "a" }, analyze: { agent: "b" }, synthesize: { agent: "c" } } });
|
|
217
|
-
const msg = formatIncompleteRun(run, spec);
|
|
218
|
-
assert.ok(msg.includes("interrupted"));
|
|
219
|
-
assert.ok(msg.includes("2/3"));
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("formats a failed run with step name", () => {
|
|
223
|
-
const run = {
|
|
224
|
-
runId: "test-20260314-120000-abcd",
|
|
225
|
-
runDir: "/tmp/runs/test-20260314-120000-abcd",
|
|
226
|
-
state: { input: {}, steps: {}, status: "failed" as const },
|
|
227
|
-
completedSteps: ["explore"],
|
|
228
|
-
failedStep: "analyze",
|
|
229
|
-
updatedAt: "2026-03-14T12:00:00.000Z",
|
|
230
|
-
};
|
|
231
|
-
const spec = makeSpec({ steps: { explore: { agent: "a" }, analyze: { agent: "b" }, synthesize: { agent: "c" } } });
|
|
232
|
-
const msg = formatIncompleteRun(run, spec);
|
|
233
|
-
assert.ok(msg.includes("failed"));
|
|
234
|
-
assert.ok(msg.includes("analyze"));
|
|
235
|
-
assert.ok(msg.includes("1/3"));
|
|
236
|
-
});
|
|
237
|
-
});
|