agentic-forge 0.0.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/.gitattributes +24 -0
- package/.github/workflows/ci.yml +70 -0
- package/.markdownlint-cli2.jsonc +16 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/.vscode/agentic-forge.code-workspace +26 -0
- package/CHANGELOG.md +100 -0
- package/CLAUDE.md +158 -0
- package/CONTRIBUTING.md +152 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/agentic-forge-banner.png +0 -0
- package/biome.json +21 -0
- package/package.json +5 -0
- package/scripts/copy-assets.js +21 -0
- package/src/agents/explorer.md +97 -0
- package/src/agents/reviewer.md +137 -0
- package/src/checkpoints/manager.ts +119 -0
- package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
- package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
- package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
- package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
- package/src/claude/.claude/skills/analyze/references/security.md +76 -0
- package/src/claude/.claude/skills/analyze/references/style.md +72 -0
- package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
- package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
- package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
- package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
- package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
- package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
- package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
- package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
- package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
- package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
- package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
- package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
- package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
- package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
- package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
- package/src/cli.ts +182 -0
- package/src/commands/config-cmd.ts +28 -0
- package/src/commands/index.ts +21 -0
- package/src/commands/init.ts +96 -0
- package/src/commands/release-notes.ts +85 -0
- package/src/commands/resume.ts +103 -0
- package/src/commands/run.ts +234 -0
- package/src/commands/shortcuts.ts +11 -0
- package/src/commands/skills-dir.ts +11 -0
- package/src/commands/status.ts +112 -0
- package/src/commands/update.ts +64 -0
- package/src/commands/version.ts +27 -0
- package/src/commands/workflows.ts +129 -0
- package/src/config.ts +129 -0
- package/src/console.ts +790 -0
- package/src/executor.ts +354 -0
- package/src/git/worktree.ts +236 -0
- package/src/logging/logger.ts +95 -0
- package/src/orchestrator.ts +815 -0
- package/src/parser.ts +225 -0
- package/src/progress.ts +306 -0
- package/src/prompts/agentic-system.md +31 -0
- package/src/ralph-loop.ts +260 -0
- package/src/renderer.ts +164 -0
- package/src/runner.ts +634 -0
- package/src/signal-manager.ts +55 -0
- package/src/steps/base.ts +71 -0
- package/src/steps/conditional-step.ts +144 -0
- package/src/steps/index.ts +15 -0
- package/src/steps/parallel-step.ts +213 -0
- package/src/steps/prompt-step.ts +121 -0
- package/src/steps/ralph-loop-step.ts +186 -0
- package/src/steps/serial-step.ts +84 -0
- package/src/templates/analysis/bug.md.j2 +35 -0
- package/src/templates/analysis/debt.md.j2 +38 -0
- package/src/templates/analysis/doc.md.j2 +45 -0
- package/src/templates/analysis/security.md.j2 +35 -0
- package/src/templates/analysis/style.md.j2 +44 -0
- package/src/templates/analysis-summary.md.j2 +58 -0
- package/src/templates/checkpoint.md.j2 +27 -0
- package/src/templates/implementation-report.md.j2 +81 -0
- package/src/templates/memory.md.j2 +16 -0
- package/src/templates/plan-bug.md.j2 +42 -0
- package/src/templates/plan-chore.md.j2 +27 -0
- package/src/templates/plan-feature.md.j2 +41 -0
- package/src/templates/progress.json.j2 +16 -0
- package/src/templates/ralph-report.md.j2 +45 -0
- package/src/types.ts +141 -0
- package/src/workflows/analyze-codebase-merge.yaml +328 -0
- package/src/workflows/analyze-codebase.yaml +196 -0
- package/src/workflows/analyze-single.yaml +56 -0
- package/src/workflows/demo.yaml +180 -0
- package/src/workflows/one-shot.yaml +54 -0
- package/src/workflows/plan-build-review.yaml +160 -0
- package/src/workflows/ralph-loop.yaml +73 -0
- package/tests/config.test.ts +219 -0
- package/tests/console.test.ts +506 -0
- package/tests/executor.test.ts +339 -0
- package/tests/init.test.ts +86 -0
- package/tests/logger.test.ts +110 -0
- package/tests/parser.test.ts +290 -0
- package/tests/progress.test.ts +345 -0
- package/tests/ralph-loop.test.ts +418 -0
- package/tests/renderer.test.ts +350 -0
- package/tests/runner.test.ts +497 -0
- package/tests/setup.test.ts +7 -0
- package/tests/signal-manager.test.ts +26 -0
- package/tests/steps.test.ts +412 -0
- package/tests/worktree.test.ts +411 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/** Tests for step executors. */
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Writable } from "node:stream";
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { ConsoleOutput } from "../src/console.js";
|
|
10
|
+
import { WorkflowLogger } from "../src/logging/logger.js";
|
|
11
|
+
import { createProgress } from "../src/progress.js";
|
|
12
|
+
import { TemplateRenderer } from "../src/renderer.js";
|
|
13
|
+
import {
|
|
14
|
+
type BranchStepExecutor,
|
|
15
|
+
type StepContext,
|
|
16
|
+
StepExecutor,
|
|
17
|
+
buildTemplateContext,
|
|
18
|
+
resolveModel,
|
|
19
|
+
} from "../src/steps/base.js";
|
|
20
|
+
import { ConditionalStepExecutor } from "../src/steps/conditional-step.js";
|
|
21
|
+
import { SerialStepExecutor } from "../src/steps/serial-step.js";
|
|
22
|
+
import type { StepDefinition, WorkflowProgress, WorkflowSettings } from "../src/types.js";
|
|
23
|
+
|
|
24
|
+
// Mock runClaude at module level
|
|
25
|
+
const mockRunClaude = vi.fn();
|
|
26
|
+
vi.mock("../src/runner.js", async (importOriginal) => {
|
|
27
|
+
const original = await importOriginal<typeof import("../src/runner.js")>();
|
|
28
|
+
return {
|
|
29
|
+
...original,
|
|
30
|
+
runClaude: (...args: unknown[]) => mockRunClaude(...args),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Now import PromptStepExecutor AFTER mock is set up
|
|
35
|
+
const { PromptStepExecutor } = await import("../src/steps/prompt-step.js");
|
|
36
|
+
|
|
37
|
+
// --- Helpers ---
|
|
38
|
+
|
|
39
|
+
function createMockStream(): Writable {
|
|
40
|
+
return new Writable({
|
|
41
|
+
write(_chunk, _encoding, cb) {
|
|
42
|
+
cb();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeStepDef(
|
|
48
|
+
overrides: Partial<StepDefinition> & { name: string; type: StepDefinition["type"] },
|
|
49
|
+
): StepDefinition {
|
|
50
|
+
return {
|
|
51
|
+
steps: [],
|
|
52
|
+
mergeStrategy: "wait-all",
|
|
53
|
+
mergeMode: "merge",
|
|
54
|
+
thenSteps: [],
|
|
55
|
+
elseSteps: [],
|
|
56
|
+
maxIterations: 5,
|
|
57
|
+
pollingInterval: 30,
|
|
58
|
+
onTimeout: "fail",
|
|
59
|
+
onError: "fail",
|
|
60
|
+
checkpoint: false,
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultSettings(): WorkflowSettings {
|
|
66
|
+
return {
|
|
67
|
+
maxRetry: 3,
|
|
68
|
+
timeoutMinutes: 60,
|
|
69
|
+
trackProgress: true,
|
|
70
|
+
autofix: "off",
|
|
71
|
+
terminalOutput: "base",
|
|
72
|
+
bypassPermissions: false,
|
|
73
|
+
strictMode: false,
|
|
74
|
+
model: null,
|
|
75
|
+
requiredTools: [],
|
|
76
|
+
git: {
|
|
77
|
+
enabled: false,
|
|
78
|
+
worktree: false,
|
|
79
|
+
autoCommit: false,
|
|
80
|
+
autoPr: false,
|
|
81
|
+
branchPrefix: "agentic",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Fixtures ---
|
|
87
|
+
|
|
88
|
+
let tempDir: string;
|
|
89
|
+
let mockLogger: WorkflowLogger;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.clearAllMocks();
|
|
93
|
+
tempDir = mkdtempSync(path.join(os.tmpdir(), "steps-test-"));
|
|
94
|
+
const logDir = path.join(tempDir, "agentic", "outputs", "test-workflow-id");
|
|
95
|
+
mkdirSync(logDir, { recursive: true });
|
|
96
|
+
mockLogger = new WorkflowLogger("test-workflow-id", tempDir);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function createStepContext(overrides?: Partial<StepContext>): StepContext {
|
|
100
|
+
return {
|
|
101
|
+
repoRoot: tempDir,
|
|
102
|
+
config: {
|
|
103
|
+
defaults: {
|
|
104
|
+
model: "sonnet",
|
|
105
|
+
maxRetry: 3,
|
|
106
|
+
timeoutMinutes: 60,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
renderer: new TemplateRenderer(),
|
|
110
|
+
workflowSettings: defaultSettings(),
|
|
111
|
+
workflowId: "test-workflow-id",
|
|
112
|
+
variables: { test_var: "test_value" },
|
|
113
|
+
outputs: {},
|
|
114
|
+
...overrides,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createWorkflowProgress(): WorkflowProgress {
|
|
119
|
+
return createProgress("test-workflow-id", "test-workflow", ["step1", "step2"], {});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Tests ---
|
|
123
|
+
|
|
124
|
+
describe("StepContext", () => {
|
|
125
|
+
it("should build template context with workflow_id", () => {
|
|
126
|
+
const context = createStepContext({
|
|
127
|
+
workflowId: "test-workflow-456",
|
|
128
|
+
variables: { task: "feature" },
|
|
129
|
+
outputs: { plan: { summary: "test" } },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const templateCtx = buildTemplateContext(context);
|
|
133
|
+
|
|
134
|
+
expect(templateCtx.workflow_id).toBe("test-workflow-456");
|
|
135
|
+
expect((templateCtx.variables as Record<string, unknown>).task).toBe("feature");
|
|
136
|
+
expect(
|
|
137
|
+
((templateCtx.outputs as Record<string, unknown>).plan as Record<string, unknown>).summary,
|
|
138
|
+
).toBe("test");
|
|
139
|
+
// Variables should also be spread at top level
|
|
140
|
+
expect(templateCtx.task).toBe("feature");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("resolveModel", () => {
|
|
145
|
+
it("should prioritize step model", () => {
|
|
146
|
+
const context = createStepContext({
|
|
147
|
+
workflowSettings: { ...defaultSettings(), model: "haiku" },
|
|
148
|
+
});
|
|
149
|
+
expect(resolveModel(context, "opus")).toBe("opus");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should use workflow model when step has none", () => {
|
|
153
|
+
const context = createStepContext({
|
|
154
|
+
workflowSettings: { ...defaultSettings(), model: "haiku" },
|
|
155
|
+
});
|
|
156
|
+
expect(resolveModel(context, null)).toBe("haiku");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should fall back to config default", () => {
|
|
160
|
+
const context = createStepContext({
|
|
161
|
+
config: { defaults: { model: "opus" } },
|
|
162
|
+
workflowSettings: defaultSettings(),
|
|
163
|
+
});
|
|
164
|
+
expect(resolveModel(context, null)).toBe("opus");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should fall back to sonnet when nothing is configured", () => {
|
|
168
|
+
const context = createStepContext({
|
|
169
|
+
config: { defaults: {} },
|
|
170
|
+
workflowSettings: defaultSettings(),
|
|
171
|
+
});
|
|
172
|
+
expect(resolveModel(context, null)).toBe("sonnet");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("StepExecutor base", () => {
|
|
177
|
+
it("should be an abstract class that subclasses extend", () => {
|
|
178
|
+
// Verify that PromptStepExecutor extends StepExecutor
|
|
179
|
+
const executor = new PromptStepExecutor();
|
|
180
|
+
expect(executor).toBeInstanceOf(StepExecutor);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("PromptStepExecutor", () => {
|
|
185
|
+
it("should be instantiable", () => {
|
|
186
|
+
const executor = new PromptStepExecutor();
|
|
187
|
+
expect(executor).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should execute a basic prompt", async () => {
|
|
191
|
+
mockRunClaude.mockResolvedValue({
|
|
192
|
+
success: true,
|
|
193
|
+
stdout: "Output",
|
|
194
|
+
stderr: "",
|
|
195
|
+
sessionOutput: {
|
|
196
|
+
isSuccess: true,
|
|
197
|
+
context: "Done",
|
|
198
|
+
sessionId: "test-session",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const step = makeStepDef({
|
|
203
|
+
name: "test-prompt",
|
|
204
|
+
type: "prompt",
|
|
205
|
+
prompt: "Test prompt content",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const context = createStepContext();
|
|
209
|
+
const progress = createWorkflowProgress();
|
|
210
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
211
|
+
|
|
212
|
+
const executor = new PromptStepExecutor();
|
|
213
|
+
await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
214
|
+
|
|
215
|
+
expect(mockRunClaude).toHaveBeenCalledOnce();
|
|
216
|
+
const callArgs = mockRunClaude.mock.calls[0][0];
|
|
217
|
+
expect(callArgs.prompt).toContain("Test prompt content");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should execute a prompt with variable interpolation", async () => {
|
|
221
|
+
mockRunClaude.mockResolvedValue({
|
|
222
|
+
success: true,
|
|
223
|
+
stdout: "Output",
|
|
224
|
+
stderr: "",
|
|
225
|
+
sessionOutput: {
|
|
226
|
+
isSuccess: true,
|
|
227
|
+
context: "Done",
|
|
228
|
+
sessionId: null,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const step = makeStepDef({
|
|
233
|
+
name: "test-prompt",
|
|
234
|
+
type: "prompt",
|
|
235
|
+
prompt: "Value is {{ variables.test_var }}",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const context = createStepContext();
|
|
239
|
+
const progress = createWorkflowProgress();
|
|
240
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
241
|
+
|
|
242
|
+
const executor = new PromptStepExecutor();
|
|
243
|
+
await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
244
|
+
|
|
245
|
+
const callArgs = mockRunClaude.mock.calls[0][0];
|
|
246
|
+
expect(callArgs.prompt).toContain("test_value");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("ConditionalStepExecutor", () => {
|
|
251
|
+
it("should be instantiable", () => {
|
|
252
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
253
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
254
|
+
expect(executor).toBeDefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should evaluate true condition", () => {
|
|
258
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
259
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
260
|
+
const context = { variables: { enabled: true } };
|
|
261
|
+
expect(executor.evaluateCondition("variables.enabled", context)).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should evaluate false condition", () => {
|
|
265
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
266
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
267
|
+
const context = { variables: { enabled: false } };
|
|
268
|
+
expect(executor.evaluateCondition("variables.enabled", context)).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should evaluate string comparison", () => {
|
|
272
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
273
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
274
|
+
const context = { variables: { status: "active" } };
|
|
275
|
+
expect(executor.evaluateCondition("variables.status == 'active'", context)).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should evaluate inequality comparison", () => {
|
|
279
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
280
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
281
|
+
const context = { variables: { status: "active" } };
|
|
282
|
+
expect(executor.evaluateCondition("variables.status != 'inactive'", context)).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should evaluate literal true/false", () => {
|
|
286
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
287
|
+
const executor = new ConditionalStepExecutor(branchExecutor);
|
|
288
|
+
expect(executor.evaluateCondition("true", {})).toBe(true);
|
|
289
|
+
expect(executor.evaluateCondition("false", {})).toBe(false);
|
|
290
|
+
expect(executor.evaluateCondition("1", {})).toBe(true);
|
|
291
|
+
expect(executor.evaluateCondition("0", {})).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("SerialStepExecutor", () => {
|
|
296
|
+
it("should be instantiable", () => {
|
|
297
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
298
|
+
const executor = new SerialStepExecutor(branchExecutor);
|
|
299
|
+
expect(executor).toBeDefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should execute serial steps in order", async () => {
|
|
303
|
+
const branchExecutor = vi.fn<BranchStepExecutor>().mockResolvedValue({
|
|
304
|
+
success: true,
|
|
305
|
+
outputSummary: "Done",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const executor = new SerialStepExecutor(branchExecutor);
|
|
309
|
+
const innerSteps = [
|
|
310
|
+
makeStepDef({ name: "inner1", type: "prompt", prompt: "Step 1" }),
|
|
311
|
+
makeStepDef({ name: "inner2", type: "prompt", prompt: "Step 2" }),
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const step = makeStepDef({
|
|
315
|
+
name: "serial-step",
|
|
316
|
+
type: "serial",
|
|
317
|
+
steps: innerSteps,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const context = createStepContext();
|
|
321
|
+
const progress = createWorkflowProgress();
|
|
322
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
323
|
+
|
|
324
|
+
await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
325
|
+
|
|
326
|
+
expect(branchExecutor).toHaveBeenCalledTimes(2);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should stop on first failure", async () => {
|
|
330
|
+
const branchExecutor = vi
|
|
331
|
+
.fn<BranchStepExecutor>()
|
|
332
|
+
.mockResolvedValueOnce({ success: true, outputSummary: "Done" })
|
|
333
|
+
.mockResolvedValueOnce({ success: false, error: "Failed" });
|
|
334
|
+
|
|
335
|
+
const executor = new SerialStepExecutor(branchExecutor);
|
|
336
|
+
const innerSteps = [
|
|
337
|
+
makeStepDef({ name: "inner1", type: "prompt", prompt: "Step 1" }),
|
|
338
|
+
makeStepDef({ name: "inner2", type: "prompt", prompt: "Step 2" }),
|
|
339
|
+
makeStepDef({ name: "inner3", type: "prompt", prompt: "Step 3" }),
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const step = makeStepDef({
|
|
343
|
+
name: "serial-step",
|
|
344
|
+
type: "serial",
|
|
345
|
+
steps: innerSteps,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const context = createStepContext();
|
|
349
|
+
const progress = createWorkflowProgress();
|
|
350
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
351
|
+
|
|
352
|
+
const result = await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
353
|
+
|
|
354
|
+
expect(result.success).toBe(false);
|
|
355
|
+
expect(branchExecutor).toHaveBeenCalledTimes(2);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should handle empty steps", async () => {
|
|
359
|
+
const branchExecutor: BranchStepExecutor = vi.fn();
|
|
360
|
+
const executor = new SerialStepExecutor(branchExecutor);
|
|
361
|
+
|
|
362
|
+
const step = makeStepDef({
|
|
363
|
+
name: "serial-step",
|
|
364
|
+
type: "serial",
|
|
365
|
+
steps: [],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const context = createStepContext();
|
|
369
|
+
const progress = createWorkflowProgress();
|
|
370
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
371
|
+
|
|
372
|
+
const result = await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
373
|
+
|
|
374
|
+
expect(result.success).toBe(true);
|
|
375
|
+
expect(result.outputSummary).toContain("No sub-steps");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("StepExecutor retry", () => {
|
|
380
|
+
it("should retry on failure then succeed", async () => {
|
|
381
|
+
mockRunClaude
|
|
382
|
+
.mockResolvedValueOnce({
|
|
383
|
+
success: false,
|
|
384
|
+
returncode: 1,
|
|
385
|
+
stdout: "",
|
|
386
|
+
stderr: "Error",
|
|
387
|
+
sessionOutput: { isSuccess: false, context: "Failed" },
|
|
388
|
+
})
|
|
389
|
+
.mockResolvedValueOnce({
|
|
390
|
+
success: true,
|
|
391
|
+
stdout: "Output",
|
|
392
|
+
stderr: "",
|
|
393
|
+
sessionOutput: { isSuccess: true, context: "Done", sessionId: null },
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const step = makeStepDef({
|
|
397
|
+
name: "test-prompt",
|
|
398
|
+
type: "prompt",
|
|
399
|
+
prompt: "Test",
|
|
400
|
+
stepMaxRetry: 2,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const context = createStepContext();
|
|
404
|
+
const progress = createWorkflowProgress();
|
|
405
|
+
const consoleOut = new ConsoleOutput(createMockStream());
|
|
406
|
+
|
|
407
|
+
const executor = new PromptStepExecutor();
|
|
408
|
+
await executor.execute(step, progress, context, mockLogger, consoleOut);
|
|
409
|
+
|
|
410
|
+
expect(mockRunClaude).toHaveBeenCalledTimes(2);
|
|
411
|
+
});
|
|
412
|
+
});
|