chore: refactoring and tests, splitting up files (#12495)
This commit is contained in:
62
packages/app/src/utils/runtime-adapters.test.ts
Normal file
62
packages/app/src/utils/runtime-adapters.test.ts
Normal file
@@ -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()
|
||||
})
|
||||
})
|
||||
39
packages/app/src/utils/runtime-adapters.ts
Normal file
39
packages/app/src/utils/runtime-adapters.ts
Normal file
@@ -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
|
||||
}
|
||||
42
packages/app/src/utils/server-health.test.ts
Normal file
42
packages/app/src/utils/server-health.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
29
packages/app/src/utils/server-health.ts
Normal file
29
packages/app/src/utils/server-health.ts
Normal file
@@ -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 }))
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
46
packages/app/src/utils/worktree.test.ts
Normal file
46
packages/app/src/utils/worktree.test.ts
Normal file
@@ -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" })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user