Compare commits

..

1 Commits

Author SHA1 Message Date
Kit Langton 876339cf6a fix(tui): retry migration status transport errors 2026-08-12 20:01:59 -04:00
3 changed files with 21 additions and 78 deletions
+5 -33
View File
@@ -10,7 +10,7 @@ import {
} from "../service-contender.js"
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
import { matchesVersion } from "../service-version.js"
import type { ServiceStopResponse } from "./generated/types.js"
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
export * from "../service.js"
@@ -130,15 +130,7 @@ async function read(file?: string) {
const text = await readFile(file ?? fallback(), "utf8").catch(() => undefined)
if (text === undefined) return undefined
try {
const value: unknown = JSON.parse(text)
if (typeof value !== "object" || value === null) return undefined
if (!("url" in value) || typeof value.url !== "string") return undefined
if (!("pid" in value) || !Number.isInteger(value.pid) || typeof value.pid !== "number" || value.pid <= 0)
return undefined
if ("id" in value && value.id !== undefined && typeof value.id !== "string") return undefined
if ("version" in value && value.version !== undefined && typeof value.version !== "string") return undefined
if ("password" in value && value.password !== undefined && typeof value.password !== "string") return undefined
return value as Info
return JSON.parse(text) as Info
} catch {
return undefined
}
@@ -171,7 +163,7 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
})
.then(async (response) => ({
response,
body: (await response.json()) as unknown,
body: (await response.json()) as ServiceHealth | { readonly healthy: true },
}))
.then(
(value) => ({ value }),
@@ -180,18 +172,7 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
const response = result.value.response
const body = result.value.body
if (
typeof body === "object" &&
body !== null &&
"healthy" in body &&
body.healthy === true &&
"version" in body &&
typeof body.version === "string" &&
"pid" in body &&
typeof body.pid === "number" &&
Number.isInteger(body.pid) &&
body.pid > 0
) {
if (body !== undefined && "version" in body && "pid" in body) {
if (body.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && body.version !== info.version) return { service: undefined, timedOut: false }
return {
@@ -205,16 +186,7 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
timedOut: false,
}
}
if (
!allowLegacy ||
typeof body !== "object" ||
body === null ||
!("healthy" in body) ||
body.healthy !== true ||
"version" in body ||
"pid" in body
)
return { service: undefined, timedOut: false }
if (!allowLegacy || body?.healthy !== true) return { service: undefined, timedOut: false }
return {
service: { info, endpoint, state: "ready", legacy: true } satisfies LocalService,
timedOut: false,
@@ -38,50 +38,6 @@ test("discovers a compatible registered service", async () => {
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("3.") })).toBeUndefined()
})
test("rejects malformed registrations without probing or signaling", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const malformed = [
null,
[],
{},
{ url: "http://127.0.0.1:1" },
{ url: "http://127.0.0.1:1", pid: 0 },
{ url: "http://127.0.0.1:1", pid: -1 },
{ url: "http://127.0.0.1:1", pid: 1.5 },
{ url: "http://127.0.0.1:1", pid: "1" },
{ url: "http://127.0.0.1:1", pid: 1, id: 1 },
]
for (const value of malformed) {
await Bun.write(registration, JSON.stringify(value))
expect(await Service.discover({ file: registration })).toBeUndefined()
}
})
test("rejects primitive and partial modern health responses", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const bodies = [
null,
1,
"healthy",
[],
{},
{ healthy: false, version: "test", pid: process.pid },
{ healthy: true, version: null, pid: process.pid },
{ healthy: true, version: "test", pid: "1" },
{ healthy: true, version: "test" },
{ healthy: true, pid: process.pid },
]
for (const body of bodies) {
using server = Bun.serve({ port: 0, fetch: () => Response.json(body) })
await Bun.write(registration, JSON.stringify({ url: server.url.toString(), pid: process.pid }))
expect(await Service.discover({ file: registration })).toBeUndefined()
}
})
test("ensures a missing service with native promises", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
@@ -1,3 +1,4 @@
import { ClientError } from "@opencode-ai/client"
import { createSignal, onCleanup, onMount, Show } from "solid-js"
import { useClient } from "../context/client"
import { useTheme } from "../context/theme"
@@ -16,9 +17,23 @@ export function MigrationOverlay() {
onMount(async () => {
await Bun.sleep(1_000)
if (abort.signal.aborted) return
void (async () => {
while (true) {
const status = await client.api.migration.v1.status({ signal: abort.signal })
const result = await client.api.migration.v1.status({ signal: abort.signal }).then(
(status) => ({ status }),
(error: unknown) => ({ error }),
)
if ("error" in result) {
if (result.error instanceof ClientError && result.error.reason === "Transport") {
if (abort.signal.aborted) return
await Bun.sleep(1_000)
if (abort.signal.aborted) return
continue
}
throw result.error
}
const status = result.status
setProgress(status.status === "running" ? status.progress : undefined)
if (status.status === "completed") return
if (status.status === "error") throw new Error(status.error)