Files
opencode-kimi-full/test/_util/fetchMock.ts
T
lemon07r 64550fb057 Release v1.0.2: broaden offline test coverage
Adds one test file per source file plus a shared fetch mock helper:

- test/constants.test.ts: OAuth scope/client_id/PROVIDER_ID/MODEL_ID pins,
  rules 1, 6, 8.
- test/headers.test.ts: seven fingerprint header keys, UA/X-Msh-Version
  track KIMI_CLI_VERSION, ASCII-only values, device-id format and
  stability across calls (rules 1, 2).
- test/oauth.test.ts: form-encoded bodies for device-start/refresh,
  authorization_pending/expired_token/unknown-error branches, non-OK and
  non-JSON error shapes.
- test/plugin.test.ts: chat.params provider+model gating, full effort
  matrix (rule 4), prompt_cache_key gating (rule 5), camelCase effort
  input, loader Authorization ownership (rule 3), refresh-on-expiry with
  persistAuth, 401 single-retry no-loop, device-flow authorize wiring.
- test/_util/fetchMock.ts: zero-dep global-fetch swap returning canned
  Responses with call capture.

Notes:

- Tests use the real ~/.kimi/device_id; getDeviceId is idempotent and the
  file is shared with kimi-cli by design (AGENTS.md rule 2), so no HOME
  redirect is needed. Attempted monkey-patching os.homedir instead is
  fragile under Bun's eager import-binding resolution.
- pollDeviceToken tests use interval=1 because `device.interval || 5`
  treats 0 as the default 5s and would push each iteration past the
  default bun-test timeout.

Total: 35 tests, ~5s to run.
2026-04-17 03:18:23 -04:00

52 lines
1.7 KiB
TypeScript

// Minimal typed fetch mock. Bun's `bun:test` has `mock()` but swapping
// `globalThis.fetch` with a plain function is enough here and keeps tests
// free of framework-specific mocking magic.
export type FetchCall = {
url: string
method: string
headers: Record<string, string>
body: string | undefined
}
export type Responder = (
call: FetchCall,
callIndex: number,
) => { status?: number; body?: unknown; bodyText?: string }
export function installFetchMock(responder: Responder) {
const calls: FetchCall[] = []
const original = globalThis.fetch
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url
const headers: Record<string, string> = {}
const hs = new Headers(init?.headers)
hs.forEach((v, k) => {
headers[k] = v
})
const body = typeof init?.body === "string" ? init.body : init?.body == null ? undefined : String(init.body)
const call: FetchCall = { url, method: (init?.method ?? "GET").toUpperCase(), headers, body }
calls.push(call)
const r = responder(call, calls.length - 1)
const status = r.status ?? 200
const text = r.bodyText ?? (r.body === undefined ? "" : JSON.stringify(r.body))
return new Response(text, {
status,
headers: { "Content-Type": "application/json" },
})
}) as typeof fetch
return {
calls,
restore: () => {
globalThis.fetch = original
},
}
}
export function parseForm(body: string | undefined): Record<string, string> {
if (!body) return {}
const out: Record<string, string> = {}
for (const [k, v] of new URLSearchParams(body)) out[k] = v
return out
}