summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/utils
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-06 10:02:31 -0600
committerGitHub <[email protected]>2026-02-06 10:02:31 -0600
commit2c58dd6203df7806f57ef6b29672091cb764e871 (patch)
tree10fca96d3098465b497f78e29de8d0a585c4dac3 /packages/app/src/utils
parenta4bc883595df9ea0f752079519081bc602408553 (diff)
downloadopencode-2c58dd6203df7806f57ef6b29672091cb764e871.tar.gz
opencode-2c58dd6203df7806f57ef6b29672091cb764e871.zip
chore: refactoring and tests, splitting up files (#12495)
Diffstat (limited to 'packages/app/src/utils')
-rw-r--r--packages/app/src/utils/runtime-adapters.test.ts62
-rw-r--r--packages/app/src/utils/runtime-adapters.ts39
-rw-r--r--packages/app/src/utils/server-health.test.ts42
-rw-r--r--packages/app/src/utils/server-health.ts29
-rw-r--r--packages/app/src/utils/speech.ts12
-rw-r--r--packages/app/src/utils/worktree.test.ts46
6 files changed, 223 insertions, 7 deletions
diff --git a/packages/app/src/utils/runtime-adapters.test.ts b/packages/app/src/utils/runtime-adapters.test.ts
new file mode 100644
index 000000000..9f408b8eb
--- /dev/null
+++ b/packages/app/src/utils/runtime-adapters.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, test } from "bun:test"
+import {
+ disposeIfDisposable,
+ getHoveredLinkText,
+ getSpeechRecognitionCtor,
+ hasSetOption,
+ isDisposable,
+ setOptionIfSupported,
+} from "./runtime-adapters"
+
+describe("runtime adapters", () => {
+ test("detects and disposes disposable values", () => {
+ let count = 0
+ const value = {
+ dispose: () => {
+ count += 1
+ },
+ }
+ expect(isDisposable(value)).toBe(true)
+ disposeIfDisposable(value)
+ expect(count).toBe(1)
+ })
+
+ test("ignores non-disposable values", () => {
+ expect(isDisposable({ dispose: "nope" })).toBe(false)
+ expect(() => disposeIfDisposable({ dispose: "nope" })).not.toThrow()
+ })
+
+ test("sets options only when setter exists", () => {
+ const calls: Array<[string, unknown]> = []
+ const value = {
+ setOption: (key: string, next: unknown) => {
+ calls.push([key, next])
+ },
+ }
+ expect(hasSetOption(value)).toBe(true)
+ setOptionIfSupported(value, "fontFamily", "Berkeley Mono")
+ expect(calls).toEqual([["fontFamily", "Berkeley Mono"]])
+ expect(() => setOptionIfSupported({}, "fontFamily", "Berkeley Mono")).not.toThrow()
+ })
+
+ test("reads hovered link text safely", () => {
+ expect(getHoveredLinkText({ currentHoveredLink: { text: "https://example.com" } })).toBe("https://example.com")
+ expect(getHoveredLinkText({ currentHoveredLink: { text: 1 } })).toBeUndefined()
+ expect(getHoveredLinkText(null)).toBeUndefined()
+ })
+
+ test("resolves speech recognition constructor with webkit precedence", () => {
+ class SpeechCtor {}
+ class WebkitCtor {}
+ const ctor = getSpeechRecognitionCtor({
+ SpeechRecognition: SpeechCtor,
+ webkitSpeechRecognition: WebkitCtor,
+ })
+ expect(ctor).toBe(WebkitCtor)
+ })
+
+ test("returns undefined when no valid speech constructor exists", () => {
+ expect(getSpeechRecognitionCtor({ SpeechRecognition: "nope" })).toBeUndefined()
+ expect(getSpeechRecognitionCtor(undefined)).toBeUndefined()
+ })
+})
diff --git a/packages/app/src/utils/runtime-adapters.ts b/packages/app/src/utils/runtime-adapters.ts
new file mode 100644
index 000000000..4c74da5dc
--- /dev/null
+++ b/packages/app/src/utils/runtime-adapters.ts
@@ -0,0 +1,39 @@
+type RecordValue = Record<string, unknown>
+
+const isRecord = (value: unknown): value is RecordValue => {
+ return typeof value === "object" && value !== null
+}
+
+export const isDisposable = (value: unknown): value is { dispose: () => void } => {
+ return isRecord(value) && typeof value.dispose === "function"
+}
+
+export const disposeIfDisposable = (value: unknown) => {
+ if (!isDisposable(value)) return
+ value.dispose()
+}
+
+export const hasSetOption = (value: unknown): value is { setOption: (key: string, next: unknown) => void } => {
+ return isRecord(value) && typeof value.setOption === "function"
+}
+
+export const setOptionIfSupported = (value: unknown, key: string, next: unknown) => {
+ if (!hasSetOption(value)) return
+ value.setOption(key, next)
+}
+
+export const getHoveredLinkText = (value: unknown) => {
+ if (!isRecord(value)) return
+ const link = value.currentHoveredLink
+ if (!isRecord(link)) return
+ if (typeof link.text !== "string") return
+ return link.text
+}
+
+export const getSpeechRecognitionCtor = <T>(value: unknown): (new () => T) | undefined => {
+ if (!isRecord(value)) return
+ const ctor =
+ typeof value.webkitSpeechRecognition === "function" ? value.webkitSpeechRecognition : value.SpeechRecognition
+ if (typeof ctor !== "function") return
+ return ctor as new () => T
+}
diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts
new file mode 100644
index 000000000..34c86685a
--- /dev/null
+++ b/packages/app/src/utils/server-health.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, test } from "bun:test"
+import { checkServerHealth } from "./server-health"
+
+describe("checkServerHealth", () => {
+ test("returns healthy response with version", async () => {
+ const fetch = (async () =>
+ new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ })) as unknown as typeof globalThis.fetch
+
+ const result = await checkServerHealth("http://localhost:4096", fetch)
+
+ expect(result).toEqual({ healthy: true, version: "1.2.3" })
+ })
+
+ test("returns unhealthy when request fails", async () => {
+ const fetch = (async () => {
+ throw new Error("network")
+ }) as unknown as typeof globalThis.fetch
+
+ const result = await checkServerHealth("http://localhost:4096", fetch)
+
+ expect(result).toEqual({ healthy: false })
+ })
+
+ test("uses provided abort signal", async () => {
+ let signal: AbortSignal | undefined
+ const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
+ signal = init?.signal ?? (input instanceof Request ? input.signal : undefined)
+ return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ })
+ }) as unknown as typeof globalThis.fetch
+
+ const abort = new AbortController()
+ await checkServerHealth("http://localhost:4096", fetch, { signal: abort.signal })
+
+ expect(signal).toBe(abort.signal)
+ })
+})
diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts
new file mode 100644
index 000000000..ab33460b2
--- /dev/null
+++ b/packages/app/src/utils/server-health.ts
@@ -0,0 +1,29 @@
+import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
+
+export type ServerHealth = { healthy: boolean; version?: string }
+
+interface CheckServerHealthOptions {
+ timeoutMs?: number
+ signal?: AbortSignal
+}
+
+function timeoutSignal(timeoutMs: number) {
+ return (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(timeoutMs)
+}
+
+export async function checkServerHealth(
+ url: string,
+ fetch: typeof globalThis.fetch,
+ opts?: CheckServerHealthOptions,
+): Promise<ServerHealth> {
+ const signal = opts?.signal ?? timeoutSignal(opts?.timeoutMs ?? 3000)
+ const sdk = createOpencodeClient({
+ baseUrl: url,
+ fetch,
+ signal,
+ })
+ return sdk.global
+ .health()
+ .then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
+ .catch(() => ({ healthy: false }))
+}
diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts
index 201c1261b..52fc46b69 100644
--- a/packages/app/src/utils/speech.ts
+++ b/packages/app/src/utils/speech.ts
@@ -1,5 +1,6 @@
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
+import { getSpeechRecognitionCtor } from "@/utils/runtime-adapters"
// Minimal types to avoid relying on non-standard DOM typings
type RecognitionResult = {
@@ -56,9 +57,8 @@ export function createSpeechRecognition(opts?: {
onFinal?: (text: string) => void
onInterim?: (text: string) => void
}) {
- const hasSupport =
- typeof window !== "undefined" &&
- Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
+ const ctor = getSpeechRecognitionCtor<Recognition>(typeof window === "undefined" ? undefined : window)
+ const hasSupport = Boolean(ctor)
const [store, setStore] = createStore({
isRecording: false,
@@ -155,10 +155,8 @@ export function createSpeechRecognition(opts?: {
}, COMMIT_DELAY)
}
- if (hasSupport) {
- const Ctor: new () => Recognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition
-
- recognition = new Ctor()
+ if (ctor) {
+ recognition = new ctor()
recognition.continuous = false
recognition.interimResults = true
recognition.lang = opts?.lang || (typeof navigator !== "undefined" ? navigator.language : "en-US")
diff --git a/packages/app/src/utils/worktree.test.ts b/packages/app/src/utils/worktree.test.ts
new file mode 100644
index 000000000..8161e7ad8
--- /dev/null
+++ b/packages/app/src/utils/worktree.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, test } from "bun:test"
+import { Worktree } from "./worktree"
+
+const dir = (name: string) => `/tmp/opencode-worktree-${name}-${crypto.randomUUID()}`
+
+describe("Worktree", () => {
+ test("normalizes trailing slashes", () => {
+ const key = dir("normalize")
+ Worktree.ready(`${key}/`)
+
+ expect(Worktree.get(key)).toEqual({ status: "ready" })
+ })
+
+ test("pending does not overwrite a terminal state", () => {
+ const key = dir("pending")
+ Worktree.failed(key, "boom")
+ Worktree.pending(key)
+
+ expect(Worktree.get(key)).toEqual({ status: "failed", message: "boom" })
+ })
+
+ test("wait resolves shared pending waiter when ready", async () => {
+ const key = dir("wait-ready")
+ Worktree.pending(key)
+
+ const a = Worktree.wait(key)
+ const b = Worktree.wait(`${key}/`)
+
+ expect(a).toBe(b)
+
+ Worktree.ready(key)
+
+ expect(await a).toEqual({ status: "ready" })
+ expect(await b).toEqual({ status: "ready" })
+ })
+
+ test("wait resolves with failure message", async () => {
+ const key = dir("wait-failed")
+ const waiting = Worktree.wait(key)
+
+ Worktree.failed(key, "permission denied")
+
+ expect(await waiting).toEqual({ status: "failed", message: "permission denied" })
+ expect(await Worktree.wait(key)).toEqual({ status: "failed", message: "permission denied" })
+ })
+})