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.
Files changed (110) hide show
  1. package/.gitattributes +24 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.markdownlint-cli2.jsonc +16 -0
  4. package/.prettierignore +3 -0
  5. package/.prettierrc +6 -0
  6. package/.vscode/agentic-forge.code-workspace +26 -0
  7. package/CHANGELOG.md +100 -0
  8. package/CLAUDE.md +158 -0
  9. package/CONTRIBUTING.md +152 -0
  10. package/LICENSE +21 -0
  11. package/README.md +145 -0
  12. package/agentic-forge-banner.png +0 -0
  13. package/biome.json +21 -0
  14. package/package.json +5 -0
  15. package/scripts/copy-assets.js +21 -0
  16. package/src/agents/explorer.md +97 -0
  17. package/src/agents/reviewer.md +137 -0
  18. package/src/checkpoints/manager.ts +119 -0
  19. package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
  20. package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
  21. package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
  22. package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
  23. package/src/claude/.claude/skills/analyze/references/security.md +76 -0
  24. package/src/claude/.claude/skills/analyze/references/style.md +72 -0
  25. package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
  26. package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
  27. package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
  28. package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
  29. package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
  30. package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
  31. package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
  32. package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
  33. package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
  34. package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
  35. package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
  36. package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
  37. package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
  38. package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
  39. package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
  40. package/src/cli.ts +182 -0
  41. package/src/commands/config-cmd.ts +28 -0
  42. package/src/commands/index.ts +21 -0
  43. package/src/commands/init.ts +96 -0
  44. package/src/commands/release-notes.ts +85 -0
  45. package/src/commands/resume.ts +103 -0
  46. package/src/commands/run.ts +234 -0
  47. package/src/commands/shortcuts.ts +11 -0
  48. package/src/commands/skills-dir.ts +11 -0
  49. package/src/commands/status.ts +112 -0
  50. package/src/commands/update.ts +64 -0
  51. package/src/commands/version.ts +27 -0
  52. package/src/commands/workflows.ts +129 -0
  53. package/src/config.ts +129 -0
  54. package/src/console.ts +790 -0
  55. package/src/executor.ts +354 -0
  56. package/src/git/worktree.ts +236 -0
  57. package/src/logging/logger.ts +95 -0
  58. package/src/orchestrator.ts +815 -0
  59. package/src/parser.ts +225 -0
  60. package/src/progress.ts +306 -0
  61. package/src/prompts/agentic-system.md +31 -0
  62. package/src/ralph-loop.ts +260 -0
  63. package/src/renderer.ts +164 -0
  64. package/src/runner.ts +634 -0
  65. package/src/signal-manager.ts +55 -0
  66. package/src/steps/base.ts +71 -0
  67. package/src/steps/conditional-step.ts +144 -0
  68. package/src/steps/index.ts +15 -0
  69. package/src/steps/parallel-step.ts +213 -0
  70. package/src/steps/prompt-step.ts +121 -0
  71. package/src/steps/ralph-loop-step.ts +186 -0
  72. package/src/steps/serial-step.ts +84 -0
  73. package/src/templates/analysis/bug.md.j2 +35 -0
  74. package/src/templates/analysis/debt.md.j2 +38 -0
  75. package/src/templates/analysis/doc.md.j2 +45 -0
  76. package/src/templates/analysis/security.md.j2 +35 -0
  77. package/src/templates/analysis/style.md.j2 +44 -0
  78. package/src/templates/analysis-summary.md.j2 +58 -0
  79. package/src/templates/checkpoint.md.j2 +27 -0
  80. package/src/templates/implementation-report.md.j2 +81 -0
  81. package/src/templates/memory.md.j2 +16 -0
  82. package/src/templates/plan-bug.md.j2 +42 -0
  83. package/src/templates/plan-chore.md.j2 +27 -0
  84. package/src/templates/plan-feature.md.j2 +41 -0
  85. package/src/templates/progress.json.j2 +16 -0
  86. package/src/templates/ralph-report.md.j2 +45 -0
  87. package/src/types.ts +141 -0
  88. package/src/workflows/analyze-codebase-merge.yaml +328 -0
  89. package/src/workflows/analyze-codebase.yaml +196 -0
  90. package/src/workflows/analyze-single.yaml +56 -0
  91. package/src/workflows/demo.yaml +180 -0
  92. package/src/workflows/one-shot.yaml +54 -0
  93. package/src/workflows/plan-build-review.yaml +160 -0
  94. package/src/workflows/ralph-loop.yaml +73 -0
  95. package/tests/config.test.ts +219 -0
  96. package/tests/console.test.ts +506 -0
  97. package/tests/executor.test.ts +339 -0
  98. package/tests/init.test.ts +86 -0
  99. package/tests/logger.test.ts +110 -0
  100. package/tests/parser.test.ts +290 -0
  101. package/tests/progress.test.ts +345 -0
  102. package/tests/ralph-loop.test.ts +418 -0
  103. package/tests/renderer.test.ts +350 -0
  104. package/tests/runner.test.ts +497 -0
  105. package/tests/setup.test.ts +7 -0
  106. package/tests/signal-manager.test.ts +26 -0
  107. package/tests/steps.test.ts +412 -0
  108. package/tests/worktree.test.ts +411 -0
  109. package/tsconfig.json +18 -0
  110. 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
+ });