@cephalization/math 0.2.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.
@@ -0,0 +1,537 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runLoop } from "./loop";
6
+ import { createMockAgent } from "./agent";
7
+ import { createOutputBuffer } from "./ui/buffer";
8
+ import { DEFAULT_PORT } from "./ui/server";
9
+
10
+ describe("runLoop dry-run mode", () => {
11
+ let testDir: string;
12
+ let originalCwd: string;
13
+
14
+ beforeEach(async () => {
15
+ // Create a temp directory for each test
16
+ testDir = await mkdtemp(join(tmpdir(), "math-loop-test-"));
17
+ originalCwd = process.cwd();
18
+ process.chdir(testDir);
19
+
20
+ // Create the todo directory with required files
21
+ const todoDir = join(testDir, "todo");
22
+ await mkdir(todoDir, { recursive: true });
23
+
24
+ // Create PROMPT.md
25
+ await writeFile(
26
+ join(todoDir, "PROMPT.md"),
27
+ "# Test Prompt\n\nTest instructions."
28
+ );
29
+
30
+ // Create TASKS.md with all tasks complete (so the loop exits after one iteration)
31
+ await writeFile(
32
+ join(todoDir, "TASKS.md"),
33
+ `# Tasks
34
+
35
+ ### test-task
36
+ - content: Test task
37
+ - status: complete
38
+ - dependencies: none
39
+ `
40
+ );
41
+ });
42
+
43
+ afterEach(async () => {
44
+ // Restore original working directory
45
+ process.chdir(originalCwd);
46
+
47
+ // Clean up temp directory
48
+ await rm(testDir, { recursive: true, force: true });
49
+ });
50
+
51
+ test("dry-run mode uses custom mock agent", async () => {
52
+ // Use a pending task so the agent gets invoked
53
+ await writeFile(
54
+ join(testDir, "todo", "TASKS.md"),
55
+ `# Tasks
56
+
57
+ ### test-task
58
+ - content: Test task
59
+ - status: pending
60
+ - dependencies: none
61
+ `
62
+ );
63
+
64
+ const mockAgent = createMockAgent({
65
+ logs: [
66
+ { category: "info", message: "Custom mock log" },
67
+ { category: "success", message: "Custom mock success" },
68
+ ],
69
+ output: ["Custom mock output\n"],
70
+ exitCode: 0,
71
+ });
72
+
73
+ const logs: string[] = [];
74
+ const outputs: string[] = [];
75
+ const originalLog = console.log;
76
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
77
+
78
+ console.log = (...args: unknown[]) => {
79
+ logs.push(args.join(" "));
80
+ };
81
+ process.stdout.write = (chunk: string | Uint8Array) => {
82
+ if (typeof chunk === "string") {
83
+ outputs.push(chunk);
84
+ }
85
+ return true;
86
+ };
87
+
88
+ try {
89
+ await runLoop({
90
+ dryRun: true,
91
+ agent: mockAgent,
92
+ maxIterations: 1,
93
+ pauseSeconds: 0,
94
+ ui: false,
95
+ });
96
+ } catch {
97
+ // Expected: max iterations exceeded since mock doesn't complete tasks
98
+ }
99
+
100
+ // Verify custom mock agent logs were emitted
101
+ const logText = logs.join("\n");
102
+ expect(logText).toContain("Custom mock log");
103
+ expect(logText).toContain("Custom mock success");
104
+
105
+ // Verify custom mock output was emitted
106
+ const outputText = outputs.join("");
107
+ expect(outputText).toContain("Custom mock output");
108
+
109
+ console.log = originalLog;
110
+ process.stdout.write = originalStdoutWrite;
111
+ });
112
+
113
+ test("dry-run mode with pending tasks runs iteration", async () => {
114
+ // Update TASKS.md to have a pending task
115
+ await writeFile(
116
+ join(testDir, "todo", "TASKS.md"),
117
+ `# Tasks
118
+
119
+ ### test-task
120
+ - content: Test task
121
+ - status: pending
122
+ - dependencies: none
123
+ `
124
+ );
125
+
126
+ const logs: string[] = [];
127
+ const originalLog = console.log;
128
+ console.log = (...args: unknown[]) => {
129
+ logs.push(args.join(" "));
130
+ };
131
+
132
+ try {
133
+ await runLoop({
134
+ dryRun: true,
135
+ maxIterations: 1,
136
+ pauseSeconds: 0,
137
+ ui: false,
138
+ });
139
+ } catch (e) {
140
+ // Expected: will exceed max iterations since mock doesn't complete tasks
141
+ } finally {
142
+ console.log = originalLog;
143
+ }
144
+
145
+ // Verify iteration ran
146
+ const logText = logs.join("\n");
147
+ expect(logText).toContain("=== Iteration 1 ===");
148
+ expect(logText).toContain("Invoking agent");
149
+ });
150
+
151
+ test("agent option allows injecting custom agent", async () => {
152
+ const callCount = { value: 0 };
153
+ const mockAgent = createMockAgent({
154
+ logs: [{ category: "info", message: "Injected agent running" }],
155
+ exitCode: 0,
156
+ });
157
+
158
+ // Wrap the run method to count calls
159
+ const originalRun = mockAgent.run.bind(mockAgent);
160
+ mockAgent.run = async (options) => {
161
+ callCount.value++;
162
+ return originalRun(options);
163
+ };
164
+
165
+ await runLoop({
166
+ dryRun: true,
167
+ agent: mockAgent,
168
+ maxIterations: 1,
169
+ pauseSeconds: 0,
170
+ ui: false,
171
+ });
172
+
173
+ // Agent should not be called since all tasks are complete
174
+ // (the task file has a complete task)
175
+ expect(callCount.value).toBe(0);
176
+ });
177
+
178
+ test("agent option with pending task invokes agent", async () => {
179
+ // Update TASKS.md to have a pending task
180
+ await writeFile(
181
+ join(testDir, "todo", "TASKS.md"),
182
+ `# Tasks
183
+
184
+ ### test-task
185
+ - content: Test task
186
+ - status: pending
187
+ - dependencies: none
188
+ `
189
+ );
190
+
191
+ const callCount = { value: 0 };
192
+ const mockAgent = createMockAgent({
193
+ logs: [{ category: "success", message: "Agent ran" }],
194
+ exitCode: 0,
195
+ });
196
+
197
+ const originalRun = mockAgent.run.bind(mockAgent);
198
+ mockAgent.run = async (options) => {
199
+ callCount.value++;
200
+ return originalRun(options);
201
+ };
202
+
203
+ try {
204
+ await runLoop({
205
+ dryRun: true,
206
+ agent: mockAgent,
207
+ maxIterations: 1,
208
+ pauseSeconds: 0,
209
+ ui: false,
210
+ });
211
+ } catch {
212
+ // Expected: max iterations exceeded
213
+ }
214
+
215
+ // Agent should be called at least once
216
+ expect(callCount.value).toBeGreaterThanOrEqual(1);
217
+ });
218
+ });
219
+
220
+ describe("runLoop stream-capture with buffer", () => {
221
+ let testDir: string;
222
+ let originalCwd: string;
223
+
224
+ beforeEach(async () => {
225
+ // Create a temp directory for each test
226
+ testDir = await mkdtemp(join(tmpdir(), "math-loop-test-"));
227
+ originalCwd = process.cwd();
228
+ process.chdir(testDir);
229
+
230
+ // Create the todo directory with required files
231
+ const todoDir = join(testDir, "todo");
232
+ await mkdir(todoDir, { recursive: true });
233
+
234
+ // Create PROMPT.md
235
+ await writeFile(
236
+ join(todoDir, "PROMPT.md"),
237
+ "# Test Prompt\n\nTest instructions."
238
+ );
239
+
240
+ // Create TASKS.md with all tasks complete
241
+ await writeFile(
242
+ join(todoDir, "TASKS.md"),
243
+ `# Tasks
244
+
245
+ ### test-task
246
+ - content: Test task
247
+ - status: complete
248
+ - dependencies: none
249
+ `
250
+ );
251
+ });
252
+
253
+ afterEach(async () => {
254
+ process.chdir(originalCwd);
255
+ await rm(testDir, { recursive: true, force: true });
256
+ });
257
+
258
+ test("loop logs are captured to buffer", async () => {
259
+ const buffer = createOutputBuffer();
260
+
261
+ // Suppress console output during test
262
+ const originalLog = console.log;
263
+ console.log = () => {};
264
+
265
+ try {
266
+ await runLoop({
267
+ dryRun: true,
268
+ maxIterations: 1,
269
+ pauseSeconds: 0,
270
+ buffer,
271
+ ui: false,
272
+ });
273
+
274
+ // Verify logs were captured
275
+ const logs = buffer.getLogs();
276
+ expect(logs.length).toBeGreaterThan(0);
277
+
278
+ // Should have info logs
279
+ const infoLogs = logs.filter((l) => l.category === "info");
280
+ expect(infoLogs.length).toBeGreaterThan(0);
281
+
282
+ // Check for expected log messages
283
+ const messages = logs.map((l) => l.message);
284
+ expect(messages.some((m) => m.includes("Starting math loop"))).toBe(true);
285
+ } finally {
286
+ console.log = originalLog;
287
+ }
288
+ });
289
+
290
+ test("loop success logs are captured with correct category", async () => {
291
+ const buffer = createOutputBuffer();
292
+
293
+ const originalLog = console.log;
294
+ console.log = () => {};
295
+
296
+ try {
297
+ await runLoop({
298
+ dryRun: true,
299
+ maxIterations: 1,
300
+ pauseSeconds: 0,
301
+ buffer,
302
+ ui: false,
303
+ });
304
+
305
+ const logs = buffer.getLogs();
306
+ const successLogs = logs.filter((l) => l.category === "success");
307
+
308
+ // Should have success logs (tasks complete)
309
+ expect(successLogs.length).toBeGreaterThan(0);
310
+ expect(
311
+ successLogs.some((l) => l.message.includes("tasks complete"))
312
+ ).toBe(true);
313
+ } finally {
314
+ console.log = originalLog;
315
+ }
316
+ });
317
+
318
+ test("agent output is captured to buffer", async () => {
319
+ // Use a pending task so the agent gets invoked
320
+ await writeFile(
321
+ join(testDir, "todo", "TASKS.md"),
322
+ `# Tasks
323
+
324
+ ### test-task
325
+ - content: Test task
326
+ - status: pending
327
+ - dependencies: none
328
+ `
329
+ );
330
+
331
+ const buffer = createOutputBuffer();
332
+ const mockAgent = createMockAgent({
333
+ logs: [{ category: "info", message: "Agent working" }],
334
+ output: ["Agent output text\n", "More output\n"],
335
+ exitCode: 0,
336
+ });
337
+
338
+ const originalLog = console.log;
339
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
340
+ console.log = () => {};
341
+ process.stdout.write = () => true;
342
+
343
+ try {
344
+ await runLoop({
345
+ dryRun: true,
346
+ agent: mockAgent,
347
+ maxIterations: 1,
348
+ pauseSeconds: 0,
349
+ buffer,
350
+ ui: false,
351
+ });
352
+ } catch {
353
+ // Expected: max iterations exceeded
354
+ } finally {
355
+ console.log = originalLog;
356
+ process.stdout.write = originalStdoutWrite;
357
+ }
358
+
359
+ // Verify agent output was captured
360
+ const output = buffer.getOutput();
361
+ expect(output.length).toBe(2);
362
+ expect(output[0]!.text).toBe("Agent output text\n");
363
+ expect(output[1]!.text).toBe("More output\n");
364
+ });
365
+
366
+ test("buffer subscribers receive logs in real-time", async () => {
367
+ const buffer = createOutputBuffer();
368
+ const receivedLogs: string[] = [];
369
+
370
+ // Subscribe before running loop
371
+ buffer.subscribeLogs((entry) => {
372
+ receivedLogs.push(entry.message);
373
+ });
374
+
375
+ const originalLog = console.log;
376
+ console.log = () => {};
377
+
378
+ try {
379
+ await runLoop({
380
+ dryRun: true,
381
+ maxIterations: 1,
382
+ pauseSeconds: 0,
383
+ buffer,
384
+ ui: false,
385
+ });
386
+
387
+ // Verify subscriber received logs
388
+ expect(receivedLogs.length).toBeGreaterThan(0);
389
+ expect(receivedLogs.some((m) => m.includes("Starting math loop"))).toBe(
390
+ true
391
+ );
392
+ } finally {
393
+ console.log = originalLog;
394
+ }
395
+ });
396
+
397
+ test("buffer subscribers receive agent output in real-time", async () => {
398
+ await writeFile(
399
+ join(testDir, "todo", "TASKS.md"),
400
+ `# Tasks
401
+
402
+ ### test-task
403
+ - content: Test task
404
+ - status: pending
405
+ - dependencies: none
406
+ `
407
+ );
408
+
409
+ const buffer = createOutputBuffer();
410
+ const receivedOutput: string[] = [];
411
+
412
+ // Subscribe before running loop
413
+ buffer.subscribeOutput((output) => {
414
+ receivedOutput.push(output.text);
415
+ });
416
+
417
+ const mockAgent = createMockAgent({
418
+ output: ["Streamed output\n"],
419
+ exitCode: 0,
420
+ });
421
+
422
+ const originalLog = console.log;
423
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
424
+ console.log = () => {};
425
+ process.stdout.write = () => true;
426
+
427
+ try {
428
+ await runLoop({
429
+ dryRun: true,
430
+ agent: mockAgent,
431
+ maxIterations: 1,
432
+ pauseSeconds: 0,
433
+ buffer,
434
+ ui: false,
435
+ });
436
+ } catch {
437
+ // Expected: max iterations exceeded
438
+ } finally {
439
+ console.log = originalLog;
440
+ process.stdout.write = originalStdoutWrite;
441
+ }
442
+
443
+ // Verify subscriber received output
444
+ expect(receivedOutput).toContain("Streamed output\n");
445
+ });
446
+
447
+ test("console.log still works without buffer", async () => {
448
+ const logs: string[] = [];
449
+ const originalLog = console.log;
450
+ console.log = (...args: unknown[]) => {
451
+ logs.push(args.join(" "));
452
+ };
453
+
454
+ try {
455
+ // Run without buffer - console.log should still work
456
+ await runLoop({
457
+ dryRun: true,
458
+ maxIterations: 1,
459
+ pauseSeconds: 0,
460
+ ui: false,
461
+ });
462
+
463
+ // Verify console.log was called
464
+ const logText = logs.join("\n");
465
+ expect(logText).toContain("Starting math loop");
466
+ } finally {
467
+ console.log = originalLog;
468
+ }
469
+ });
470
+ });
471
+
472
+ describe("runLoop UI server integration", () => {
473
+ let testDir: string;
474
+ let originalCwd: string;
475
+
476
+ beforeEach(async () => {
477
+ // Create a temp directory for each test
478
+ testDir = await mkdtemp(join(tmpdir(), "math-loop-ui-test-"));
479
+ originalCwd = process.cwd();
480
+ process.chdir(testDir);
481
+
482
+ // Create the todo directory with required files
483
+ const todoDir = join(testDir, "todo");
484
+ await mkdir(todoDir, { recursive: true });
485
+
486
+ // Create PROMPT.md
487
+ await writeFile(
488
+ join(todoDir, "PROMPT.md"),
489
+ "# Test Prompt\n\nTest instructions."
490
+ );
491
+
492
+ // Create TASKS.md with all tasks complete
493
+ await writeFile(
494
+ join(todoDir, "TASKS.md"),
495
+ `# Tasks
496
+
497
+ ### test-task
498
+ - content: Test task
499
+ - status: complete
500
+ - dependencies: none
501
+ `
502
+ );
503
+ });
504
+
505
+ afterEach(async () => {
506
+ process.chdir(originalCwd);
507
+ await rm(testDir, { recursive: true, force: true });
508
+ });
509
+
510
+ // NOTE: UI server tests are skipped in automated testing because:
511
+ // 1. They require exclusive port access (can't run in parallel)
512
+ // 2. The server stays running after tests complete (as designed)
513
+ // Manual testing should verify UI server integration works correctly.
514
+
515
+ test("ui: false disables the server", async () => {
516
+ const logs: string[] = [];
517
+ const originalLog = console.log;
518
+ console.log = (...args: unknown[]) => {
519
+ logs.push(args.join(" "));
520
+ };
521
+
522
+ try {
523
+ await runLoop({
524
+ dryRun: true,
525
+ maxIterations: 1,
526
+ pauseSeconds: 0,
527
+ ui: false,
528
+ });
529
+
530
+ // Verify UI server URL is NOT logged
531
+ const logText = logs.join("\n");
532
+ expect(logText).not.toContain("Web UI available");
533
+ } finally {
534
+ console.log = originalLog;
535
+ }
536
+ });
537
+ });