@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/dispatch.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subprocess dispatch for workflow steps — spawns pi in JSON mode,
|
|
3
|
-
* collects events, usage, and output. Supports cancellation and timeout.
|
|
4
|
-
*/
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import type { StepSpec, StepResult, StepUsage, AgentSpec } from "./types.ts";
|
|
10
|
-
import { SIGKILL_GRACE_MS } from "./step-shared.ts";
|
|
11
|
-
|
|
12
|
-
export interface ModelConfig {
|
|
13
|
-
default?: string;
|
|
14
|
-
by_role?: Record<string, string>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DispatchOptions {
|
|
18
|
-
cwd: string;
|
|
19
|
-
sessionLogDir: string; // directory for session log file (e.g. <runDir>/sessions/)
|
|
20
|
-
stepName: string; // used for naming the session log file
|
|
21
|
-
signal?: AbortSignal; // for cancellation
|
|
22
|
-
timeoutMs?: number; // timeout in milliseconds — kills subprocess after deadline
|
|
23
|
-
onEvent?: (event: ProcessEvent) => void; // live event callback for TUI updates
|
|
24
|
-
modelConfig?: ModelConfig; // project-level model assignments by role
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ProcessEvent {
|
|
28
|
-
type: string;
|
|
29
|
-
raw: unknown; // the parsed JSON line
|
|
30
|
-
// Extracted fields for common events:
|
|
31
|
-
toolName?: string; // for tool_execution_start/end
|
|
32
|
-
toolArgs?: string; // preview string for tool_execution_start
|
|
33
|
-
messageText?: string; // text content for message_end (assistant messages)
|
|
34
|
-
usage?: Partial<StepUsage>; // usage delta for message_end
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Prompt length threshold (chars) for switching to @file argument passing.
|
|
39
|
-
* Avoids OS-level argument length limits (typically 128–256 KB) with headroom
|
|
40
|
-
* for the rest of the CLI args. 8000 chars is well under all limits while
|
|
41
|
-
* keeping short prompts as direct args for simplicity.
|
|
42
|
-
*/
|
|
43
|
-
const PROMPT_ARG_LIMIT = 8000;
|
|
44
|
-
|
|
45
|
-
/** Maximum stdout buffer size (10 MB) to prevent OOM from runaway subprocesses. */
|
|
46
|
-
const MAX_STDOUT_BYTES = 10 * 1024 * 1024;
|
|
47
|
-
|
|
48
|
-
export function extractText(content: unknown): string {
|
|
49
|
-
if (!content || !Array.isArray(content)) return "";
|
|
50
|
-
for (const part of content) {
|
|
51
|
-
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
52
|
-
return String(part.text);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function extractToolArgsPreview(args: unknown): string {
|
|
59
|
-
if (!args || typeof args !== "object") return "";
|
|
60
|
-
const obj = args as Record<string, unknown>;
|
|
61
|
-
for (const key of ["command", "path", "pattern", "query", "task"]) {
|
|
62
|
-
if (typeof obj[key] === "string") {
|
|
63
|
-
const val = obj[key] as string;
|
|
64
|
-
return val.length > 60 ? val.slice(0, 57) + "..." : val;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return "";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function buildArgs(step: StepSpec, agentSpec: AgentSpec, prompt: string, options: DispatchOptions): string[] {
|
|
71
|
-
const args = ["--mode", "json"];
|
|
72
|
-
|
|
73
|
-
// Session log
|
|
74
|
-
args.push("--session-dir", options.sessionLogDir);
|
|
75
|
-
|
|
76
|
-
// Model: step > agent spec > model-config by role > model-config default
|
|
77
|
-
const model = step.model ?? agentSpec.model
|
|
78
|
-
?? (agentSpec.role && options.modelConfig?.by_role?.[agentSpec.role])
|
|
79
|
-
?? options.modelConfig?.default;
|
|
80
|
-
if (model) {
|
|
81
|
-
const modelArg = `${model}:${agentSpec.thinking ?? "off"}`;
|
|
82
|
-
args.push("--models", modelArg);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Tool filtering
|
|
86
|
-
if (agentSpec.tools?.length) {
|
|
87
|
-
const builtinTools: string[] = [];
|
|
88
|
-
const extensionPaths: string[] = [];
|
|
89
|
-
for (const tool of agentSpec.tools) {
|
|
90
|
-
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
91
|
-
extensionPaths.push(tool);
|
|
92
|
-
} else {
|
|
93
|
-
builtinTools.push(tool);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (builtinTools.length > 0) args.push("--tools", builtinTools.join(","));
|
|
97
|
-
for (const ext of extensionPaths) args.push("--extension", ext);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Extension scoping
|
|
101
|
-
if (agentSpec.extensions !== undefined) {
|
|
102
|
-
args.push("--no-extensions");
|
|
103
|
-
for (const ext of agentSpec.extensions) args.push("--extension", ext);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Skill scoping
|
|
107
|
-
if (agentSpec.skills?.length) {
|
|
108
|
-
args.push("--no-skills");
|
|
109
|
-
// Skills are injected via --append-system-prompt, not CLI flags
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// System prompt (if agent spec has one)
|
|
113
|
-
// Write to temp file if present, pass via --append-system-prompt
|
|
114
|
-
// (handled in dispatch() body, not buildArgs)
|
|
115
|
-
|
|
116
|
-
// Prompt — use @file for long prompts
|
|
117
|
-
args.push("-p");
|
|
118
|
-
args.push(prompt); // or @<tmpfile> if prompt > 8000 chars
|
|
119
|
-
|
|
120
|
-
return args;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Spawn a pi subprocess for a workflow step and collect the result.
|
|
125
|
-
*
|
|
126
|
-
* Builds CLI args from the step spec and agent spec.
|
|
127
|
-
* Streams stdout as newline-delimited JSON.
|
|
128
|
-
* Collects messages, usage, timing.
|
|
129
|
-
* Returns StepResult when the process exits.
|
|
130
|
-
*/
|
|
131
|
-
export async function dispatch(
|
|
132
|
-
step: StepSpec,
|
|
133
|
-
agentSpec: AgentSpec,
|
|
134
|
-
prompt: string,
|
|
135
|
-
options: DispatchOptions,
|
|
136
|
-
): Promise<StepResult> {
|
|
137
|
-
const startTime = Date.now();
|
|
138
|
-
const args = buildArgs(step, agentSpec, prompt, options);
|
|
139
|
-
|
|
140
|
-
// Handle long prompts: write to temp file
|
|
141
|
-
let tmpDir: string | null = null;
|
|
142
|
-
if (prompt.length > PROMPT_ARG_LIMIT) {
|
|
143
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wf-"));
|
|
144
|
-
const promptFile = path.join(tmpDir, "prompt.md");
|
|
145
|
-
fs.writeFileSync(promptFile, prompt, { mode: 0o600 });
|
|
146
|
-
// Replace last two args ("-p", prompt) with ("-p", "@<file>")
|
|
147
|
-
args[args.length - 1] = `@${promptFile}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Handle system prompt: write to temp file
|
|
151
|
-
if (agentSpec.systemPrompt) {
|
|
152
|
-
if (!tmpDir) tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wf-"));
|
|
153
|
-
const sysFile = path.join(tmpDir, "system.md");
|
|
154
|
-
fs.writeFileSync(sysFile, agentSpec.systemPrompt, { mode: 0o600 });
|
|
155
|
-
// Insert before -p flag
|
|
156
|
-
const pIdx = args.indexOf("-p");
|
|
157
|
-
args.splice(pIdx, 0, "--append-system-prompt", sysFile);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const proc = spawn("pi", args, {
|
|
161
|
-
cwd: options.cwd,
|
|
162
|
-
env: process.env,
|
|
163
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Cancellation support
|
|
167
|
-
if (options.signal) {
|
|
168
|
-
const kill = () => {
|
|
169
|
-
proc.kill("SIGTERM");
|
|
170
|
-
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, SIGKILL_GRACE_MS);
|
|
171
|
-
};
|
|
172
|
-
if (options.signal.aborted) kill();
|
|
173
|
-
else options.signal.addEventListener("abort", kill, { once: true });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Timeout support: SIGTERM after deadline, SIGKILL after 5s grace
|
|
177
|
-
let timedOut = false;
|
|
178
|
-
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
179
|
-
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
180
|
-
if (options.timeoutMs) {
|
|
181
|
-
timeoutTimer = setTimeout(() => {
|
|
182
|
-
timedOut = true;
|
|
183
|
-
proc.kill("SIGTERM");
|
|
184
|
-
killTimer = setTimeout(() => {
|
|
185
|
-
if (!proc.killed) proc.kill("SIGKILL");
|
|
186
|
-
}, SIGKILL_GRACE_MS);
|
|
187
|
-
}, options.timeoutMs);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Collect result
|
|
191
|
-
const messages: unknown[] = [];
|
|
192
|
-
const usage: StepUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
193
|
-
let lastAssistantText = "";
|
|
194
|
-
let stderrBuf = "";
|
|
195
|
-
|
|
196
|
-
function processEvent(evt: { type?: string; message?: any; toolName?: string; args?: any }) {
|
|
197
|
-
// Track messages
|
|
198
|
-
if (evt.type === "message_end" && evt.message) {
|
|
199
|
-
messages.push(evt.message);
|
|
200
|
-
if (evt.message.role === "assistant") {
|
|
201
|
-
usage.turns++;
|
|
202
|
-
const u = evt.message.usage;
|
|
203
|
-
if (u) {
|
|
204
|
-
usage.input += u.input || 0;
|
|
205
|
-
usage.output += u.output || 0;
|
|
206
|
-
usage.cacheRead += u.cacheRead || 0;
|
|
207
|
-
usage.cacheWrite += u.cacheWrite || 0;
|
|
208
|
-
usage.cost += u.cost?.total || 0;
|
|
209
|
-
}
|
|
210
|
-
// Extract text from last assistant message
|
|
211
|
-
const text = extractText(evt.message.content);
|
|
212
|
-
if (text) lastAssistantText = text;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Forward to TUI callback
|
|
217
|
-
if (options.onEvent) {
|
|
218
|
-
options.onEvent({
|
|
219
|
-
type: evt.type || "unknown",
|
|
220
|
-
raw: evt,
|
|
221
|
-
toolName: evt.type === "tool_execution_start" ? evt.toolName : undefined,
|
|
222
|
-
toolArgs: evt.type === "tool_execution_start" ? extractToolArgsPreview(evt.args) : undefined,
|
|
223
|
-
messageText: evt.type === "message_end" && evt.message?.role === "assistant"
|
|
224
|
-
? extractText(evt.message.content)
|
|
225
|
-
: undefined,
|
|
226
|
-
usage: evt.type === "message_end" && evt.message?.role === "assistant" && evt.message.usage
|
|
227
|
-
? {
|
|
228
|
-
input: evt.message.usage.input,
|
|
229
|
-
output: evt.message.usage.output,
|
|
230
|
-
cost: evt.message.usage.cost?.total,
|
|
231
|
-
}
|
|
232
|
-
: undefined,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Stream stdout as newline-delimited JSON
|
|
238
|
-
let buf = "";
|
|
239
|
-
let bufBytes = 0;
|
|
240
|
-
let stdoutTruncated = false;
|
|
241
|
-
proc.stdout!.on("data", (chunk: Buffer) => {
|
|
242
|
-
bufBytes += chunk.length;
|
|
243
|
-
if (bufBytes > MAX_STDOUT_BYTES) {
|
|
244
|
-
if (!stdoutTruncated) {
|
|
245
|
-
stdoutTruncated = true;
|
|
246
|
-
// Process any remaining partial line in buf before discarding
|
|
247
|
-
if (buf.trim()) {
|
|
248
|
-
try {
|
|
249
|
-
const evt = JSON.parse(buf);
|
|
250
|
-
processEvent(evt);
|
|
251
|
-
} catch {
|
|
252
|
-
// incomplete JSON line — discard
|
|
253
|
-
}
|
|
254
|
-
buf = "";
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
buf += chunk.toString();
|
|
260
|
-
const lines = buf.split("\n");
|
|
261
|
-
buf = lines.pop() || "";
|
|
262
|
-
for (const line of lines) {
|
|
263
|
-
if (!line.trim()) continue;
|
|
264
|
-
try {
|
|
265
|
-
const evt = JSON.parse(line);
|
|
266
|
-
processEvent(evt);
|
|
267
|
-
} catch {
|
|
268
|
-
// Skip unparseable lines
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
proc.stderr!.on("data", (chunk: Buffer) => {
|
|
274
|
-
stderrBuf += chunk.toString();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Wait for process to exit
|
|
278
|
-
const exitCode = await new Promise<number | null>((resolve) => {
|
|
279
|
-
proc.on("close", (code) => {
|
|
280
|
-
// Process remaining buffer
|
|
281
|
-
if (buf.trim()) {
|
|
282
|
-
try {
|
|
283
|
-
const evt = JSON.parse(buf);
|
|
284
|
-
processEvent(evt);
|
|
285
|
-
} catch {
|
|
286
|
-
// Skip unparseable remainder
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
resolve(code);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Clear timeout timers
|
|
294
|
-
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
295
|
-
if (killTimer) clearTimeout(killTimer);
|
|
296
|
-
|
|
297
|
-
// Cleanup temp files
|
|
298
|
-
if (tmpDir) {
|
|
299
|
-
try {
|
|
300
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
301
|
-
} catch {
|
|
302
|
-
// Best-effort cleanup
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Timeout overrides exit code interpretation
|
|
307
|
-
if (timedOut) {
|
|
308
|
-
const warnings: string[] = [];
|
|
309
|
-
if (stdoutTruncated) {
|
|
310
|
-
warnings.push(
|
|
311
|
-
`Stdout exceeded ${MAX_STDOUT_BYTES / (1024 * 1024)}MB limit — ` +
|
|
312
|
-
`output stream was truncated at ${bufBytes} bytes. ` +
|
|
313
|
-
`Usage counters (tokens, cost, turns) and textOutput may be incomplete.`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
return {
|
|
317
|
-
step: options.stepName,
|
|
318
|
-
agent: step.agent,
|
|
319
|
-
status: "failed",
|
|
320
|
-
output: undefined,
|
|
321
|
-
textOutput: lastAssistantText,
|
|
322
|
-
sessionLog: options.sessionLogDir,
|
|
323
|
-
usage,
|
|
324
|
-
durationMs: Date.now() - startTime,
|
|
325
|
-
error: `Step timed out after ${options.timeoutMs! / 1000}s`,
|
|
326
|
-
truncated: stdoutTruncated || undefined,
|
|
327
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const warnings: string[] = [];
|
|
332
|
-
if (stdoutTruncated) {
|
|
333
|
-
warnings.push(
|
|
334
|
-
`Stdout exceeded ${MAX_STDOUT_BYTES / (1024 * 1024)}MB limit — ` +
|
|
335
|
-
`output stream was truncated at ${bufBytes} bytes. ` +
|
|
336
|
-
`Usage counters (tokens, cost, turns) and textOutput may be incomplete.`
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
step: options.stepName,
|
|
342
|
-
agent: step.agent,
|
|
343
|
-
status: exitCode === 0 ? "completed" : "failed",
|
|
344
|
-
output: undefined, // structured output handled by caller (workflow-executor)
|
|
345
|
-
textOutput: lastAssistantText,
|
|
346
|
-
sessionLog: options.sessionLogDir,
|
|
347
|
-
usage,
|
|
348
|
-
durationMs: Date.now() - startTime,
|
|
349
|
-
error: exitCode !== 0 ? (stderrBuf.trim() || "Process exited with code " + exitCode) : undefined,
|
|
350
|
-
truncated: stdoutTruncated || undefined,
|
|
351
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
352
|
-
};
|
|
353
|
-
}
|
package/src/expression.test.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { resolveExpressions, resolveExpression, evaluateCondition, ExpressionError } from "./expression.ts";
|
|
4
|
-
import type { ExpressionScope, CompletionScope } from "./types.ts";
|
|
5
|
-
|
|
6
|
-
const scope: ExpressionScope = {
|
|
7
|
-
input: {
|
|
8
|
-
description: "null pointer in login",
|
|
9
|
-
maxAttempts: 3,
|
|
10
|
-
tags: ["auth", "critical"],
|
|
11
|
-
},
|
|
12
|
-
steps: {
|
|
13
|
-
diagnose: {
|
|
14
|
-
step: "diagnose",
|
|
15
|
-
agent: "diagnostician",
|
|
16
|
-
status: "completed",
|
|
17
|
-
output: {
|
|
18
|
-
summary: "NPE in UserService.login()",
|
|
19
|
-
rootCause: "session param is null",
|
|
20
|
-
fixLocation: { file: "src/auth/user-service.ts", line: 47 },
|
|
21
|
-
reproCommand: "npm test -- --grep login",
|
|
22
|
-
},
|
|
23
|
-
textOutput: "Found the bug...",
|
|
24
|
-
usage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.03, turns: 2 },
|
|
25
|
-
durationMs: 42000,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
describe("resolveExpression", () => {
|
|
31
|
-
it("resolves input properties", () => {
|
|
32
|
-
assert.strictEqual(resolveExpression("input.description", scope), "null pointer in login");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("resolves input number", () => {
|
|
36
|
-
assert.strictEqual(resolveExpression("input.maxAttempts", scope), 3);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("resolves step output object", () => {
|
|
40
|
-
const result = resolveExpression("steps.diagnose.output", scope);
|
|
41
|
-
assert.strictEqual(typeof result, "object");
|
|
42
|
-
assert.strictEqual((result as any).summary, "NPE in UserService.login()");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("resolves nested step output", () => {
|
|
46
|
-
assert.strictEqual(
|
|
47
|
-
resolveExpression("steps.diagnose.output.fixLocation.file", scope),
|
|
48
|
-
"src/auth/user-service.ts",
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("resolves step metadata", () => {
|
|
53
|
-
assert.strictEqual(resolveExpression("steps.diagnose.status", scope), "completed");
|
|
54
|
-
assert.strictEqual(resolveExpression("steps.diagnose.usage.cost", scope), 0.03);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("returns undefined for missing optional property on input", () => {
|
|
58
|
-
assert.strictEqual(resolveExpression("input.nonexistent", scope), undefined);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("returns undefined for missing optional property on step output", () => {
|
|
62
|
-
assert.strictEqual(resolveExpression("steps.diagnose.output.missingField", scope), undefined);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("throws on unexecuted step", () => {
|
|
66
|
-
assert.throws(
|
|
67
|
-
() => resolveExpression("steps.fix.output", scope),
|
|
68
|
-
(err: unknown) => err instanceof ExpressionError && err.message.includes("fix"),
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("throws on invalid root path", () => {
|
|
73
|
-
assert.throws(
|
|
74
|
-
() => resolveExpression("typo.something", scope),
|
|
75
|
-
(err: unknown) => err instanceof ExpressionError,
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ── Filter tests ──
|
|
81
|
-
|
|
82
|
-
describe("resolveExpression filters", () => {
|
|
83
|
-
it("applies duration filter", () => {
|
|
84
|
-
assert.strictEqual(
|
|
85
|
-
resolveExpression("steps.diagnose.durationMs | duration", scope),
|
|
86
|
-
"42s",
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("applies currency filter", () => {
|
|
91
|
-
assert.strictEqual(
|
|
92
|
-
resolveExpression("steps.diagnose.usage.cost | currency", scope),
|
|
93
|
-
"$0.03",
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("applies json filter", () => {
|
|
98
|
-
const result = resolveExpression("steps.diagnose.output.fixLocation | json", scope);
|
|
99
|
-
assert.strictEqual(typeof result, "string");
|
|
100
|
-
const parsed = JSON.parse(result as string);
|
|
101
|
-
assert.strictEqual(parsed.file, "src/auth/user-service.ts");
|
|
102
|
-
assert.strictEqual(parsed.line, 47);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("applies length filter to array", () => {
|
|
106
|
-
const result = resolveExpression("input.tags | length", scope);
|
|
107
|
-
assert.strictEqual(result, 2);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("applies length filter to string", () => {
|
|
111
|
-
const result = resolveExpression("input.description | length", scope);
|
|
112
|
-
assert.strictEqual(result, 21);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("applies length filter to non-array/string returns 0", () => {
|
|
116
|
-
const result = resolveExpression("input.maxAttempts | length", scope);
|
|
117
|
-
assert.strictEqual(result, 0);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("applies keys filter to object", () => {
|
|
121
|
-
const result = resolveExpression("steps.diagnose.output.fixLocation | keys", scope);
|
|
122
|
-
assert.ok(Array.isArray(result));
|
|
123
|
-
assert.ok((result as string[]).includes("file"));
|
|
124
|
-
assert.ok((result as string[]).includes("line"));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("applies filter filter removes falsy values", () => {
|
|
128
|
-
const scopeWithFalsy = {
|
|
129
|
-
...scope,
|
|
130
|
-
input: { ...scope.input, items: [1, 0, null, "hello", "", false, true] },
|
|
131
|
-
};
|
|
132
|
-
const result = resolveExpression("input.items | filter", scopeWithFalsy);
|
|
133
|
-
assert.deepStrictEqual(result, [1, "hello", true]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("handles whitespace around pipe", () => {
|
|
137
|
-
assert.strictEqual(
|
|
138
|
-
resolveExpression("steps.diagnose.durationMs | duration", scope),
|
|
139
|
-
"42s",
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("throws on unknown filter", () => {
|
|
144
|
-
assert.throws(
|
|
145
|
-
() => resolveExpression("steps.diagnose.durationMs | bogus", scope),
|
|
146
|
-
(err: unknown) => err instanceof ExpressionError && err.message.includes("bogus"),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("applies filter in embedded expression", () => {
|
|
151
|
-
const result = resolveExpressions(
|
|
152
|
-
"Took ${{ steps.diagnose.durationMs | duration }}, cost ${{ steps.diagnose.usage.cost | currency }}",
|
|
153
|
-
scope,
|
|
154
|
-
);
|
|
155
|
-
assert.strictEqual(result, "Took 42s, cost $0.03");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("applies filter in whole-value expression", () => {
|
|
159
|
-
const result = resolveExpressions("${{ steps.diagnose.durationMs | duration }}", scope);
|
|
160
|
-
assert.strictEqual(result, "42s");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// ── Wider scope (CompletionScope) tests ──
|
|
165
|
-
|
|
166
|
-
describe("resolveExpression with CompletionScope", () => {
|
|
167
|
-
const completionScope: CompletionScope = {
|
|
168
|
-
input: { path: "/src" },
|
|
169
|
-
steps: scope.steps,
|
|
170
|
-
totalUsage: { input: 1500, output: 700, cacheRead: 0, cacheWrite: 0, cost: 0.05, turns: 3 },
|
|
171
|
-
totalDurationMs: 92000,
|
|
172
|
-
runDir: "/tmp/runs/test-run",
|
|
173
|
-
runId: "test-20260313-120000-abcd",
|
|
174
|
-
workflow: "explore-summarize",
|
|
175
|
-
status: "completed",
|
|
176
|
-
output: "Final summary text",
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
it("resolves root-level completion fields", () => {
|
|
180
|
-
assert.strictEqual(resolveExpression("workflow", completionScope), "explore-summarize");
|
|
181
|
-
assert.strictEqual(resolveExpression("status", completionScope), "completed");
|
|
182
|
-
assert.strictEqual(resolveExpression("runDir", completionScope), "/tmp/runs/test-run");
|
|
183
|
-
assert.strictEqual(resolveExpression("runId", completionScope), "test-20260313-120000-abcd");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("resolves totalUsage fields", () => {
|
|
187
|
-
assert.strictEqual(
|
|
188
|
-
resolveExpression("totalUsage.cost", completionScope),
|
|
189
|
-
0.05,
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("applies filters on completion fields", () => {
|
|
194
|
-
assert.strictEqual(
|
|
195
|
-
resolveExpression("totalDurationMs | duration", completionScope),
|
|
196
|
-
"1m32s",
|
|
197
|
-
);
|
|
198
|
-
assert.strictEqual(
|
|
199
|
-
resolveExpression("totalUsage.cost | currency", completionScope),
|
|
200
|
-
"$0.05",
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("still resolves input and steps from completion scope", () => {
|
|
205
|
-
assert.strictEqual(
|
|
206
|
-
resolveExpression("input.path", completionScope),
|
|
207
|
-
"/src",
|
|
208
|
-
);
|
|
209
|
-
assert.strictEqual(
|
|
210
|
-
resolveExpression("steps.diagnose.status", completionScope),
|
|
211
|
-
"completed",
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// ── evaluateCondition tests ──
|
|
217
|
-
|
|
218
|
-
describe("evaluateCondition", () => {
|
|
219
|
-
it("returns true for truthy value", () => {
|
|
220
|
-
assert.strictEqual(evaluateCondition("input.description", scope), true);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("returns true for truthy number", () => {
|
|
224
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts", scope), true);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("returns false for undefined (missing optional field)", () => {
|
|
228
|
-
assert.strictEqual(evaluateCondition("input.nonexistent", scope), false);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("supports negation with ! prefix", () => {
|
|
232
|
-
assert.strictEqual(evaluateCondition("!input.nonexistent", scope), true);
|
|
233
|
-
assert.strictEqual(evaluateCondition("!input.description", scope), false);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("supports equality with string literal", () => {
|
|
237
|
-
assert.strictEqual(evaluateCondition("input.description == 'null pointer in login'", scope), true);
|
|
238
|
-
assert.strictEqual(evaluateCondition("input.description == 'something else'", scope), false);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("supports numeric comparison", () => {
|
|
242
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts > 2", scope), true);
|
|
243
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts > 5", scope), false);
|
|
244
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts >= 3", scope), true);
|
|
245
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts < 10", scope), true);
|
|
246
|
-
assert.strictEqual(evaluateCondition("input.maxAttempts <= 3", scope), true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("treats boolean false as falsy", () => {
|
|
250
|
-
const boolScope = { flag: false } as unknown as Record<string, unknown>;
|
|
251
|
-
assert.strictEqual(evaluateCondition("flag", boolScope), false);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("supports != operator", () => {
|
|
255
|
-
assert.strictEqual(evaluateCondition("steps.diagnose.status != 'failed'", scope), true);
|
|
256
|
-
assert.strictEqual(evaluateCondition("steps.diagnose.status != 'completed'", scope), false);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("supports comparison with boolean literal", () => {
|
|
260
|
-
const boolScope = { enabled: true } as unknown as Record<string, unknown>;
|
|
261
|
-
assert.strictEqual(evaluateCondition("enabled == true", boolScope), true);
|
|
262
|
-
assert.strictEqual(evaluateCondition("enabled == false", boolScope), false);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("resolveExpressions", () => {
|
|
267
|
-
it("resolves whole-value expression preserving type", () => {
|
|
268
|
-
const result = resolveExpressions("${{ steps.diagnose.output }}", scope);
|
|
269
|
-
assert.strictEqual(typeof result, "object");
|
|
270
|
-
assert.strictEqual((result as any).summary, "NPE in UserService.login()");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("resolves whole-value expression preserving number", () => {
|
|
274
|
-
const result = resolveExpressions("${{ input.maxAttempts }}", scope);
|
|
275
|
-
assert.strictEqual(result, 3);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("resolves whole-value expression preserving array", () => {
|
|
279
|
-
const result = resolveExpressions("${{ input.tags }}", scope);
|
|
280
|
-
assert.ok(Array.isArray(result));
|
|
281
|
-
assert.deepStrictEqual(result, ["auth", "critical"]);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("resolves embedded expression as string interpolation", () => {
|
|
285
|
-
const result = resolveExpressions(
|
|
286
|
-
"Bug in ${{ steps.diagnose.output.fixLocation.file }} at line ${{ steps.diagnose.output.fixLocation.line }}",
|
|
287
|
-
scope,
|
|
288
|
-
);
|
|
289
|
-
assert.strictEqual(result, "Bug in src/auth/user-service.ts at line 47");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("resolves object values recursively", () => {
|
|
293
|
-
const input = {
|
|
294
|
-
description: "${{ input.description }}",
|
|
295
|
-
diagnosis: "${{ steps.diagnose.output }}",
|
|
296
|
-
nested: {
|
|
297
|
-
file: "${{ steps.diagnose.output.fixLocation.file }}",
|
|
298
|
-
plain: "no expression here",
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
const result = resolveExpressions(input, scope) as any;
|
|
302
|
-
assert.strictEqual(result.description, "null pointer in login");
|
|
303
|
-
assert.strictEqual(typeof result.diagnosis, "object");
|
|
304
|
-
assert.strictEqual(result.nested.file, "src/auth/user-service.ts");
|
|
305
|
-
assert.strictEqual(result.nested.plain, "no expression here");
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("resolves array elements", () => {
|
|
309
|
-
const input = ["${{ input.description }}", "${{ input.maxAttempts }}"];
|
|
310
|
-
const result = resolveExpressions(input, scope) as unknown[];
|
|
311
|
-
assert.strictEqual(result[0], "null pointer in login");
|
|
312
|
-
assert.strictEqual(result[1], 3);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("passes through non-string primitives", () => {
|
|
316
|
-
assert.strictEqual(resolveExpressions(42, scope), 42);
|
|
317
|
-
assert.strictEqual(resolveExpressions(true, scope), true);
|
|
318
|
-
assert.strictEqual(resolveExpressions(null, scope), null);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("passes through strings with no expressions", () => {
|
|
322
|
-
assert.strictEqual(resolveExpressions("plain string", scope), "plain string");
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("handles whitespace in expressions", () => {
|
|
326
|
-
assert.strictEqual(
|
|
327
|
-
resolveExpressions("${{ input.description }}", scope),
|
|
328
|
-
"null pointer in login",
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("renders undefined as empty string in embedded expressions", () => {
|
|
333
|
-
const result = resolveExpressions(
|
|
334
|
-
"Question: ${{ input.nonexistent }}",
|
|
335
|
-
scope,
|
|
336
|
-
);
|
|
337
|
-
assert.strictEqual(result, "Question: ");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("resolves whole-value undefined for missing optional field", () => {
|
|
341
|
-
const result = resolveExpressions("${{ input.nonexistent }}", scope);
|
|
342
|
-
assert.strictEqual(result, undefined);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("stringifies objects in embedded expressions", () => {
|
|
346
|
-
const result = resolveExpressions(
|
|
347
|
-
"Location: ${{ steps.diagnose.output.fixLocation }}",
|
|
348
|
-
scope,
|
|
349
|
-
);
|
|
350
|
-
assert.ok(typeof result === "string");
|
|
351
|
-
assert.ok((result as string).includes("src/auth/user-service.ts"));
|
|
352
|
-
});
|
|
353
|
-
});
|