Buckets:
| import { describe, expect } from "bun:test" | |
| import fs from "node:fs/promises" | |
| import os from "node:os" | |
| import path from "node:path" | |
| import { Effect, Exit, Stream } from "effect" | |
| import type * as PlatformError from "effect/PlatformError" | |
| import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" | |
| import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" | |
| import { testEffect } from "../lib/effect" | |
| const live = CrossSpawnSpawner.defaultLayer | |
| const fx = testEffect(live) | |
| function js(code: string, opts?: ChildProcess.CommandOptions) { | |
| return ChildProcess.make("node", ["-e", code], opts) | |
| } | |
| function decodeByteStream(stream: Stream.Stream<Uint8Array, PlatformError.PlatformError>) { | |
| return Stream.runCollect(stream).pipe( | |
| Effect.map((chunks) => { | |
| const total = chunks.reduce((acc, x) => acc + x.length, 0) | |
| const out = new Uint8Array(total) | |
| let off = 0 | |
| for (const chunk of chunks) { | |
| out.set(chunk, off) | |
| off += chunk.length | |
| } | |
| return new TextDecoder("utf-8").decode(out).trim() | |
| }), | |
| ) | |
| } | |
| function alive(pid: number) { | |
| try { | |
| process.kill(pid, 0) | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |
| async function tmpdir() { | |
| const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-test-")) | |
| return { | |
| path: dir, | |
| async [Symbol.asyncDispose]() { | |
| await fs.rm(dir, { recursive: true, force: true }) | |
| }, | |
| } | |
| } | |
| async function gone(pid: number, timeout = 5_000) { | |
| const end = Date.now() + timeout | |
| while (Date.now() < end) { | |
| if (!alive(pid)) return true | |
| await new Promise((resolve) => setTimeout(resolve, 50)) | |
| } | |
| return !alive(pid) | |
| } | |
| describe("cross-spawn spawner", () => { | |
| describe("basic spawning", () => { | |
| fx.effect( | |
| "captures stdout", | |
| Effect.gen(function* () { | |
| const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => | |
| svc.string(ChildProcess.make(process.execPath, ["-e", 'process.stdout.write("ok")'])), | |
| ) | |
| expect(out).toBe("ok") | |
| }), | |
| ) | |
| fx.effect( | |
| "captures multiple lines", | |
| Effect.gen(function* () { | |
| const handle = yield* js('console.log("line1"); console.log("line2"); console.log("line3")') | |
| const out = yield* decodeByteStream(handle.stdout) | |
| expect(out).toBe("line1\nline2\nline3") | |
| }), | |
| ) | |
| fx.effect( | |
| "returns exit code", | |
| Effect.gen(function* () { | |
| const handle = yield* js("process.exit(0)") | |
| const code = yield* handle.exitCode | |
| expect(code).toBe(ChildProcessSpawner.ExitCode(0)) | |
| }), | |
| ) | |
| fx.effect( | |
| "returns non-zero exit code", | |
| Effect.gen(function* () { | |
| const handle = yield* js("process.exit(42)") | |
| const code = yield* handle.exitCode | |
| expect(code).toBe(ChildProcessSpawner.ExitCode(42)) | |
| }), | |
| ) | |
| }) | |
| describe("cwd option", () => { | |
| fx.effect( | |
| "uses cwd when spawning commands", | |
| Effect.gen(function* () { | |
| const tmp = yield* Effect.acquireRelease( | |
| Effect.promise(() => tmpdir()), | |
| (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), | |
| ) | |
| const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => | |
| svc.string( | |
| ChildProcess.make(process.execPath, ["-e", "process.stdout.write(process.cwd())"], { cwd: tmp.path }), | |
| ), | |
| ) | |
| expect(yield* Effect.promise(() => fs.realpath(out))).toBe(yield* Effect.promise(() => fs.realpath(tmp.path))) | |
| }), | |
| ) | |
| fx.effect( | |
| "fails for invalid cwd", | |
| Effect.gen(function* () { | |
| const exit = yield* Effect.exit( | |
| ChildProcessSpawner.ChildProcessSpawner.use((svc) => | |
| svc.spawn(ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" })), | |
| ), | |
| ) | |
| expect(Exit.isFailure(exit)).toBe(true) | |
| }), | |
| ) | |
| }) | |
| describe("env option", () => { | |
| fx.effect( | |
| "passes environment variables with extendEnv", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stdout.write(process.env.TEST_VAR ?? "")', { | |
| env: { TEST_VAR: "test_value" }, | |
| extendEnv: true, | |
| }) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| expect(out).toBe("test_value") | |
| }), | |
| ) | |
| fx.effect( | |
| "passes multiple environment variables", | |
| Effect.gen(function* () { | |
| const handle = yield* js( | |
| "process.stdout.write(`${process.env.VAR1}-${process.env.VAR2}-${process.env.VAR3}`)", | |
| { | |
| env: { VAR1: "one", VAR2: "two", VAR3: "three" }, | |
| extendEnv: true, | |
| }, | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| expect(out).toBe("one-two-three") | |
| }), | |
| ) | |
| }) | |
| describe("stderr", () => { | |
| fx.effect( | |
| "captures stderr output", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stderr.write("error message")') | |
| const err = yield* decodeByteStream(handle.stderr) | |
| expect(err).toBe("error message") | |
| }), | |
| ) | |
| fx.effect( | |
| "captures both stdout and stderr", | |
| Effect.gen(function* () { | |
| const handle = yield* js( | |
| [ | |
| "let pending = 2", | |
| "const done = () => {", | |
| " pending -= 1", | |
| " if (pending === 0) setTimeout(() => process.exit(0), 0)", | |
| "}", | |
| 'process.stdout.write("stdout\\n", done)', | |
| 'process.stderr.write("stderr\\n", done)', | |
| ].join("\n"), | |
| ) | |
| const [stdout, stderr] = yield* Effect.all([decodeByteStream(handle.stdout), decodeByteStream(handle.stderr)], { | |
| concurrency: 2, | |
| }) | |
| expect(stdout).toBe("stdout") | |
| expect(stderr).toBe("stderr") | |
| }), | |
| ) | |
| }) | |
| describe("combined output (all)", () => { | |
| fx.effect( | |
| "captures stdout via .all when no stderr", | |
| Effect.gen(function* () { | |
| const handle = yield* ChildProcess.make("echo", ["hello from stdout"]) | |
| const all = yield* decodeByteStream(handle.all) | |
| expect(all).toBe("hello from stdout") | |
| }), | |
| ) | |
| fx.effect( | |
| "captures stderr via .all when no stdout", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stderr.write("hello from stderr")') | |
| const all = yield* decodeByteStream(handle.all) | |
| expect(all).toBe("hello from stderr") | |
| }), | |
| ) | |
| }) | |
| describe("stdin", () => { | |
| fx.effect( | |
| "allows providing standard input to a command", | |
| Effect.gen(function* () { | |
| const input = "a b c" | |
| const stdin = Stream.make(Buffer.from(input, "utf-8")) | |
| const handle = yield* js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))', | |
| { stdin }, | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| yield* handle.exitCode | |
| expect(out).toBe("a b c") | |
| }), | |
| ) | |
| }) | |
| describe("process control", () => { | |
| fx.effect( | |
| "kills a running process", | |
| Effect.gen(function* () { | |
| const exit = yield* Effect.exit( | |
| Effect.gen(function* () { | |
| const handle = yield* js("setTimeout(() => {}, 10_000)") | |
| yield* handle.kill() | |
| return yield* handle.exitCode | |
| }), | |
| ) | |
| expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true) | |
| }), | |
| ) | |
| fx.effect( | |
| "kills a child when scope exits", | |
| Effect.gen(function* () { | |
| const pid = yield* Effect.scoped( | |
| Effect.gen(function* () { | |
| const handle = yield* js("setInterval(() => {}, 10_000)") | |
| return Number(handle.pid) | |
| }), | |
| ) | |
| const done = yield* Effect.promise(() => gone(pid)) | |
| expect(done).toBe(true) | |
| }), | |
| ) | |
| fx.effect( | |
| "forceKillAfter escalates for stubborn processes", | |
| Effect.gen(function* () { | |
| if (process.platform === "win32") return | |
| const started = Date.now() | |
| const exit = yield* Effect.exit( | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.on("SIGTERM", () => {}); setInterval(() => {}, 10_000)') | |
| yield* handle.kill({ forceKillAfter: 100 }) | |
| return yield* handle.exitCode | |
| }), | |
| ) | |
| expect(Date.now() - started).toBeLessThan(1_000) | |
| expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true) | |
| }), | |
| ) | |
| fx.effect( | |
| "isRunning reflects process state", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stdout.write("done")') | |
| yield* handle.exitCode | |
| const running = yield* handle.isRunning | |
| expect(running).toBe(false) | |
| }), | |
| ) | |
| }) | |
| describe("error handling", () => { | |
| fx.effect( | |
| "fails for invalid command", | |
| Effect.gen(function* () { | |
| const exit = yield* Effect.exit( | |
| Effect.gen(function* () { | |
| const handle = yield* ChildProcess.make("nonexistent-command-12345") | |
| return yield* handle.exitCode | |
| }), | |
| ) | |
| expect(Exit.isFailure(exit) ? true : exit.value !== ChildProcessSpawner.ExitCode(0)).toBe(true) | |
| }), | |
| ) | |
| }) | |
| describe("pipeline", () => { | |
| fx.effect( | |
| "pipes stdout of one command to stdin of another", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stdout.write("hello world")').pipe( | |
| ChildProcess.pipeTo( | |
| js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.toUpperCase()))', | |
| ), | |
| ), | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| yield* handle.exitCode | |
| expect(out).toBe("HELLO WORLD") | |
| }), | |
| ) | |
| fx.effect( | |
| "three-stage pipeline", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stdout.write("hello world")').pipe( | |
| ChildProcess.pipeTo( | |
| js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.toUpperCase()))', | |
| ), | |
| ), | |
| ChildProcess.pipeTo( | |
| js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out.replaceAll(" ", "-")))', | |
| ), | |
| ), | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| yield* handle.exitCode | |
| expect(out).toBe("HELLO-WORLD") | |
| }), | |
| ) | |
| fx.effect( | |
| "pipes stderr with { from: 'stderr' }", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stderr.write("error")').pipe( | |
| ChildProcess.pipeTo( | |
| js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))', | |
| ), | |
| { from: "stderr" }, | |
| ), | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| yield* handle.exitCode | |
| expect(out).toBe("error") | |
| }), | |
| ) | |
| fx.effect( | |
| "pipes combined output with { from: 'all' }", | |
| Effect.gen(function* () { | |
| const handle = yield* js('process.stdout.write("stdout\\n"); process.stderr.write("stderr\\n")').pipe( | |
| ChildProcess.pipeTo( | |
| js( | |
| 'process.stdin.setEncoding("utf8"); let out = ""; process.stdin.on("data", (chunk) => out += chunk); process.stdin.on("end", () => process.stdout.write(out))', | |
| ), | |
| { from: "all" }, | |
| ), | |
| ) | |
| const out = yield* decodeByteStream(handle.stdout) | |
| yield* handle.exitCode | |
| expect(out).toContain("stdout") | |
| expect(out).toContain("stderr") | |
| }), | |
| ) | |
| }) | |
| describe("Windows-specific", () => { | |
| fx.effect( | |
| "uses shell routing on Windows", | |
| Effect.gen(function* () { | |
| if (process.platform !== "win32") return | |
| const out = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => | |
| svc.string( | |
| ChildProcess.make("set", ["OPENCODE_TEST_SHELL"], { | |
| shell: true, | |
| extendEnv: true, | |
| env: { OPENCODE_TEST_SHELL: "ok" }, | |
| }), | |
| ), | |
| ) | |
| expect(out).toContain("OPENCODE_TEST_SHELL=ok") | |
| }), | |
| ) | |
| fx.effect( | |
| "runs cmd scripts with spaces on Windows without shell", | |
| Effect.gen(function* () { | |
| if (process.platform !== "win32") return | |
| const tmp = yield* Effect.acquireRelease( | |
| Effect.promise(() => tmpdir()), | |
| (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), | |
| ) | |
| const dir = path.join(tmp.path, "with space") | |
| const file = path.join(dir, "echo cmd.cmd") | |
| yield* Effect.promise(() => fs.mkdir(dir, { recursive: true })) | |
| yield* Effect.promise(() => fs.writeFile(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) | |
| const code = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => | |
| svc.exitCode( | |
| ChildProcess.make(file, ["--stdio"], { | |
| stdin: "pipe", | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| }), | |
| ), | |
| ) | |
| expect(code).toBe(ChildProcessSpawner.ExitCode(0)) | |
| }), | |
| ) | |
| }) | |
| }) | |
Xet Storage Details
- Size:
- 13.7 kB
- Xet hash:
- e07df930333cf6b2c1a1ccf03226a9bc3af45dbccaa10825def33d03c621a888
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.