test(cli): opt-in pre-built binary for ~3x faster subprocess spawns
\`bun run --conditions=browser src/index.ts\` pays ~15s of JIT + plugin init + DB migration per subprocess spawn in isolation mode. A pre-built binary cuts that to ~5s — most of which is now the SQLite \`:memory:\` migration that runs regardless of execution mode. Adds \`script/prebuild-test-cli.ts\` which wraps the existing build.ts with \`--single --skip-embed-web-ui --skip-install\`, then symlinks the platform-specific output to \`dist/test-cli/bin/opencode\` so the harness has a stable path. The harness (test/lib/cli-process.ts) reads OPENCODE_TEST_CLI_PATH and spawns the binary directly when set; falls back to dev mode otherwise. Strictly opt-in — default behavior, CI, and local iteration are unchanged. Anyone who wants the speedup runs: bun script/prebuild-test-cli.ts export OPENCODE_TEST_CLI_PATH="\$PWD/dist/test-cli/bin/opencode" bun test test/cli/ Measured locally: Dev mode (default): 29.9s (331 tests) Binary mode: 22.1s (-26%, after one-time 2.8s build) The win compounds as more subprocess tests are added — every new test that hits DB migration saves ~10s vs dev mode.
This commit is contained in:
parent
7051796c38
commit
5bc6a7f6d0
2 changed files with 69 additions and 3 deletions
53
packages/opencode/script/prebuild-test-cli.ts
Normal file
53
packages/opencode/script/prebuild-test-cli.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
// Build a pre-compiled `opencode` binary for subprocess tests, then expose
|
||||||
|
// it at `dist/test-cli/bin/opencode` for the harness to consume.
|
||||||
|
//
|
||||||
|
// Why: each `bun run --conditions=browser src/index.ts <cmd>` spawn pays
|
||||||
|
// ~15s of JIT + plugin init + DB migration in isolation mode. The
|
||||||
|
// pre-compiled binary cuts that to ~5s — a 3x improvement on subprocess
|
||||||
|
// tests that touch the DB (mcp, providers list, etc.).
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// bun script/prebuild-test-cli.ts
|
||||||
|
// export OPENCODE_TEST_CLI_PATH="$PWD/dist/test-cli/bin/opencode"
|
||||||
|
// bun test test/cli/
|
||||||
|
//
|
||||||
|
// The harness (see test/lib/cli-process.ts) reads OPENCODE_TEST_CLI_PATH; if
|
||||||
|
// set, it spawns the binary directly instead of `bun run src/index.ts`. If
|
||||||
|
// unset, it falls back to dev mode — so this script is strictly opt-in.
|
||||||
|
//
|
||||||
|
// Build cost amortizes after ~1 spawn that touches the DB. Recommended for
|
||||||
|
// CI, manual `bun test test/cli/` runs, and any local iteration where the
|
||||||
|
// CLI surface itself isn't under change. Skip for normal src/* editing — the
|
||||||
|
// dev path picks up source changes without rebuild.
|
||||||
|
import { $ } from "bun"
|
||||||
|
import fs from "node:fs/promises"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
|
process.chdir(dir)
|
||||||
|
|
||||||
|
const platform = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux"
|
||||||
|
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x64"
|
||||||
|
const targetDir = path.join(dir, "dist", `opencode-${platform}-${arch}`)
|
||||||
|
const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode"
|
||||||
|
const builtBinary = path.join(targetDir, "bin", binaryName)
|
||||||
|
|
||||||
|
// Stable path the harness reads via OPENCODE_TEST_CLI_PATH. Symlinked so
|
||||||
|
// the binary itself remains the platform-specific one (build.ts manages it).
|
||||||
|
const stableBinary = path.join(dir, "dist", "test-cli", "bin", binaryName)
|
||||||
|
|
||||||
|
console.log(`Building test CLI binary for ${platform}-${arch}...`)
|
||||||
|
const start = Date.now()
|
||||||
|
await $`bun script/build.ts --single --skip-embed-web-ui --skip-install`
|
||||||
|
const buildMs = Date.now() - start
|
||||||
|
console.log(`Build complete in ${buildMs}ms: ${builtBinary}`)
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(stableBinary), { recursive: true })
|
||||||
|
await fs.rm(stableBinary, { force: true })
|
||||||
|
await fs.symlink(builtBinary, stableBinary)
|
||||||
|
console.log(`Symlinked stable path: ${stableBinary}`)
|
||||||
|
console.log(``)
|
||||||
|
console.log(`To use in tests:`)
|
||||||
|
console.log(` export OPENCODE_TEST_CLI_PATH="${stableBinary}"`)
|
||||||
|
console.log(` bun test test/cli/`)
|
||||||
|
|
@ -31,6 +31,19 @@ import { it } from "./effect"
|
||||||
const opencodeRoot = path.resolve(import.meta.dir, "../../")
|
const opencodeRoot = path.resolve(import.meta.dir, "../../")
|
||||||
const cliEntry = path.join(opencodeRoot, "src/index.ts")
|
const cliEntry = path.join(opencodeRoot, "src/index.ts")
|
||||||
|
|
||||||
|
// Opt-in pre-built binary path. If set, subprocess tests spawn the binary
|
||||||
|
// directly instead of `bun run src/index.ts`, skipping JIT + plugin init
|
||||||
|
// (~3x speedup on isolation-env spawns that hit DB migration). Produced by
|
||||||
|
// `bun script/prebuild-test-cli.ts`; the harness silently falls back to dev
|
||||||
|
// mode if the var is unset, so this is strictly opt-in.
|
||||||
|
const prebuiltCli = process.env["OPENCODE_TEST_CLI_PATH"]
|
||||||
|
|
||||||
|
// Argv prefix for spawning the CLI. Either the pre-built binary (single
|
||||||
|
// argument) or `bun run --conditions=browser src/index.ts` (four arguments).
|
||||||
|
function cliArgv(): string[] {
|
||||||
|
return prebuiltCli ? [prebuiltCli] : ["bun", "run", "--conditions=browser", cliEntry]
|
||||||
|
}
|
||||||
|
|
||||||
export const testModelID = "test/test-model"
|
export const testModelID = "test/test-model"
|
||||||
|
|
||||||
// Wrap a Bun subprocess pipe (or any ReadableStream<Uint8Array>) as a Stream.
|
// Wrap a Bun subprocess pipe (or any ReadableStream<Uint8Array>) as a Stream.
|
||||||
|
|
@ -196,7 +209,7 @@ export function withCliFixture<A, E>(
|
||||||
Effect.promise(async () => {
|
Effect.promise(async () => {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
// Process.run pipes stdout/stderr by default and returns them as Buffers.
|
// Process.run pipes stdout/stderr by default and returns them as Buffers.
|
||||||
const result = await Process.run(["bun", "run", "--conditions=browser", cliEntry, ...args], {
|
const result = await Process.run([...cliArgv(), ...args], {
|
||||||
cwd: home,
|
cwd: home,
|
||||||
timeout: opts?.timeoutMs ?? 30_000,
|
timeout: opts?.timeoutMs ?? 30_000,
|
||||||
env: { ...process.env, ...env, ...opts?.env },
|
env: { ...process.env, ...env, ...opts?.env },
|
||||||
|
|
@ -235,7 +248,7 @@ export function withCliFixture<A, E>(
|
||||||
// as a finalizer error during test teardown.
|
// as a finalizer error during test teardown.
|
||||||
const proc = yield* Effect.acquireRelease(
|
const proc = yield* Effect.acquireRelease(
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...argv], {
|
Bun.spawn([...cliArgv(), ...argv], {
|
||||||
cwd: home,
|
cwd: home,
|
||||||
env: { ...process.env, ...env, ...opts?.env },
|
env: { ...process.env, ...env, ...opts?.env },
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
|
|
@ -306,7 +319,7 @@ export function withCliFixture<A, E>(
|
||||||
// Either way we await proc.exited so the test scope doesn't leak.
|
// Either way we await proc.exited so the test scope doesn't leak.
|
||||||
const proc = yield* Effect.acquireRelease(
|
const proc = yield* Effect.acquireRelease(
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
Bun.spawn(["bun", "run", "--conditions=browser", cliEntry, ...argv], {
|
Bun.spawn([...cliArgv(), ...argv], {
|
||||||
cwd: opts?.cwd ?? home,
|
cwd: opts?.cwd ?? home,
|
||||||
env: { ...process.env, ...env, ...opts?.env },
|
env: { ...process.env, ...env, ...opts?.env },
|
||||||
stdin: "pipe",
|
stdin: "pipe",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue