agent-dbg 0.1.2 → 0.1.3

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,156 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { DaemonLogger } from "../../src/daemon/logger.ts";
6
+ import type { DaemonLogEntry } from "../../src/daemon/logger.ts";
7
+ import { getDaemonLogPath } from "../../src/daemon/paths.ts";
8
+ import { DebugSession } from "../../src/daemon/session.ts";
9
+
10
+ function readEntries(logPath: string): DaemonLogEntry[] {
11
+ if (!existsSync(logPath)) return [];
12
+ const content = readFileSync(logPath, "utf-8");
13
+ return content
14
+ .split("\n")
15
+ .filter((l) => l.trim())
16
+ .map((l) => JSON.parse(l) as DaemonLogEntry);
17
+ }
18
+
19
+ function hasEvent(entries: DaemonLogEntry[], event: string): boolean {
20
+ return entries.some((e) => e.event === event);
21
+ }
22
+
23
+ async function waitForState(
24
+ session: DebugSession,
25
+ state: "idle" | "running" | "paused",
26
+ timeoutMs = 2000,
27
+ ): Promise<void> {
28
+ const deadline = Date.now() + timeoutMs;
29
+ while (session.sessionState !== state && Date.now() < deadline) {
30
+ await Bun.sleep(50);
31
+ }
32
+ }
33
+
34
+ describe("DaemonLogger integration", () => {
35
+ test("DaemonLogger writes to daemon.log file", () => {
36
+ const logPath = join(tmpdir(), `test-daemon-write-${Date.now()}.daemon.log`);
37
+ try {
38
+ const logger = new DaemonLogger(logPath);
39
+ logger.info("test.event", "hello world", { key: "value" });
40
+
41
+ const entries = readEntries(logPath);
42
+ expect(entries).toHaveLength(1);
43
+ expect(entries[0]!.level).toBe("info");
44
+ expect(entries[0]!.event).toBe("test.event");
45
+ expect(entries[0]!.message).toBe("hello world");
46
+ expect(entries[0]!.data).toEqual({ key: "value" });
47
+ } finally {
48
+ if (existsSync(logPath)) unlinkSync(logPath);
49
+ }
50
+ });
51
+
52
+ test("getDaemonLogPath returns correct path", () => {
53
+ const path = getDaemonLogPath("my-session");
54
+ expect(path).toEndWith("/my-session.daemon.log");
55
+ });
56
+
57
+ test("DebugSession logs launch events", async () => {
58
+ const sessionName = `test-daemon-log-${Date.now()}`;
59
+ const session = new DebugSession(sessionName);
60
+ const logPath = getDaemonLogPath(sessionName);
61
+ try {
62
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
63
+ brk: true,
64
+ });
65
+ await waitForState(session, "paused");
66
+
67
+ const entries = readEntries(logPath);
68
+ expect(hasEvent(entries, "child.spawn")).toBe(true);
69
+ expect(hasEvent(entries, "inspector.detected")).toBe(true);
70
+ expect(hasEvent(entries, "cdp.connected")).toBe(true);
71
+ } finally {
72
+ await session.stop();
73
+ if (existsSync(logPath)) unlinkSync(logPath);
74
+ }
75
+ });
76
+
77
+ test("DebugSession logs child stderr", async () => {
78
+ const sessionName = `test-daemon-stderr-${Date.now()}`;
79
+ const session = new DebugSession(sessionName);
80
+ const logPath = getDaemonLogPath(sessionName);
81
+ try {
82
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
83
+ brk: true,
84
+ });
85
+ await waitForState(session, "paused");
86
+
87
+ const entries = readEntries(logPath);
88
+ // The "Debugger listening on..." line comes from child stderr
89
+ const stderrEntries = entries.filter((e) => e.event === "child.stderr");
90
+ expect(stderrEntries.length).toBeGreaterThan(0);
91
+ const hasDebuggerLine = stderrEntries.some((e) =>
92
+ e.message.includes("Debugger listening on"),
93
+ );
94
+ expect(hasDebuggerLine).toBe(true);
95
+ } finally {
96
+ await session.stop();
97
+ if (existsSync(logPath)) unlinkSync(logPath);
98
+ }
99
+ });
100
+
101
+ test("DebugSession logs inspector URL detection failure", async () => {
102
+ const sessionName = `test-daemon-fail-${Date.now()}`;
103
+ const session = new DebugSession(sessionName);
104
+ const logPath = getDaemonLogPath(sessionName);
105
+ try {
106
+ // Use echo (not node), which won't emit an inspector URL
107
+ await expect(
108
+ session.launch(["echo", "hello"], { brk: true }),
109
+ ).rejects.toThrow("Failed to detect inspector URL");
110
+
111
+ const entries = readEntries(logPath);
112
+ expect(hasEvent(entries, "child.spawn")).toBe(true);
113
+ expect(hasEvent(entries, "inspector.failed")).toBe(true);
114
+ // Should capture the accumulated stderr in the failure entry
115
+ const failEntry = entries.find((e) => e.event === "inspector.failed");
116
+ expect(failEntry?.data?.stderr).toBeDefined();
117
+ } finally {
118
+ await session.stop();
119
+ if (existsSync(logPath)) unlinkSync(logPath);
120
+ }
121
+ });
122
+
123
+ test("DebugSession logs process exit", async () => {
124
+ const sessionName = `test-daemon-exit-${Date.now()}`;
125
+ const session = new DebugSession(sessionName);
126
+ const logPath = getDaemonLogPath(sessionName);
127
+ try {
128
+ // Use process.exit() to force the child to terminate.
129
+ // Node.js waits for the debugger to disconnect before exiting,
130
+ // so we disconnect CDP after a delay to allow the natural exit.
131
+ await session.launch(
132
+ ["node", "-e", "setTimeout(() => process.exit(0), 200)"],
133
+ { brk: false },
134
+ );
135
+
136
+ // Disconnect CDP so Node.js can actually exit
137
+ // (Node prints "Waiting for the debugger to disconnect..." otherwise)
138
+ await Bun.sleep(300);
139
+ session.cdp?.disconnect();
140
+
141
+ // Wait for the child.exit log entry to be written
142
+ const deadline = Date.now() + 5000;
143
+ while (Date.now() < deadline) {
144
+ const entries = readEntries(logPath);
145
+ if (hasEvent(entries, "child.exit")) break;
146
+ await Bun.sleep(100);
147
+ }
148
+
149
+ const entries = readEntries(logPath);
150
+ expect(hasEvent(entries, "child.exit")).toBe(true);
151
+ } finally {
152
+ await session.stop();
153
+ if (existsSync(logPath)) unlinkSync(logPath);
154
+ }
155
+ });
156
+ });
@@ -0,0 +1,117 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { DaemonLogger } from "../../src/daemon/logger.ts";
6
+ import type { DaemonLogEntry } from "../../src/daemon/logger.ts";
7
+
8
+ const testDir = tmpdir();
9
+
10
+ function tempLogPath(): string {
11
+ return join(testDir, `test-daemon-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
12
+ }
13
+
14
+ function readEntries(logPath: string): DaemonLogEntry[] {
15
+ const content = readFileSync(logPath, "utf-8");
16
+ return content
17
+ .split("\n")
18
+ .filter((l) => l.trim())
19
+ .map((l) => JSON.parse(l) as DaemonLogEntry);
20
+ }
21
+
22
+ describe("DaemonLogger", () => {
23
+ const paths: string[] = [];
24
+
25
+ afterEach(() => {
26
+ for (const p of paths) {
27
+ if (existsSync(p)) unlinkSync(p);
28
+ }
29
+ paths.length = 0;
30
+ });
31
+
32
+ test("truncates on creation", () => {
33
+ const logPath = tempLogPath();
34
+ paths.push(logPath);
35
+ writeFileSync(logPath, "pre-existing content\n");
36
+
37
+ new DaemonLogger(logPath);
38
+
39
+ const content = readFileSync(logPath, "utf-8");
40
+ expect(content).toBe("");
41
+ });
42
+
43
+ test("appends JSON lines", () => {
44
+ const logPath = tempLogPath();
45
+ paths.push(logPath);
46
+ const logger = new DaemonLogger(logPath);
47
+
48
+ logger.info("event.one", "first message");
49
+ logger.warn("event.two", "second message");
50
+ logger.error("event.three", "third message");
51
+
52
+ const entries = readEntries(logPath);
53
+ expect(entries).toHaveLength(3);
54
+ expect(entries[0]!.event).toBe("event.one");
55
+ expect(entries[1]!.event).toBe("event.two");
56
+ expect(entries[2]!.event).toBe("event.three");
57
+ });
58
+
59
+ test("clear() truncates", () => {
60
+ const logPath = tempLogPath();
61
+ paths.push(logPath);
62
+ const logger = new DaemonLogger(logPath);
63
+
64
+ logger.info("test", "message");
65
+ logger.info("test", "another");
66
+ expect(readEntries(logPath)).toHaveLength(2);
67
+
68
+ logger.clear();
69
+
70
+ const content = readFileSync(logPath, "utf-8");
71
+ expect(content).toBe("");
72
+ });
73
+
74
+ test("entries have correct structure", () => {
75
+ const logPath = tempLogPath();
76
+ paths.push(logPath);
77
+ const logger = new DaemonLogger(logPath);
78
+ const before = Date.now();
79
+
80
+ logger.info("child.spawn", "Process spawned", { pid: 1234 });
81
+
82
+ const entries = readEntries(logPath);
83
+ expect(entries).toHaveLength(1);
84
+ const entry = entries[0]!;
85
+
86
+ expect(entry.ts).toBeGreaterThanOrEqual(before);
87
+ expect(entry.ts).toBeLessThanOrEqual(Date.now());
88
+ expect(entry.level).toBe("info");
89
+ expect(entry.event).toBe("child.spawn");
90
+ expect(entry.message).toBe("Process spawned");
91
+ expect(entry.data).toEqual({ pid: 1234 });
92
+ });
93
+
94
+ test("debug level works", () => {
95
+ const logPath = tempLogPath();
96
+ paths.push(logPath);
97
+ const logger = new DaemonLogger(logPath);
98
+
99
+ logger.debug("test.debug", "debug message");
100
+
101
+ const entries = readEntries(logPath);
102
+ expect(entries).toHaveLength(1);
103
+ expect(entries[0]!.level).toBe("debug");
104
+ });
105
+
106
+ test("entries without data omit data field", () => {
107
+ const logPath = tempLogPath();
108
+ paths.push(logPath);
109
+ const logger = new DaemonLogger(logPath);
110
+
111
+ logger.info("test", "no data");
112
+
113
+ const entries = readEntries(logPath);
114
+ expect(entries).toHaveLength(1);
115
+ expect(entries[0]!.data).toBeUndefined();
116
+ });
117
+ });
@@ -251,6 +251,66 @@ describe("dead socket detection", () => {
251
251
  });
252
252
  });
253
253
 
254
+ describe("stale daemon detection (Docker-style PID check)", () => {
255
+ test("isRunning returns false when socket exists but no lock file", () => {
256
+ const session = testSession("nolck");
257
+ const socketPath = getSocketPath(session);
258
+
259
+ writeFileSync(socketPath, "");
260
+
261
+ expect(DaemonClient.isRunning(session)).toBe(false);
262
+ });
263
+
264
+ test("isRunning returns false when socket+lock exist but PID is dead", () => {
265
+ const session = testSession("dpid");
266
+ const socketPath = getSocketPath(session);
267
+ const lockPath = getLockPath(session);
268
+
269
+ writeFileSync(socketPath, "");
270
+ writeFileSync(lockPath, "999999"); // non-existent PID
271
+
272
+ expect(DaemonClient.isRunning(session)).toBe(false);
273
+ });
274
+
275
+ test("isRunning returns true when daemon is actually alive", async () => {
276
+ const session = testSession("alive");
277
+ const server = new DaemonServer(session, { idleTimeout: 60 });
278
+ server.onRequest(async () => ({ ok: true }));
279
+ await server.start();
280
+
281
+ try {
282
+ expect(DaemonClient.isRunning(session)).toBe(true);
283
+ } finally {
284
+ await server.stop();
285
+ }
286
+ });
287
+
288
+ test("isRunning returns false when no socket exists", () => {
289
+ const session = testSession("nosck");
290
+ expect(DaemonClient.isRunning(session)).toBe(false);
291
+ });
292
+
293
+ test("cleanStaleFiles removes orphaned socket and lock", () => {
294
+ const session = testSession("clnst");
295
+ const socketPath = getSocketPath(session);
296
+ const lockPath = getLockPath(session);
297
+
298
+ writeFileSync(socketPath, "");
299
+ writeFileSync(lockPath, "999999");
300
+
301
+ DaemonClient.cleanStaleFiles(session);
302
+
303
+ expect(existsSync(socketPath)).toBe(false);
304
+ expect(existsSync(lockPath)).toBe(false);
305
+ });
306
+
307
+ test("cleanStaleFiles is safe when files don't exist", () => {
308
+ const session = testSession("clnno");
309
+ // Should not throw
310
+ DaemonClient.cleanStaleFiles(session);
311
+ });
312
+ });
313
+
254
314
  describe("listSessions", () => {
255
315
  test("returns active sessions", async () => {
256
316
  const session1 = testSession("la");