CodeNomad/scripts/test-opencode-setup-wsl.mjs
Pascal André 91c6523f1f
test(opencode): certify bounded WSL setup and reload effects
Exercise the real Windows-to-WSL service lifecycle and plugin provisioning through native, mounted and aliased discovery roots. Use guarded fixture-owned registrations, unique service ports and fully isolated environments; record actual watcher behavior, measured lease heartbeats, explicit reload, restart and cleanup.

Characterize reload with a held synthetic provider stream and native pending Form, Shell and PTY. Verify one successful provider request without replay and record native resource teardown. Update all locale descriptions to explicitly include terminals and background commands.

Bound the whole fixture, native requests, predicates and cleanup independently. Handle detached event-consumer failures immediately while preserving awaited failure reporting. Seven guard regressions and final real aliased reload-safety acceptance pass; autonomous gatekeeper re-review reports no remaining fixture findings.
2026-09-20 17:21:31 +02:00

278 lines
17 KiB
JavaScript

// Real Windows -> WSL official lifecycle, authenticated native connection and
// plugin provisioning. The wrapper forces every command into fresh fixture state.
import assert from "node:assert/strict"
import { execFile } from "node:child_process"
import { randomUUID } from "node:crypto"
import { createServer } from "node:net"
import { watch } from "node:fs"
import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { setTimeout as delay } from "node:timers/promises"
import { tsImport } from "tsx/esm/api"
import { OpenCode } from "@opencode/client"
import { Service } from "@opencode/client/service"
import { boundedFixtureOperation } from "./fixtures/wsl-fixture-bounds.mjs"
const deadlineAt = Date.now() + 180_000
const signal = AbortSignal.timeout(180_000)
const [distro, cli, mode = "aliased", check = ""] = process.argv.slice(2)
if (process.platform !== "win32" || !distro || !cli?.startsWith("/")) throw new Error("Pass a WSL distro and absolute Linux CLI path on Windows")
assert.ok(["native", "mounted", "aliased"].includes(mode), "Config mode must be native, mounted or aliased")
assert.ok(check === "" || check === "reload-safety", "Optional check must be reload-safety")
const quote = value => `'${value.replaceAll("'", `'"'"'`)}'`
function wsl(script, cleanup = false) {
if (!cleanup) signal.throwIfAborted()
return new Promise((resolve, reject) => {
const child = execFile("wsl.exe", ["--distribution", distro, "--exec", "/bin/bash", "-s"],
{ encoding: "utf8", timeout: cleanup ? 15_000 : Math.max(1, Math.min(60_000, deadlineAt - Date.now())),
...(!cleanup ? { signal } : {}), maxBuffer: 1024 * 1024, windowsHide: true },
(error, stdout, stderr) => error ? reject(new Error(`${error.message}\n${stderr}`)) : resolve(stdout.trim()))
child.stdin.end(`set -euo pipefail\n${script}\n`)
})
}
const parent = path.join(os.tmpdir(), "opencode")
await mkdir(parent, { recursive: true })
const mounted = await mkdtemp(path.join(parent, "codenomad-wsl-setup-"))
const mount = await wsl(`wslpath -au ${quote(mounted)}`)
const root = await wsl('mkdir -p "$HOME/.cache"\nmktemp -d "$HOME/.cache/codenomad-wsl-setup-XXXXXX"')
const password = randomUUID(), wrapper = `${root}/opencode-fixture`
const config = mode === "native" ? `${root}/config` : mode === "mounted" ? `${mount}/config` : `${root}/config-alias`
const unc = directory => `\\\\wsl.localhost\\${distro}${directory.replaceAll("/", "\\")}`
const configFile = mode === "native" ? unc(`${config}/opencode.json`) : path.join(mounted, "config/opencode.json")
// Stable CLI channels default to 49374 even with different XDG roots. Allocate
// an unused Windows port and verify it is unused in Linux before configuring it.
const reservation = createServer()
await new Promise((resolve, reject) => { reservation.once("error", reject); reservation.listen(0, "127.0.0.1", resolve) })
const port = reservation.address().port
await new Promise(resolve => reservation.close(resolve))
await wsl(`mkdir -p ${quote(`${mount}/config`)} ${quote(`${root}/config`)} ${quote(`${root}/home`)} ${quote(`${root}/repo`)}
${mode === "aliased" ? `ln -s ${quote(`${mount}/config`)} ${quote(config)}` : ""}
mkdir -m 700 ${quote(`${root}/runtime`)}
python3 -c ${quote(`import socket; s=socket.socket(); s.bind(('127.0.0.1', ${port})); s.close()`)}`)
const environment = {
PATH: "/usr/local/bin:/usr/bin:/bin", HOME: `${root}/home`, USERPROFILE: `${root}/home`,
XDG_DATA_HOME: `${root}/data`, XDG_CONFIG_HOME: `${root}/xdg-config`, XDG_STATE_HOME: `${root}/state`,
XDG_CACHE_HOME: `${root}/cache`, XDG_RUNTIME_DIR: `${root}/runtime`, OPENCODE_TEST_HOME: `${root}/home`, OPENCODE_CONFIG_DIR: config,
OPENCODE_DB: `${root}/fixture.db`,
OPENCODE_CONFIG_CONTENT: JSON.stringify({ plugins: [`${root}/probe-plugin`] }),
OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_FFF: "1",
}
let reloadProvider, reloadSafety, lifecycle, shared, automation, pruning, configWatcher
let started = false
const evidence = { root, mounted, mode, port }
async function until(predicate, message) {
const deadline = Math.min(deadlineAt, Date.now() + 30_000)
while (true) {
const options = { signal: AbortSignal.any([signal, AbortSignal.timeout(Math.max(1, deadline - Date.now()))]) }
if (await boundedFixtureOperation(() => predicate(options), deadline, message, signal)) return
await delay(100, undefined, { signal })
}
}
async function startOrRestart(restart = false) {
signal.throwIfAborted()
const startDeadline = Math.min(deadlineAt, Date.now() + 30_000)
try { return await (restart ? lifecycle.restart(startDeadline) : lifecycle.ensure(startDeadline)) }
catch (error) {
if (!error.message.startsWith("Cannot reach the WSL OpenCode service from Windows")) throw error
// Forwarding may trail Linux readiness. Only rediscover our guarded service.
let endpoint
await until(async () => {
try { endpoint = await lifecycle.discover(Math.min(deadlineAt, Date.now() + 15_000)); return Boolean(endpoint) }
catch (retry) {
if (!retry.message.startsWith("Cannot reach the WSL OpenCode service from Windows")) throw retry
return false
}
}, "Fixture WSL localhost forwarding did not become ready")
return endpoint
}
}
async function runFixture() {
if (check === "reload-safety") {
const { startReloadSafetyProvider } = await import("./fixtures/wsl-reload-safety.mjs")
reloadProvider = await startReloadSafetyProvider({ distro, root, environment, unc, signal })
environment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
plugins: [`${root}/probe-plugin`], model: "fixture/fixture",
providers: { fixture: { package: "@opencode/ai/providers/openai-compatible",
settings: { baseURL: `http://127.0.0.1:${reloadProvider.port}/v1`, apiKey: "fixture" }, models: { fixture: {} } } },
})
}
// Check the isolated registration and Linux process environment before any CLI
// command can probe or stop it. Never infer ownership from an endpoint or PID.
const guard = `import json, os, pathlib, socket
root = pathlib.Path(${JSON.stringify(root)})
registration = root / 'state/opencode/service.json'
if registration.exists():
info = json.loads(registration.read_text())
assert info['url'] == 'http://127.0.0.1:${port}', 'Unexpected fixture endpoint'
env = pathlib.Path('/proc/%s/environ' % info['pid']).read_bytes().split(b'\\0')
for key, value in json.loads(${JSON.stringify(JSON.stringify(environment))}).items():
assert (key + '=' + value).encode() in env, 'Fixture process ownership mismatch: ' + key
else:
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', ${port}))
s.close()
`
const script = `#!/bin/sh\nset -eu\n/usr/bin/python3 ${quote(`${root}/guard.py`)}\ncd ${quote(`${root}/repo`)}\nexec env -i ${Object.entries(environment).map(([key, value]) => `${key}=${quote(value)}`).join(" ")} ${quote(cli)} "$@"\n`
await wsl(`cat > ${quote(wrapper)} <<'CODENOMAD_FIXTURE'
${script}CODENOMAD_FIXTURE
cat > ${quote(`${root}/guard.py`)} <<'CODENOMAD_GUARD'
${guard}CODENOMAD_GUARD
chmod 700 ${quote(wrapper)}`)
await wsl(`mkdir ${quote(`${root}/probe-plugin`)}
cat > ${quote(`${root}/probe-plugin/index.ts`)} <<'CODENOMAD_PLUGIN'
import { appendFile } from 'node:fs/promises'
export default { id: 'wsl-fixture-probe', async setup(ctx) {
await appendFile(${JSON.stringify(`${root}/plugin-setup.log`)}, 'setup\\n')
await ctx.session.hook('http.request', event => event.request.headers.set('x-reload-kind', event.kind))
} }
CODENOMAD_PLUGIN`)
await wsl(`${quote(wrapper)} service set port ${port}
${quote(wrapper)} service set password ${quote(password)}`)
await writeFile(configFile, "{}\n")
const { WslOpenCodeService } = await tsImport("../packages/server/src/workspaces/wsl-opencode-service.ts", import.meta.url)
const { OpenCodeSharedService } = await tsImport("../packages/server/src/workspaces/opencode-service.ts", import.meta.url)
const { resolveDesktopPluginPaths } = await tsImport("../packages/server/src/opencode/desktop-plugin-paths.ts", import.meta.url)
const { DesktopPluginLifecycle, prepareDesktopPluginPresence } = await tsImport("../packages/server/src/opencode/desktop-plugin-lifecycle.ts", import.meta.url)
lifecycle = new WslOpenCodeService({ distro, binary: wrapper, timeoutMs: 30_000 })
shared = new OpenCodeSharedService({ headers: Service.headers, makeClient: options => OpenCode.make({ ...options,
fetch: (input, init) => options.fetch(input, { ...init,
signal: AbortSignal.any([signal, AbortSignal.timeout(15_000), ...(init?.signal ? [init.signal] : [])]),
}),
}) })
automation = new DesktopPluginLifecycle("automation")
pruning = new DesktopPluginLifecycle("session-pruning")
assert.equal(await lifecycle.discover(Math.min(deadlineAt, Date.now() + 15_000)), undefined, "Fresh fixture must never discover an existing service")
started = true
const endpoint = await startOrRestart()
assert.equal(endpoint.url, `http://127.0.0.1:${port}`)
const options = { kind: "lifecycle", identity: `wsl-fixture:${root}`, lifecycle }
const client = await shared.client(options, { deadlineAt })
const before = await client.server.info()
evidence.started = { endpoint: endpoint.url, pid: before.pid, version: before.version }
console.log(`Authenticated isolated ${distro} service: ${endpoint.url}, PID ${before.pid}; ${root}`)
assert.equal(before.version, "2.0.11")
const connection = await shared.acquire()
const assertCurrent = () => { signal.throwIfAborted(); connection.assertCurrent() }
const paths = await resolveDesktopPluginPaths({ ...connection, assertCurrent }, { kind: "wsl", distro, binary: wrapper }, Math.min(deadlineAt, Date.now() + 15_000))
evidence.paths = paths
if (mode === "native") assert.ok(paths.config.startsWith(`\\\\wsl.localhost\\${distro}\\`))
else assert.equal(paths.config.toLowerCase(), path.dirname(configFile).toLowerCase(), "Mounted discovery roots, including symlink aliases, need Windows-native access paths")
assert.equal(await readFile(path.join(paths.config, "opencode.json"), "utf8"), "{}\n")
assert.ok(!paths.data.startsWith(`${paths.config}\\`), "Presence leases must stay outside the aliased/mounted config tree")
const location = { directory: `${root}/repo` }
await client.location.get({ location })
await until(async request => (await client.plugin.list({ location }, request)).data.some(plugin => plugin.id === "wsl-fixture-probe" && plugin.state.status === "active"), "Probe plugin did not activate")
assert.ok(!(await client.plugin.list({ location })).data.some(plugin => plugin.id === "codenomad.automation"), "Location must be loaded before desktop plugins are installed")
if (reloadProvider) {
const { prepareReloadSafety } = await import("./fixtures/wsl-reload-safety.mjs")
reloadSafety = await prepareReloadSafety({ client, root, unc, provider: reloadProvider })
evidence.reloadSafety = { before: reloadSafety.before }
}
await prepareDesktopPluginPresence(paths, assertCurrent, { automation, pruning })
await delay(1_500)
evidence.beforeExplicitReload = (await client.plugin.list({ location })).data
.filter(plugin => plugin.id.startsWith("codenomad"))
.map(plugin => ({ id: plugin.id, status: plugin.state.status }))
// This explicitly tests the informed recovery action. It is never evidence
// of automatic watcher discovery; pre-reload native state is recorded above.
await client.location.reload({ signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]) })
if (reloadSafety) evidence.reloadSafety = await reloadSafety.observe()
await until(async request => {
const plugins = (await client.plugin.list({ location }, request)).data
evidence.plugins = plugins.filter(plugin => plugin.id.startsWith("codenomad") || plugin.id === "wsl-fixture-probe")
.map(plugin => ({ id: plugin.id, status: plugin.state.status }))
return ["codenomad.automation", "codenomad-session-pruning"].every(id => plugins.some(plugin => plugin.id === id && plugin.state.status === "active"))
}, "Late plugin discovery after native reload failed")
const changes = []
const watcher = mode === "native" ? undefined : watch(path.dirname(configFile), { recursive: true }, (event, filename) => changes.push({ event, filename }))
configWatcher = watcher
if (watcher) {
try {
const probe = path.join(path.dirname(configFile), ".watch-probe")
await writeFile(probe, "")
await rm(probe)
await until(async () => changes.some(change => change.filename === ".watch-probe"), "Windows config observer did not receive its control event")
} catch (error) { watcher.close(); throw error }
}
await delay(2_500)
const setupBefore = await readFile(unc(`${root}/plugin-setup.log`), "utf8")
const leasePaths = (await Promise.all(["automation", "session-pruning"].map(async feature => {
const directory = path.join(paths.data, feature, "presence")
return (await readdir(directory)).filter(name => name.endsWith(".lease")).map(name => path.join(directory, name))
}))).flat()
assert.equal(leasePaths.length, 2)
const leaseBefore = await Promise.all(leasePaths.map(async file => (await stat(file)).mtimeMs))
changes.length = 0
try {
// Windows ReadDirectoryChangesW cannot watch native WSL UNC directories.
// Observe Linux inotify there instead, including existing nested folders.
if (mode === "native") {
const events = await wsl(`python3 - ${quote(config)} ${quote(`${root}/runtime`)} <<'CODENOMAD_WATCH'
import ctypes, json, os, select, sys, time
libc = ctypes.CDLL(None, use_errno=True)
fd = libc.inotify_init1(os.O_NONBLOCK)
assert fd >= 0
# Prove this observer is live before measuring the real heartbeat interval.
control = libc.inotify_add_watch(fd, os.fsencode(sys.argv[2]), 0x00000fce)
assert control >= 0
probe = os.path.join(sys.argv[2], '.watch-probe')
open(probe, 'w').close()
os.unlink(probe)
assert select.select([fd], [], [], 2)[0]
os.read(fd, 1048576)
libc.inotify_rm_watch(fd, control)
os.read(fd, 1048576)
for directory, _, _ in os.walk(sys.argv[1]):
assert libc.inotify_add_watch(fd, os.fsencode(directory), 0x00000fce) >= 0
deadline = time.monotonic() + 5
events = []
while time.monotonic() < deadline:
if select.select([fd], [], [], max(0, deadline-time.monotonic()))[0]:
events.append(os.read(fd, 1048576).hex())
os.close(fd)
print(json.dumps(events))
CODENOMAD_WATCH`)
changes.push(...JSON.parse(events))
} else await delay(5_000)
} finally { watcher?.close() }
const leaseAfter = await Promise.all(leasePaths.map(async file => (await stat(file)).mtimeMs))
assert.ok(leaseAfter.every((time, index) => time > leaseBefore[index]), "Both backend leases must actually heartbeat")
assert.deepEqual(changes, [], "Heartbeats must leave the entire watched config tree unchanged")
assert.equal(await readFile(unc(`${root}/plugin-setup.log`), "utf8"), setupBefore, "Heartbeats must not rerun plugin setup")
assert.equal((await client.server.info()).pid, before.pid)
evidence.heartbeat = { durationMs: 5_000, leaseBefore, leaseAfter, changes, stableSetup: true }
evidence.nativeWatcherFailures = (await readFile(unc(`${root}/data/opencode/log/opencode.log`), "utf8"))
.split("\n").filter(line => line.includes('message="failed to subscribe"')).slice(-4)
await automation.stop(); await pruning.stop(); await shared.shutdown()
await startOrRestart(true)
const replacement = await shared.client(options, { deadlineAt })
evidence.restarted = { pid: (await replacement.server.info()).pid }
assert.notEqual(evidence.restarted.pid, before.pid)
assert.equal((await replacement.location.get({ location })).directory, location.directory)
evidence.passed = true
console.log(`PASS Windows→${distro} (${mode}): real service start/authentication/restart/reconnect, translated filesystem provisioning, late plugin discovery after explicit native reload and measured heartbeat stability; ${mounted}`)
}
try {
await boundedFixtureOperation(runFixture, deadlineAt, "WSL native fixture", signal)
} catch (error) {
evidence.error = error.message
throw error
} finally {
configWatcher?.close()
const cleanupDeadline = Date.now() + 20_000
const cleanups = [
["reload observer", () => reloadSafety?.dispose()], ["local provider", () => reloadProvider?.stop()],
["automation presence", () => automation?.stop()], ["pruning presence", () => pruning?.stop()],
["native client", () => shared?.shutdown()],
["isolated daemon", () => started ? wsl(`${quote(wrapper)} service stop`, true) : undefined],
]
const results = await Promise.allSettled(cleanups.map(([label, operation]) => boundedFixtureOperation(operation, cleanupDeadline, label)))
evidence.cleanup = results.map((result, index) => ({ operation: cleanups[index][0], status: result.status,
...(result.status === "rejected" ? { error: String(result.reason) } : {}) }))
await boundedFixtureOperation(() => writeFile(path.join(mounted, "evidence.json"), JSON.stringify(evidence, null, 2)), Date.now() + 5_000, "write evidence")
const failures = results.filter(result => result.status === "rejected").map(result => result.reason)
if (failures.length) throw new AggregateError(failures, "Isolated WSL fixture cleanup failed")
}