diff --git a/src/cli/doctor/checks/system-binary.test.ts b/src/cli/doctor/checks/system-binary.test.ts
new file mode 100644
index 000000000..701f54d93
--- /dev/null
+++ b/src/cli/doctor/checks/system-binary.test.ts
@@ -0,0 +1,62 @@
+///
+
+import { describe, expect, it } from "bun:test"
+import { extractSemverFromOutput } from "./system-binary"
+
+describe("extractSemverFromOutput", () => {
+ describe("#given clean version output #when extractSemverFromOutput #then returns the semver token", () => {
+ it("plain semver", () => {
+ expect(extractSemverFromOutput("1.14.33")).toBe("1.14.33")
+ })
+
+ it("v-prefixed semver strips the prefix", () => {
+ expect(extractSemverFromOutput("v1.14.33")).toBe("1.14.33")
+ })
+
+ it("trailing whitespace and newlines are tolerated", () => {
+ expect(extractSemverFromOutput(" 1.14.33\n")).toBe("1.14.33")
+ })
+
+ it("pre-release suffix is preserved", () => {
+ expect(extractSemverFromOutput("1.0.0-beta.1")).toBe("1.0.0-beta.1")
+ })
+
+ it("build metadata is preserved", () => {
+ expect(extractSemverFromOutput("1.0.0+build.42")).toBe("1.0.0+build.42")
+ })
+ })
+
+ describe("#given Electron log-polluted stdout #when extractSemverFromOutput #then ignores the timestamp and finds the version", () => {
+ it("regression for #3765: Electron desktop dumps log lines into stdout", () => {
+ const polluted = "00:24:25.202 > app starting { version: '1.14.33', packaged: true }"
+ expect(extractSemverFromOutput(polluted)).toBe("1.14.33")
+ })
+
+ it("multi-line stdout with log prefix and trailing version", () => {
+ const polluted = "12:00:00.001 [info] starting opencode\n1.14.33\n"
+ expect(extractSemverFromOutput(polluted)).toBe("1.14.33")
+ })
+
+ it("timestamp-only stdout returns null", () => {
+ expect(extractSemverFromOutput("00:24:25.202 some log line")).toBe(null)
+ })
+ })
+
+ describe("#given empty or invalid output #when extractSemverFromOutput #then returns null", () => {
+ it("empty string", () => {
+ expect(extractSemverFromOutput("")).toBe(null)
+ })
+
+ it("only whitespace", () => {
+ expect(extractSemverFromOutput(" \n ")).toBe(null)
+ })
+
+ it("text without any semver-shaped token", () => {
+ expect(extractSemverFromOutput("hello world")).toBe(null)
+ })
+
+ it("incomplete semver (only major.minor) is rejected", () => {
+ expect(extractSemverFromOutput("1.14")).toBe(null)
+ })
+ })
+})
diff --git a/src/cli/doctor/checks/system-binary.ts b/src/cli/doctor/checks/system-binary.ts
index da020e4eb..4a76648fd 100644
--- a/src/cli/doctor/checks/system-binary.ts
+++ b/src/cli/doctor/checks/system-binary.ts
@@ -105,6 +105,17 @@ export async function findOpenCodeBinary(): Promise {
return findDesktopBinary()
}
+export function extractSemverFromOutput(output: string): string | null {
+ const trimmed = output.trim()
+ if (!trimmed) return null
+ // Match a semver-shaped token, allowing optional `v` prefix and `-prerelease+build` suffix.
+ // The negative lookbehind `(?