Files
orca/src/relay/subprocess-test-utils.ts
T
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00

176 lines
4.8 KiB
TypeScript

import { spawn, type ChildProcess, type SpawnOptions } from 'node:child_process'
import {
RELAY_SENTINEL,
FrameDecoder,
encodeJsonRpcFrame,
parseJsonRpcMessage,
MessageType,
type JsonRpcRequest,
type JsonRpcResponse,
type JsonRpcNotification
} from './protocol'
export type RelayProcess = {
proc: ChildProcess
responses: (JsonRpcResponse | JsonRpcNotification)[]
sentinelReceived: Promise<void>
send: (method: string, params?: Record<string, unknown>) => number
sendNotification: (method: string, params?: Record<string, unknown>) => void
waitForResponse: (id: number, timeoutMs?: number) => Promise<JsonRpcResponse>
waitForNotification: (method: string, timeoutMs?: number) => Promise<JsonRpcNotification>
kill: (signal?: NodeJS.Signals) => void
waitForExit: (timeoutMs?: number) => Promise<number | null>
}
export function spawnRelay(
entryPath: string,
args: string[] = [],
options: Pick<SpawnOptions, 'env'> = {}
): RelayProcess {
const proc = spawn('node', [entryPath, ...args], {
stdio: ['pipe', 'pipe', 'pipe'],
...options
})
const responses: (JsonRpcResponse | JsonRpcNotification)[] = []
let nextSeq = 1
let sentinelResolved = false
let stdoutBuffer = Buffer.alloc(0)
let sentinelResolve: () => void
let decoderActive = false
const sentinelReceived = new Promise<void>((resolve) => {
sentinelResolve = resolve
})
const decoder = new FrameDecoder((frame) => {
if (frame.type !== MessageType.Regular) {
return
}
try {
const msg = parseJsonRpcMessage(frame.payload)
responses.push(msg as JsonRpcResponse | JsonRpcNotification)
} catch {
/* skip malformed */
}
})
proc.stdout!.on('data', (chunk: Buffer) => {
if (!sentinelResolved) {
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk])
const sentinelBuf = Buffer.from(RELAY_SENTINEL, 'utf-8')
const idx = stdoutBuffer.indexOf(sentinelBuf)
if (idx !== -1) {
sentinelResolved = true
decoderActive = true
sentinelResolve()
const remainder = stdoutBuffer.subarray(idx + sentinelBuf.length)
if (remainder.length > 0) {
decoder.feed(remainder)
}
}
} else if (decoderActive) {
decoder.feed(chunk)
}
})
proc.stderr!.on('data', () => {
/* drain */
})
const send = (method: string, params?: Record<string, unknown>): number => {
const id = nextSeq++
const req: JsonRpcRequest = {
jsonrpc: '2.0',
id,
method,
...(params !== undefined ? { params } : {})
}
proc.stdin!.write(encodeJsonRpcFrame(req, id, 0))
return id
}
const sendNotification = (method: string, params?: Record<string, unknown>): void => {
const seq = nextSeq++
const notif: JsonRpcNotification = {
jsonrpc: '2.0',
method,
...(params !== undefined ? { params } : {})
}
proc.stdin!.write(encodeJsonRpcFrame(notif, seq, 0))
}
const waitForResponse = (id: number, timeoutMs = 5000): Promise<JsonRpcResponse> => {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeoutMs
const check = () => {
const found = responses.find((r) => 'id' in r && r.id === id) as JsonRpcResponse | undefined
if (found) {
resolve(found)
return
}
if (Date.now() > deadline) {
reject(new Error(`Timed out waiting for response id=${id}`))
return
}
setTimeout(check, 10)
}
check()
})
}
const waitForNotification = (method: string, timeoutMs = 5000): Promise<JsonRpcNotification> => {
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeoutMs
const seen = responses.length
const check = () => {
for (let i = seen; i < responses.length; i++) {
const r = responses[i]
if ('method' in r && r.method === method) {
resolve(r as JsonRpcNotification)
return
}
}
if (Date.now() > deadline) {
reject(new Error(`Timed out waiting for notification "${method}"`))
return
}
setTimeout(check, 10)
}
check()
})
}
const kill = (signal: NodeJS.Signals = 'SIGTERM') => {
proc.kill(signal)
}
const waitForExit = (timeoutMs = 5000): Promise<number | null> => {
return new Promise((resolve, reject) => {
if (proc.exitCode !== null) {
resolve(proc.exitCode)
return
}
const timer = setTimeout(() => {
reject(new Error('Timed out waiting for process exit'))
}, timeoutMs)
proc.once('exit', (code) => {
clearTimeout(timer)
resolve(code)
})
})
}
return {
proc,
responses,
sentinelReceived,
send,
sendNotification,
waitForResponse,
waitForNotification,
kill,
waitForExit
}
}