import { expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; test("BUN_WATCHER_TRACE creates trace file with watch events", async () => { using dir = tempDir("watcher-trace", { "script.js": `console.log("ready");`, }); const traceFile = join(String(dir), "trace.log"); const env = { ...bunEnv, BUN_WATCHER_TRACE: traceFile }; const proc = Bun.spawn({ cmd: [bunExe(), "--watch", "script.js"], env, cwd: String(dir), stdout: "pipe", stderr: "inherit", stdin: "ignore", }); const decoder = new TextDecoder(); let wroteModification = false; // Wait for the initial run, trigger a change, then wait for the reload for await (const chunk of proc.stdout) { const str = decoder.decode(chunk); if (!wroteModification && str.includes("ready")) { wroteModification = true; await Bun.write(join(String(dir), "script.js"), `console.log("modified");`); continue; } if (wroteModification && str.includes("modified")) { break; } } proc.kill(); await proc.exited; // Check that trace file was created expect(existsSync(traceFile)).toBe(true); const traceContent = readFileSync(traceFile, "utf-8"); const lines = traceContent .trim() .split("\n") .filter(l => l.trim()); // Should have at least one event expect(lines.length).toBeGreaterThan(0); // Parse and validate JSON structure for (const line of lines) { const event = JSON.parse(line); // Check required fields exist expect(event).toHaveProperty("timestamp"); expect(event).toHaveProperty("files"); // Validate types expect(typeof event.timestamp).toBe("number"); expect(typeof event.files).toBe("object"); // Validate files object structure for (const [path, fileEvent] of Object.entries(event.files)) { expect(typeof path).toBe("string"); expect(fileEvent).toHaveProperty("events"); expect(Array.isArray(fileEvent.events)).toBe(true); // "changed" field is optional if (fileEvent.changed) { expect(Array.isArray(fileEvent.changed)).toBe(true); } } } }, 10000); test("BUN_WATCHER_TRACE with --watch flag", async () => { using dir = tempDir("watcher-trace-watch", { "script.js": `console.log("run", 0);`, }); const traceFile = join(String(dir), "watch-trace.log"); const env = { ...bunEnv, BUN_WATCHER_TRACE: traceFile }; const proc = Bun.spawn({ cmd: [bunExe(), "--watch", "script.js"], env, cwd: String(dir), stdout: "pipe", stderr: "inherit", stdin: "ignore", }); let i = 0; for await (const chunk of proc.stdout) { const str = new TextDecoder().decode(chunk); if (str.includes(`run ${i}`)) { i++; if (i === 3) break; // Stop after 3 runs await Bun.write(join(String(dir), "script.js"), `console.log("run", ${i});`); } } proc.kill(); await proc.exited; // Check that trace file was created expect(existsSync(traceFile)).toBe(true); const traceContent = readFileSync(traceFile, "utf-8"); const lines = traceContent .trim() .split("\n") .filter(l => l.trim()); // Should have events from watching script.js expect(lines.length).toBeGreaterThan(0); // Validate JSON structure and find script.js events let foundScriptEvent = false; for (const line of lines) { const event = JSON.parse(line); // Check required fields exist expect(event).toHaveProperty("timestamp"); expect(event).toHaveProperty("files"); // Validate types expect(typeof event.timestamp).toBe("number"); expect(typeof event.files).toBe("object"); // Check for script.js events for (const [path, fileEvent] of Object.entries(event.files)) { expect(fileEvent).toHaveProperty("events"); expect(Array.isArray(fileEvent.events)).toBe(true); if ( path.includes("script.js") || (Array.isArray(fileEvent.changed) && fileEvent.changed.some((f: string) => f?.includes("script.js"))) ) { foundScriptEvent = true; // Should have write event expect(fileEvent.events).toContain("write"); } } } expect(foundScriptEvent).toBe(true); }, 10000); test("BUN_WATCHER_TRACE with empty path does not create trace", async () => { using dir = tempDir("watcher-trace-empty", { "test.js": `console.log("ready");`, }); const env = { ...bunEnv, BUN_WATCHER_TRACE: "" }; const proc = Bun.spawn({ cmd: [bunExe(), "--watch", "test.js"], env, cwd: String(dir), stdout: "pipe", stderr: "inherit", stdin: "ignore", }); // Wait for first run, then exit for await (const chunk of proc.stdout) { const str = new TextDecoder().decode(chunk); if (str.includes("ready")) { break; } } proc.kill(); await proc.exited; // Should not create any trace file in the directory const files = Array.from(new Bun.Glob("*.log").scanSync({ cwd: String(dir) })); expect(files.length).toBe(0); }); test("BUN_WATCHER_TRACE appends across reloads", async () => { using dir = tempDir("watcher-trace-append", { "app.js": `console.log("first-0");`, }); const traceFile = join(String(dir), "append-trace.log"); const env = { ...bunEnv, BUN_WATCHER_TRACE: traceFile }; // First run const proc1 = Bun.spawn({ cmd: [bunExe(), "--watch", "app.js"], env, cwd: String(dir), stdout: "pipe", stderr: "inherit", stdin: "ignore", }); let i = 0; for await (const chunk of proc1.stdout) { const str = new TextDecoder().decode(chunk); if (str.includes(`first-${i}`)) { i++; if (i === 2) break; // Stop after 2 runs await Bun.write(join(String(dir), "app.js"), `console.log("first-${i}");`); } } proc1.kill(); await proc1.exited; const firstContent = readFileSync(traceFile, "utf-8"); const firstLines = firstContent .trim() .split("\n") .filter(l => l.trim()); expect(firstLines.length).toBeGreaterThan(0); // Second run - should append to the same file const proc2 = Bun.spawn({ cmd: [bunExe(), "--watch", "app.js"], env, cwd: String(dir), stdout: "pipe", stderr: "inherit", stdin: "ignore", }); let j = 0; for await (const chunk of proc2.stdout) { const str = new TextDecoder().decode(chunk); if (str.includes(`second-${j}`)) { j++; if (j === 2) break; // Stop after 2 runs await Bun.write(join(String(dir), "app.js"), `console.log("second-${j}");`); } else if (str.includes("first-1")) { // Second process starts with previous file content ("first-1"), trigger first modification await Bun.write(join(String(dir), "app.js"), `console.log("second-0");`); } } proc2.kill(); await proc2.exited; const secondContent = readFileSync(traceFile, "utf-8"); const secondLines = secondContent .trim() .split("\n") .filter(l => l.trim()); // Should have more lines after second run expect(secondLines.length).toBeGreaterThan(firstLines.length); // All lines should be valid JSON for (const line of secondLines) { const event = JSON.parse(line); expect(event).toHaveProperty("timestamp"); expect(event).toHaveProperty("files"); } }, 10000);