mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(tui): surface remote auth startup errors (#49016)
Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7f209437b5
commit
a97622c801
@@ -303,7 +303,7 @@ export const TuiThreadCommand = cmd({
|
||||
unguard?.()
|
||||
} catch {}
|
||||
}
|
||||
process.exit(0)
|
||||
process.exit()
|
||||
},
|
||||
})
|
||||
// scratch
|
||||
|
||||
@@ -20,7 +20,8 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
||||
ConfigErrorV1.JsonError.isInstance(error) ||
|
||||
ConfigErrorV1.InvalidError.isInstance(error) ||
|
||||
ConfigErrorV1.FrontmatterError.isInstance(error) ||
|
||||
ConfigErrorV1.DirectoryTypoError.isInstance(error)
|
||||
ConfigErrorV1.DirectoryTypoError.isInstance(error) ||
|
||||
ConfigErrorV1.RemoteAuthError.isInstance(error)
|
||||
) {
|
||||
return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 400 }))
|
||||
}
|
||||
|
||||
@@ -83,6 +83,27 @@ describe("HttpApi error middleware", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("returns remote auth defects as structured client errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const configError = new ConfigErrorV1.RemoteAuthError({
|
||||
url: "https://example.com",
|
||||
remote: "https://config.example.com/opencode.json",
|
||||
})
|
||||
|
||||
yield* HttpRouter.add("GET", "/remote-auth-error", Effect.die(configError)).pipe(
|
||||
Layer.provide(errorLayer),
|
||||
HttpRouter.serve,
|
||||
Layer.build,
|
||||
)
|
||||
|
||||
const response = yield* HttpClientRequest.get("/remote-auth-error").pipe(HttpClient.execute)
|
||||
const body = yield* response.json
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(body).toEqual(configError.toObject())
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not map storage not-found defects to 404", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* HttpRouter.add(
|
||||
|
||||
@@ -356,8 +356,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
)
|
||||
yield* Effect.sync(() => {
|
||||
win32FlushInputBuffer()
|
||||
if (result.reason !== undefined)
|
||||
if (result.reason !== undefined) {
|
||||
process.stderr.write((cliErrorMessage(result.reason) ?? errorFormat(result.reason)) + "\n")
|
||||
process.exitCode = 1
|
||||
}
|
||||
if (result.epilogue) process.stdout.write(result.epilogue + "\n")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,6 +47,17 @@ export function cliErrorMessage(input: unknown): string | undefined {
|
||||
const frontmatter = configData(input, "ConfigFrontmatterError")
|
||||
if (frontmatter) return field(frontmatter, "message") ?? ""
|
||||
|
||||
const remoteAuth = configData(input, "ConfigRemoteAuthError")
|
||||
if (remoteAuth) {
|
||||
const url = field(remoteAuth, "url")
|
||||
const remote = field(remoteAuth, "remote")
|
||||
return [
|
||||
`Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`,
|
||||
"Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).",
|
||||
...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
const invalid = configData(input, "ConfigInvalidError")
|
||||
if (invalid) {
|
||||
const path = field(invalid, "path")
|
||||
|
||||
@@ -126,3 +126,65 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
|
||||
mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test("fatal startup errors set a nonzero exit after scoped cleanup", async () => {
|
||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||
const core = await import("@opentui/core")
|
||||
await mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||
const events = createEventSource()
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === "/config")
|
||||
return json(
|
||||
{
|
||||
name: "ConfigRemoteAuthError",
|
||||
data: {
|
||||
url: "https://example.com",
|
||||
remote: "https://config.example.com/opencode.json",
|
||||
},
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
return undefined
|
||||
})
|
||||
let disposes = 0
|
||||
const originalWrite = process.stderr.write.bind(process.stderr)
|
||||
const originalExitCode = process.exitCode ?? 0
|
||||
let stderr = ""
|
||||
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += String(chunk)
|
||||
return true
|
||||
}) as typeof process.stderr.write
|
||||
|
||||
try {
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
url: "http://test",
|
||||
directory,
|
||||
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||
fetch: calls.fetch,
|
||||
events: events.source,
|
||||
args: {},
|
||||
pluginHost: {
|
||||
async start() {},
|
||||
async dispose() {
|
||||
disposes++
|
||||
},
|
||||
},
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
|
||||
)
|
||||
|
||||
await task
|
||||
expect(stderr).toContain("Run `opencode auth login https://example.com` to re-authenticate.")
|
||||
expect(stderr).not.toContain("Unexpected server error")
|
||||
expect(process.exitCode).toBe(1)
|
||||
expect(setup.renderer.isDestroyed).toBe(true)
|
||||
expect(disposes).toBe(1)
|
||||
} finally {
|
||||
process.stderr.write = originalWrite
|
||||
process.exitCode = originalExitCode
|
||||
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||
mock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user