CodeNomad/scripts/test-plugin-runtime-native.mjs
Pascal André dd5a1f387d
fix(opencode): share host plugin runtime and require 2.0.27 (#920)
## 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.
2026-10-12 16:42:07 +02:00

48 lines
3 KiB
JavaScript

// Explicit isolated CLI/bundle only; no shared service, real credentials or user storage.
import assert from "node:assert/strict"
import { readFile, mkdir } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { setTimeout as delay } from "node:timers/promises"
import { tsImport } from "tsx/esm/api"
import { withProductRuntime } from "./native-product-fixture.mjs"
const bundle = process.argv[3] ?? fileURLToPath(new URL("../packages/server/dist/plugins/session-pruning/plugin.mjs", import.meta.url))
assert(path.isAbsolute(bundle), "Pass an absolute private bundle path")
const { installPruningPresence } = await tsImport("../packages/server/src/opencode/pruning-installation.ts", import.meta.url)
let closePresence
try {
await withProductRuntime(process.argv[2], async ({ root, configDirectory }) => {
closePresence = await installPruningPresence(await readFile(bundle), { config: configDirectory, data: path.join(root, "presence") })
return {}
}, async ({ client, root, connection }) => {
const location = { directory: root }
await client.location.get({ location })
const deadline = Date.now() + 15_000
let plugin
do {
plugin = (await client.plugin.list({ location })).data.find(item => item.id === "codenomad-session-pruning" || item.source.type === "local" && item.state.status === "failed")
if (plugin?.state.status === "failed") throw new Error(JSON.stringify(plugin))
if (plugin?.state.status === "active") break
await delay(50)
} while (Date.now() < deadline)
assert.equal(plugin?.state.status, "active", "Shipped plugin must load")
const session = await client.session.create({ location })
const form = await client.session.form.create({ sessionID: session.id, title: "Runtime contract fixture", fields: [{ key: "answer", type: "string" }] })
const cold = path.join(root, "cold")
await mkdir(cold)
const url = new URL("/api/rpc/codenomad.pending-requests/snapshot", connection.endpoint.url)
url.searchParams.set("location[directory]", root)
const response = await connection.fetch(url, { method: "POST", headers: { "content-type": "application/json",
authorization: `Basic ${Buffer.from(`opencode:${connection.endpoint.auth.password}`).toString("base64")}` },
body: JSON.stringify({ input: { directories: [root, cold] } }) })
const text = await response.text()
assert.equal(response.status, 200, text)
const snapshot = JSON.parse(text).output
assert.equal(snapshot.originDirectory, root)
assert(snapshot.data.find(item => item.directory === root).locations.some(item => item.forms.some(item => item.id === form.id)))
assert.deepEqual(snapshot.data.find(item => item.directory === cold).locations, [])
await client.session.form.cancel({ sessionID: session.id, formID: form.id })
console.log("PASS: native plugin loading, host Effect/Schema/RPC, existing-only pending snapshot and Form settlement")
})
} finally { await closePresence?.() }