CodeNomad/scripts/test-opencode-upgrade-native.mjs
Pascal André f26bed2f8f
chore(opencode): qualify and recommend stable 2.0.18
Align server/UI client and bundled plugin pins with the latest stable runtime while preserving the demonstrated 2.0.7 timestamp minimum. Review the published declarations and authenticated native schema: pairing and Shell signal additions require no CodeNomad API adaptation.

Make the isolated native upgrade fixture follow the recommendation while retaining its 2.0.15 source boundary and live-daemon PID assertion. Extend setup version coverage and update the current compatibility and architecture references.

Validate both minimum/current native runtimes, historical migrations, server/UI tests, setup and tool-image browser scenarios, typechecks and production bundles. Record the intermittent Windows Node pruning/UI heap exit and successful diagnostic rerun without claiming its cause is fixed.
2026-09-26 16:38:40 +02:00

74 lines
4.3 KiB
JavaScript

// Run with: node --import tsx scripts/test-opencode-upgrade-native.mjs
// Real npm + native CLI, isolated home/prefix/service. Never uses the shared daemon.
import assert from "node:assert/strict"
import { execFile } from "node:child_process"
import { promisify } from "node:util"
import { mkdtemp, mkdir, copyFile, writeFile, open } from "node:fs/promises"
import { createServer } from "node:net"
import os from "node:os"
import path from "node:path"
import { installSharedOpenCode, npmExecutable } from "../packages/server/src/opencode-update/shared-installation.ts"
import { bundledNpm } from "../packages/server/src/opencode-update/npm-runtime.ts"
import { RECOMMENDED_OPENCODE_VERSION } from "../packages/server/src/opencode/runtime-support.ts"
const root = await mkdtemp(path.join(os.tmpdir(), "codenomad-native-upgrade-"))
const node = process.env.CODENOMAD_FIXTURE_NODE || process.execPath
const npm = bundledNpm(node)
assert.ok(npm, "npm must be bundled beside the selected Node")
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) =>
!/^(OPENCODE_|CODENOMAD_|CLI_|XDG_|npm_|PATH$|NODE_|ELECTRON_|WEBVIEW2_)/i.test(key)))
Object.assign(env, {
HOME: root, USERPROFILE: root, APPDATA: path.join(root, "AppData"), LOCALAPPDATA: path.join(root, "LocalAppData"),
XDG_CONFIG_HOME: path.join(root, "config"), XDG_DATA_HOME: path.join(root, "data"), XDG_STATE_HOME: path.join(root, "state"),
XDG_CACHE_HOME: path.join(root, "cache"), OPENCODE_TEST_HOME: root, OPENCODE_CONFIG_DIR: path.join(root, "opencode"),
OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_CONFIG_CONTENT: "{}",
npm_config_prefix: path.join(root, "npm prefix"), npm_config_cache: path.join(root, "npm-cache"),
npm_config_userconfig: path.join(root, "npmrc"),
PATH: process.platform === "win32" ? `${path.dirname(node)};${process.env.SystemRoot}/System32` : `${path.dirname(node)}:/usr/bin:/bin`,
})
for (const key of ["APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME", "OPENCODE_CONFIG_DIR"]) {
await mkdir(env[key], { recursive: true })
}
const binary = npmExecutable(env.npm_config_prefix)
const bootstrap = path.join(root, "service-control.exe")
const run = (file, args) => promisify(execFile)(file, args, { cwd: root, env, timeout: 60_000, maxBuffer: 1024 * 1024, windowsHide: true })
const options = { home: root, env, node, npm, registerPath: async () => {} }
const reservation = createServer()
await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve))
const port = reservation.address().port
await new Promise(resolve => reservation.close(resolve))
let password, started = false
const info = async () => {
const response = await fetch(`http://127.0.0.1:${port}/api/info`, {
headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }, signal: AbortSignal.timeout(10_000),
})
assert.equal(response.status, 200)
const data = await response.json()
return { version: data.version, pid: data.pid }
}
const result = { root, port }
try {
await installSharedOpenCode("2.0.15", options)
// Keep an independent controller so cleanup still works after failed replacement.
await copyFile(binary, bootstrap)
await run(bootstrap, ["service", "set", "port", String(port)])
await run(binary, ["service", "start"])
started = true
password = (await run(bootstrap, ["service", "get", "password"])).stdout.trim()
result.before = await info()
assert.equal(result.before.version, "2.0.15")
assert.ok(Number.isInteger(result.before.pid))
if (process.platform === "win32") {
await assert.rejects(async () => { await (await open(binary, "r+")).close() }, "old write-access preflight rejects this live image")
}
await installSharedOpenCode(RECOMMENDED_OPENCODE_VERSION, options)
result.after = await info()
result.installedVersion = (await run(binary, ["--version"])).stdout.trim()
assert.equal(result.installedVersion.replace(/^opencode2? v?/, ""), RECOMMENDED_OPENCODE_VERSION)
assert.deepEqual(result.after, result.before, "upgrade must not restart the service")
result.success = true
} finally {
if (started) await run(bootstrap, ["service", "stop"])
await writeFile(path.join(root, "result.json"), JSON.stringify(result, null, 2))
console.log(JSON.stringify(result, null, 2))
}