|
| 1 | +#!/usr/bin/env bun |
| 2 | +// Boots a real sandbox end-to-end on Windows: serves a locally built rootfs |
| 3 | +// tarball over loopback HTTP, creates the distribution (download -> verify -> |
| 4 | +// wsl --import), starts the guest server, and waits for the codev server's |
| 5 | +// authenticated health check over WSL2's localhost forwarding. Dev utility. |
| 6 | +// |
| 7 | +// Run from packages/codev-sandbox on the Windows machine: |
| 8 | +// bun script/e2e-wsl.ts --image dist-images\bwrap-x64.tar.gz ^ |
| 9 | +// --bundle ..\opencode\dist\node [--project dir] [--hold] [--keep] |
| 10 | +// |
| 11 | +// --hold keeps the distribution running so you can poke at it; --keep skips the |
| 12 | +// unregister so it survives the run (clean up with `wsl --unregister codev-e2e`). |
| 13 | +import { createHash } from "node:crypto" |
| 14 | +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises" |
| 15 | +import { tmpdir } from "node:os" |
| 16 | +import { basename, join, resolve } from "node:path" |
| 17 | +import { checkSandboxHealth } from "../src/boot" |
| 18 | +import type { SandboxConfig } from "../src/provider" |
| 19 | +import { createWslProvider } from "../src/wsl/provider" |
| 20 | + |
| 21 | +// Both paths cross into the distribution through wslpath, which resolves |
| 22 | +// relative paths against the guest's cwd — so they have to be absolute here. |
| 23 | +const image = resolve(requireArg("--image")) |
| 24 | +const bundle = resolve(requireArg("--bundle")) |
| 25 | +const hold = process.argv.includes("--hold") |
| 26 | +const keep = process.argv.includes("--keep") |
| 27 | + |
| 28 | +const work = await mkdtemp(join(tmpdir(), "codev-wsl-e2e-")) |
| 29 | +const projectDir = argValue("--project") ? resolve(argValue("--project")!) : join(work, "project") |
| 30 | +await mkdir(projectDir, { recursive: true }) |
| 31 | +await writeFile(join(projectDir, "hello.txt"), "hello from the host project\n") |
| 32 | + |
| 33 | +// Digest once, outside resolveImage: a hash object cannot be digested twice, so |
| 34 | +// computing it inside the callback breaks the moment the provider asks again. |
| 35 | +const payload = Bun.file(image) |
| 36 | +const hash = createHash("sha256") |
| 37 | +for await (const chunk of payload.stream()) hash.update(chunk) |
| 38 | +const sha256 = hash.digest("hex") |
| 39 | + |
| 40 | +const server = Bun.serve({ port: 0, fetch: () => new Response(Bun.file(image)) }) |
| 41 | + |
| 42 | +const provider = createWslProvider({ |
| 43 | + dataDir: join(work, "data"), |
| 44 | + cacheDir: join(work, "cache"), |
| 45 | + bundleDir: bundle, |
| 46 | + resolveImage: async () => ({ |
| 47 | + url: `http://127.0.0.1:${server.port}/${basename(image)}`, |
| 48 | + sha256, |
| 49 | + bytes: payload.size, |
| 50 | + }), |
| 51 | + startTimeoutMs: 180_000, |
| 52 | +}) |
| 53 | + |
| 54 | +const availability = await provider.availability() |
| 55 | +console.log("availability:", availability) |
| 56 | +if (!availability.supported) { |
| 57 | + console.error(`cannot run here: ${availability.message}`) |
| 58 | + await cleanup() |
| 59 | + process.exit(1) |
| 60 | +} |
| 61 | +if (availability.warning) console.log(`warning: ${availability.warning}`) |
| 62 | + |
| 63 | +const config: SandboxConfig = { id: "sandbox:e2e", name: "e2e", backend: "wsl", projectDir } |
| 64 | + |
| 65 | +console.time("create") |
| 66 | +await provider.create(config) |
| 67 | +console.timeEnd("create") |
| 68 | + |
| 69 | +console.time("start") |
| 70 | +const sandbox = await provider.start(config) |
| 71 | +console.timeEnd("start") |
| 72 | +console.log(`server: ${sandbox.url} (user ${sandbox.username})`) |
| 73 | + |
| 74 | +const healthy = await checkSandboxHealth(sandbox.url, { username: sandbox.username ?? "", password: sandbox.password }) |
| 75 | +console.log(`health: ${healthy ? "OK" : "FAILED"}`) |
| 76 | + |
| 77 | +if (hold) { |
| 78 | + console.log("holding the distribution up; Ctrl-C to stop") |
| 79 | + await new Promise(() => {}) |
| 80 | +} |
| 81 | + |
| 82 | +const exited = new Promise<void>((resolve) => sandbox.onExit(() => resolve())) |
| 83 | +sandbox.stop() |
| 84 | +await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 10_000))]) |
| 85 | +if (keep) console.log(`keeping the distribution; remove it with: wsl --unregister codev-${config.name}`) |
| 86 | +if (!keep) await provider.destroy(config) |
| 87 | +await cleanup() |
| 88 | +if (!healthy) process.exit(1) |
| 89 | +console.log("e2e OK") |
| 90 | +process.exit(0) |
| 91 | + |
| 92 | +async function cleanup() { |
| 93 | + server.stop(true) |
| 94 | + await rm(work, { recursive: true, force: true }) |
| 95 | +} |
| 96 | + |
| 97 | +function argValue(flag: string) { |
| 98 | + const index = process.argv.indexOf(flag) |
| 99 | + return index >= 0 ? process.argv[index + 1] : undefined |
| 100 | +} |
| 101 | + |
| 102 | +function requireArg(flag: string) { |
| 103 | + const value = argValue(flag) |
| 104 | + if (!value) { |
| 105 | + console.error(`${flag} is required`) |
| 106 | + process.exit(1) |
| 107 | + } |
| 108 | + return value |
| 109 | +} |
0 commit comments