Skip to content

Commit 779db6f

Browse files
quickbeardclaude
andcommitted
feat(sandbox): add the WSL end-to-end driver
Mirrors e2e-vz.ts for Windows: serves a locally built rootfs tarball over loopback HTTP, imports the distribution, boots the guest server, and waits for the authenticated health check over WSL2 localhost forwarding. This is what makes the WSL provider runnable on a real machine before any desktop wiring exists. Two differences from e2e-vz.ts, both deliberate: - The sha256 is digested once up front rather than inside resolveImage. A hash object cannot be digested twice, so computing it in the callback breaks as soon as the provider resolves the image more than once. - --image and --bundle are resolved to absolute paths, because wslpath resolves relative paths against the guest's cwd rather than the host's. --hold keeps the distribution up for inspection; --keep skips the unregister. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6784d57 commit 779db6f

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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

Comments
 (0)