@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/step-shared.test.ts
DELETED
|
@@ -1,355 +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 nunjucks from "nunjucks";
|
|
7
|
-
import {
|
|
8
|
-
SIGKILL_GRACE_MS,
|
|
9
|
-
WIDGET_ID,
|
|
10
|
-
DEFAULT_MAX_ATTEMPTS,
|
|
11
|
-
zeroUsage,
|
|
12
|
-
addUsage,
|
|
13
|
-
resolveSchemaPath,
|
|
14
|
-
buildPrompt,
|
|
15
|
-
persistStep,
|
|
16
|
-
compileAgentSpec,
|
|
17
|
-
} from "./step-shared.ts";
|
|
18
|
-
import type { StepUsage, StepResult, ExecutionState, AgentSpec } from "./types.ts";
|
|
19
|
-
|
|
20
|
-
describe("constants", () => {
|
|
21
|
-
it("SIGKILL_GRACE_MS is 3000", () => {
|
|
22
|
-
assert.strictEqual(SIGKILL_GRACE_MS, 3000);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("WIDGET_ID is 'workflow-progress'", () => {
|
|
26
|
-
assert.strictEqual(WIDGET_ID, "workflow-progress");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("DEFAULT_MAX_ATTEMPTS is 3", () => {
|
|
30
|
-
assert.strictEqual(DEFAULT_MAX_ATTEMPTS, 3);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("zeroUsage", () => {
|
|
35
|
-
it("returns all-zero StepUsage with correct keys", () => {
|
|
36
|
-
const u = zeroUsage();
|
|
37
|
-
assert.strictEqual(u.input, 0);
|
|
38
|
-
assert.strictEqual(u.output, 0);
|
|
39
|
-
assert.strictEqual(u.cacheRead, 0);
|
|
40
|
-
assert.strictEqual(u.cacheWrite, 0);
|
|
41
|
-
assert.strictEqual(u.cost, 0);
|
|
42
|
-
assert.strictEqual(u.turns, 0);
|
|
43
|
-
assert.deepStrictEqual(Object.keys(u).sort(), ["cacheRead", "cacheWrite", "cost", "input", "output", "turns"]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns a fresh object each call (no shared reference)", () => {
|
|
47
|
-
const a = zeroUsage();
|
|
48
|
-
const b = zeroUsage();
|
|
49
|
-
assert.notStrictEqual(a, b);
|
|
50
|
-
a.input = 999;
|
|
51
|
-
assert.strictEqual(b.input, 0);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("addUsage", () => {
|
|
56
|
-
it("accumulates all six fields into total", () => {
|
|
57
|
-
const total = zeroUsage();
|
|
58
|
-
const step: StepUsage = { input: 10, output: 20, cacheRead: 30, cacheWrite: 40, cost: 0.5, turns: 2 };
|
|
59
|
-
addUsage(total, step);
|
|
60
|
-
assert.strictEqual(total.input, 10);
|
|
61
|
-
assert.strictEqual(total.output, 20);
|
|
62
|
-
assert.strictEqual(total.cacheRead, 30);
|
|
63
|
-
assert.strictEqual(total.cacheWrite, 40);
|
|
64
|
-
assert.strictEqual(total.cost, 0.5);
|
|
65
|
-
assert.strictEqual(total.turns, 2);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("handles adding zero usage (no change)", () => {
|
|
69
|
-
const total: StepUsage = { input: 5, output: 10, cacheRead: 15, cacheWrite: 20, cost: 0.1, turns: 1 };
|
|
70
|
-
addUsage(total, zeroUsage());
|
|
71
|
-
assert.strictEqual(total.input, 5);
|
|
72
|
-
assert.strictEqual(total.output, 10);
|
|
73
|
-
assert.strictEqual(total.cacheRead, 15);
|
|
74
|
-
assert.strictEqual(total.cacheWrite, 20);
|
|
75
|
-
assert.strictEqual(total.cost, 0.1);
|
|
76
|
-
assert.strictEqual(total.turns, 1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("accumulates across multiple additions", () => {
|
|
80
|
-
const total = zeroUsage();
|
|
81
|
-
addUsage(total, { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.1, turns: 1 });
|
|
82
|
-
addUsage(total, { input: 20, output: 15, cacheRead: 5, cacheWrite: 3, cost: 0.2, turns: 2 });
|
|
83
|
-
addUsage(total, { input: 30, output: 25, cacheRead: 10, cacheWrite: 7, cost: 0.3, turns: 3 });
|
|
84
|
-
assert.strictEqual(total.input, 60);
|
|
85
|
-
assert.strictEqual(total.output, 45);
|
|
86
|
-
assert.strictEqual(total.cacheRead, 15);
|
|
87
|
-
assert.strictEqual(total.cacheWrite, 10);
|
|
88
|
-
assert.strictEqual(total.cost, 0.1 + 0.2 + 0.3);
|
|
89
|
-
assert.strictEqual(total.turns, 6);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("resolveSchemaPath", () => {
|
|
94
|
-
it("returns absolute path unchanged", () => {
|
|
95
|
-
assert.strictEqual(resolveSchemaPath("/abs/path/schema.json", "/some/spec.yaml"), "/abs/path/schema.json");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("resolves relative path against spec file directory", () => {
|
|
99
|
-
const result = resolveSchemaPath("schema.json", "/project/specs/workflow.yaml");
|
|
100
|
-
assert.strictEqual(result, path.resolve("/project/specs", "schema.json"));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("handles spec file in nested directory", () => {
|
|
104
|
-
const result = resolveSchemaPath("schemas/output.json", "/a/b/c/spec.yaml");
|
|
105
|
-
assert.strictEqual(result, path.resolve("/a/b/c", "schemas/output.json"));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("handles schema path with parent traversal (../schemas/out.json)", () => {
|
|
109
|
-
const result = resolveSchemaPath("../schemas/out.json", "/project/specs/workflow.yaml");
|
|
110
|
-
assert.strictEqual(result, path.resolve("/project/specs", "../schemas/out.json"));
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("buildPrompt", () => {
|
|
115
|
-
it("includes task template when agentSpec.taskTemplate is set", () => {
|
|
116
|
-
const step = { agent: "test" };
|
|
117
|
-
const agentSpec: AgentSpec = { name: "test", taskTemplate: "Do this task with {{ data }}" };
|
|
118
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
119
|
-
assert.ok(prompt.includes("Do this task with {{ data }}"));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("serializes non-empty object input as JSON code block when no task template", () => {
|
|
123
|
-
const step = { agent: "test" };
|
|
124
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
125
|
-
const prompt = buildPrompt(step, agentSpec, { key: "value" }, "/tmp/run", "step1");
|
|
126
|
-
assert.ok(prompt.includes("## Input"));
|
|
127
|
-
assert.ok(prompt.includes("```json"));
|
|
128
|
-
assert.ok(prompt.includes('"key": "value"'));
|
|
129
|
-
assert.ok(prompt.includes("```"));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("passes through string input directly when no task template", () => {
|
|
133
|
-
const step = { agent: "test" };
|
|
134
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
135
|
-
const prompt = buildPrompt(step, agentSpec, "raw string input", "/tmp/run", "step1");
|
|
136
|
-
assert.ok(prompt.includes("raw string input"));
|
|
137
|
-
assert.ok(!prompt.includes("## Input"));
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("produces empty-ish prompt for empty object input with no task template", () => {
|
|
141
|
-
const step = { agent: "test" };
|
|
142
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
143
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
144
|
-
assert.ok(!prompt.includes("## Input"));
|
|
145
|
-
assert.ok(!prompt.includes("```json"));
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("appends output instructions when output.format is json", () => {
|
|
149
|
-
const step = { agent: "test", output: { format: "json" as const } };
|
|
150
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
151
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
152
|
-
assert.ok(prompt.includes("**Output:**"));
|
|
153
|
-
assert.ok(prompt.includes("step1.json"));
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("appends output instructions with schema path when output.schema is set", () => {
|
|
157
|
-
const step = { agent: "test", output: { schema: "schemas/out.json" } };
|
|
158
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
159
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
160
|
-
assert.ok(prompt.includes("**Output:**"));
|
|
161
|
-
assert.ok(prompt.includes("JSON Schema"));
|
|
162
|
-
assert.ok(prompt.includes("schemas/out.json"));
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("omits output instructions when no output spec", () => {
|
|
166
|
-
const step = { agent: "test" };
|
|
167
|
-
const agentSpec: AgentSpec = { name: "test" };
|
|
168
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
169
|
-
assert.ok(!prompt.includes("**Output:**"));
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("combines task template with output instructions", () => {
|
|
173
|
-
const step = { agent: "test", output: { format: "json" as const } };
|
|
174
|
-
const agentSpec: AgentSpec = { name: "test", taskTemplate: "Analyze this" };
|
|
175
|
-
const prompt = buildPrompt(step, agentSpec, {}, "/tmp/run", "step1");
|
|
176
|
-
assert.ok(prompt.includes("Analyze this"));
|
|
177
|
-
assert.ok(prompt.includes("**Output:**"));
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe("persistStep", () => {
|
|
182
|
-
it("writes result into state.steps[stepName]", (t) => {
|
|
183
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
|
|
184
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
185
|
-
|
|
186
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
187
|
-
const result: StepResult = {
|
|
188
|
-
step: "myStep",
|
|
189
|
-
agent: "test-agent",
|
|
190
|
-
status: "completed",
|
|
191
|
-
usage: zeroUsage(),
|
|
192
|
-
durationMs: 100,
|
|
193
|
-
};
|
|
194
|
-
const widgetState = {
|
|
195
|
-
spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
|
|
196
|
-
state,
|
|
197
|
-
startTime: Date.now(),
|
|
198
|
-
};
|
|
199
|
-
const ctx = { hasUI: false, ui: { setWidget: () => {}, notify: () => {} } };
|
|
200
|
-
|
|
201
|
-
persistStep(state, "myStep", result, tmpDir, widgetState, ctx);
|
|
202
|
-
assert.strictEqual(state.steps.myStep, result);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("calls writeState (state.json is written to disk)", (t) => {
|
|
206
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
|
|
207
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
208
|
-
|
|
209
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
210
|
-
const result: StepResult = {
|
|
211
|
-
step: "s1",
|
|
212
|
-
agent: "agent",
|
|
213
|
-
status: "completed",
|
|
214
|
-
usage: zeroUsage(),
|
|
215
|
-
durationMs: 50,
|
|
216
|
-
};
|
|
217
|
-
const widgetState = {
|
|
218
|
-
spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
|
|
219
|
-
state,
|
|
220
|
-
startTime: Date.now(),
|
|
221
|
-
};
|
|
222
|
-
const ctx = { hasUI: false, ui: { setWidget: () => {}, notify: () => {} } };
|
|
223
|
-
|
|
224
|
-
persistStep(state, "s1", result, tmpDir, widgetState, ctx);
|
|
225
|
-
assert.ok(fs.existsSync(path.join(tmpDir, "state.json")));
|
|
226
|
-
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, "state.json"), "utf-8"));
|
|
227
|
-
assert.ok(written.steps.s1);
|
|
228
|
-
assert.strictEqual(written.steps.s1.status, "completed");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("calls ctx.ui.setWidget when ctx.hasUI is true", (t) => {
|
|
232
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
|
|
233
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
234
|
-
|
|
235
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
236
|
-
const result: StepResult = {
|
|
237
|
-
step: "s1",
|
|
238
|
-
agent: "agent",
|
|
239
|
-
status: "completed",
|
|
240
|
-
usage: zeroUsage(),
|
|
241
|
-
durationMs: 50,
|
|
242
|
-
};
|
|
243
|
-
const widgetState = {
|
|
244
|
-
spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
|
|
245
|
-
state,
|
|
246
|
-
startTime: Date.now(),
|
|
247
|
-
};
|
|
248
|
-
let widgetCalled = false;
|
|
249
|
-
const ctx = {
|
|
250
|
-
hasUI: true,
|
|
251
|
-
ui: {
|
|
252
|
-
setWidget: (id: string, _w: unknown) => {
|
|
253
|
-
widgetCalled = true;
|
|
254
|
-
assert.strictEqual(id, WIDGET_ID);
|
|
255
|
-
},
|
|
256
|
-
notify: () => {},
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
persistStep(state, "s1", result, tmpDir, widgetState, ctx);
|
|
261
|
-
assert.ok(widgetCalled, "setWidget should have been called");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("does not call ctx.ui.setWidget when ctx.hasUI is false", (t) => {
|
|
265
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-persist-"));
|
|
266
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
267
|
-
|
|
268
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
269
|
-
const result: StepResult = {
|
|
270
|
-
step: "s1",
|
|
271
|
-
agent: "agent",
|
|
272
|
-
status: "completed",
|
|
273
|
-
usage: zeroUsage(),
|
|
274
|
-
durationMs: 50,
|
|
275
|
-
};
|
|
276
|
-
const widgetState = {
|
|
277
|
-
spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
|
|
278
|
-
state,
|
|
279
|
-
startTime: Date.now(),
|
|
280
|
-
};
|
|
281
|
-
let widgetCalled = false;
|
|
282
|
-
const ctx = {
|
|
283
|
-
hasUI: false,
|
|
284
|
-
ui: {
|
|
285
|
-
setWidget: () => { widgetCalled = true; },
|
|
286
|
-
notify: () => {},
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
persistStep(state, "s1", result, tmpDir, widgetState, ctx);
|
|
291
|
-
assert.ok(!widgetCalled, "setWidget should not have been called");
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
describe("compileAgentSpec", () => {
|
|
296
|
-
it("returns agentSpec unchanged when no templateEnv", () => {
|
|
297
|
-
const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
|
|
298
|
-
const result = compileAgentSpec(agentSpec, { name: "world" });
|
|
299
|
-
assert.strictEqual(result, agentSpec);
|
|
300
|
-
assert.strictEqual(result.systemPrompt, "Hello {{ name }}");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("renders inline systemPrompt through Nunjucks with input context", () => {
|
|
304
|
-
const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
|
|
305
|
-
const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
|
|
306
|
-
const result = compileAgentSpec(agentSpec, { name: "world" }, env);
|
|
307
|
-
assert.strictEqual(result.systemPrompt, "Hello world");
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("renders promptTemplate file path through Nunjucks (replaces with rendered text, clears promptTemplate)", (t) => {
|
|
311
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-compile-"));
|
|
312
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
313
|
-
|
|
314
|
-
fs.writeFileSync(path.join(tmpDir, "system.md"), "System for {{ role }}");
|
|
315
|
-
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(tmpDir), {
|
|
316
|
-
autoescape: false,
|
|
317
|
-
throwOnUndefined: false,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const agentSpec: AgentSpec = { name: "test", promptTemplate: "system.md" };
|
|
321
|
-
const result = compileAgentSpec(agentSpec, { role: "analyzer" }, env);
|
|
322
|
-
assert.strictEqual(result.systemPrompt, "System for analyzer");
|
|
323
|
-
assert.strictEqual(result.promptTemplate, undefined);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("renders taskTemplate file path through Nunjucks", (t) => {
|
|
327
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-compile-"));
|
|
328
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
329
|
-
|
|
330
|
-
fs.writeFileSync(path.join(tmpDir, "task.md"), "Analyze {{ target }}");
|
|
331
|
-
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(tmpDir), {
|
|
332
|
-
autoescape: false,
|
|
333
|
-
throwOnUndefined: false,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
const agentSpec: AgentSpec = { name: "test", taskTemplate: "task.md" };
|
|
337
|
-
const result = compileAgentSpec(agentSpec, { target: "codebase" }, env);
|
|
338
|
-
assert.strictEqual(result.taskTemplate, "Analyze codebase");
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("handles non-object resolvedInput (uses empty context)", () => {
|
|
342
|
-
const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
|
|
343
|
-
const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
|
|
344
|
-
const result = compileAgentSpec(agentSpec, "string input", env);
|
|
345
|
-
// {{ name }} renders to "" with throwOnUndefined: false
|
|
346
|
-
assert.strictEqual(result.systemPrompt, "Hello ");
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("handles null resolvedInput", () => {
|
|
350
|
-
const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
|
|
351
|
-
const agentSpec: AgentSpec = { name: "test", systemPrompt: "Hello {{ name }}" };
|
|
352
|
-
const result = compileAgentSpec(agentSpec, null, env);
|
|
353
|
-
assert.strictEqual(result.systemPrompt, "Hello ");
|
|
354
|
-
});
|
|
355
|
-
});
|
package/src/step-shared.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared helpers for step executors — constants, usage aggregation,
|
|
3
|
-
* prompt building, schema resolution, state persistence, and template resolution.
|
|
4
|
-
*/
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import type { StepUsage, StepResult, ExecutionState, AgentSpec } from "./types.ts";
|
|
7
|
-
import type { ProgressWidgetState } from "./tui.ts";
|
|
8
|
-
import { writeState } from "./state.ts";
|
|
9
|
-
import { createProgressWidget } from "./tui.ts";
|
|
10
|
-
import { renderTemplate, renderTemplateFile } from "./template.ts";
|
|
11
|
-
import type nunjucks from "nunjucks";
|
|
12
|
-
|
|
13
|
-
/** Grace period (ms) between SIGTERM and SIGKILL when killing subprocesses. */
|
|
14
|
-
export const SIGKILL_GRACE_MS = 3000;
|
|
15
|
-
|
|
16
|
-
/** Widget ID used for the workflow progress widget. */
|
|
17
|
-
export const WIDGET_ID = "workflow-progress";
|
|
18
|
-
|
|
19
|
-
/** Default max loop attempts when not specified. */
|
|
20
|
-
export const DEFAULT_MAX_ATTEMPTS = 3;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Helper that returns a StepUsage with all zeroes.
|
|
24
|
-
*/
|
|
25
|
-
export function zeroUsage(): StepUsage {
|
|
26
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Add step usage into a running total (mutates `total`).
|
|
31
|
-
*/
|
|
32
|
-
export function addUsage(total: StepUsage, step: StepUsage): void {
|
|
33
|
-
total.input += step.input;
|
|
34
|
-
total.output += step.output;
|
|
35
|
-
total.cacheRead += step.cacheRead;
|
|
36
|
-
total.cacheWrite += step.cacheWrite;
|
|
37
|
-
total.cost += step.cost;
|
|
38
|
-
total.turns += step.turns;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a schema path relative to the workflow spec file.
|
|
43
|
-
* If the schema path is absolute, return as-is.
|
|
44
|
-
* If relative, resolve against the directory containing the workflow spec.
|
|
45
|
-
*/
|
|
46
|
-
export function resolveSchemaPath(schemaPath: string, specFilePath: string): string {
|
|
47
|
-
if (path.isAbsolute(schemaPath)) return schemaPath;
|
|
48
|
-
return path.resolve(path.dirname(specFilePath), schemaPath);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build the prompt string sent to the subprocess.
|
|
53
|
-
*
|
|
54
|
-
* The prompt includes:
|
|
55
|
-
* 1. The compiled task template (if set), or the resolved input as context
|
|
56
|
-
* 2. Output instructions (if schema-bound)
|
|
57
|
-
*/
|
|
58
|
-
export function buildPrompt(
|
|
59
|
-
step: { agent?: string; input?: Record<string, unknown>; output?: { format?: string; schema?: string } },
|
|
60
|
-
agentSpec: AgentSpec,
|
|
61
|
-
resolvedInput: unknown,
|
|
62
|
-
runDir: string,
|
|
63
|
-
stepName: string,
|
|
64
|
-
): string {
|
|
65
|
-
const parts: string[] = [];
|
|
66
|
-
|
|
67
|
-
// Task template was compiled by compileAgentSpec — use it
|
|
68
|
-
if (agentSpec.taskTemplate) {
|
|
69
|
-
parts.push(agentSpec.taskTemplate);
|
|
70
|
-
} else if (resolvedInput && typeof resolvedInput === "object" && Object.keys(resolvedInput).length > 0) {
|
|
71
|
-
// No task template — serialize input as JSON
|
|
72
|
-
parts.push("## Input\n");
|
|
73
|
-
parts.push("```json");
|
|
74
|
-
parts.push(JSON.stringify(resolvedInput, null, 2));
|
|
75
|
-
parts.push("```\n");
|
|
76
|
-
} else if (typeof resolvedInput === "string") {
|
|
77
|
-
parts.push(resolvedInput);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Output instructions (if schema-bound)
|
|
81
|
-
if (step.output?.format === "json" || step.output?.schema) {
|
|
82
|
-
const outputPath = path.join(runDir, "outputs", `${stepName}.json`);
|
|
83
|
-
parts.push("\n---");
|
|
84
|
-
parts.push(`**Output:** Write your result as valid JSON to: ${outputPath}`);
|
|
85
|
-
if (step.output.schema) {
|
|
86
|
-
parts.push(`The output must conform to the JSON Schema at: ${resolveSchemaPath(step.output.schema, "")}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return parts.join("\n");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Persist step result to state and update TUI widget.
|
|
95
|
-
* Replaces the repeated writeState + setWidget pattern.
|
|
96
|
-
*/
|
|
97
|
-
export function persistStep(
|
|
98
|
-
state: ExecutionState,
|
|
99
|
-
stepName: string,
|
|
100
|
-
result: StepResult,
|
|
101
|
-
runDir: string,
|
|
102
|
-
widgetState: ProgressWidgetState,
|
|
103
|
-
ctx: { hasUI: boolean; ui: { setWidget(id: string, w: unknown): void; notify?(msg: string, level: string): void } },
|
|
104
|
-
): void {
|
|
105
|
-
state.steps[stepName] = result;
|
|
106
|
-
// Clear activity buffer for completed step
|
|
107
|
-
widgetState.activities?.delete(stepName);
|
|
108
|
-
try {
|
|
109
|
-
writeState(runDir, state);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (ctx.hasUI && ctx.ui.notify) {
|
|
112
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
-
ctx.ui.notify(`State write failed after step '${stepName}': ${msg}`, "error");
|
|
114
|
-
}
|
|
115
|
-
throw err; // re-throw — state write failure is fatal
|
|
116
|
-
}
|
|
117
|
-
if (ctx.hasUI) {
|
|
118
|
-
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Compile an agent spec: render system and task templates through Nunjucks.
|
|
124
|
-
*
|
|
125
|
-
* Every agent's prompts go through Nunjucks. Plain text without template
|
|
126
|
-
* tags renders to itself. The .md that pi receives is compiled output.
|
|
127
|
-
*/
|
|
128
|
-
export function compileAgentSpec(
|
|
129
|
-
agentSpec: AgentSpec,
|
|
130
|
-
resolvedInput: unknown,
|
|
131
|
-
templateEnv?: nunjucks.Environment,
|
|
132
|
-
): AgentSpec {
|
|
133
|
-
if (!templateEnv) return agentSpec;
|
|
134
|
-
|
|
135
|
-
const ctx = typeof resolvedInput === "object" && resolvedInput !== null
|
|
136
|
-
? resolvedInput as Record<string, unknown>
|
|
137
|
-
: {};
|
|
138
|
-
|
|
139
|
-
let result = agentSpec;
|
|
140
|
-
|
|
141
|
-
// System prompt: file template or inline — always rendered
|
|
142
|
-
if (agentSpec.promptTemplate) {
|
|
143
|
-
const rendered = renderTemplateFile(templateEnv, agentSpec.promptTemplate, ctx);
|
|
144
|
-
result = { ...result, systemPrompt: rendered, promptTemplate: undefined };
|
|
145
|
-
} else if (agentSpec.systemPrompt) {
|
|
146
|
-
const rendered = renderTemplate(templateEnv, agentSpec.systemPrompt, ctx);
|
|
147
|
-
result = { ...result, systemPrompt: rendered };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Task prompt: file template — rendered from typed input
|
|
151
|
-
if (agentSpec.taskTemplate) {
|
|
152
|
-
const rendered = renderTemplateFile(templateEnv, agentSpec.taskTemplate, ctx);
|
|
153
|
-
result = { ...result, taskTemplate: rendered };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
@@ -1,155 +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 { executeTransform } from "./step-transform.ts";
|
|
7
|
-
import { zeroUsage } from "./step-shared.ts";
|
|
8
|
-
|
|
9
|
-
describe("executeTransform", () => {
|
|
10
|
-
// Happy path
|
|
11
|
-
it("resolves simple mapping to output object", () => {
|
|
12
|
-
const result = executeTransform({ mapping: { greeting: "hello" } }, "step1", {});
|
|
13
|
-
assert.strictEqual(result.status, "completed");
|
|
14
|
-
assert.deepStrictEqual(result.output, { greeting: "hello" });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("resolves ${{ }} expressions in mapping values", () => {
|
|
18
|
-
const result = executeTransform(
|
|
19
|
-
{ mapping: { name: "${{ input.name }}" } },
|
|
20
|
-
"step1",
|
|
21
|
-
{ input: { name: "Alice" } },
|
|
22
|
-
);
|
|
23
|
-
assert.strictEqual(result.status, "completed");
|
|
24
|
-
assert.strictEqual((result.output as any).name, "Alice");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("resolves nested expressions", () => {
|
|
28
|
-
const result = executeTransform(
|
|
29
|
-
{ mapping: { val: "${{ steps.x.output.field }}" } },
|
|
30
|
-
"step1",
|
|
31
|
-
{
|
|
32
|
-
input: {},
|
|
33
|
-
steps: {
|
|
34
|
-
x: {
|
|
35
|
-
step: "x",
|
|
36
|
-
agent: "test",
|
|
37
|
-
status: "completed",
|
|
38
|
-
output: { field: "deep-value" },
|
|
39
|
-
usage: zeroUsage(),
|
|
40
|
-
durationMs: 0,
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
assert.strictEqual(result.status, "completed");
|
|
46
|
-
assert.strictEqual((result.output as any).val, "deep-value");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("preserves non-expression values (numbers, booleans, null)", () => {
|
|
50
|
-
const result = executeTransform(
|
|
51
|
-
{ mapping: { count: 42, flag: true, empty: null } },
|
|
52
|
-
"step1",
|
|
53
|
-
{},
|
|
54
|
-
);
|
|
55
|
-
assert.strictEqual(result.status, "completed");
|
|
56
|
-
const output = result.output as any;
|
|
57
|
-
assert.strictEqual(output.count, 42);
|
|
58
|
-
assert.strictEqual(output.flag, true);
|
|
59
|
-
assert.strictEqual(output.empty, null);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("sets agent to 'transform'", () => {
|
|
63
|
-
const result = executeTransform({ mapping: {} }, "step1", {});
|
|
64
|
-
assert.strictEqual(result.agent, "transform");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("sets step name correctly", () => {
|
|
68
|
-
const result = executeTransform({ mapping: {} }, "my-transform", {});
|
|
69
|
-
assert.strictEqual(result.step, "my-transform");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("has zero usage", () => {
|
|
73
|
-
const result = executeTransform({ mapping: {} }, "step1", {});
|
|
74
|
-
assert.deepStrictEqual(result.usage, zeroUsage());
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("has non-negative durationMs", () => {
|
|
78
|
-
const result = executeTransform({ mapping: {} }, "step1", {});
|
|
79
|
-
assert.ok(result.durationMs >= 0);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("includes textOutput as JSON-stringified output", () => {
|
|
83
|
-
const result = executeTransform({ mapping: { a: 1 } }, "step1", {});
|
|
84
|
-
assert.strictEqual(result.textOutput, JSON.stringify({ a: 1 }, null, 2));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Output persistence
|
|
88
|
-
it("persists output to runDir when runDir is provided", (t) => {
|
|
89
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-transform-"));
|
|
90
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
91
|
-
|
|
92
|
-
const result = executeTransform({ mapping: { x: 1 } }, "step1", {}, tmpDir);
|
|
93
|
-
assert.ok(result.outputPath);
|
|
94
|
-
assert.ok(fs.existsSync(result.outputPath!));
|
|
95
|
-
const persisted = JSON.parse(fs.readFileSync(result.outputPath!, "utf-8"));
|
|
96
|
-
assert.deepStrictEqual(persisted, { x: 1 });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("persists output to outputPath when provided", (t) => {
|
|
100
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-transform-"));
|
|
101
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
102
|
-
|
|
103
|
-
const customPath = path.join(tmpDir, "custom-out.json");
|
|
104
|
-
const result = executeTransform({ mapping: { y: 2 } }, "step1", {}, tmpDir, customPath);
|
|
105
|
-
assert.strictEqual(result.outputPath, customPath);
|
|
106
|
-
assert.ok(fs.existsSync(customPath));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("does not persist when runDir is omitted", () => {
|
|
110
|
-
const result = executeTransform({ mapping: { z: 3 } }, "step1", {});
|
|
111
|
-
assert.strictEqual(result.outputPath, undefined);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Error paths
|
|
115
|
-
it("returns failed status when expression resolution throws", () => {
|
|
116
|
-
const result = executeTransform(
|
|
117
|
-
{ mapping: { bad: "${{ steps.nonexistent.output }}" } },
|
|
118
|
-
"step1",
|
|
119
|
-
{ input: {}, steps: {} },
|
|
120
|
-
);
|
|
121
|
-
assert.strictEqual(result.status, "failed");
|
|
122
|
-
assert.ok(result.error);
|
|
123
|
-
assert.ok(result.error!.length > 0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("returns failed status with error message string", () => {
|
|
127
|
-
const result = executeTransform(
|
|
128
|
-
{ mapping: { bad: "${{ steps.nonexistent.output }}" } },
|
|
129
|
-
"step1",
|
|
130
|
-
{ input: {}, steps: {} },
|
|
131
|
-
);
|
|
132
|
-
assert.strictEqual(result.status, "failed");
|
|
133
|
-
assert.strictEqual(typeof result.error, "string");
|
|
134
|
-
assert.ok(result.error!.includes("nonexistent"));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Edge cases
|
|
138
|
-
it("handles empty mapping object", () => {
|
|
139
|
-
const result = executeTransform({ mapping: {} }, "step1", {});
|
|
140
|
-
assert.strictEqual(result.status, "completed");
|
|
141
|
-
assert.deepStrictEqual(result.output, {});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("handles mapping with mixed expressions and literals", () => {
|
|
145
|
-
const result = executeTransform(
|
|
146
|
-
{ mapping: { resolved: "${{ input.x }}", literal: "plain" } },
|
|
147
|
-
"step1",
|
|
148
|
-
{ input: { x: 42 } },
|
|
149
|
-
);
|
|
150
|
-
assert.strictEqual(result.status, "completed");
|
|
151
|
-
const output = result.output as any;
|
|
152
|
-
assert.strictEqual(output.resolved, 42);
|
|
153
|
-
assert.strictEqual(output.literal, "plain");
|
|
154
|
-
});
|
|
155
|
-
});
|