mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-24 15:21:21 +00:00
## Résultat Corrige le chargement des plugins CodeNomad avec OpenCode 2.0.27 et établit cette version comme minimum pour les prochaines releases et les prereleases dev. Aucun déploiement de l’application active ni redémarrage du daemon partagé. ## Cause démontrée - Le runtime natif passe d’Effect 4.0.0-rc.112 à 4.0.1. Le bundle précédent reproduit `cache.stackFrame` sur une CLI 2.0.27 isolée. - Embarquer Effect 4.0.1 ne préserve pas la 2.0.26 : échec inverse `cache.scheduler`. - Les schémas 2.0.27 consomment `Schema.isStartingWith`, absent du moteur 2.0.26. Le plancher est fondé sur cette rupture mesurée, pas sur une liste de versions testées. ## Implémentation - Pins client/plugin synchronisés à 2.0.27 ; Effect et le SDK public des plugins sont fournis par OpenCode, hors bundles natifs pruning/Missions/automation. - Réglages de build partagés et régression qui interdit une seconde copie du runtime ; smoke natif ajouté à la matrice minimum/latest. - Diagnostic `plugin_runtime` traduit dans les dix locales ; versions futures et personnalisées conservent la reconnaissance authentifiée du contrat. - Adaptations du SDK : annulation des lectures projet, références de credentials externes et connexion fournisseur externe avec formulaire natif, sans fallback OAuth/API key ni rejeu. - Le runtime OpenTunnel du backend reste isolé et inchangé. Aucun tunnel, route ou identité distant n’est modifié. ## Validation locale - Échec reproduit sur 2.0.26 et réussite sur 2.0.27 pour les variantes de bundle comparées. - Suites natives isolées automation, pruning/historique/proxy/concurrence/redémarrage et Missions réussies sur 2.0.27. - Régressions ciblées de build, store/serveur et navigateur (diagnostic du minimum, connexion externe et brouillon conservé) réussies ; typechecks UI/serveur/Electron/OpenTunnel réussis. - Suite serveur complète et CI multiplateforme encore en validation à l’ouverture ; aucun résultat entièrement vert anticipé. La revue gatekeeper indépendante sera publiée ici avant fusion. Les anciennes preuves de qualification restent historiques, pas une promesse de support des moteurs antérieurs au nouveau plancher.
77 lines
4.6 KiB
JavaScript
77 lines
4.6 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 {
|
|
// Seed the historical native-upgrade boundary using npm in this fixture's
|
|
// private prefix. Production installation correctly refuses pre-minimum targets.
|
|
await run(node, [npm, "install", "--global", "--prefix", env.npm_config_prefix,
|
|
"--no-audit", "--no-fund", "--registry", "https://registry.npmjs.org", "@opencode/cli@2.0.15"])
|
|
// 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))
|
|
}
|