Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402import { describe, expect } from "bun:test"import fs from "fs/promises"import { randomUUID } from "node:crypto"import { realpathSync } from "node:fs"import { tmpdir } from "node:os"import path from "node:path"import { Effect, Exit, Fiber, Stream } from "effect"import { ChildProcess } from "effect/unstable/process"import { LayerNode } from "@opencode/util/effect/layer-node"import { AppProcess } from "@opencode/util/process"import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(AppProcess.node))
const NODE = process.execPathconst MISSING_CWD = path.join(tmpdir(), `opencode-missing-cwd-${randomUUID()}`)const cmd = (...args: string[]) => ChildProcess.make(NODE, args)
const waitForFile = (file: string) => Effect.promise(async () => { while (true) { try { const contents = await fs.readFile(file, "utf8") if (contents) return contents } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error } await new Promise<void>((resolve) => setTimeout(resolve, 10)) } })
describe("AppProcess", () => { describe("run", () => { it.effect( "captures stdout and exit code zero", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.stdout.write('hi\\n')")) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("hi\n") expect(result.stdoutTruncated).toBe(false) expect(result.stderrTruncated).toBe(false) }), )
it.live( "maps command setup failures to AppProcessError", Effect.gen(function* () { const svc = yield* AppProcess.Service const error = yield* svc.run(ChildProcess.make(NODE, [], { cwd: MISSING_CWD })).pipe(Effect.flip)
expect(error).toBeInstanceOf(AppProcess.AppProcessError) expect(error.command).toBe(NODE) }), )
it.effect( "captures stdout and stderr in emission order", Effect.gen(function* () { const svc = yield* AppProcess.Service const script = [ 'process.stdout.write("out 1\\n")', 'setTimeout(() => process.stderr.write("err 1\\n"), 10)', 'setTimeout(() => process.stdout.write("out 2\\n"), 20)', ].join(";") const result = yield* svc.run(cmd("-e", script), { combineOutput: true }) expect(result.output?.toString("utf8")).toBe("out 1\nerr 1\nout 2\n") expect(result.stdout.toString("utf8")).toBe("") expect(result.stderr.toString("utf8")).toBe("") }), )
it.effect( "non-zero exit returns RunResult; caller can require success", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.exit(1)")) expect(result.exitCode).toBe(1) }), )
it.effect( "requireSuccess fails on non-zero exit", Effect.gen(function* () { const svc = yield* AppProcess.Service const error = yield* svc .run(cmd("-e", "process.exit(1)")) .pipe(Effect.flatMap(AppProcess.requireSuccess), Effect.flip) expect(error).toBeInstanceOf(AppProcess.AppProcessError) expect(error.exitCode).toBe(1) expect(error.message).toContain("Command failed (exit 1)") }), )
it.effect( "requireSuccess succeeds on exit 0", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(AppProcess.requireSuccess)) expect(result.exitCode).toBe(0) }), )
it.effect( "requireExitIn allowlists multiple exit codes", Effect.gen(function* () { const svc = yield* AppProcess.Service const requireZeroOrOne = AppProcess.requireExitIn([0, 1]) const okZero = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(requireZeroOrOne)) expect(okZero.exitCode).toBe(0) const okOne = yield* svc.run(cmd("-e", "process.exit(1)")).pipe(Effect.flatMap(requireZeroOrOne)) expect(okOne.exitCode).toBe(1) const error = yield* svc.run(cmd("-e", "process.exit(2)")).pipe(Effect.flatMap(requireZeroOrOne), Effect.flip) expect(error).toBeInstanceOf(AppProcess.AppProcessError) expect(error.exitCode).toBe(2) }), )
it.effect( "truncates stdout when maxOutputBytes is set", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.stdout.write('0123456789')"), { maxOutputBytes: 5 }) expect(result.exitCode).toBe(0) expect(result.stdoutTruncated).toBe(true) expect(result.stderrTruncated).toBe(false) expect(result.stdout.length).toBe(5) expect(result.stdout.toString("utf8")).toBe("01234") }), )
it.effect( "truncates stderr when maxErrorBytes is set", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.stderr.write('0123456789')"), { maxErrorBytes: 5 }) expect(result.exitCode).toBe(0) expect(result.stdoutTruncated).toBe(false) expect(result.stderrTruncated).toBe(true) expect(result.stderr.length).toBe(5) expect(result.stderr.toString("utf8")).toBe("01234") }), )
it.effect( "result includes command description", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", "process.stdout.write('hi')")) expect(result.command).toBe(`${NODE} -e process.stdout.write('hi')`) }), )
if (process.platform !== "win32") { it.live( "timeout cleans up the scoped child process", Effect.acquireUseRelease( Effect.promise(() => fs.mkdtemp(path.join(tmpdir(), "opencode-process-timeout-"))), (directory) => { const ready = path.join(directory, "ready") const settled = path.join(directory, "settled") const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)` return Effect.gen(function* () { const svc = yield* AppProcess.Service const exit = yield* Effect.exit(svc.run(cmd("-e", script), { timeout: "250 millis" })) expect(Exit.isFailure(exit)).toBe(true) expect(yield* waitForFile(ready)).toMatch(/^\d+$/) expect(yield* waitForFile(settled)).toBe("settled") }) }, (directory) => Effect.promise(() => fs.rm(directory, { recursive: true, force: true })), ), 5_000, )
it.live( "fiber interruption cleans up the scoped child process after readiness", Effect.acquireUseRelease( Effect.promise(() => fs.mkdtemp(path.join(tmpdir(), "opencode-process-interrupt-"))), (directory) => { const ready = path.join(directory, "ready") const settled = path.join(directory, "settled") const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)` return Effect.gen(function* () { const svc = yield* AppProcess.Service const fiber = yield* svc.run(cmd("-e", script)).pipe(Effect.forkChild) expect(yield* waitForFile(ready)).toMatch(/^\d+$/) yield* Fiber.interrupt(fiber) expect(yield* waitForFile(settled)).toBe("settled") }) }, (directory) => Effect.promise(() => fs.rm(directory, { recursive: true, force: true })), ), 5_000, ) } })
describe("inherited platform methods", () => { it.effect( "string returns stdout as string", Effect.gen(function* () { const svc = yield* AppProcess.Service const out = yield* svc.string(cmd("-e", "process.stdout.write('hi\\n')")) expect(out).toBe("hi\n") }), )
it.effect( "lines returns the platform's array of lines", Effect.gen(function* () { const svc = yield* AppProcess.Service const out = yield* svc.lines(cmd("-e", "process.stdout.write('a\\nb\\n')")) expect(Array.from(out)).toEqual(["a", "b"]) }), ) })
describe("run with stdin option", () => { const echoStdin = "process.stdin.on('data', c => process.stdout.write(c))"
it.effect( "feeds a string to stdin and returns it on stdout", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", echoStdin), { stdin: "hello" }) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("hello") }), )
it.effect( "feeds a Uint8Array to stdin", Effect.gen(function* () { const svc = yield* AppProcess.Service const bytes = new TextEncoder().encode("bytes") const result = yield* svc.run(cmd("-e", echoStdin), { stdin: bytes }) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("bytes") }), )
it.effect( "feeds a Stream of Uint8Array chunks to stdin", Effect.gen(function* () { const svc = yield* AppProcess.Service const enc = new TextEncoder() const stream = Stream.fromIterable([enc.encode("one"), enc.encode("-two"), enc.encode("-three")]) const result = yield* svc.run(cmd("-e", echoStdin), { stdin: stream }) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("one-two-three") }), )
it.effect( "completes correctly with empty input", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.run(cmd("-e", echoStdin), { stdin: "" }) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("") }), )
it.effect( "carries existing Command options like env", Effect.gen(function* () { const svc = yield* AppProcess.Service const script = "process.stdout.write(process.env.FEED + ':'); process.stdin.on('data', c => process.stdout.write(c))" const command = ChildProcess.make(NODE, ["-e", script], { env: { FEED: "envset" }, extendEnv: true }) const result = yield* svc.run(command, { stdin: "payload" }) expect(result.exitCode).toBe(0) expect(result.stdout.toString("utf8")).toBe("envset:payload") }), )
it.effect( "carries existing Command options like cwd", Effect.gen(function* () { const svc = yield* AppProcess.Service const dir = realpathSync(tmpdir()) const script = "process.stdout.write(process.cwd() + '|'); process.stdin.on('data', c => process.stdout.write(c))" const command = ChildProcess.make(NODE, ["-e", script], { cwd: dir }) const result = yield* svc.run(command, { stdin: "ok" }) expect(result.exitCode).toBe(0) const [cwd, stdin] = result.stdout.toString("utf8").split("|") expect(realpathSync(cwd)).toBe(dir) expect(stdin).toBe("ok") }), ) })
describe("runStream", () => { it.live( "maps streaming command setup failures to AppProcessError", Effect.gen(function* () { const svc = yield* AppProcess.Service const error = yield* svc .runStream(ChildProcess.make(NODE, [], { cwd: MISSING_CWD })) .pipe(Stream.runCollect, Effect.flip)
expect(error).toBeInstanceOf(AppProcess.AppProcessError) expect(error.command).toBe(NODE) }), )
it.live( "emits lines incrementally and ends cleanly on exit 0", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc .runStream(cmd("-e", "console.log('one'); console.log('two'); console.log('three')")) .pipe(Stream.runCollect) expect(Array.from(result)).toEqual(["one", "two", "three"]) }), )
it.live( "okExitCodes determines whether a non-zero exit fails the stream", Effect.gen(function* () { const svc = yield* AppProcess.Service const allowed = yield* svc .runStream(cmd("-e", "console.log('only'); process.exit(1)"), { okExitCodes: [0, 1] }) .pipe(Stream.runCollect) expect(Array.from(allowed)).toEqual(["only"]) const error = yield* svc .runStream(cmd("-e", "console.log('a'); process.exit(2)"), { okExitCodes: [0, 1] }) .pipe(Stream.runCollect, Effect.flip) expect(error).toBeInstanceOf(AppProcess.AppProcessError) }), )
it.live( "includes stderr in output while retaining capped failure diagnostics", Effect.gen(function* () { const svc = yield* AppProcess.Service const lines: string[] = [] const exit = yield* Effect.exit( svc .runStream( cmd( "-e", "console.log('stdout-line'); console.error('stderr-line'); console.error('diagnostic-tail'); process.exit(2)", ), { includeStderr: true, maxErrorBytes: 20, okExitCodes: [0] }, ) .pipe(Stream.runForEach((line) => Effect.sync(() => lines.push(line)))), )
expect(lines.toSorted()).toEqual(["diagnostic-tail", "stderr-line", "stdout-line"]) expect(Exit.isFailure(exit)).toBe(true) if (!Exit.isFailure(exit)) return const reason = exit.cause.reasons[0] expect(reason?._tag).toBe("Fail") if (!reason || reason._tag !== "Fail") return expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError) expect(reason.error.stderr).toBe("stderr-line\ndiagnost") }), )
it.live( "without okExitCodes, never fails on exit code", Effect.gen(function* () { const svc = yield* AppProcess.Service const result = yield* svc.runStream(cmd("-e", "console.log('only'); process.exit(7)")).pipe(Stream.runCollect) expect(Array.from(result)).toEqual(["only"]) }), )
it.live( "AbortSignal interrupts the stream", Effect.gen(function* () { const svc = yield* AppProcess.Service const controller = new AbortController() controller.abort() const exit = yield* Effect.exit( svc .runStream(cmd("-e", "setInterval(() => {}, 60_000)"), { signal: controller.signal }) .pipe(Stream.runCollect), ) expect(Exit.isFailure(exit)).toBe(true) }), ) })
describe("spawn (inherited)", () => { it.live( "returns the platform ChildProcessHandle for advanced use", Effect.scoped( Effect.gen(function* () { const svc = yield* AppProcess.Service const handle = yield* svc.spawn(cmd("-e", "setInterval(() => {}, 1_000)")) expect(yield* handle.isRunning).toBe(true) yield* handle.kill() }), ), ) })})