diff options
Diffstat (limited to 'packages/exec-backend/src/local.test.ts')
| -rw-r--r-- | packages/exec-backend/src/local.test.ts | 372 |
1 files changed, 186 insertions, 186 deletions
diff --git a/packages/exec-backend/src/local.test.ts b/packages/exec-backend/src/local.test.ts index 5357d6f..7bf2ef5 100644 --- a/packages/exec-backend/src/local.test.ts +++ b/packages/exec-backend/src/local.test.ts @@ -10,190 +10,190 @@ import { createLocalExecBackend, localExecBackend } from "./local.js"; * (real fs/spawn). Zero internal mocks; no mocking of @dispatch/*. */ describe("LocalExecBackend", () => { - const backend: ExecBackend = createLocalExecBackend(); - let tmpDir: string; - - beforeEach(async () => { - tmpDir = await mkdtemp(join(tmpdir(), "exec-backend-test-")); - }); - - afterEach(async () => { - await rm(tmpDir, { recursive: true, force: true }); - }); - - describe("spawn", () => { - it("runs a real `sh -c 'echo hi'` and returns exitCode 0 + captured stdout", async () => { - let output = ""; - const result = await backend.spawn({ - command: "echo hi", - cwd: tmpDir, - signal: AbortSignal.timeout(5000), - timeout: 5000, - onOutput: (data) => { - output += data; - }, - }); - expect(result.exitCode).toBe(0); - expect(result.timedOut).toBe(false); - expect(result.aborted).toBe(false); - expect(output).toContain("hi"); - }); - - it("returns a non-zero exit code for a failing command", async () => { - const result = await backend.spawn({ - command: "false", - cwd: tmpDir, - signal: AbortSignal.timeout(5000), - timeout: 5000, - onOutput: () => {}, - }); - expect(result.exitCode).toBe(1); - expect(result.aborted).toBe(false); - expect(result.timedOut).toBe(false); - }); - - it("streams stderr separately from stdout", async () => { - const streams: Array<{ data: string; stream: "stdout" | "stderr" }> = []; - const result = await backend.spawn({ - command: "echo out; echo err 1>&2", - cwd: tmpDir, - signal: AbortSignal.timeout(5000), - timeout: 5000, - onOutput: (data, stream) => streams.push({ data, stream }), - }); - expect(result.exitCode).toBe(0); - expect(streams.some((s) => s.stream === "stdout" && s.data.includes("out"))).toBe(true); - expect(streams.some((s) => s.stream === "stderr" && s.data.includes("err"))).toBe(true); - }); - - it("resolves with aborted: true when the signal fires", async () => { - const controller = new AbortController(); - const promise = backend.spawn({ - command: "sleep 30", - cwd: tmpDir, - signal: controller.signal, - timeout: 60_000, - onOutput: () => {}, - }); - // Let the sleep actually start. - await new Promise((r) => setTimeout(r, 300)); - controller.abort(); - const result = await promise; - expect(result.aborted).toBe(true); - expect(result.timedOut).toBe(false); - }); - - it("resolves with timedOut: true when the timeout elapses", async () => { - const start = Date.now(); - const result = await backend.spawn({ - command: "sleep 30", - cwd: tmpDir, - signal: AbortSignal.timeout(60_000), - timeout: 300, - onOutput: () => {}, - }); - const elapsed = Date.now() - start; - expect(result.timedOut).toBe(true); - expect(result.aborted).toBe(false); - // Should resolve shortly after the 300ms timeout, well under 30s. - expect(elapsed).toBeLessThan(10_000); - }); - }); - - describe("stat", () => { - it("distinguishes file vs directory", async () => { - await fsWriteFile(join(tmpDir, "file.txt"), "hello"); - await mkdir(join(tmpDir, "subdir")); - - const fileStat = await backend.stat(join(tmpDir, "file.txt")); - expect(fileStat.isFile).toBe(true); - expect(fileStat.isDirectory).toBe(false); - - const dirStat = await backend.stat(join(tmpDir, "subdir")); - expect(dirStat.isFile).toBe(false); - expect(dirStat.isDirectory).toBe(true); - }); - - it("throws ENOENT with .code for a missing path", async () => { - try { - await backend.stat(join(tmpDir, "nope")); - expect.fail("stat should have thrown for a missing path"); - } catch (err: unknown) { - expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); - } - }); - }); - - describe("readFile / writeFile / readdir / exists round-trip", () => { - it("writes then reads a file (utf8 round-trip)", async () => { - const filePath = join(tmpDir, "round.txt"); - await backend.writeFile(filePath, "round-trip content"); - const content = await backend.readFile(filePath); - expect(content).toBe("round-trip content"); - }); - - it("readdir lists entries with correct isDirectory flags", async () => { - await fsWriteFile(join(tmpDir, "a.txt"), "a"); - await mkdir(join(tmpDir, "sub")); - - const entries = await backend.readdir(tmpDir); - const names = entries.map((e) => e.name).sort(); - expect(names).toEqual(["a.txt", "sub"]); - - const sub = entries.find((e) => e.name === "sub"); - expect(sub?.isDirectory).toBe(true); - - const file = entries.find((e) => e.name === "a.txt"); - expect(file?.isDirectory).toBe(false); - }); - - it("exists returns true for an existing file, false for a missing one", async () => { - const filePath = join(tmpDir, "exists.txt"); - await fsWriteFile(filePath, "x"); - expect(await backend.exists(filePath)).toBe(true); - expect(await backend.exists(join(tmpDir, "missing"))).toBe(false); - }); - - it("exists returns true for an existing directory", async () => { - await mkdir(join(tmpDir, "adir")); - expect(await backend.exists(join(tmpDir, "adir"))).toBe(true); - }); - - it("readFile throws ENOENT with .code for a missing file", async () => { - try { - await backend.readFile(join(tmpDir, "missing.txt")); - expect.fail("readFile should have thrown for a missing file"); - } catch (err: unknown) { - expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); - } - }); - - it("readdir throws ENOENT with .code for a missing directory", async () => { - try { - await backend.readdir(join(tmpDir, "missingdir")); - expect.fail("readdir should have thrown for a missing directory"); - } catch (err: unknown) { - expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); - } - }); - - it("writeFile throws an error with .code when the parent dir is missing", async () => { - try { - await backend.writeFile(join(tmpDir, "missing-parent", "child.txt"), "x"); - expect.fail("writeFile should have thrown for a missing parent dir"); - } catch (err: unknown) { - expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); - } - }); - }); - - describe("singleton", () => { - it("localExecBackend singleton satisfies ExecBackend and behaves identically", async () => { - expect(typeof localExecBackend.spawn).toBe("function"); - expect(typeof localExecBackend.readFile).toBe("function"); - const filePath = join(tmpDir, "singleton.txt"); - await localExecBackend.writeFile(filePath, "singleton"); - expect(await localExecBackend.readFile(filePath)).toBe("singleton"); - }); - }); + const backend: ExecBackend = createLocalExecBackend(); + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "exec-backend-test-")); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + describe("spawn", () => { + it("runs a real `sh -c 'echo hi'` and returns exitCode 0 + captured stdout", async () => { + let output = ""; + const result = await backend.spawn({ + command: "echo hi", + cwd: tmpDir, + signal: AbortSignal.timeout(5000), + timeout: 5000, + onOutput: (data) => { + output += data; + }, + }); + expect(result.exitCode).toBe(0); + expect(result.timedOut).toBe(false); + expect(result.aborted).toBe(false); + expect(output).toContain("hi"); + }); + + it("returns a non-zero exit code for a failing command", async () => { + const result = await backend.spawn({ + command: "false", + cwd: tmpDir, + signal: AbortSignal.timeout(5000), + timeout: 5000, + onOutput: () => {}, + }); + expect(result.exitCode).toBe(1); + expect(result.aborted).toBe(false); + expect(result.timedOut).toBe(false); + }); + + it("streams stderr separately from stdout", async () => { + const streams: Array<{ data: string; stream: "stdout" | "stderr" }> = []; + const result = await backend.spawn({ + command: "echo out; echo err 1>&2", + cwd: tmpDir, + signal: AbortSignal.timeout(5000), + timeout: 5000, + onOutput: (data, stream) => streams.push({ data, stream }), + }); + expect(result.exitCode).toBe(0); + expect(streams.some((s) => s.stream === "stdout" && s.data.includes("out"))).toBe(true); + expect(streams.some((s) => s.stream === "stderr" && s.data.includes("err"))).toBe(true); + }); + + it("resolves with aborted: true when the signal fires", async () => { + const controller = new AbortController(); + const promise = backend.spawn({ + command: "sleep 30", + cwd: tmpDir, + signal: controller.signal, + timeout: 60_000, + onOutput: () => {}, + }); + // Let the sleep actually start. + await new Promise((r) => setTimeout(r, 300)); + controller.abort(); + const result = await promise; + expect(result.aborted).toBe(true); + expect(result.timedOut).toBe(false); + }); + + it("resolves with timedOut: true when the timeout elapses", async () => { + const start = Date.now(); + const result = await backend.spawn({ + command: "sleep 30", + cwd: tmpDir, + signal: AbortSignal.timeout(60_000), + timeout: 300, + onOutput: () => {}, + }); + const elapsed = Date.now() - start; + expect(result.timedOut).toBe(true); + expect(result.aborted).toBe(false); + // Should resolve shortly after the 300ms timeout, well under 30s. + expect(elapsed).toBeLessThan(10_000); + }); + }); + + describe("stat", () => { + it("distinguishes file vs directory", async () => { + await fsWriteFile(join(tmpDir, "file.txt"), "hello"); + await mkdir(join(tmpDir, "subdir")); + + const fileStat = await backend.stat(join(tmpDir, "file.txt")); + expect(fileStat.isFile).toBe(true); + expect(fileStat.isDirectory).toBe(false); + + const dirStat = await backend.stat(join(tmpDir, "subdir")); + expect(dirStat.isFile).toBe(false); + expect(dirStat.isDirectory).toBe(true); + }); + + it("throws ENOENT with .code for a missing path", async () => { + try { + await backend.stat(join(tmpDir, "nope")); + expect.fail("stat should have thrown for a missing path"); + } catch (err: unknown) { + expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); + } + }); + }); + + describe("readFile / writeFile / readdir / exists round-trip", () => { + it("writes then reads a file (utf8 round-trip)", async () => { + const filePath = join(tmpDir, "round.txt"); + await backend.writeFile(filePath, "round-trip content"); + const content = await backend.readFile(filePath); + expect(content).toBe("round-trip content"); + }); + + it("readdir lists entries with correct isDirectory flags", async () => { + await fsWriteFile(join(tmpDir, "a.txt"), "a"); + await mkdir(join(tmpDir, "sub")); + + const entries = await backend.readdir(tmpDir); + const names = entries.map((e) => e.name).sort(); + expect(names).toEqual(["a.txt", "sub"]); + + const sub = entries.find((e) => e.name === "sub"); + expect(sub?.isDirectory).toBe(true); + + const file = entries.find((e) => e.name === "a.txt"); + expect(file?.isDirectory).toBe(false); + }); + + it("exists returns true for an existing file, false for a missing one", async () => { + const filePath = join(tmpDir, "exists.txt"); + await fsWriteFile(filePath, "x"); + expect(await backend.exists(filePath)).toBe(true); + expect(await backend.exists(join(tmpDir, "missing"))).toBe(false); + }); + + it("exists returns true for an existing directory", async () => { + await mkdir(join(tmpDir, "adir")); + expect(await backend.exists(join(tmpDir, "adir"))).toBe(true); + }); + + it("readFile throws ENOENT with .code for a missing file", async () => { + try { + await backend.readFile(join(tmpDir, "missing.txt")); + expect.fail("readFile should have thrown for a missing file"); + } catch (err: unknown) { + expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); + } + }); + + it("readdir throws ENOENT with .code for a missing directory", async () => { + try { + await backend.readdir(join(tmpDir, "missingdir")); + expect.fail("readdir should have thrown for a missing directory"); + } catch (err: unknown) { + expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); + } + }); + + it("writeFile throws an error with .code when the parent dir is missing", async () => { + try { + await backend.writeFile(join(tmpDir, "missing-parent", "child.txt"), "x"); + expect.fail("writeFile should have thrown for a missing parent dir"); + } catch (err: unknown) { + expect((err as NodeJS.ErrnoException).code).toBe("ENOENT"); + } + }); + }); + + describe("singleton", () => { + it("localExecBackend singleton satisfies ExecBackend and behaves identically", async () => { + expect(typeof localExecBackend.spawn).toBe("function"); + expect(typeof localExecBackend.readFile).toBe("function"); + const filePath = join(tmpDir, "singleton.txt"); + await localExecBackend.writeFile(filePath, "singleton"); + expect(await localExecBackend.readFile(filePath)).toBe("singleton"); + }); + }); }); |
