Files
bun.sh/test/cli/process-manager.test.ts
Claude Bot 9062e7ad92 Add workspace-local process manager with bun start/stop/list/logs
Implements a lightweight process manager for managing background processes
within a workspace. Each workspace (cwd) gets its own daemon that manages
processes via Unix socket IPC.

Commands:
- bun start [script] - Start a process in the background
- bun stop [name] - Stop a running process
- bun list - List all running processes in this workspace
- bun logs [name] [-f] - View process logs (with optional follow mode)

Implementation details:
- Uses uSockets for efficient Unix socket IPC between client and daemon
- Daemon auto-spawns on first command and runs in the background
- Processes are workspace-isolated based on cwd hash
- Logs are captured to /tmp/bun-logs/{workspace-hash}/
- Simple process supervision with pid tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 21:32:10 +00:00

296 lines
7.6 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
describe("bun process manager", () => {
test("bun start - starts a process", async () => {
using dir = tempDir("process-manager-start", {
"server.js": `
console.log("Server started");
setInterval(() => {}, 1000); // Keep alive
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "start", "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"✓ Started: server.js"`);
expect(exitCode).toBe(0);
// Clean up - stop the process
const stopProc = Bun.spawn({
cmd: [bunExe(), "stop", "server.js"],
env: bunEnv,
cwd: String(dir),
});
await stopProc.exited;
});
test("bun list - lists running processes", async () => {
using dir = tempDir("process-manager-list", {
"worker.js": `
console.log("Worker started");
setInterval(() => {}, 1000);
`,
});
// Start a process first
const startProc = Bun.spawn({
cmd: [bunExe(), "start", "worker.js"],
env: bunEnv,
cwd: String(dir),
});
await startProc.exited;
// Now list processes
await using listProc = Bun.spawn({
cmd: [bunExe(), "list"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
listProc.stdout.text(),
listProc.stderr.text(),
listProc.exited,
]);
// Should show the worker process
expect(stdout).toContain("worker.js");
expect(stdout).toContain("NAME");
expect(stdout).toContain("PID");
expect(exitCode).toBe(0);
// Clean up
const stopProc = Bun.spawn({
cmd: [bunExe(), "stop", "worker.js"],
env: bunEnv,
cwd: String(dir),
});
await stopProc.exited;
});
test("bun stop - stops a running process", async () => {
using dir = tempDir("process-manager-stop", {
"service.js": `
console.log("Service running");
setInterval(() => {}, 1000);
`,
});
// Start a process
const startProc = Bun.spawn({
cmd: [bunExe(), "start", "service.js"],
env: bunEnv,
cwd: String(dir),
});
await startProc.exited;
// Stop the process
await using stopProc = Bun.spawn({
cmd: [bunExe(), "stop", "service.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
stopProc.stdout.text(),
stopProc.stderr.text(),
stopProc.exited,
]);
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"✓ Stopped: service.js"`);
expect(exitCode).toBe(0);
// Verify it's not in the list anymore
const listProc = Bun.spawn({
cmd: [bunExe(), "list"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
});
const listOutput = await listProc.stdout.text();
await listProc.exited;
// Should either show no processes or not include service.js
if (!listOutput.includes("No processes")) {
expect(listOutput).not.toContain("service.js");
}
});
test("bun logs - shows process logs", async () => {
using dir = tempDir("process-manager-logs", {
"logger.js": `
console.log("Log message 1");
console.error("Error message 1");
console.log("Log message 2");
`,
});
// Start and let it finish
const startProc = Bun.spawn({
cmd: [bunExe(), "start", "logger.js"],
env: bunEnv,
cwd: String(dir),
});
await startProc.exited;
// Wait a bit for logs to be written
await Bun.sleep(100);
// Check logs
await using logsProc = Bun.spawn({
cmd: [bunExe(), "logs", "logger.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
logsProc.stdout.text(),
logsProc.stderr.text(),
logsProc.exited,
]);
expect(stdout).toContain("Log message 1");
expect(stdout).toContain("Log message 2");
expect(exitCode).toBe(0);
// Clean up
const stopProc = Bun.spawn({
cmd: [bunExe(), "stop", "logger.js"],
env: bunEnv,
cwd: String(dir),
});
await stopProc.exited;
});
test("bun start - prevents duplicate process names", async () => {
using dir = tempDir("process-manager-duplicate", {
"app.js": `
console.log("App started");
setInterval(() => {}, 1000);
`,
});
// Start first process
const start1 = Bun.spawn({
cmd: [bunExe(), "start", "app.js"],
env: bunEnv,
cwd: String(dir),
});
await start1.exited;
// Try to start again with same name
await using start2 = Bun.spawn({
cmd: [bunExe(), "start", "app.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([start2.stdout.text(), start2.stderr.text(), start2.exited]);
expect(exitCode).not.toBe(0);
expect(stderr.toLowerCase()).toMatch(/already|exists/);
// Clean up
const stopProc = Bun.spawn({
cmd: [bunExe(), "stop", "app.js"],
env: bunEnv,
cwd: String(dir),
});
await stopProc.exited;
});
test("bun list - shows empty list when no processes running", async () => {
using dir = tempDir("process-manager-empty");
await using listProc = Bun.spawn({
cmd: [bunExe(), "list"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
listProc.stdout.text(),
listProc.stderr.text(),
listProc.exited,
]);
expect(stdout.toLowerCase()).toMatch(/no processes|not running/);
expect(exitCode).toBe(0);
});
test("workspace isolation - processes in different directories are separate", async () => {
using dir1 = tempDir("process-manager-ws1", {
"proc.js": `setInterval(() => {}, 1000);`,
});
using dir2 = tempDir("process-manager-ws2", {
"proc.js": `setInterval(() => {}, 1000);`,
});
// Start process in dir1
const start1 = Bun.spawn({
cmd: [bunExe(), "start", "proc.js"],
env: bunEnv,
cwd: String(dir1),
});
await start1.exited;
// Start process in dir2
const start2 = Bun.spawn({
cmd: [bunExe(), "start", "proc.js"],
env: bunEnv,
cwd: String(dir2),
});
await start2.exited;
// List in dir1 should only show dir1's process
const list1 = Bun.spawn({
cmd: [bunExe(), "list"],
env: bunEnv,
cwd: String(dir1),
stdout: "pipe",
});
const out1 = await list1.stdout.text();
await list1.exited;
// List in dir2 should only show dir2's process
const list2 = Bun.spawn({
cmd: [bunExe(), "list"],
env: bunEnv,
cwd: String(dir2),
stdout: "pipe",
});
const out2 = await list2.stdout.text();
await list2.exited;
// Both should show exactly one process
const count1 = (out1.match(/proc\.js/g) || []).length;
const count2 = (out2.match(/proc\.js/g) || []).length;
expect(count1).toBe(1);
expect(count2).toBe(1);
// Clean up
Bun.spawn({ cmd: [bunExe(), "stop", "proc.js"], env: bunEnv, cwd: String(dir1) });
Bun.spawn({ cmd: [bunExe(), "stop", "proc.js"], env: bunEnv, cwd: String(dir2) });
});
});