import assert from "node:assert/strict"
import { after, before, test } from "node:test"
import { fileURLToPath } from "node:url"
import { chromium, type Browser } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import path from "node:path"
let server: ViteDevServer, browser: Browser, url: string
before(async () => {
server = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error",
cacheDir: "node_modules/.vite-opencode-setup",
plugins: [solid(), { name: "setup-fixture", configureServer(s) {
s.middlewares.use("/fixture", async (_req, res) => {
res.setHeader("Content-Type", "text/html")
res.end(await s.transformIndexHtml("/fixture", '
'))
})
} }], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] },
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null },
})
await server.listen()
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/fixture`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await server?.close() })
for (const surface of ["recovery", "settings"]) test(`${surface} shows sustained install progress through checking and connection`, async () => {
const page = await browser.newPage()
let finishInstall!: () => void, finishCheck!: () => void, finishConnect!: () => void
const installGate = new Promise(resolve => { finishInstall = resolve })
const checkGate = new Promise(resolve => { finishCheck = resolve })
const connectGate = new Promise(resolve => { finishConnect = resolve })
let installed = false, connected = false, installs = 0, connects = 0
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
installs++
await installGate
installed = true
return route.fulfill({ json: { success: true, version: "2.0.17" } })
}
if (request.url().endsWith("/api/opencode/service")) {
connects++
assert.equal(request.postDataJSON().restart, false)
await connectGate
connected = true
} else if (installed) await checkGate
return route.fulfill({ json: { state: installed ? "ready" : "missing", currentVersion: installed ? "2.0.17" : null,
latestVersion: "2.0.17", minimumVersion: "2.0.7", recommendedVersion: "2.0.16", binaryPath: "opencode2", target: "host",
canUpgrade: !installed, updateAvailable: !installed, serviceState: connected ? "ready" : "stopped", canRestart: false } })
})
try {
await page.clock.install()
await page.goto(`${url}${surface === "settings" ? "?settings" : ""}`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByRole("button", { name: "Install and start OpenCode" }).click()
const progress = page.locator(".opencode-setup-progress")
await progress.getByText("Installing OpenCode…", { exact: true }).waitFor()
assert.equal(await progress.getByText(/Keep CodeNomad open/).count(), 1)
assert.equal(await progress.locator("svg.animate-spin").count(), 1)
assert.equal(await progress.evaluate(el => el.closest('[aria-busy="true"]') === null), true, "live progress must not be suppressed by a busy ancestor")
await page.clock.fastForward(65_000)
await progress.getByRole("timer").filter({ hasText: "1:05" }).waitFor()
assert.equal(await page.getByRole("button", { name: "Updating OpenCode...", exact: true }).isDisabled(), true)
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, `opencode-install-${surface}.png`) })
if (surface === "recovery") {
await page.getByRole("button", { name: "Close", exact: true }).click()
await page.locator(".opencode-setup-reminder").getByText("Installing OpenCode…", { exact: true }).waitFor()
await page.getByRole("button", { name: "OpenCode setup required" }).click()
await page.getByRole("dialog").getByRole("timer").filter({ hasText: "1:05" }).waitFor()
}
finishInstall()
await progress.getByText("Reading installed version...", { exact: true }).waitFor()
finishCheck()
await progress.getByText("Connecting to OpenCode…", { exact: true }).waitFor()
assert.equal(await progress.getByText(/Keep CodeNomad open/).count(), 1)
if (surface === "recovery") {
await page.getByRole("button", { name: "Close", exact: true }).click()
await page.locator(".opencode-setup-reminder").getByText("Connecting to OpenCode…", { exact: true }).waitFor()
await page.getByRole("button", { name: "OpenCode setup required" }).click()
}
finishConnect()
await page.waitForFunction(() => !document.querySelector(".opencode-setup-progress"))
assert.equal(installs, 1, "closing/reopening must not retry installation")
assert.equal(connects, 1)
assert.deepEqual(errors, [])
} finally {
finishInstall(); finishCheck(); finishConnect()
await page.close()
}
})
test("missing installation and incompatible daemon expose different actions; restart is explicit", async () => {
const page = await browser.newPage()
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
let installed = false, running = false, restarts = 0, installs = 0
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
installed = true; installs++
return route.fulfill({ json: { success: true, version: "2.0.11" } })
}
if (request.url().endsWith("/api/opencode/service")) {
assert.equal(request.postDataJSON().restart, true)
restarts++; running = true
}
return route.fulfill({ json: { state: installed ? "ready" : "missing", currentVersion: installed ? "2.0.11" : null,
latestVersion: "2.0.11", minimumVersion: "2.0.7", recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "host",
canUpgrade: !installed, updateAvailable: !installed, daemonVersion: running ? "2.0.11" : "2.0.6",
serviceState: installed ? running ? "ready" : "restart_required" : undefined, canRestart: installed && !running } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
const installBounds = await page.getByRole("button", { name: "Install and start OpenCode" }).boundingBox()
const selectorBounds = await page.locator(".selector-input").boundingBox()
assert.ok(installBounds && selectorBounds && installBounds.y < selectorBounds.y, "recovery presents diagnosis and installation before executable selection")
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-setup-missing.png"), fullPage: true })
await page.getByRole("button", { name: "Install and start OpenCode" }).click()
await page.getByRole("button", { name: "Restart shared service" }).waitFor()
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-setup-restart.png") })
assert.equal(installs, 1)
assert.equal(restarts, 0)
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
await page.getByText(/Restart interrupts active work/).waitFor()
await page.getByRole("button", { name: "Restart shared service" }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(restarts, 1)
assert.equal(await page.getByRole("dialog").count(), 0)
assert.deepEqual(errors, [])
} catch (error) { console.error(errors, await page.locator("body").innerText()); throw error }
finally { await page.close() }
})
test("closing required-update screen leaves recovery clickable above a pending folder overlay", async () => {
const page = await browser.newPage()
await page.route("**/api/**", route => route.fulfill({ json: { state: "update_required", currentVersion: "2.0.32",
latestVersion: "2.1.2", minimumVersion: "2.1.2", recommendedVersion: "2.1.2", incompatibilityReason: "native_contract", binaryPath: "opencode2", target: "host", canUpgrade: true, canRestart: false } }))
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByRole("dialog").waitFor()
await page.evaluate(() => {
const overlay = document.createElement("div")
overlay.className = "folder-loading-overlay"
document.getElementById("root")!.append(overlay)
})
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-setup-required.png") })
await page.getByRole("button", { name: "Close", exact: true }).click()
await page.getByRole("button", { name: "OpenCode setup required" }).click()
await page.getByRole("dialog").waitFor()
assert.equal(await page.getByText("2.0.32", { exact: true }).count(), 1)
assert.equal(await page.locator("details[open]").count(), 0)
await page.getByText("Version details", { exact: true }).click()
await page.getByText(/requires OpenCode 2\.1\.2 or later for correct JavaScript job ordering in Code Mode/).waitFor()
assert.equal(await page.getByText("settings.opencode.setup.native_contract", { exact: true }).count(), 0)
} catch (error) { console.error(await page.locator("body").innerText()); throw error }
finally { await page.close() }
})
test("optional updates stay non-blocking and a failed install remains recoverable", async () => {
const page = await browser.newPage()
let attempts = 0, starts = 0
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
attempts++
return route.fulfill({ status: 500, json: { error: "upgrade_failed" } })
}
if (request.url().endsWith("/api/opencode/service")) starts++
return route.fulfill({ json: { state: "ready", currentVersion: "2.0.10", latestVersion: "2.0.11",
minimumVersion: "2.0.7", recommendedVersion: "2.0.11", versionAssessment: "untested", binaryPath: "opencode2", target: "host", canUpgrade: true, canRestart: false,
serviceState: "ready", daemonVersion: "2.0.10" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
assert.equal(await page.getByRole("dialog").count(), 0)
await page.evaluate(() => (window as any).fixture.open())
await page.getByText(/OpenCode 2.0.10 has not been fully validated/).waitFor()
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-setup-optional.png") })
await page.getByRole("button", { name: "Update to OpenCode 2.0.11" }).click()
await page.locator(".settings-error-message").waitFor()
assert.equal(starts, 0)
assert.equal(attempts, 1)
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
await page.getByRole("button", { name: "Check status and updates" }).click()
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'))
assert.equal(await page.locator(".settings-error-message").count(), 1, "a successful status read must not erase the install failure")
await page.getByRole("button", { name: "Close", exact: true }).click()
await page.evaluate(() => (window as any).fixture.open())
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'))
assert.equal(await page.locator(".settings-error-message").count(), 1, "reopening recovery preserves the failed action")
await page.getByRole("button", { name: "Update to OpenCode 2.0.11" }).click()
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'))
assert.equal(attempts, 2)
} finally { await page.close() }
})
test("unsupported proxy calls open recovery once without replaying mutations", async () => {
const page = await browser.newPage()
let blocked = false, prompts = 0
await page.route("**/api/**", route => {
if (route.request().url().includes("/instance/api/")) {
prompts++; blocked = true
return route.fulfill({ status: 426, json: { code: "opencode_update_required" } })
}
return route.fulfill({ json: { state: "ready", currentVersion: "2.0.11", latestVersion: "2.0.11",
minimumVersion: "2.0.7", recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "host", canUpgrade: false,
serviceState: blocked ? "restart_required" : "ready", canRestart: blocked, daemonVersion: blocked ? "2.0.6" : "2.0.11" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
assert.equal(await page.evaluate(() => (window as any).fixture.unsupported()), 426)
await page.getByRole("button", { name: "Restart shared service" }).waitFor()
await page.getByRole("button", { name: "Close", exact: true }).click()
assert.equal(await page.evaluate(() => (window as any).fixture.unsupported()), 426)
assert.equal(await page.getByRole("dialog").count(), 0)
assert.equal(prompts, 2, "each intentional call occurs once; recovery never retries a prompt")
} finally { await page.close() }
})
test("optional install reconnects without restarting and leaves explicit activation available", async () => {
const page = await browser.newPage()
let installed = false, restarts = 0, connects = 0
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
installed = true
return route.fulfill({ json: { success: true, version: "2.0.12" } })
}
if (request.url().endsWith("/api/opencode/service")) {
if (request.postDataJSON().restart) restarts++
else connects++
}
return route.fulfill({ json: { state: "ready", currentVersion: installed ? "2.0.12" : "2.0.11",
latestVersion: "2.0.12", minimumVersion: "2.0.7", recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "host",
canUpgrade: !installed, updateAvailable: !installed, daemonVersion: restarts ? "2.0.12" : "2.0.11",
serviceState: installed && !restarts ? "restart_available" : "ready", canRestart: installed && !restarts } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
await page.getByRole("button", { name: "Update to OpenCode 2.0.12" }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(connects, 1, "rebind backend authority while retaining the supported daemon")
assert.equal(restarts, 0)
await page.getByRole("button", { name: "Restart shared service" }).waitFor()
await page.getByText(/You can keep using the running service and restart later/).waitFor()
assert.equal(await page.getByText(/The running service needs updating/).count(), 0)
await page.getByRole("button", { name: "Close", exact: true }).click()
assert.equal(await page.getByRole("button", { name: "OpenCode setup required" }).count(), 0)
await page.evaluate(() => (window as any).fixture.open())
await page.getByRole("button", { name: "Restart shared service" }).click()
await page.waitForFunction(() => !document.querySelector('[role="dialog"]'))
assert.equal(restarts, 1)
} finally { await page.close() }
})
test("configuration reload is an informed explicit action and never runs on setup reads", async () => {
const page = await browser.newPage()
let reloads = 0
await page.route("**/api/**", route => {
if (route.request().url().endsWith("/api/opencode/service")) {
assert.deepEqual(route.request().postDataJSON(), { reload: true })
reloads++
}
return route.fulfill({ json: { state: "ready", currentVersion: "2.0.11", latestVersion: "2.0.11",
minimumVersion: "2.0.7", recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "wsl", canUpgrade: false,
serviceState: "ready", canRestart: false, canReload: true, daemonVersion: "2.0.11" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
await page.getByText("Troubleshooting", { exact: true }).click()
await page.getByText(/cancels pending permissions and forms/).waitFor()
assert.equal(reloads, 0)
await page.getByRole("button", { name: "Reload OpenCode configuration" }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(reloads, 1)
assert.equal(await page.getByRole("dialog").count(), 0)
} finally { await page.close() }
})
test("activation failure after installation retains the installed version and retries connection", async () => {
const page = await browser.newPage()
let installed = false, connected = false, installations = 0, attempts = 0
await page.route("**/api/**", route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
installed = true; installations++
return route.fulfill({ json: { success: true, version: "2.0.11" } })
}
if (request.url().endsWith("/api/opencode/service")) {
attempts++
if (attempts === 1) return route.fulfill({ status: 502, json: { error: "service_activation_failed" } })
connected = true
}
return route.fulfill({ json: { state: installed ? "ready" : "missing", currentVersion: installed ? "2.0.11" : null,
latestVersion: "2.0.11", minimumVersion: "2.0.7", recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "host",
canUpgrade: !installed, canRestart: false, serviceState: connected ? "ready" : "stopped" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByRole("button", { name: "Install and start OpenCode" }).click()
await page.getByText("The OpenCode action could not be completed. Check the current status above and retry.").waitFor()
assert.equal(await page.getByRole("button", { name: "Install and start OpenCode" }).count(), 0)
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
await page.getByRole("button", { name: "Connect and continue" }).click()
await page.getByText("OpenCode is connected.", { exact: true }).waitFor()
assert.equal(await page.getByRole("button", { name: "Connect and continue" }).count(), 0)
assert.equal(installations, 1)
assert.equal(attempts, 2)
} finally { await page.close() }
})
test("settings show management first, executable selection next, disclosures collapsed and logs last", async () => {
const page = await browser.newPage({ viewport: { width: 1000, height: 800 } })
const errors: string[] = []
page.on("pageerror", error => errors.push(error.message))
let reads = 0, starts = 0, reloads = 0
let holdCheck = false, releaseCheck: (() => void) | undefined
let releaseReload: (() => void) | undefined
const status = { state: "ready", currentVersion: "2.0.11", latestVersion: "2.0.11", minimumVersion: "2.0.7",
recommendedVersion: "2.0.11", binaryPath: "C:/fixture/opencode.exe", target: "host", canUpgrade: false,
serviceState: "ready", daemonVersion: "2.0.11", canRestart: false, canReload: true }
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/storage/binaries/validate")) return route.fulfill({ json: { valid: true, version: "2.0.11" } })
if (request.url().endsWith("/api/opencode/service")) {
if (request.postDataJSON().reload) {
reloads++
await new Promise(resolve => { releaseReload = resolve })
} else starts++
} else if (request.url().includes("/api/opencode/")) {
reads++
if (holdCheck) await new Promise(resolve => { releaseCheck = resolve })
}
return route.fulfill({ json: status })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByText("OpenCode is connected.", { exact: true }).waitFor()
assert.equal(await page.getByRole("dialog").count(), 0)
assert.equal(await page.getByText("Minimum version", { exact: true }).isVisible(), false)
assert.equal(await page.getByRole("button", { name: "Connect and continue" }).count(), 0)
assert.equal(await page.getByRole("button", { name: "Manage OpenCode…", exact: true }).count(), 0)
const panel = page.locator(".opencode-setup-panel")
assert.equal(await panel.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).isVisible(), false)
assert.equal(await page.locator("details").count(), 2)
assert.equal(await page.locator("details[open]").count(), 0)
const cards = page.locator("main .settings-section-stack > .settings-card")
// Version and service management first, then the executable selector without a separate Runtime header.
await cards.first().locator(".opencode-setup-panel").waitFor()
await cards.nth(1).locator(".selector-input").waitFor()
assert.equal(await cards.nth(1).locator(".settings-card-header").count(), 0)
await cards.nth(1).getByText("OpenCode Binary", { exact: true }).waitFor()
assert.equal(await page.getByRole("heading", { name: "Runtime", exact: true }).count(), 0)
assert.equal(await cards.last().getByRole("heading", { name: "OpenCode Log Level", exact: true }).count(), 1, "log levels stay below all runtime controls and disclosures")
await cards.last().locator(".selector-trigger").waitFor()
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-settings-summary.png"), fullPage: true })
await page.locator(".selector-input").fill("C:/fixture/custom-opencode.exe")
await page.getByRole("button", { name: "Add", exact: true }).click()
await page.waitForFunction(() => (window as any).fixture.selectedBinary() === "C:/fixture/custom-opencode.exe")
assert.equal(await page.getByRole("dialog").count(), 0)
await page.getByText("Version details", { exact: true }).click()
await page.getByText("Minimum recommended version", { exact: true }).waitFor()
await page.getByText("Version details", { exact: true }).click()
const before = reads
holdCheck = true
await panel.getByRole("button", { name: "Check status and updates" }).click()
await panel.getByRole("button", { name: "Reading installed version..." }).waitFor()
assert.equal(await panel.getByRole("button", { name: "Reading installed version..." }).isDisabled(), true)
await page.waitForTimeout(30)
holdCheck = false; releaseCheck?.()
await panel.getByText("Status and update check complete.").waitFor()
assert.ok(reads > before)
await panel.getByText("Troubleshooting", { exact: true }).click()
await panel.getByText(/cancels pending permissions and forms/).waitFor()
await panel.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.locator(".opencode-setup-progress").getByText("Reloading OpenCode configuration…", { exact: true }).waitFor()
await page.waitForFunction(() => document.querySelector('[aria-busy="true"]'))
// Wait for the request to reach the route before releasing its result.
for (let count = 0; count < 100 && !releaseReload; count++) await page.waitForTimeout(10)
assert.equal(reloads, 1)
releaseReload?.()
await panel.getByText("OpenCode configuration reloaded.").waitFor()
assert.equal(starts, 0, "already-connected management does not start the service")
assert.equal(await page.getByRole("dialog").count(), 0)
assert.deepEqual(errors, [])
} finally { releaseCheck?.(); releaseReload?.(); await page.close() }
})
test("French settings remain readable at narrow widths and failed checks recover visibly", async () => {
const page = await browser.newPage({ viewport: { width: 420, height: 800 } })
let fail = false
await page.route("**/api/**", route => route.fulfill(fail
? { status: 503, json: { error: "unavailable" } }
: { json: { state: "ready", currentVersion: "2.0.11", latestVersion: "2.0.11", minimumVersion: "2.0.7",
recommendedVersion: "2.0.11", binaryPath: "C:/fixture/opencode.exe", target: "host", canUpgrade: false,
serviceState: "ready", daemonVersion: "2.0.11", canReload: true } }))
try {
await page.goto(`${url}?settings=1&locale=fr&theme=dark`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode est connecté.", { exact: true }).waitFor()
await page.locator(".selector-input").waitFor()
assert.equal(await page.getByRole("dialog").count(), 0)
assert.equal(await page.locator("details[open]").count(), 0)
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-settings-fr-narrow.png"), fullPage: true })
assert.equal(await page.locator("main").evaluate(element => element.scrollWidth <= element.clientWidth), true)
await panel.getByText("Détails des versions", { exact: true }).click()
await panel.getByText("Version minimale recommandée", { exact: true }).waitFor()
fail = true
await panel.getByRole("button", { name: "Vérifier l’état et les mises à jour" }).click()
await panel.getByRole("alert").waitFor()
assert.equal(await panel.getByText("OpenCode est connecté.", { exact: true }).count(), 0, "a failed read cannot retain connected as current status")
await panel.getByText("Dépannage", { exact: true }).click()
assert.equal(await panel.getByRole("button", { name: "Recharger la configuration OpenCode", exact: true }).isDisabled(), true)
fail = false
await panel.getByRole("button", { name: "Vérifier l’état et les mises à jour" }).click()
await panel.getByText("Vérification de l’état et des mises à jour terminée.").waitFor()
assert.equal(await panel.getByRole("alert").count(), 0)
} finally { await page.close() }
})
const healthyStatus = { state: "ready", currentVersion: "2.0.16", latestVersion: "2.0.16", minimumVersion: "2.0.7",
recommendedVersion: "2.0.15", binaryPath: "opencode2", target: "host", canUpgrade: false, canRestart: false,
serviceState: "ready", daemonVersion: "2.0.16" }
for (const surface of ["settings", "recovery"]) test(`${surface} keeps same-version restart in troubleshooting, confirmed and separate from reload`, async () => {
const page = await browser.newPage()
const mutations: unknown[] = [], errors: string[] = []
let release: (() => void) | undefined
page.on("pageerror", error => errors.push(error.message))
await page.route("**/api/**", async route => {
const request = route.request()
if (request.method() === "POST" && /\/api\/opencode\/(service|update)$/.test(request.url())) {
assert.ok(request.url().endsWith("/api/opencode/service"), "maintenance must not install an update")
mutations.push(request.postDataJSON())
if (request.postDataJSON().restart) await new Promise(resolve => { release = resolve })
}
return route.fulfill({ json: { ...healthyStatus, canRestart: true, canReload: true,
latestVersion: null, checkError: "update_check_failed", target: surface === "recovery" ? "wsl" : "host" } })
})
try {
await page.goto(`${url}${surface === "settings" ? "?settings=1" : ""}`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
if (surface === "recovery") await page.evaluate(() => (window as any).fixture.open())
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
assert.equal(await panel.getByRole("button", { name: "Restart shared service", exact: true }).count(), 0, "no update-only shortcut on a healthy same-version service")
await panel.getByText("Troubleshooting", { exact: true }).click()
const restart = panel.getByRole("button", { name: "Restart OpenCode service", exact: true })
assert.equal(await restart.isEnabled(), true, "registry availability is not restart authority")
await restart.click()
const confirm = page.getByRole("dialog", { name: "Restart OpenCode service", exact: true })
await confirm.getByText(/interrupts active work in all connected clients/).waitFor()
assert.deepEqual(mutations, [])
await confirm.getByRole("button", { name: "Cancel", exact: true }).click()
assert.deepEqual(mutations, [])
await restart.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
assert.equal(await confirm.count(), 1)
await confirm.getByRole("button", { name: "Restart OpenCode service", exact: true }).click()
await page.waitForFunction(() => document.querySelector(".opencode-setup-progress"))
assert.equal(await restart.isDisabled(), true)
for (let count = 0; count < 100 && !release; count++) await page.waitForTimeout(10)
assert.ok(release)
release()
await page.waitForFunction(() => !document.querySelector(".opencode-setup-progress"))
assert.deepEqual(mutations, [{ restart: true }])
assert.equal(await restart.isEnabled(), true, "restart remains available without another update")
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0, "troubleshooting never resumes a workspace-open continuation")
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, `opencode-troubleshooting-${surface}.png`), fullPage: true })
await panel.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.waitForFunction(() => !document.querySelector(".opencode-setup-progress"))
assert.deepEqual(mutations, [{ restart: true }, { reload: true }])
assert.deepEqual(errors, [])
} finally { release?.(); await page.close() }
})
test("troubleshooting remains visible with restart disabled when lifecycle authority is unavailable", async () => {
const page = await browser.newPage({ viewport: { width: 390, height: 800 } })
let fail = false, mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill(fail ? { status: 503, json: { error: "unavailable" } }
: { json: { ...healthyStatus, canRestart: false, canReload: false } })
})
try {
await page.goto(`${url}?settings=1&locale=fr&theme=dark`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode est connecté.", { exact: true }).waitFor()
await panel.getByText("Dépannage", { exact: true }).click()
const restart = panel.getByRole("button", { name: "Redémarrer le service OpenCode", exact: true })
assert.equal(await restart.isDisabled(), true)
assert.equal(await panel.getByRole("button", { name: "Recharger la configuration OpenCode", exact: true }).count(), 0)
assert.equal(await page.locator("main").evaluate(element => element.scrollWidth <= element.clientWidth), true)
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-troubleshooting-fr-narrow.png"), fullPage: true })
fail = true
await panel.getByRole("button", { name: "Vérifier l’état et les mises à jour", exact: true }).click()
await panel.getByRole("alert").waitFor()
assert.equal(await restart.isDisabled(), true)
assert.equal(mutations, 0)
} finally { await page.close() }
})
test("superseding a restart confirmation cancels it and unlocks maintenance without a mutation", async () => {
for (const replacement of ["alert", "confirm", "prompt"]) {
const page = await browser.newPage()
let mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill({ json: { ...healthyStatus, canRestart: true, canReload: true } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
await panel.getByText("Troubleshooting", { exact: true }).click()
const restart = panel.getByRole("button", { name: "Restart OpenCode service", exact: true })
await restart.click()
await page.getByRole("dialog", { name: "Restart OpenCode service", exact: true }).waitFor()
await page.evaluate(type => (window as any).fixture.replaceDialog(type), replacement)
const next = page.getByRole("dialog", { name: "Fixture replacement", exact: true })
await next.waitFor()
await next.getByRole("button", { name: replacement === "alert" ? "OK" : "Cancel", exact: true }).click()
await next.waitFor({ state: "hidden" })
await page.waitForFunction(() => [...document.querySelectorAll(".opencode-setup-panel button")]
.filter(button => /^(Restart OpenCode service|Reload OpenCode configuration|Check status and updates)$/.test(button.textContent ?? ""))
.every(button => !(button as HTMLButtonElement).disabled))
assert.equal(await restart.isEnabled(), true)
assert.equal(await panel.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).isEnabled(), true)
assert.equal(await panel.getByRole("button", { name: "Check status and updates", exact: true }).isEnabled(), true)
assert.equal(mutations, 0)
// Superseding a prompt must also settle its caller, without treating the
// replacement's dismissal as an affirmative answer to the old dialog.
await page.evaluate(() => (window as any).fixture.replaceDialog("prompt"))
await page.getByRole("dialog", { name: "Fixture replacement", exact: true }).waitFor()
await page.evaluate(() => (window as any).fixture.replaceDialog("alert"))
await page.waitForFunction(() => (window as any).fixture.replacementResult() === null)
await page.getByRole("dialog", { name: "Fixture replacement", exact: true }).getByRole("button", { name: "OK", exact: true }).click()
await restart.click()
await page.getByRole("dialog", { name: "Restart OpenCode service", exact: true }).getByRole("button", { name: "Cancel", exact: true }).click()
assert.equal(mutations, 0)
} finally { await page.close() }
}
})
test("a changed executable or lost restart capability fences a pending confirmation", async () => {
for (const change of ["binary", "capability"]) {
const page = await browser.newPage()
let permitted = true, mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill({ json: { ...healthyStatus, canRestart: permitted } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
await panel.getByText("Troubleshooting", { exact: true }).click()
await panel.getByRole("button", { name: "Restart OpenCode service", exact: true }).click()
const confirm = page.getByRole("dialog", { name: "Restart OpenCode service", exact: true })
await confirm.waitFor()
if (change === "binary") {
await page.evaluate(() => (window as any).fixture.selectBinary("/replacement/opencode"))
await page.waitForFunction(() => (window as any).fixture.selectedBinary() === "/replacement/opencode")
} else {
permitted = false
await page.evaluate(() => (window as any).fixture.refresh())
}
await confirm.getByRole("button", { name: "Restart OpenCode service", exact: true }).click()
await confirm.waitFor({ state: "hidden" })
assert.equal(mutations, 0)
} finally { await page.close() }
}
})
test("settings troubleshooting restart retains a dismissed recovery continuation for explicit Continue", async () => {
const page = await browser.newPage()
let restarts = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) {
assert.deepEqual(route.request().postDataJSON(), { restart: true })
restarts++
}
return route.fulfill({ json: { ...healthyStatus, canRestart: true } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
await page.evaluate(() => (window as any).fixture.open())
await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).click()
await panel.getByText("Troubleshooting", { exact: true }).click()
await panel.getByRole("button", { name: "Restart OpenCode service", exact: true }).click()
await page.getByRole("dialog", { name: "Restart OpenCode service", exact: true }).getByRole("button", { name: "Restart OpenCode service", exact: true }).click()
await page.waitForFunction(() => !document.querySelector(".opencode-setup-progress"))
assert.equal(restarts, 1)
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
await page.evaluate(() => (window as any).fixture.reopen())
await page.getByRole("dialog").getByRole("button", { name: "Continue", exact: true }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(restarts, 1)
} finally { await page.close() }
})
test("instance info explicitly confirms a global V2 reload and never calls the retired dispose endpoint", async () => {
const page = await browser.newPage()
const mutations: Array<{ url: string; body: unknown }> = []
let release!: () => void
await page.route("**/*", async route => {
const request = route.request()
if (!request.url().includes("/api/") && !request.url().includes("instance/dispose")) return route.continue()
if (request.method() === "POST" && (request.url().endsWith("/api/opencode/service") || request.url().includes("instance/dispose"))) {
mutations.push({ url: new URL(request.url()).pathname, body: request.postDataJSON() })
await new Promise(resolve => { release = resolve })
}
return route.fulfill({ json: { ...healthyStatus, canReload: true } })
})
try {
await page.goto(`${url}?info=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
const reload = page.getByRole("button", { name: "Reload OpenCode configuration", exact: true })
await reload.click()
const dialog = page.getByRole("dialog")
await dialog.getByText(/every loaded location for all connected clients/).waitFor()
await dialog.getByText(/cancels pending permissions and forms/).waitFor()
assert.equal(mutations.length, 0)
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
assert.equal(mutations.length, 0)
await reload.click()
await dialog.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.getByRole("button", { name: "Reloading OpenCode configuration…", exact: true }).waitFor()
assert.equal(await page.getByRole("button", { name: "Reloading OpenCode configuration…", exact: true }).isDisabled(), true)
for (let count = 0; count < 100 && !release; count++) await page.waitForTimeout(10)
assert.ok(release)
release()
await page.waitForFunction(() => (window as any).fixture.notifications().some((item: any) => item.variant === "success"))
assert.deepEqual(mutations, [{ url: "/api/opencode/service", body: { reload: true } }])
assert.equal(await page.getByRole("button", { name: "Dispose instance", exact: true }).count(), 0)
assert.equal(await reload.isEnabled(), true)
} finally { release?.(); await page.close() }
})
test("instance reload failure reports an error without replaying the mutation and permits explicit retry", async () => {
const page = await browser.newPage()
let attempts = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && route.request().url().endsWith("/api/opencode/service")) {
assert.deepEqual(route.request().postDataJSON(), { reload: true })
if (++attempts === 1) return route.fulfill({ status: 502, json: { error: "service_activation_failed" } })
}
return route.fulfill({ json: { ...healthyStatus, canReload: true } })
})
try {
await page.goto(`${url}?info=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
for (const outcome of ["error", "success"]) {
await page.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.getByRole("dialog").getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.waitForFunction(outcome => (window as any).fixture.notifications()[0]?.variant === outcome, outcome)
assert.equal(attempts, outcome === "error" ? 1 : 2)
}
} finally { await page.close() }
})
test("Info reload preserves a dismissed recovery continuation without opening its pending workspace", async () => {
const page = await browser.newPage()
let reloads = 0
await page.route("**/api/**", route => {
if (route.request().method() === "POST" && route.request().url().endsWith("/api/opencode/service")) {
assert.deepEqual(route.request().postDataJSON(), { reload: true })
reloads++
}
return route.fulfill({ json: { ...healthyStatus, canReload: true } })
})
try {
await page.goto(`${url}?info=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).click()
await page.getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.getByRole("dialog").getByRole("button", { name: "Reload OpenCode configuration", exact: true }).click()
await page.waitForFunction(() => (window as any).fixture.notifications()[0]?.variant === "success")
assert.equal(reloads, 1)
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
assert.equal(await page.getByRole("dialog").count(), 0)
await page.evaluate(() => (window as any).fixture.reopen())
await page.getByRole("dialog").getByRole("button", { name: "Continue", exact: true }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(reloads, 1, "explicit recovery retains the callback without another reload")
} finally { await page.close() }
})
test("Continue rechecks a service stopped after the recovery screen opened and preserves the workspace retry", async () => {
const page = await browser.newPage()
let stopped = false, mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() !== "GET" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill({ json: { ...healthyStatus, serviceState: stopped ? "stopped" : "ready" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
const proceed = page.getByRole("button", { name: "Continue", exact: true })
await proceed.waitFor()
stopped = true
await proceed.click()
await page.getByText("OpenCode is not connected.", { exact: true }).waitFor()
assert.equal(await page.evaluate(() => (window as any).fixture.resumed()), 0)
assert.equal(await page.getByRole("dialog").count(), 1)
stopped = false
await page.getByRole("button", { name: "Check status and updates" }).click()
await proceed.click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(mutations, 0)
} finally { await page.close() }
})
test("refresh events during a read coalesce into a trailing check before reporting success", async () => {
const page = await browser.newPage()
let held = false, reads = 0, release!: () => void
await page.route("**/api/**", async route => {
const stale = held
if (held) {
reads++
await new Promise(resolve => { release = resolve })
} else if (reads) reads++
return route.fulfill({ json: { ...healthyStatus, serviceState: stale || !reads ? "ready" : "stopped" } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByText("OpenCode is connected.", { exact: true }).waitFor()
held = true
await page.getByRole("button", { name: "Check status and updates" }).click()
await page.waitForFunction(() => document.querySelector('[aria-busy="true"]'))
for (let count = 0; count < 100 && !release; count++) await page.waitForTimeout(10)
assert.ok(release)
await page.evaluate(() => { for (let i = 0; i < 10; i++) void (window as any).fixture.refresh() })
held = false; release()
await page.getByText("Status and update check complete.", { exact: true }).waitFor()
await page.getByText("OpenCode is not connected.", { exact: true }).waitFor()
assert.equal(reads, 2, "a burst queues one follow-up rather than losing the event or issuing ten checks")
} finally { held = false; release?.(); await page.close() }
})
test("a failed pre-action status read blocks activation and retains a visible connection retry", async () => {
const page = await browser.newPage()
let fail = false, mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() !== "GET" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill(fail ? { status: 503, json: { error: "unavailable" } }
: { json: { ...healthyStatus, serviceState: "stopped" } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByText("OpenCode is not connected.", { exact: true }).waitFor()
fail = true
await page.getByRole("button", { name: "Connect and continue", exact: true }).click()
await page.getByRole("alert").waitFor()
assert.equal(mutations, 0)
assert.equal(await page.getByRole("button", { name: "Connect and continue", exact: true }).isEnabled(), true)
fail = false
await page.getByRole("button", { name: "Check status and updates" }).click()
await page.getByText("Status and update check complete.", { exact: true }).waitFor()
assert.equal(await page.getByRole("alert").count(), 0)
} finally { await page.close() }
})
test("a service discovery error is not announced as a successful check", async () => {
const page = await browser.newPage()
await page.route("**/api/**", route => route.fulfill({ json: { ...healthyStatus,
serviceState: "error", serviceError: "service_check_failed" } }))
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByRole("alert").waitFor()
await page.getByRole("button", { name: "Check status and updates" }).click()
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'))
assert.equal(await page.getByText("Status and update check complete.", { exact: true }).count(), 0)
assert.equal(await page.getByText("OpenCode is connected.", { exact: true }).count(), 0)
} finally { await page.close() }
})
test("changing executable fences an old read and its queued follow-up", async () => {
const page = await browser.newPage()
let hold = false, changed = false, release!: () => void, reads = 0
await page.route("**/api/**", async route => {
const status = { ...healthyStatus, binaryPath: changed ? "new-opencode" : "old-opencode", serviceState: changed ? "stopped" : "ready" }
if (hold) { reads++; await new Promise(resolve => { release = resolve }) }
else if (reads) reads++
return route.fulfill({ json: status })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByText("OpenCode is connected.", { exact: true }).waitFor()
hold = true
await page.getByRole("button", { name: "Check status and updates" }).click()
for (let count = 0; count < 100 && !release; count++) await page.waitForTimeout(10)
assert.ok(release)
await page.evaluate(() => { void (window as any).fixture.refresh() })
hold = false; changed = true
await page.evaluate(() => (window as any).fixture.invalidate())
await page.getByText("OpenCode is not connected.", { exact: true }).waitFor()
const oldRead = page.waitForResponse(response => response.url().includes("/api/opencode/update"))
release()
await oldRead
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 30)))
assert.equal(await page.getByText("old-opencode", { exact: true }).count(), 0)
assert.equal(await page.getByText("OpenCode is connected.", { exact: true }).count(), 0)
assert.equal(reads, 2)
} finally { hold = false; release?.(); await page.close() }
})
test("continuing an already-connected recovery retries only the pending workspace", async () => {
const page = await browser.newPage()
let mutations = 0
await page.route("**/api/**", route => {
if (route.request().method() !== "GET" && /\/api\/opencode\/(service|update)$/.test(route.request().url())) mutations++
return route.fulfill({ json: { state: "ready", currentVersion: "2.0.11", latestVersion: "2.0.11", minimumVersion: "2.0.7",
recommendedVersion: "2.0.11", binaryPath: "opencode2", target: "host", canUpgrade: false, serviceState: "ready", daemonVersion: "2.0.11" } })
})
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.evaluate(() => (window as any).fixture.open())
await page.getByRole("button", { name: "Continue", exact: true }).click()
await page.waitForFunction(() => (window as any).fixture.resumed() === 1)
assert.equal(mutations, 0)
assert.equal(await page.getByRole("dialog").count(), 0)
} finally { await page.close() }
})
test("settings expose and install an optional update directly, with retry feedback and no implicit restart", async () => {
const page = await browser.newPage({ viewport: { width: 1000, height: 800 } })
let installed = false, attempts = 0, starts = 0
let releaseInstall: (() => void) | undefined
await page.route("**/api/**", async route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
attempts++
if (attempts === 1) return route.fulfill({ status: 502, json: { error: "upgrade_failed" } })
await new Promise(resolve => { releaseInstall = resolve })
installed = true
return route.fulfill({ json: { success: true, version: "2.0.11" } })
}
if (request.url().endsWith("/api/opencode/service")) {
assert.equal(request.postDataJSON().restart, false)
starts++
}
return route.fulfill({ json: { state: "ready", currentVersion: installed ? "2.0.11" : "2.0.9",
latestVersion: "2.0.11", updateAvailable: !installed, canUpgrade: !installed,
minimumVersion: "2.0.7", recommendedVersion: "2.0.11", versionAssessment: "tested",
binaryPath: "opencode2", target: "host", daemonVersion: "2.0.9",
serviceState: installed ? "restart_available" : "ready", canRestart: installed } })
})
try {
await page.goto(`${url}?settings=1&locale=fr&theme=dark`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as any).fixture))
await page.getByText("OpenCode 2.0.11 est disponible.", { exact: true }).waitFor()
const update = page.getByRole("button", { name: "Mettre à jour vers OpenCode 2.0.11", exact: true })
await update.waitFor()
assert.equal(await page.getByRole("dialog").count(), 0)
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-settings-update.png") })
await update.click()
await page.getByRole("alert").waitFor()
assert.equal(await page.getByRole("dialog").count(), 0)
await update.click()
await page.locator(".opencode-setup-panel button:disabled:visible").filter({ hasText: "OpenCode" }).waitFor()
for (let count = 0; count < 100 && !releaseInstall; count++) await page.waitForTimeout(10)
assert.ok(releaseInstall)
releaseInstall()
await page.getByText("OpenCode est à jour.", { exact: true }).waitFor()
await page.waitForFunction(() => !document.querySelector('[role="alert"]'))
assert.equal(attempts, 2)
assert.equal(starts, 1)
assert.equal(await update.count(), 0)
assert.equal(await page.getByRole("dialog").count(), 0)
await page.getByText(/Vous pouvez continuer avec le service/).waitFor()
} finally { releaseInstall?.(); await page.close() }
})
test("settings distinguish manual updates and failed registry checks from an up-to-date installation", async () => {
const page = await browser.newPage()
let offline = false
await page.route("**/api/**", route => route.fulfill({ json: { state: "ready", currentVersion: "2.0.9",
latestVersion: offline ? null : "2.0.11", updateAvailable: offline ? null : true, canUpgrade: false,
checkError: offline ? "update_check_failed" : undefined, minimumVersion: "2.0.7", recommendedVersion: "2.0.11",
binaryPath: "custom-opencode", target: "host", serviceState: "ready", daemonVersion: "2.0.9" } }))
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
await page.getByText("OpenCode 2.0.11 is available.", { exact: true }).waitFor()
await page.getByText(/Select a supported executable/).waitFor()
assert.equal(await page.getByRole("button", { name: "Update to OpenCode 2.0.11" }).count(), 0)
offline = true
await page.reload({ waitUntil: "domcontentloaded" })
await page.getByText("Could not read the installed OpenCode version.", { exact: true }).waitFor()
assert.equal(await page.getByText("OpenCode is up to date.", { exact: true }).count(), 0)
assert.equal(await page.getByText("OpenCode 2.0.11 is available.", { exact: true }).count(), 0)
} finally { await page.close() }
})
test("OpenCode 1.x npm replacement is confirmed explicitly and a failure after removal stays retryable", async () => {
const page = await browser.newPage({ viewport: { width: 420, height: 900 } })
const posts: unknown[] = []
let phase: "legacy" | "removed" | "installed" = "legacy"
await page.route("**/api/**", route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
posts.push(request.postDataJSON())
if (phase === "legacy") { phase = "removed"; return route.fulfill({ status: 502, json: { success: false, error: "legacy_removed_install_failed" } }) }
phase = "installed"
return route.fulfill({ json: { success: true, version: "2.0.30" } })
}
return route.fulfill({ json: {
state: phase === "installed" ? "ready" : phase === "legacy" ? "update_required" : "missing",
currentVersion: phase === "installed" ? "2.0.30" : phase === "legacy" ? "1.18.10" : null, latestVersion: "2.0.30",
updateAvailable: phase !== "installed", canUpgrade: phase !== "installed", minimumVersion: "2.0.27", recommendedVersion: "2.0.29",
binaryPath: phase === "removed" ? "opencode2" : "C:/Users/fixture/AppData/Roaming/npm/opencode.cmd", installationSource: phase === "removed" ? undefined : "path",
...(phase === "legacy" ? { legacyReplacement: { path: "C:/Users/fixture/AppData/Roaming/npm/opencode.cmd", version: "1.18.10" } } : {}),
versionAssessment: phase === "installed" ? "untested" : "incompatible", target: "host", canRestart: false,
serviceState: phase === "installed" ? "ready" : undefined, daemonVersion: phase === "installed" ? "2.0.30" : undefined,
} })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
const replace = panel.getByRole("button", { name: "Replace OpenCode 1.18.10 with OpenCode 2", exact: true })
await replace.waitFor()
await panel.getByText(/OpenCode 1\.18\.10 was installed with npm/).waitFor()
assert.equal(await panel.getByText(/Select a supported executable/).count(), 0, "a replaceable npm installation is never a manual dead end")
await replace.click()
const dialog = page.getByRole("dialog", { name: "Replace OpenCode 1.18.10?", exact: true })
await dialog.getByText(/imports OpenCode 1\.x conversations itself/).waitFor()
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-legacy-confirm.png") })
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await dialog.waitFor({ state: "hidden" })
assert.deepEqual(posts, [], "dismissing the confirmation never installs")
await replace.click()
await dialog.getByRole("button", { name: "Replace OpenCode 1.18.10 with OpenCode 2", exact: true }).click()
await page.getByRole("alert").filter({ hasText: /OpenCode 1\.x was removed, but OpenCode 2 could not be installed/ }).waitFor()
const consent = { replaceLegacy: { path: "C:/Users/fixture/AppData/Roaming/npm/opencode.cmd", version: "1.18.10" } }
assert.deepEqual(posts, [consent], "the confirmation names the exact installation shown")
const retry = panel.getByRole("button", { name: "Install and start OpenCode", exact: true })
await retry.click()
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
assert.deepEqual(posts, [consent, {}], "retry is an ordinary installation, never an automatic replay")
assert.equal(await page.getByRole("alert").count(), 0)
} finally { await page.close() }
})
test("an older winget OpenCode stays untouched while OpenCode 2 is installed beside it", async () => {
const page = await browser.newPage()
const posts: unknown[] = []
const statusReads: string[] = []
let installed = false
const foreignInstallation = { path: "C:/Users/fixture/AppData/Local/Microsoft/WinGet/Links/opencode.exe", version: "1.19.0", source: "winget", packageId: "SST.opencode" }
await page.route("**/api/**", route => {
const request = route.request()
if (request.method() === "GET" && new URL(request.url()).pathname === "/api/opencode/update") statusReads.push(new URL(request.url()).search)
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
posts.push(request.postDataJSON()); installed = true
return route.fulfill({ json: { success: true, version: "2.0.30" } })
}
return route.fulfill({ json: { state: installed ? "ready" : "update_required", currentVersion: installed ? "2.0.30" : "1.19.0",
latestVersion: "2.0.30", updateAvailable: !installed, canUpgrade: !installed, minimumVersion: "2.0.27", recommendedVersion: "2.0.29",
binaryPath: installed ? "C:/Users/fixture/AppData/Roaming/npm/node_modules/@opencode/cli/bin/opencode.exe" : foreignInstallation.path,
installationSource: installed ? "managed" : "path", foreignInstallation, versionAssessment: installed ? "untested" : "incompatible",
target: "host", canRestart: false, serviceState: installed ? "ready" : undefined, daemonVersion: installed ? "2.0.30" : undefined } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
const install = panel.getByRole("button", { name: "Install OpenCode 2 for CodeNomad", exact: true })
await install.waitFor()
const notice = panel.getByRole("note")
await notice.getByText(/OpenCode 1\.19\.0 was found at .*WinGet.*requires OpenCode 2\.0\.27 or later and does not modify this installation/).waitFor()
await notice.getByText("This older version was installed with winget.", { exact: true }).waitFor()
await notice.getByText(/alongside the older version/).waitFor()
await notice.getByText(/may still start the older version/).waitFor()
assert.equal(await notice.locator("code").innerText(), "winget uninstall SST.opencode")
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-winget-alongside.png"), fullPage: true })
assert.ok(statusReads.length > 0 && statusReads.every(search => search === ""), "automatic reads use cached server probes")
await panel.getByRole("button", { name: "Check status and updates", exact: true }).click()
await panel.getByText("Status and update check complete.", { exact: true }).waitFor()
assert.equal(statusReads.at(-1), "?recheck=1", "the explicit Check re-probes installations")
const automaticReads = statusReads.length
await install.click()
await panel.getByText("OpenCode is connected.", { exact: true }).waitFor()
assert.equal(await page.getByRole("dialog").count(), 0, "installing beside a foreign OpenCode needs no removal confirmation")
assert.deepEqual(posts, [{}])
assert.ok(statusReads.slice(automaticReads).every(search => search === ""), "post-install reads never force a probe")
await panel.getByText(/uses its OpenCode 2 installation in your user npm folder/).waitFor()
assert.equal(await notice.getByText(/alongside the older version/).count(), 0)
await notice.getByText(/may still start the older version/).waitFor()
assert.equal(await install.count(), 0)
} finally { await page.close() }
})
test("a broken OpenCode 1.x npm install stays replaceable and a partial removal is reported truthfully", async () => {
const page = await browser.newPage()
const posts: unknown[] = []
const legacyReplacement = { path: "C:/Users/fixture/AppData/Roaming/npm/opencode.cmd", version: "1.18.10" }
await page.route("**/api/**", route => {
const request = route.request()
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
posts.push(request.postDataJSON())
return route.fulfill({ status: 502, json: { success: false, error: "legacy_partially_removed" } })
}
// The 1.x placeholder executable fails `--version`, so the probe reports an error state.
return route.fulfill({ json: { state: "error", currentVersion: null, latestVersion: "2.0.30", updateAvailable: true, canUpgrade: true,
minimumVersion: "2.0.27", recommendedVersion: "2.0.29", binaryPath: legacyReplacement.path, installationSource: "path", legacyReplacement,
versionAssessment: "untested", target: "host", canRestart: false } })
})
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
await panel.getByRole("button", { name: "Replace OpenCode 1.18.10 with OpenCode 2", exact: true }).click()
await page.getByRole("dialog", { name: "Replace OpenCode 1.18.10?", exact: true })
.getByRole("button", { name: "Replace OpenCode 1.18.10 with OpenCode 2", exact: true }).click()
await page.getByRole("alert").filter({ hasText: /may be only partly removed.*npm uninstall -g opencode-ai/ }).waitFor()
assert.deepEqual(posts, [{ replaceLegacy: legacyReplacement }])
} finally { await page.close() }
})
test("an outdated foreign OpenCode 2 is described by the minimum version, not as OpenCode 1.x", async () => {
const page = await browser.newPage()
await page.route("**/api/**", route => route.fulfill({ json: { state: "update_required", currentVersion: "2.0.20", latestVersion: "2.0.30",
updateAvailable: true, canUpgrade: true, minimumVersion: "2.0.27", recommendedVersion: "2.0.29", binaryPath: "/opt/homebrew/bin/opencode",
installationSource: "path", foreignInstallation: { path: "/opt/homebrew/bin/opencode", version: "2.0.20", source: "other" },
versionAssessment: "incompatible", target: "host", canRestart: false } }))
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const notice = page.locator(".opencode-setup-panel").getByRole("note")
await notice.getByText("OpenCode 2.0.20 was found at /opt/homebrew/bin/opencode. CodeNomad requires OpenCode 2.0.27 or later and does not modify this installation.", { exact: true }).waitFor()
assert.equal(await notice.getByText(/winget/).count(), 0)
} finally { await page.close() }
})
test("an older OpenCode occupying the user npm folder explains why OpenCode 2 cannot be installed beside it", async () => {
const page = await browser.newPage()
const occupying = "/home/fixture/.local/bin/opencode"
await page.route("**/api/**", route => route.fulfill({ json: { state: "update_required", currentVersion: "1.17.18", latestVersion: "2.1.2",
updateAvailable: true, canUpgrade: false, minimumVersion: "2.1.2", recommendedVersion: "2.1.2", binaryPath: occupying, installationSource: "path",
foreignInstallation: { path: occupying, version: "1.17.18", source: "other", blocked: { reason: "occupied", path: occupying } },
versionAssessment: "incompatible", target: "host", canRestart: false } }))
try {
await page.goto(`${url}?settings=1`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const panel = page.locator(".opencode-setup-panel")
const notice = panel.getByRole("note")
await notice.getByText(`OpenCode 2 cannot be installed for CodeNomad while ${occupying} uses the opencode command name in your user npm folder. Move or rename that command, then choose Check status and updates.`, { exact: true }).waitFor()
assert.equal(await panel.getByRole("button", { name: "Install OpenCode 2 for CodeNomad", exact: true }).count(), 0)
assert.equal(await notice.getByText(/alongside the older version/).count(), 0)
await panel.getByRole("button", { name: "Check status and updates", exact: true }).waitFor()
} finally { await page.close() }
})
test("user npm PATH repair is available at the current version and installation conflicts retain explicit retry", async () => {
const page = await browser.newPage({ viewport: { width: 420, height: 900 } })
let migrated = false, attempts = 0, connects = 0
await page.route("**/api/**", route => {
const request = route.request()
if (request.url().endsWith("/api/storage/binaries/validate")) return route.fulfill({ json: { valid: true, version: "2.0.14" } })
if (request.url().endsWith("/api/opencode/update") && request.method() === "POST") {
if (++attempts === 1) return route.fulfill({ status: 409, json: { error: "installation_in_use" } })
migrated = true
return route.fulfill({ json: { success: true, version: "2.0.15" } })
}
if (request.url().endsWith("/api/opencode/service")) {
assert.equal(request.postDataJSON().restart, false)
connects++
}
return route.fulfill({ json: { state: "ready", currentVersion: "2.0.15", latestVersion: "2.0.15",
updateAvailable: false, canUpgrade: !migrated, needsSharedInstallation: !migrated,
installationSource: migrated ? "path" : "user", binaryPath: migrated ? "C:/Users/fixture/AppData/Roaming/npm/opencode2.cmd" : "C:/Users/fixture/AppData/Roaming/npm/node_modules/@opencode/cli/bin/opencode.exe",
minimumVersion: "2.0.7", recommendedVersion: "2.0.11", versionAssessment: "untested", target: "host",
serviceState: "ready", daemonVersion: "2.0.15", canRestart: false } })
})
try {
await page.goto(`${url}?settings=1&locale=fr&theme=dark`, { waitUntil: "domcontentloaded", timeout: 90_000 })
const install = page.getByRole("button", { name: "Installer pour l’utilisateur et configurer le PATH", exact: true })
await install.waitFor()
await page.getByText("Installation npm utilisateur trouvée hors du PATH actuel.", { exact: true }).waitFor()
assert.match(await page.locator(".selector-badge-version").innerText(), /2\.0\.15/, "selector uses the current effective version rather than its old validation cache")
assert.equal(await page.locator("main").evaluate(element => element.scrollWidth <= element.clientWidth), true)
if (process.env.CODENOMAD_SETUP_CAPTURE) await page.screenshot({ path: path.join(process.env.CODENOMAD_SETUP_CAPTURE, "opencode-shared-path-repair-fr.png"), fullPage: true })
await install.click()
await page.getByRole("alert").filter({ hasText: /L’exécutable OpenCode est utilisé/ }).waitFor()
assert.equal(connects, 0)
await install.click()
await page.getByText("Exécutable trouvé dans le PATH du serveur.", { exact: true }).waitFor()
await page.waitForFunction(() => !document.querySelector('[aria-busy="true"]'))
assert.equal(connects, 1)
assert.equal(await install.count(), 0)
assert.equal(await page.getByRole("alert").count(), 0)
} finally { await page.close() }
})