@os-eco/overstory-cli 0.8.7 → 0.9.2
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/README.md +26 -8
- package/agents/coordinator.md +30 -6
- package/agents/lead.md +11 -1
- package/agents/ov-co-creation.md +90 -0
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +9 -1
- package/src/agents/hooks-deployer.ts +2 -1
- package/src/agents/overlay.test.ts +26 -0
- package/src/agents/overlay.ts +31 -4
- package/src/canopy/client.test.ts +107 -0
- package/src/canopy/client.ts +179 -0
- package/src/commands/agents.ts +1 -1
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/clean.ts +1 -58
- package/src/commands/completions.test.ts +18 -6
- package/src/commands/completions.ts +40 -1
- package/src/commands/coordinator.test.ts +77 -4
- package/src/commands/coordinator.ts +304 -146
- package/src/commands/dashboard.ts +47 -10
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.ts +3 -1
- package/src/commands/ecosystem.test.ts +126 -1
- package/src/commands/ecosystem.ts +7 -53
- package/src/commands/feed.test.ts +117 -2
- package/src/commands/feed.ts +46 -30
- package/src/commands/group.test.ts +274 -155
- package/src/commands/group.ts +11 -5
- package/src/commands/init.test.ts +2 -1
- package/src/commands/init.ts +8 -0
- package/src/commands/log.test.ts +35 -0
- package/src/commands/log.ts +10 -6
- package/src/commands/logs.test.ts +423 -1
- package/src/commands/logs.ts +99 -104
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +177 -2
- package/src/commands/prime.ts +4 -2
- package/src/commands/sling.ts +23 -3
- package/src/commands/update.test.ts +1 -0
- package/src/commands/upgrade.test.ts +2 -0
- package/src/commands/upgrade.ts +1 -17
- package/src/commands/watch.test.ts +67 -1
- package/src/commands/watch.ts +13 -88
- package/src/config.test.ts +250 -0
- package/src/config.ts +43 -0
- package/src/doctor/agents.test.ts +72 -5
- package/src/doctor/agents.ts +10 -10
- package/src/doctor/consistency.test.ts +35 -0
- package/src/doctor/consistency.ts +7 -3
- package/src/doctor/dependencies.test.ts +58 -1
- package/src/doctor/dependencies.ts +4 -2
- package/src/doctor/providers.test.ts +41 -5
- package/src/doctor/types.ts +2 -1
- package/src/doctor/version.test.ts +106 -2
- package/src/doctor/version.ts +4 -2
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +158 -0
- package/src/e2e/init-sling-lifecycle.test.ts +4 -2
- package/src/errors.test.ts +350 -0
- package/src/events/tailer.test.ts +25 -0
- package/src/events/tailer.ts +8 -1
- package/src/index.ts +9 -1
- package/src/mail/store.test.ts +110 -0
- package/src/mail/store.ts +2 -1
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +4 -2
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/pi-guards.ts +2 -1
- package/src/runtimes/pi.test.ts +9 -9
- package/src/runtimes/pi.ts +6 -7
- package/src/runtimes/registry.test.ts +1 -1
- package/src/runtimes/registry.ts +13 -4
- package/src/runtimes/sapling.ts +2 -1
- package/src/runtimes/types.ts +2 -2
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/store.ts +25 -4
- package/src/types.ts +65 -1
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +68 -0
- package/src/utils/pid.ts +45 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/watchdog/daemon.test.ts +255 -1
- package/src/watchdog/daemon.ts +87 -9
- package/src/watchdog/health.test.ts +15 -1
- package/src/watchdog/health.ts +1 -1
- package/src/watchdog/triage.test.ts +49 -9
- package/src/watchdog/triage.ts +21 -5
- package/templates/overlay.md.tmpl +2 -0
package/src/commands/log.ts
CHANGED
|
@@ -83,13 +83,13 @@ function updateLastActivity(projectRoot: string, agentName: string): void {
|
|
|
83
83
|
* The Stop hook fires every turn for these agents (not just at session end),
|
|
84
84
|
* so they must NOT auto-transition to 'completed' on session-end events.
|
|
85
85
|
*/
|
|
86
|
-
const PERSISTENT_CAPABILITIES = new Set(["coordinator", "monitor"]);
|
|
86
|
+
const PERSISTENT_CAPABILITIES = new Set(["coordinator", "orchestrator", "monitor"]);
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Transition agent state to 'completed' in the SessionStore.
|
|
90
90
|
* Called when session-end event fires.
|
|
91
91
|
*
|
|
92
|
-
* Skips the transition for persistent agent types (coordinator, monitor)
|
|
92
|
+
* Skips the transition for persistent agent types (coordinator, orchestrator, monitor)
|
|
93
93
|
* whose Stop hook fires every turn, not just at true session end.
|
|
94
94
|
*
|
|
95
95
|
* Non-fatal: silently ignores errors to avoid breaking hook execution.
|
|
@@ -101,15 +101,19 @@ function transitionToCompleted(projectRoot: string, agentName: string): void {
|
|
|
101
101
|
try {
|
|
102
102
|
const session = store.getByName(agentName);
|
|
103
103
|
if (session && PERSISTENT_CAPABILITIES.has(session.capability)) {
|
|
104
|
-
// Check if
|
|
104
|
+
// Check if a persistent top-level agent self-exited by verifying the run
|
|
105
|
+
// is already completed.
|
|
105
106
|
// If `ov run complete` was called before session-end, the run status is 'completed'
|
|
106
|
-
// and we should transition the
|
|
107
|
-
if (
|
|
107
|
+
// and we should transition the persistent session to completed too.
|
|
108
|
+
if (
|
|
109
|
+
(session.capability === "coordinator" || session.capability === "orchestrator") &&
|
|
110
|
+
session.runId
|
|
111
|
+
) {
|
|
108
112
|
const runStore = createRunStore(join(overstoryDir, "sessions.db"));
|
|
109
113
|
try {
|
|
110
114
|
const run = runStore.getRun(session.runId);
|
|
111
115
|
if (run && run.status === "completed") {
|
|
112
|
-
// Self-exit:
|
|
116
|
+
// Self-exit: the persistent agent called ov run complete before session ended
|
|
113
117
|
store.updateState(agentName, "completed");
|
|
114
118
|
store.updateLastActivity(agentName);
|
|
115
119
|
return;
|
|
@@ -5,7 +5,14 @@ import { join } from "node:path";
|
|
|
5
5
|
import { ValidationError } from "../errors.ts";
|
|
6
6
|
import { cleanupTempDir } from "../test-helpers.ts";
|
|
7
7
|
import type { LogEvent } from "../types.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildLogDetail,
|
|
10
|
+
discoverLogFiles,
|
|
11
|
+
filterEvents,
|
|
12
|
+
logsCommand,
|
|
13
|
+
parseLogFile,
|
|
14
|
+
pollLogTick,
|
|
15
|
+
} from "./logs.ts";
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Test helper: capture stdout during command execution.
|
|
@@ -377,3 +384,418 @@ this is not json
|
|
|
377
384
|
expect(output).not.toContain("this is not json");
|
|
378
385
|
});
|
|
379
386
|
});
|
|
387
|
+
|
|
388
|
+
// parseRelativeTime tests moved to src/utils/time.test.ts
|
|
389
|
+
|
|
390
|
+
describe("buildLogDetail", () => {
|
|
391
|
+
test("builds key=value pairs from data fields", () => {
|
|
392
|
+
const event: LogEvent = {
|
|
393
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
394
|
+
level: "info",
|
|
395
|
+
event: "test",
|
|
396
|
+
agentName: "a",
|
|
397
|
+
data: { toolName: "Bash", file: "index.ts" },
|
|
398
|
+
};
|
|
399
|
+
const result = buildLogDetail(event);
|
|
400
|
+
expect(result).toContain("toolName=Bash");
|
|
401
|
+
expect(result).toContain("file=index.ts");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("truncates values longer than 80 characters", () => {
|
|
405
|
+
const longValue = "x".repeat(100);
|
|
406
|
+
const event: LogEvent = {
|
|
407
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
408
|
+
level: "info",
|
|
409
|
+
event: "test",
|
|
410
|
+
agentName: "a",
|
|
411
|
+
data: { message: longValue },
|
|
412
|
+
};
|
|
413
|
+
const result = buildLogDetail(event);
|
|
414
|
+
expect(result).not.toContain(longValue);
|
|
415
|
+
expect(result).toContain("...");
|
|
416
|
+
// 77 chars + "..." = 80
|
|
417
|
+
expect(result).toContain("x".repeat(77));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("skips null and undefined values", () => {
|
|
421
|
+
const event: LogEvent = {
|
|
422
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
423
|
+
level: "info",
|
|
424
|
+
event: "test",
|
|
425
|
+
agentName: "a",
|
|
426
|
+
data: { present: "yes", missing: null, also: undefined },
|
|
427
|
+
};
|
|
428
|
+
const result = buildLogDetail(event);
|
|
429
|
+
expect(result).toContain("present=yes");
|
|
430
|
+
expect(result).not.toContain("missing");
|
|
431
|
+
expect(result).not.toContain("also");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("returns empty string for empty data", () => {
|
|
435
|
+
const event: LogEvent = {
|
|
436
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
437
|
+
level: "info",
|
|
438
|
+
event: "test",
|
|
439
|
+
agentName: "a",
|
|
440
|
+
data: {},
|
|
441
|
+
};
|
|
442
|
+
expect(buildLogDetail(event)).toBe("");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("stringifies non-string values as JSON", () => {
|
|
446
|
+
const event: LogEvent = {
|
|
447
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
448
|
+
level: "info",
|
|
449
|
+
event: "test",
|
|
450
|
+
agentName: "a",
|
|
451
|
+
data: { count: 42, active: true },
|
|
452
|
+
};
|
|
453
|
+
const result = buildLogDetail(event);
|
|
454
|
+
expect(result).toContain("count=42");
|
|
455
|
+
expect(result).toContain("active=true");
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("discoverLogFiles", () => {
|
|
460
|
+
let tmpDir: string;
|
|
461
|
+
|
|
462
|
+
beforeEach(async () => {
|
|
463
|
+
tmpDir = join(
|
|
464
|
+
tmpdir(),
|
|
465
|
+
`overstory-discover-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
466
|
+
);
|
|
467
|
+
await mkdir(tmpDir, { recursive: true });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
afterEach(async () => {
|
|
471
|
+
await cleanupTempDir(tmpDir);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("discovers log files in proper agent/session structure", async () => {
|
|
475
|
+
// Create agent-a/session-1/events.ndjson
|
|
476
|
+
const dir1 = join(tmpDir, "agent-a", "2026-01-01T00-00-00");
|
|
477
|
+
await mkdir(dir1, { recursive: true });
|
|
478
|
+
await writeFile(join(dir1, "events.ndjson"), "{}");
|
|
479
|
+
|
|
480
|
+
// Create agent-b/session-2/events.ndjson
|
|
481
|
+
const dir2 = join(tmpDir, "agent-b", "2026-01-02T00-00-00");
|
|
482
|
+
await mkdir(dir2, { recursive: true });
|
|
483
|
+
await writeFile(join(dir2, "events.ndjson"), "{}");
|
|
484
|
+
|
|
485
|
+
const result = await discoverLogFiles(tmpDir);
|
|
486
|
+
expect(result).toHaveLength(2);
|
|
487
|
+
expect(result[0]?.agentName).toBe("agent-a");
|
|
488
|
+
expect(result[1]?.agentName).toBe("agent-b");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("filters by agent name when provided", async () => {
|
|
492
|
+
const dir1 = join(tmpDir, "agent-a", "2026-01-01T00-00-00");
|
|
493
|
+
await mkdir(dir1, { recursive: true });
|
|
494
|
+
await writeFile(join(dir1, "events.ndjson"), "{}");
|
|
495
|
+
|
|
496
|
+
const dir2 = join(tmpDir, "agent-b", "2026-01-02T00-00-00");
|
|
497
|
+
await mkdir(dir2, { recursive: true });
|
|
498
|
+
await writeFile(join(dir2, "events.ndjson"), "{}");
|
|
499
|
+
|
|
500
|
+
const result = await discoverLogFiles(tmpDir, "agent-a");
|
|
501
|
+
expect(result).toHaveLength(1);
|
|
502
|
+
expect(result[0]?.agentName).toBe("agent-a");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("returns empty array for nonexistent directory", async () => {
|
|
506
|
+
const result = await discoverLogFiles(join(tmpDir, "nonexistent"));
|
|
507
|
+
expect(result).toEqual([]);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("sorts by session timestamp", async () => {
|
|
511
|
+
const dir1 = join(tmpDir, "agent-a", "2026-01-02T00-00-00");
|
|
512
|
+
await mkdir(dir1, { recursive: true });
|
|
513
|
+
await writeFile(join(dir1, "events.ndjson"), "{}");
|
|
514
|
+
|
|
515
|
+
const dir2 = join(tmpDir, "agent-a", "2026-01-01T00-00-00");
|
|
516
|
+
await mkdir(dir2, { recursive: true });
|
|
517
|
+
await writeFile(join(dir2, "events.ndjson"), "{}");
|
|
518
|
+
|
|
519
|
+
const result = await discoverLogFiles(tmpDir);
|
|
520
|
+
expect(result[0]?.sessionTimestamp).toBe("2026-01-01T00-00-00");
|
|
521
|
+
expect(result[1]?.sessionTimestamp).toBe("2026-01-02T00-00-00");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("parseLogFile", () => {
|
|
526
|
+
let tmpDir: string;
|
|
527
|
+
|
|
528
|
+
beforeEach(async () => {
|
|
529
|
+
tmpDir = join(
|
|
530
|
+
tmpdir(),
|
|
531
|
+
`overstory-parse-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
532
|
+
);
|
|
533
|
+
await mkdir(tmpDir, { recursive: true });
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
afterEach(async () => {
|
|
537
|
+
await cleanupTempDir(tmpDir);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("parses valid NDJSON lines", async () => {
|
|
541
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
542
|
+
const lines = [
|
|
543
|
+
JSON.stringify({
|
|
544
|
+
timestamp: "2026-01-01T10:00:00Z",
|
|
545
|
+
event: "tool.start",
|
|
546
|
+
level: "info",
|
|
547
|
+
agentName: "a",
|
|
548
|
+
data: {},
|
|
549
|
+
}),
|
|
550
|
+
JSON.stringify({
|
|
551
|
+
timestamp: "2026-01-01T10:01:00Z",
|
|
552
|
+
event: "tool.end",
|
|
553
|
+
level: "info",
|
|
554
|
+
agentName: "a",
|
|
555
|
+
data: {},
|
|
556
|
+
}),
|
|
557
|
+
];
|
|
558
|
+
await writeFile(filePath, lines.join("\n"));
|
|
559
|
+
|
|
560
|
+
const events = await parseLogFile(filePath);
|
|
561
|
+
expect(events).toHaveLength(2);
|
|
562
|
+
expect(events[0]?.event).toBe("tool.start");
|
|
563
|
+
expect(events[1]?.event).toBe("tool.end");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("skips malformed JSON lines silently", async () => {
|
|
567
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
568
|
+
const content = [
|
|
569
|
+
JSON.stringify({
|
|
570
|
+
timestamp: "2026-01-01T10:00:00Z",
|
|
571
|
+
event: "valid",
|
|
572
|
+
level: "info",
|
|
573
|
+
agentName: "a",
|
|
574
|
+
data: {},
|
|
575
|
+
}),
|
|
576
|
+
"not valid json",
|
|
577
|
+
'{"incomplete": true',
|
|
578
|
+
JSON.stringify({
|
|
579
|
+
timestamp: "2026-01-01T10:02:00Z",
|
|
580
|
+
event: "also-valid",
|
|
581
|
+
level: "info",
|
|
582
|
+
agentName: "a",
|
|
583
|
+
data: {},
|
|
584
|
+
}),
|
|
585
|
+
].join("\n");
|
|
586
|
+
await writeFile(filePath, content);
|
|
587
|
+
|
|
588
|
+
const events = await parseLogFile(filePath);
|
|
589
|
+
expect(events).toHaveLength(2);
|
|
590
|
+
expect(events[0]?.event).toBe("valid");
|
|
591
|
+
expect(events[1]?.event).toBe("also-valid");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("returns empty array for nonexistent file", async () => {
|
|
595
|
+
const events = await parseLogFile(join(tmpDir, "nonexistent.ndjson"));
|
|
596
|
+
expect(events).toEqual([]);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("skips objects missing required fields", async () => {
|
|
600
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
601
|
+
const content = [
|
|
602
|
+
JSON.stringify({ timestamp: "2026-01-01T00:00:00Z" }), // missing "event"
|
|
603
|
+
JSON.stringify({ event: "test" }), // missing "timestamp"
|
|
604
|
+
JSON.stringify({
|
|
605
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
606
|
+
event: "good",
|
|
607
|
+
level: "info",
|
|
608
|
+
agentName: "a",
|
|
609
|
+
data: {},
|
|
610
|
+
}),
|
|
611
|
+
].join("\n");
|
|
612
|
+
await writeFile(filePath, content);
|
|
613
|
+
|
|
614
|
+
const events = await parseLogFile(filePath);
|
|
615
|
+
expect(events).toHaveLength(1);
|
|
616
|
+
expect(events[0]?.event).toBe("good");
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("filterEvents", () => {
|
|
621
|
+
const baseEvents: LogEvent[] = [
|
|
622
|
+
{
|
|
623
|
+
timestamp: "2026-01-01T10:00:00.000Z",
|
|
624
|
+
level: "info",
|
|
625
|
+
event: "e1",
|
|
626
|
+
agentName: "a",
|
|
627
|
+
data: {},
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
timestamp: "2026-01-01T11:00:00.000Z",
|
|
631
|
+
level: "error",
|
|
632
|
+
event: "e2",
|
|
633
|
+
agentName: "a",
|
|
634
|
+
data: {},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
timestamp: "2026-01-01T12:00:00.000Z",
|
|
638
|
+
level: "warn",
|
|
639
|
+
event: "e3",
|
|
640
|
+
agentName: "a",
|
|
641
|
+
data: {},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
timestamp: "2026-01-01T13:00:00.000Z",
|
|
645
|
+
level: "debug",
|
|
646
|
+
event: "e4",
|
|
647
|
+
agentName: "a",
|
|
648
|
+
data: {},
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
test("filters by level", () => {
|
|
653
|
+
const result = filterEvents(baseEvents, { level: "error" });
|
|
654
|
+
expect(result).toHaveLength(1);
|
|
655
|
+
expect(result[0]?.event).toBe("e2");
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("filters by since", () => {
|
|
659
|
+
const since = new Date("2026-01-01T11:30:00.000Z");
|
|
660
|
+
const result = filterEvents(baseEvents, { since });
|
|
661
|
+
expect(result).toHaveLength(2);
|
|
662
|
+
expect(result[0]?.event).toBe("e3");
|
|
663
|
+
expect(result[1]?.event).toBe("e4");
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("filters by until", () => {
|
|
667
|
+
const until = new Date("2026-01-01T11:30:00.000Z");
|
|
668
|
+
const result = filterEvents(baseEvents, { until });
|
|
669
|
+
expect(result).toHaveLength(2);
|
|
670
|
+
expect(result[0]?.event).toBe("e1");
|
|
671
|
+
expect(result[1]?.event).toBe("e2");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("combines level + since + until", () => {
|
|
675
|
+
const result = filterEvents(baseEvents, {
|
|
676
|
+
level: "info",
|
|
677
|
+
since: new Date("2026-01-01T09:00:00.000Z"),
|
|
678
|
+
until: new Date("2026-01-01T10:30:00.000Z"),
|
|
679
|
+
});
|
|
680
|
+
expect(result).toHaveLength(1);
|
|
681
|
+
expect(result[0]?.event).toBe("e1");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("returns all events with no filters", () => {
|
|
685
|
+
const result = filterEvents(baseEvents, {});
|
|
686
|
+
expect(result).toHaveLength(4);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe("pollLogTick", () => {
|
|
691
|
+
let tmpDir: string;
|
|
692
|
+
|
|
693
|
+
beforeEach(async () => {
|
|
694
|
+
tmpDir = join(
|
|
695
|
+
tmpdir(),
|
|
696
|
+
`overstory-poll-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
697
|
+
);
|
|
698
|
+
await mkdir(tmpDir, { recursive: true });
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
afterEach(async () => {
|
|
702
|
+
await cleanupTempDir(tmpDir);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("returns 0 for empty files", async () => {
|
|
706
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
707
|
+
await writeFile(filePath, "");
|
|
708
|
+
|
|
709
|
+
const lastKnownSizes = new Map<string, number>();
|
|
710
|
+
const count = await pollLogTick([{ path: filePath }], lastKnownSizes, {});
|
|
711
|
+
expect(count).toBe(0);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("returns count of new events from file with new lines", async () => {
|
|
715
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
716
|
+
const line1 = JSON.stringify({
|
|
717
|
+
timestamp: "2026-01-01T10:00:00Z",
|
|
718
|
+
event: "e1",
|
|
719
|
+
level: "info",
|
|
720
|
+
agentName: "a",
|
|
721
|
+
data: {},
|
|
722
|
+
});
|
|
723
|
+
const line2 = JSON.stringify({
|
|
724
|
+
timestamp: "2026-01-01T10:01:00Z",
|
|
725
|
+
event: "e2",
|
|
726
|
+
level: "info",
|
|
727
|
+
agentName: "a",
|
|
728
|
+
data: {},
|
|
729
|
+
});
|
|
730
|
+
await writeFile(filePath, `${line1}\n${line2}\n`);
|
|
731
|
+
|
|
732
|
+
const lastKnownSizes = new Map<string, number>();
|
|
733
|
+
// Capture stdout to prevent test noise
|
|
734
|
+
const origWrite = process.stdout.write;
|
|
735
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
736
|
+
try {
|
|
737
|
+
const count = await pollLogTick([{ path: filePath }], lastKnownSizes, {});
|
|
738
|
+
expect(count).toBe(2);
|
|
739
|
+
// lastKnownSizes should be updated
|
|
740
|
+
expect(lastKnownSizes.get(filePath)).toBeGreaterThan(0);
|
|
741
|
+
} finally {
|
|
742
|
+
process.stdout.write = origWrite;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("returns 0 when no new data since last position", async () => {
|
|
747
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
748
|
+
const line = JSON.stringify({
|
|
749
|
+
timestamp: "2026-01-01T10:00:00Z",
|
|
750
|
+
event: "e1",
|
|
751
|
+
level: "info",
|
|
752
|
+
agentName: "a",
|
|
753
|
+
data: {},
|
|
754
|
+
});
|
|
755
|
+
await writeFile(filePath, `${line}\n`);
|
|
756
|
+
|
|
757
|
+
const lastKnownSizes = new Map<string, number>();
|
|
758
|
+
const origWrite = process.stdout.write;
|
|
759
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
760
|
+
try {
|
|
761
|
+
// First tick reads everything
|
|
762
|
+
await pollLogTick([{ path: filePath }], lastKnownSizes, {});
|
|
763
|
+
// Second tick should find nothing new
|
|
764
|
+
const count = await pollLogTick([{ path: filePath }], lastKnownSizes, {});
|
|
765
|
+
expect(count).toBe(0);
|
|
766
|
+
} finally {
|
|
767
|
+
process.stdout.write = origWrite;
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test("applies level filter", async () => {
|
|
772
|
+
const filePath = join(tmpDir, "events.ndjson");
|
|
773
|
+
const line1 = JSON.stringify({
|
|
774
|
+
timestamp: "2026-01-01T10:00:00Z",
|
|
775
|
+
event: "e1",
|
|
776
|
+
level: "info",
|
|
777
|
+
agentName: "a",
|
|
778
|
+
data: {},
|
|
779
|
+
});
|
|
780
|
+
const line2 = JSON.stringify({
|
|
781
|
+
timestamp: "2026-01-01T10:01:00Z",
|
|
782
|
+
event: "e2",
|
|
783
|
+
level: "error",
|
|
784
|
+
agentName: "a",
|
|
785
|
+
data: {},
|
|
786
|
+
});
|
|
787
|
+
await writeFile(filePath, `${line1}\n${line2}\n`);
|
|
788
|
+
|
|
789
|
+
const lastKnownSizes = new Map<string, number>();
|
|
790
|
+
const origWrite = process.stdout.write;
|
|
791
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
792
|
+
try {
|
|
793
|
+
const count = await pollLogTick([{ path: filePath }], lastKnownSizes, {
|
|
794
|
+
level: "error",
|
|
795
|
+
});
|
|
796
|
+
expect(count).toBe(1);
|
|
797
|
+
} finally {
|
|
798
|
+
process.stdout.write = origWrite;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|