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,339 @@
|
|
|
1
|
+
/** Tests for workflow executor. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, 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 { WorkflowParser } from "../src/parser.js";
|
|
10
|
+
import { WORKFLOW_STATUS } from "../src/progress.js";
|
|
11
|
+
|
|
12
|
+
// Mock PromptStepExecutor.execute at module level
|
|
13
|
+
const mockPromptExecute = vi.fn();
|
|
14
|
+
vi.mock("../src/steps/prompt-step.js", async (importOriginal) => {
|
|
15
|
+
const original = await importOriginal<typeof import("../src/steps/prompt-step.js")>();
|
|
16
|
+
return {
|
|
17
|
+
...original,
|
|
18
|
+
PromptStepExecutor: class extends original.PromptStepExecutor {
|
|
19
|
+
execute = mockPromptExecute;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Mock runClaude
|
|
25
|
+
vi.mock("../src/runner.js", async (importOriginal) => {
|
|
26
|
+
const original = await importOriginal<typeof import("../src/runner.js")>();
|
|
27
|
+
return {
|
|
28
|
+
...original,
|
|
29
|
+
runClaude: vi.fn().mockResolvedValue({
|
|
30
|
+
success: true,
|
|
31
|
+
stdout: "ok",
|
|
32
|
+
stderr: "",
|
|
33
|
+
returncode: 0,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const { WorkflowExecutor } = await import("../src/executor.js");
|
|
39
|
+
|
|
40
|
+
// --- Helpers ---
|
|
41
|
+
|
|
42
|
+
let tempDir: string;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
mockPromptExecute.mockResolvedValue({ success: true, outputSummary: "done" });
|
|
47
|
+
tempDir = mkdtempSync(path.join(os.tmpdir(), "executor-test-"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const sampleWorkflowYaml = `
|
|
51
|
+
name: test-workflow
|
|
52
|
+
version: "1.0"
|
|
53
|
+
description: Test workflow
|
|
54
|
+
steps:
|
|
55
|
+
- name: test-step
|
|
56
|
+
type: prompt
|
|
57
|
+
prompt: "Hello"
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
// --- TestWorkflowExecutor ---
|
|
61
|
+
|
|
62
|
+
describe("WorkflowExecutor", () => {
|
|
63
|
+
it("initializes correctly", () => {
|
|
64
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
65
|
+
|
|
66
|
+
expect(executor.repoRoot).toBe(tempDir);
|
|
67
|
+
expect(executor.config).toBeTruthy();
|
|
68
|
+
expect(executor.renderer).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("initializes all step executors", () => {
|
|
72
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
73
|
+
|
|
74
|
+
expect(executor.executors.prompt).toBeTruthy();
|
|
75
|
+
expect(executor.executors.parallel).toBeTruthy();
|
|
76
|
+
expect(executor.executors.serial).toBeTruthy();
|
|
77
|
+
expect(executor.executors.conditional).toBeTruthy();
|
|
78
|
+
expect(executor.executors["ralph-loop"]).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("uses cwd when no root specified", () => {
|
|
82
|
+
const originalCwd = process.cwd();
|
|
83
|
+
try {
|
|
84
|
+
process.chdir(tempDir);
|
|
85
|
+
const executor = new WorkflowExecutor();
|
|
86
|
+
expect(executor.repoRoot).toBe(tempDir);
|
|
87
|
+
} finally {
|
|
88
|
+
process.chdir(originalCwd);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- TestWorkflowExecutorRun ---
|
|
94
|
+
|
|
95
|
+
describe("WorkflowExecutor.run", () => {
|
|
96
|
+
it("runs a minimal workflow", async () => {
|
|
97
|
+
const parser = new WorkflowParser();
|
|
98
|
+
const workflow = parser.parseString(sampleWorkflowYaml);
|
|
99
|
+
|
|
100
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
101
|
+
const progress = await executor.run(workflow);
|
|
102
|
+
|
|
103
|
+
expect(progress.workflowName).toBe("test-workflow");
|
|
104
|
+
expect(progress.status).toBe(WORKFLOW_STATUS.COMPLETED);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("creates progress file", async () => {
|
|
108
|
+
const parser = new WorkflowParser();
|
|
109
|
+
const workflow = parser.parseString(sampleWorkflowYaml);
|
|
110
|
+
|
|
111
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
112
|
+
const progress = await executor.run(workflow);
|
|
113
|
+
|
|
114
|
+
const progressDir = path.join(tempDir, "agentic", "outputs", progress.workflowId);
|
|
115
|
+
expect(existsSync(progressDir)).toBe(true);
|
|
116
|
+
expect(existsSync(path.join(progressDir, "progress.json"))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("runs with custom variables", async () => {
|
|
120
|
+
const workflowYaml = `
|
|
121
|
+
name: variable-test
|
|
122
|
+
version: "1.0"
|
|
123
|
+
description: Test with variables
|
|
124
|
+
variables:
|
|
125
|
+
- name: custom_var
|
|
126
|
+
type: string
|
|
127
|
+
default: default_value
|
|
128
|
+
steps:
|
|
129
|
+
- name: test
|
|
130
|
+
type: prompt
|
|
131
|
+
prompt: "{{ variables.custom_var }}"
|
|
132
|
+
`;
|
|
133
|
+
const parser = new WorkflowParser();
|
|
134
|
+
const workflow = parser.parseString(workflowYaml);
|
|
135
|
+
|
|
136
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
137
|
+
const progress = await executor.run(workflow, { custom_var: "custom_value" });
|
|
138
|
+
|
|
139
|
+
expect(progress.variables.custom_var).toBe("custom_value");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses default variable values", async () => {
|
|
143
|
+
const workflowYaml = `
|
|
144
|
+
name: default-var-test
|
|
145
|
+
version: "1.0"
|
|
146
|
+
description: Test with default variables
|
|
147
|
+
variables:
|
|
148
|
+
- name: my_var
|
|
149
|
+
type: string
|
|
150
|
+
default: the_default
|
|
151
|
+
steps:
|
|
152
|
+
- name: test
|
|
153
|
+
type: prompt
|
|
154
|
+
prompt: "Test"
|
|
155
|
+
`;
|
|
156
|
+
const parser = new WorkflowParser();
|
|
157
|
+
const workflow = parser.parseString(workflowYaml);
|
|
158
|
+
|
|
159
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
160
|
+
const progress = await executor.run(workflow);
|
|
161
|
+
|
|
162
|
+
expect(progress.variables.my_var).toBe("the_default");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("raises for missing required variable", async () => {
|
|
166
|
+
const workflowYaml = `
|
|
167
|
+
name: required-var-test
|
|
168
|
+
version: "1.0"
|
|
169
|
+
description: Test with required variable
|
|
170
|
+
variables:
|
|
171
|
+
- name: required_var
|
|
172
|
+
type: string
|
|
173
|
+
required: true
|
|
174
|
+
steps:
|
|
175
|
+
- name: test
|
|
176
|
+
type: prompt
|
|
177
|
+
prompt: "Test"
|
|
178
|
+
`;
|
|
179
|
+
const parser = new WorkflowParser();
|
|
180
|
+
const workflow = parser.parseString(workflowYaml);
|
|
181
|
+
|
|
182
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
183
|
+
|
|
184
|
+
await expect(executor.run(workflow)).rejects.toThrow("Missing required variable");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("runs from a specific step", async () => {
|
|
188
|
+
const workflowYaml = `
|
|
189
|
+
name: multi-step
|
|
190
|
+
version: "1.0"
|
|
191
|
+
description: Multi-step workflow
|
|
192
|
+
steps:
|
|
193
|
+
- name: step1
|
|
194
|
+
type: prompt
|
|
195
|
+
prompt: "Step 1"
|
|
196
|
+
- name: step2
|
|
197
|
+
type: prompt
|
|
198
|
+
prompt: "Step 2"
|
|
199
|
+
- name: step3
|
|
200
|
+
type: prompt
|
|
201
|
+
prompt: "Step 3"
|
|
202
|
+
`;
|
|
203
|
+
const parser = new WorkflowParser();
|
|
204
|
+
const workflow = parser.parseString(workflowYaml);
|
|
205
|
+
|
|
206
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
207
|
+
const progress = await executor.run(workflow, undefined, "step2");
|
|
208
|
+
|
|
209
|
+
expect(progress.status).toBe(WORKFLOW_STATUS.COMPLETED);
|
|
210
|
+
// step1 skipped, step2 and step3 executed
|
|
211
|
+
expect(mockPromptExecute).toHaveBeenCalledTimes(2);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("sets terminal output level correctly", async () => {
|
|
215
|
+
const parser = new WorkflowParser();
|
|
216
|
+
const workflow = parser.parseString(sampleWorkflowYaml);
|
|
217
|
+
|
|
218
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
219
|
+
|
|
220
|
+
const progress1 = await executor.run(workflow, undefined, null, "base");
|
|
221
|
+
expect(progress1).toBeTruthy();
|
|
222
|
+
|
|
223
|
+
mockPromptExecute.mockClear();
|
|
224
|
+
|
|
225
|
+
const progress2 = await executor.run(workflow, undefined, null, "all");
|
|
226
|
+
expect(progress2).toBeTruthy();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// --- TestWorkflowExecutorStepDispatch ---
|
|
231
|
+
|
|
232
|
+
describe("WorkflowExecutor step dispatch", () => {
|
|
233
|
+
it("dispatches prompt step to correct executor", async () => {
|
|
234
|
+
const workflowYaml = `
|
|
235
|
+
name: prompt-dispatch
|
|
236
|
+
version: "1.0"
|
|
237
|
+
description: Test prompt dispatch
|
|
238
|
+
steps:
|
|
239
|
+
- name: test-prompt
|
|
240
|
+
type: prompt
|
|
241
|
+
prompt: "Test"
|
|
242
|
+
`;
|
|
243
|
+
const parser = new WorkflowParser();
|
|
244
|
+
const workflow = parser.parseString(workflowYaml);
|
|
245
|
+
|
|
246
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
247
|
+
await executor.run(workflow);
|
|
248
|
+
|
|
249
|
+
expect(mockPromptExecute).toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- TestWorkflowExecutorOutputs ---
|
|
254
|
+
|
|
255
|
+
describe("WorkflowExecutor outputs", () => {
|
|
256
|
+
it("renders outputs on completion", async () => {
|
|
257
|
+
// Create template directory
|
|
258
|
+
const templatesDir = path.join(tempDir, "agentic", "templates");
|
|
259
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
260
|
+
|
|
261
|
+
const workflowYaml = `
|
|
262
|
+
name: output-test
|
|
263
|
+
version: "1.0"
|
|
264
|
+
description: Test output rendering
|
|
265
|
+
steps: []
|
|
266
|
+
outputs:
|
|
267
|
+
- name: test-output
|
|
268
|
+
template: test-output.md.j2
|
|
269
|
+
path: output.md
|
|
270
|
+
when: always
|
|
271
|
+
`;
|
|
272
|
+
const parser = new WorkflowParser();
|
|
273
|
+
const workflow = parser.parseString(workflowYaml);
|
|
274
|
+
|
|
275
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
276
|
+
const progress = await executor.run(workflow);
|
|
277
|
+
|
|
278
|
+
expect(progress).toBeTruthy();
|
|
279
|
+
expect(progress.status).toBe(WORKFLOW_STATUS.COMPLETED);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// --- TestWorkflowExecutorErrorHandling ---
|
|
284
|
+
|
|
285
|
+
describe("WorkflowExecutor error handling", () => {
|
|
286
|
+
it("marks workflow as failed on step failure", async () => {
|
|
287
|
+
mockPromptExecute.mockRejectedValue(new Error("Step execution failed"));
|
|
288
|
+
|
|
289
|
+
const workflowYaml = `
|
|
290
|
+
name: failing-workflow
|
|
291
|
+
version: "1.0"
|
|
292
|
+
description: Test failure handling
|
|
293
|
+
steps:
|
|
294
|
+
- name: failing-step
|
|
295
|
+
type: prompt
|
|
296
|
+
prompt: "This will fail"
|
|
297
|
+
`;
|
|
298
|
+
const parser = new WorkflowParser();
|
|
299
|
+
const workflow = parser.parseString(workflowYaml);
|
|
300
|
+
|
|
301
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
302
|
+
const progress = await executor.run(workflow);
|
|
303
|
+
|
|
304
|
+
expect(progress.status).toBe(WORKFLOW_STATUS.FAILED);
|
|
305
|
+
expect(progress.errors.length).toBeGreaterThan(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("stops execution on step failure", async () => {
|
|
309
|
+
let callCount = 0;
|
|
310
|
+
mockPromptExecute.mockImplementation(async () => {
|
|
311
|
+
callCount++;
|
|
312
|
+
if (callCount === 1) {
|
|
313
|
+
throw new Error("First step failed");
|
|
314
|
+
}
|
|
315
|
+
return { success: true, outputSummary: "done" };
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const workflowYaml = `
|
|
319
|
+
name: multi-step-fail
|
|
320
|
+
version: "1.0"
|
|
321
|
+
description: Test failure stops execution
|
|
322
|
+
steps:
|
|
323
|
+
- name: step1
|
|
324
|
+
type: prompt
|
|
325
|
+
prompt: "Step 1"
|
|
326
|
+
- name: step2
|
|
327
|
+
type: prompt
|
|
328
|
+
prompt: "Step 2"
|
|
329
|
+
`;
|
|
330
|
+
const parser = new WorkflowParser();
|
|
331
|
+
const workflow = parser.parseString(workflowYaml);
|
|
332
|
+
|
|
333
|
+
const executor = new WorkflowExecutor(tempDir);
|
|
334
|
+
const progress = await executor.run(workflow);
|
|
335
|
+
|
|
336
|
+
expect(callCount).toBe(1);
|
|
337
|
+
expect(progress.status).toBe(WORKFLOW_STATUS.FAILED);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { cmdInit } from "../src/commands/init.js";
|
|
6
|
+
|
|
7
|
+
function makeTempDir(): string {
|
|
8
|
+
const dir = path.join(
|
|
9
|
+
tmpdir(),
|
|
10
|
+
`agentic-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
11
|
+
);
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("cmdInit", () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
let originalCwd: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tempDir = makeTempDir();
|
|
22
|
+
originalCwd = process.cwd();
|
|
23
|
+
process.chdir(tempDir);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
process.chdir(originalCwd);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should create config.json", () => {
|
|
31
|
+
cmdInit({ force: false, listOnly: false });
|
|
32
|
+
|
|
33
|
+
const configPath = path.join(tempDir, "agentic", "config.json");
|
|
34
|
+
expect(existsSync(configPath)).toBe(true);
|
|
35
|
+
|
|
36
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
37
|
+
expect(config).toHaveProperty("outputDirectory");
|
|
38
|
+
expect(config).toHaveProperty("defaults");
|
|
39
|
+
expect(config.defaults.model).toBe("sonnet");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should copy workflows", () => {
|
|
43
|
+
cmdInit({ force: false, listOnly: false });
|
|
44
|
+
|
|
45
|
+
const workflowsDir = path.join(tempDir, "agentic", "workflows");
|
|
46
|
+
expect(existsSync(workflowsDir)).toBe(true);
|
|
47
|
+
|
|
48
|
+
const files = require("node:fs")
|
|
49
|
+
.readdirSync(workflowsDir)
|
|
50
|
+
.filter((f: string) => f.endsWith(".yaml"));
|
|
51
|
+
expect(files.length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should not overwrite config without --force", () => {
|
|
55
|
+
const configDir = path.join(tempDir, "agentic");
|
|
56
|
+
mkdirSync(configDir, { recursive: true });
|
|
57
|
+
const configPath = path.join(configDir, "config.json");
|
|
58
|
+
const customConfig = { outputDirectory: "custom", defaults: { model: "opus" } };
|
|
59
|
+
writeFileSync(configPath, JSON.stringify(customConfig));
|
|
60
|
+
|
|
61
|
+
cmdInit({ force: false, listOnly: false });
|
|
62
|
+
|
|
63
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
64
|
+
expect(config.defaults.model).toBe("opus");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should overwrite config with --force", () => {
|
|
68
|
+
const configDir = path.join(tempDir, "agentic");
|
|
69
|
+
mkdirSync(configDir, { recursive: true });
|
|
70
|
+
const configPath = path.join(configDir, "config.json");
|
|
71
|
+
const customConfig = { outputDirectory: "custom", defaults: { model: "opus" } };
|
|
72
|
+
writeFileSync(configPath, JSON.stringify(customConfig));
|
|
73
|
+
|
|
74
|
+
cmdInit({ force: true, listOnly: false });
|
|
75
|
+
|
|
76
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
77
|
+
expect(config.defaults.model).toBe("sonnet");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should not create config with --list", () => {
|
|
81
|
+
cmdInit({ force: false, listOnly: true });
|
|
82
|
+
|
|
83
|
+
const configPath = path.join(tempDir, "agentic", "config.json");
|
|
84
|
+
expect(existsSync(configPath)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { LOG_LEVEL, WorkflowLogger, getLogPath, readLogs } from "../src/logging/logger.js";
|
|
6
|
+
|
|
7
|
+
function makeTempDir(): string {
|
|
8
|
+
const dir = path.join(
|
|
9
|
+
tmpdir(),
|
|
10
|
+
`agentic-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
11
|
+
);
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("LOG_LEVEL constants", () => {
|
|
17
|
+
it("should have all expected values", () => {
|
|
18
|
+
expect(LOG_LEVEL.CRITICAL).toBe("Critical");
|
|
19
|
+
expect(LOG_LEVEL.ERROR).toBe("Error");
|
|
20
|
+
expect(LOG_LEVEL.WARNING).toBe("Warning");
|
|
21
|
+
expect(LOG_LEVEL.INFORMATION).toBe("Information");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("getLogPath", () => {
|
|
26
|
+
it("should return correct path", () => {
|
|
27
|
+
const tempDir = makeTempDir();
|
|
28
|
+
const p = getLogPath("test-workflow", tempDir);
|
|
29
|
+
const expected = path.join(tempDir, "agentic", "outputs", "test-workflow", "logs.ndjson");
|
|
30
|
+
expect(p).toBe(expected);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("WorkflowLogger", () => {
|
|
35
|
+
it("should create log directory on construction", () => {
|
|
36
|
+
const tempDir = makeTempDir();
|
|
37
|
+
const logger = new WorkflowLogger("test-logger", tempDir);
|
|
38
|
+
const logDir = path.dirname(logger.logPath);
|
|
39
|
+
expect(existsSync(logDir)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should write NDJSON entries", () => {
|
|
43
|
+
const tempDir = makeTempDir();
|
|
44
|
+
const logger = new WorkflowLogger("test-ndjson", tempDir);
|
|
45
|
+
|
|
46
|
+
logger.info("step1", "Starting step");
|
|
47
|
+
logger.warning("step1", "Something odd");
|
|
48
|
+
logger.error("step1", "Failed", { code: 42 });
|
|
49
|
+
|
|
50
|
+
const content = readFileSync(logger.logPath, "utf-8");
|
|
51
|
+
const lines = content.trim().split("\n");
|
|
52
|
+
expect(lines).toHaveLength(3);
|
|
53
|
+
|
|
54
|
+
const entry1 = JSON.parse(lines[0]);
|
|
55
|
+
expect(entry1.level).toBe("Information");
|
|
56
|
+
expect(entry1.step).toBe("step1");
|
|
57
|
+
expect(entry1.message).toBe("Starting step");
|
|
58
|
+
expect(entry1.timestamp).toBeTruthy();
|
|
59
|
+
|
|
60
|
+
const entry2 = JSON.parse(lines[1]);
|
|
61
|
+
expect(entry2.level).toBe("Warning");
|
|
62
|
+
|
|
63
|
+
const entry3 = JSON.parse(lines[2]);
|
|
64
|
+
expect(entry3.level).toBe("Error");
|
|
65
|
+
expect(entry3.context).toEqual({ code: 42 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should write critical entries", () => {
|
|
69
|
+
const tempDir = makeTempDir();
|
|
70
|
+
const logger = new WorkflowLogger("test-critical", tempDir);
|
|
71
|
+
|
|
72
|
+
logger.critical("orchestrator", "Fatal error");
|
|
73
|
+
|
|
74
|
+
const entries = readLogs("test-critical", tempDir);
|
|
75
|
+
expect(entries).toHaveLength(1);
|
|
76
|
+
expect(entries[0].level).toBe("Critical");
|
|
77
|
+
expect(entries[0].message).toBe("Fatal error");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should write null context when none provided", () => {
|
|
81
|
+
const tempDir = makeTempDir();
|
|
82
|
+
const logger = new WorkflowLogger("test-null-ctx", tempDir);
|
|
83
|
+
|
|
84
|
+
logger.info("step1", "No context");
|
|
85
|
+
|
|
86
|
+
const entries = readLogs("test-null-ctx", tempDir);
|
|
87
|
+
expect(entries[0].context).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("readLogs", () => {
|
|
92
|
+
it("should return empty array for nonexistent log", () => {
|
|
93
|
+
const tempDir = makeTempDir();
|
|
94
|
+
const entries = readLogs("nonexistent", tempDir);
|
|
95
|
+
expect(entries).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should read all entries", () => {
|
|
99
|
+
const tempDir = makeTempDir();
|
|
100
|
+
const logger = new WorkflowLogger("test-read", tempDir);
|
|
101
|
+
|
|
102
|
+
logger.info("s1", "msg1");
|
|
103
|
+
logger.error("s2", "msg2");
|
|
104
|
+
|
|
105
|
+
const entries = readLogs("test-read", tempDir);
|
|
106
|
+
expect(entries).toHaveLength(2);
|
|
107
|
+
expect(entries[0].step).toBe("s1");
|
|
108
|
+
expect(entries[1].step).toBe("s2");
|
|
109
|
+
});
|
|
110
|
+
});
|