Files
orca/src/main/computer/desktop-script-runtime-host.test.ts
T
OrcaWin 2847bbe9e8 [comment audit] Consolidate completed code comment cleanup
Combine the completed comment cleanup from 319 stacked PRs into one change against `main`, so the full CI suite validates the combined result once. This replaces the previous audit stack; no additional audit work is included.

The change shortens redundant comments across 2,753 source and shell-snapshot files (25,235 net lines removed). Conflicts preserve current-main executable code. Runtime behavior, test assertions, directives, licenses, generated notices, SAFETY rationales, and compatibility markers are preserved. The audit tracker remains local, outside the repository.

Validation:
- Equivalent source/emitted AST and executable tokens across all 2,750 source files, with separate checks for embedded shell, SQL, JavaScript, CSS, and generated fixtures.
- Identical executable lines in the three shell snapshots; protected comments unchanged.
- Changed-code quality gate passed with no new findings.
- 358 tests passed, 2 skipped across 23 focused test files.
- `git diff --check` passed.
2026-10-23 13:15:45 -07:00

811 lines
28 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ProcessSpec } from '@orca/process-host/process-spec'
import type { RuntimeChildProcess } from './desktop-script-serve-channel'
import { DesktopScriptRuntimeHost, isRuntimeHostUnavailable } from './desktop-script-runtime-host'
const POLICY_ERROR =
'File runtime.ps1 cannot be loaded because running scripts\nis disabled on this system.\n + CategoryInfo : SecurityError'
class FakeRuntimeChild extends EventEmitter {
readonly stdout = new EventEmitter()
readonly stderr = new EventEmitter()
readonly writes: string[] = []
killed = false
stdinEnded = false
/** Holds write callbacks so a late stdin failure can be fired deliberately. */
deferWrites = false
private readonly pendingWrites: ((error?: Error | null) => void)[] = []
readonly stdin = {
write: (chunk: string, callback?: (error?: Error | null) => void): boolean => {
this.writes.push(chunk)
if (this.deferWrites) {
if (callback) {
this.pendingWrites.push(callback)
}
return true
}
callback?.(null)
return true
},
end: (): void => {
this.stdinEnded = true
},
on: (): void => {}
}
kill(): boolean {
this.killed = true
return true
}
failQueuedWrites(): void {
for (const callback of this.pendingWrites.splice(0)) {
callback(new Error('ERR_STREAM_DESTROYED'))
}
}
failQueuedWrite(index: number): void {
this.pendingWrites.splice(index, 1)[0](new Error('EPIPE'))
}
requests(): Record<string, unknown>[] {
return this.writes.map((line) => JSON.parse(line) as Record<string, unknown>)
}
pendingId(): number {
return this.requests().at(-1)?.requestId as number
}
ready(): void {
this.write('{"ready":true}\n')
}
respond(response: Record<string, unknown>, requestId = this.pendingId()): void {
this.write(`${JSON.stringify({ ...response, requestId })}\n`)
}
write(raw: string): void {
this.stdout.emit('data', Buffer.from(raw, 'utf8'))
}
exit(code: number | null, stderr = ''): void {
if (stderr) {
this.stderr.emit('data', Buffer.from(stderr, 'utf8'))
}
this.emit('close', code, null)
}
}
function createHost(
options: {
idleShutdownMs?: number
requestTimeoutMs?: number
cooldownMs?: number
now?: () => number
deferWrites?: boolean
} = {}
) {
const children: FakeRuntimeChild[] = []
const specs: ProcessSpec[] = []
const warnings: string[] = []
const host = new DesktopScriptRuntimeHost('C:\\orca\\runtime.ps1', {
...options,
powerShellPath: () => 'C:\\Windows\\System32\\powershell.exe',
warn: (message) => warnings.push(message),
spawn: (spec) => {
specs.push(spec)
const child = new FakeRuntimeChild()
child.deferWrites = options.deferWrites === true
children.push(child)
return child as unknown as RuntimeChildProcess
}
})
return { host, children, specs, warnings }
}
/** Let the host's queue microtasks drain so the next request reaches its child. */
async function settle(): Promise<void> {
for (let index = 0; index < 6; index++) {
await Promise.resolve()
}
}
function remainingCooldownMs(error: Error | null): number {
const match = /retrying the runtime host in (\d+)ms/.exec(error?.message ?? '')
return match ? Number(match[1]) : Number.NaN
}
async function failEveryStart(children: FakeRuntimeChild[], stderr: string): Promise<void> {
for (let index = 0; index < 8; index++) {
if (index >= children.length) {
return
}
children[index].exit(1, stderr)
await settle()
}
}
describe('DesktopScriptRuntimeHost', () => {
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
it('starts one helper for many operations and never writes an operation file', async () => {
const { host, children, specs } = createHost()
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await expect(first).resolves.toMatchObject({ ok: true })
for (let index = 0; index < 5; index++) {
const next = host.request({ tool: 'click', app: 'Notepad' })
await settle()
children[0].respond({ ok: true, action: { path: 'synthetic' } })
await expect(next).resolves.toMatchObject({ ok: true })
}
expect(children).toHaveLength(1)
expect(children[0].requests()).toHaveLength(6)
expect(specs[0].args).toEqual([
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'RemoteSigned',
'-File',
'C:\\orca\\runtime.ps1',
'-Serve'
])
host.dispose()
})
it('serializes requests so only one operation is ever in flight', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'click', app: 'A' })
const second = host.request({ tool: 'click', app: 'B' })
await settle()
expect(children[0].requests()).toEqual([{ tool: 'click', app: 'A', requestId: 1 }])
children[0].respond({ ok: true, action: { path: 'synthetic' } })
await expect(first).resolves.toMatchObject({ ok: true })
await settle()
expect(children[0].requests()).toHaveLength(2)
children[0].respond({ ok: true, action: { path: 'accessibility' } })
await expect(second).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('strips the echoed id from the response it hands back', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await expect(promise).resolves.toEqual({ ok: true, capabilities: {} })
host.dispose()
})
it('reassembles a response split across chunks, including a split code point', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'get_app_state', app: 'Editor' })
await settle()
const payload = Buffer.from(
`${JSON.stringify({ ok: true, snapshot: { app: 'né' }, requestId: 1 })}\r\n`,
'utf8'
)
const split = payload.indexOf(Buffer.from('é', 'utf8')) + 1
children[0].stdout.emit('data', payload.subarray(0, split))
children[0].stdout.emit('data', payload.subarray(split))
await expect(promise).resolves.toEqual({ ok: true, snapshot: { app: 'né' } })
host.dispose()
})
it('kills the helper rather than answering a request with another reply', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} }, 999)
await expect(first).rejects.toThrow(/did not match the pending request/)
expect(children[0].killed).toBe(true)
host.dispose()
})
it('kills the helper when an unsolicited line arrives with nothing pending', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await first
children[0].write(`${JSON.stringify({ ok: true, requestId: 77 })}\n`)
expect(children[0].killed).toBe(true)
host.dispose()
})
it('times out a wedged operation and starts a fresh helper for the next one', async () => {
vi.useFakeTimers()
const { host, children } = createHost({ requestTimeoutMs: 30_000 })
const promise = host.request({ tool: 'click', app: 'Frozen' })
await settle()
await vi.advanceTimersByTimeAsync(30_001)
await expect(promise).rejects.toMatchObject({ code: 'action_timeout' })
expect(children[0].killed).toBe(true)
const next = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(2)
children[1].respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('rejects the in-flight request when a working helper crashes, then restarts', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await first
const second = host.request({ tool: 'click', app: 'Notepad' })
await settle()
children[0].exit(1, 'boom')
await expect(second).rejects.toMatchObject({ code: 'accessibility_error' })
await expect(second).rejects.toThrow(/runtime host exited/)
const third = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(2)
children[1].respond({ ok: true, capabilities: {} })
await expect(third).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('stops respawning a helper that dies on every second operation', async () => {
let clock = 1_000
const { host, children } = createHost({ cooldownMs: 60_000, now: () => clock })
// Alternating replies and crashes must still trip the failure threshold.
for (let round = 0; round < 3; round++) {
const good = host.request({ tool: 'handshake' })
await settle()
children.at(-1)?.respond({ ok: true, capabilities: {} })
await expect(good).resolves.toMatchObject({ ok: true })
await settle()
const crash = host.request({ tool: 'click', app: 'Crashy' })
await settle()
children.at(-1)?.exit(1, 'boom')
await expect(crash).rejects.toThrow(/runtime host exited/)
await settle()
}
const spawned = children.length
await expect(host.request({ tool: 'handshake' })).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(children).toHaveLength(spawned)
host.dispose()
})
it('keeps serving a healthy helper after an isolated crash', async () => {
const { host, children } = createHost({ cooldownMs: 60_000 })
const crashed = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await crashed
const second = host.request({ tool: 'click', app: 'Notepad' })
await settle()
children[0].exit(1, 'boom')
await expect(second).rejects.toThrow(/runtime host exited/)
for (let index = 0; index < 4; index++) {
const next = host.request({ tool: 'handshake' })
await settle()
children.at(-1)?.respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
}
expect(children).toHaveLength(2)
host.dispose()
})
it('stops respawning a helper that keeps answering the wrong request', async () => {
let clock = 1_000
const { host, children } = createHost({ cooldownMs: 60_000, now: () => clock })
// Host-detected desync must count toward cooldown even without a child exit event.
for (let round = 0; round < 3; round++) {
const promise = host.request({ tool: 'handshake' })
await settle()
const child = children.at(-1)
child?.respond({ ok: true, capabilities: {} }, child.pendingId() + 500)
await expect(promise).rejects.toThrow(/did not match the pending request/)
await settle()
}
const spawned = children.length
await expect(host.request({ tool: 'handshake' })).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(children).toHaveLength(spawned)
host.dispose()
})
it('stops respawning a helper that times out on every operation', async () => {
vi.useFakeTimers()
let clock = 1_000
const { host, children } = createHost({
requestTimeoutMs: 1_000,
cooldownMs: 60_000,
now: () => clock
})
for (let round = 0; round < 3; round++) {
const promise = host.request({ tool: 'get_app_state', app: 'Frozen' })
await settle()
await vi.advanceTimersByTimeAsync(1_001)
await expect(promise).rejects.toMatchObject({ code: 'action_timeout' })
await settle()
}
const spawned = children.length
await expect(host.request({ tool: 'handshake' })).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(children).toHaveLength(spawned)
host.dispose()
})
it('never re-sends a mutation to a fresh helper after a pre-answer death', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'click', app: 'Notepad', x: 10, y: 10 })
await settle()
children[0].exit(1, 'Add-Type : Cannot access the temporary directory')
await expect(promise).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(children).toHaveLength(1)
host.dispose()
})
it('never replays a mutation once the helper announced it was reading', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'click', app: 'Notepad', x: 10, y: 10 })
await settle()
children[0].ready()
// A snapshot can fault after input was delivered; missing replies do not justify replay.
children[0].exit(1, 'faulting module gdiplus.dll')
await expect(promise).rejects.toThrow(/runtime host exited/)
expect(children).toHaveLength(1)
host.dispose()
})
it('replays a mutation only for a helper that died before announcing readiness', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'handshake' })
await settle()
children[0].ready()
children[0].respond({ ok: true, capabilities: {} })
await first
const crashed = host.request({ tool: 'click', app: 'Notepad', x: 1, y: 1 })
await settle()
children[0].exit(1, 'boom')
await expect(crashed).rejects.toThrow(/runtime host exited/)
const retried = host.request({ tool: 'click', app: 'Notepad', x: 1, y: 1 })
await settle()
// This protocol announces before reading requests, so a pre-ready failure is safe to replay.
children[1].exit(1, 'Add-Type : Cannot access the temporary directory')
await settle()
expect(children).toHaveLength(3)
children[2].ready()
children[2].respond({ ok: true, action: { path: 'synthetic' } })
await expect(retried).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('does not treat the readiness announcement as an unmatched reply', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].ready()
expect(children[0].killed).toBe(false)
children[0].respond({ ok: true, capabilities: {} })
await expect(promise).resolves.toEqual({ ok: true, capabilities: {} })
host.dispose()
})
it('charges one cooldown per outage, not one per later death', async () => {
let clock = 1_000
const { host, children } = createHost({ cooldownMs: 60_000, now: () => clock })
const failed = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, 'The term is not recognized')
await expect(failed).rejects.toSatisfy(isRuntimeHostUnavailable)
clock += 61_000
const recovered = host.request({ tool: 'handshake' })
await settle()
children.at(-1)?.respond({ ok: true, capabilities: {} })
await recovered
const crashed = host.request({ tool: 'handshake' })
await settle()
children.at(-1)?.exit(1, 'boom')
await expect(crashed).rejects.toBeInstanceOf(Error)
const next = host.request({ tool: 'handshake' })
await settle()
children.at(-1)?.respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('charges one failure when a write fails after the helper was torn down', async () => {
const { host, children, warnings } = createHost({ deferWrites: true })
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} }, 999)
await expect(promise).rejects.toThrow(/did not match the pending request/)
// A destroyed stdin's late callback must not count the same operation twice.
children[0].failQueuedWrites()
expect(warnings.filter((line) => /helper stopped/.test(line))).toHaveLength(1)
host.dispose()
})
it('never lets a stale write error stop a replacement helper', async () => {
const { host, children } = createHost({ deferWrites: true })
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} }, 999)
await expect(first).rejects.toBeInstanceOf(Error)
const second = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(2)
children[0].failQueuedWrites()
expect(children[1].killed).toBe(false)
children[1].respond({ ok: true, capabilities: {} })
await expect(second).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('ignores a write error for a request that already finished', async () => {
const { host, children } = createHost({ deferWrites: true })
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await first
const second = host.request({ tool: 'handshake' })
await settle()
// A write callback can fail after its reply; only the request ID detects staleness on a live channel.
children[0].failQueuedWrite(0)
expect(children[0].killed).toBe(false)
children[0].respond({ ok: true, capabilities: {} })
await expect(second).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('shuts the helper down when idle and starts a new one on the next operation', async () => {
vi.useFakeTimers()
const { host, children } = createHost({ idleShutdownMs: 60_000 })
const first = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await first
await settle()
expect(children[0].killed).toBe(false)
await vi.advanceTimersByTimeAsync(60_001)
expect(children[0].stdinEnded).toBe(true)
expect(children[0].killed).toBe(true)
const next = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(2)
children[1].respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('disposes the helper and rejects the in-flight request', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'click', app: 'Notepad' })
await settle()
host.dispose()
expect(children[0].stdinEnded).toBe(true)
expect(children[0].killed).toBe(true)
await expect(promise).rejects.toThrow(/shut down/)
})
it('never respawns for a request queued behind dispose', async () => {
const { host, children } = createHost()
const first = host.request({ tool: 'handshake' })
const queued = host.request({ tool: 'handshake' })
await settle()
host.dispose()
await expect(first).rejects.toBeInstanceOf(Error)
await expect(queued).rejects.toSatisfy(isRuntimeHostUnavailable)
await settle()
expect(children).toHaveLength(1)
})
it('falls back to Bypass once when the execution policy blocks the start', async () => {
const { host, children, specs, warnings } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].exit(1, POLICY_ERROR)
await settle()
expect(children).toHaveLength(2)
expect(specs[1].args).toContain('Bypass')
children[1].respond({ ok: true, capabilities: {} })
await expect(promise).resolves.toMatchObject({ ok: true })
expect(warnings.some((line) => /trying Bypass/.test(line))).toBe(true)
const next = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(2)
children[1].respond({ ok: true, capabilities: {} })
await next
expect(warnings.some((line) => /returning to RemoteSigned/.test(line))).toBe(false)
host.dispose()
})
it('returns to RemoteSigned when Bypass does not start a helper either', async () => {
let clock = 1_000
const { host, children, specs, warnings } = createHost({ cooldownMs: 60_000, now: () => clock })
// AppLocker/WDAC can report SecurityError even though Bypass cannot remove the restriction.
const promise = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, POLICY_ERROR)
await expect(promise).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(specs[1].args).toContain('Bypass')
expect(warnings.some((line) => /returning to RemoteSigned/.test(line))).toBe(true)
expect(specs).toHaveLength(3)
expect(specs[2].args).not.toContain('Bypass')
clock += 61_000
const recovered = host.request({ tool: 'handshake' })
await settle()
expect(specs.at(-1)?.args).not.toContain('Bypass')
children.at(-1)?.respond({ ok: true, capabilities: {} })
await expect(recovered).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('reports itself unavailable when Bypass is also refused', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, POLICY_ERROR)
await expect(promise).rejects.toSatisfy(isRuntimeHostUnavailable)
host.dispose()
})
it('reports itself unavailable when the helper cannot be spawned at all', async () => {
const host = new DesktopScriptRuntimeHost('C:\\orca\\runtime.ps1', {
powerShellPath: () => 'C:\\Windows\\System32\\powershell.exe',
warn: () => {},
spawn: () => {
throw new Error('spawn ENOENT')
}
})
await expect(host.request({ tool: 'handshake' })).rejects.toSatisfy(isRuntimeHostUnavailable)
host.dispose()
})
it('retries a transient pre-answer death without the caller ever seeing it', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].exit(1, 'Add-Type : Cannot access the temporary directory')
await settle()
expect(children).toHaveLength(2)
children[1].respond({ ok: true, capabilities: {} })
await expect(promise).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('gives up only after repeated start failures, then serves from the host again after the cooldown', async () => {
let clock = 1_000
const { host, children, warnings } = createHost({ cooldownMs: 60_000, now: () => clock })
const failed = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, 'The term is not recognized')
await expect(failed).rejects.toSatisfy(isRuntimeHostUnavailable)
const attempts = children.length
expect(attempts).toBe(3)
clock += 30_000
await expect(host.request({ tool: 'handshake' })).rejects.toSatisfy(isRuntimeHostUnavailable)
expect(children).toHaveLength(attempts)
clock += 31_000
const recovered = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(attempts + 1)
children[attempts].respond({ ok: true, capabilities: {} })
await expect(recovered).resolves.toMatchObject({ ok: true })
expect(warnings.at(-1)).toMatch(/recovered/)
host.dispose()
})
it('keeps the helper account of a reply it could not tag', async () => {
const { host, children } = createHost()
const promise = host.request({ tool: 'handshake' })
await settle()
const child = children[0]
// Preserve real helper errors even when the response ID is wrong.
child.respond({ ok: false, error: 'Invalid object passed in' }, child.pendingId() + 500)
await expect(promise).rejects.toThrow(
/did not match the pending request: Invalid object passed in/
)
host.dispose()
})
it('does not charge a cooldown for requests the helper rejects as malformed', async () => {
const { host, children } = createHost({ cooldownMs: 60_000 })
for (let round = 0; round < 3; round++) {
const promise = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: false, error: 'Invalid object passed in' })
await expect(promise).resolves.toMatchObject({ ok: false })
await settle()
}
expect(children).toHaveLength(1)
const next = host.request({ tool: 'handshake' })
await settle()
children[0].respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('fails a request that spends its whole timeout queued behind others', async () => {
vi.useFakeTimers()
const { host, children } = createHost({ requestTimeoutMs: 1_000 })
// Two predecessors keep the queued click behind its wait deadline.
const first = host.request({ tool: 'get_app_state', app: 'Frozen' })
const second = host.request({ tool: 'get_app_state', app: 'Frozen' })
const queued = host.request({ tool: 'click', app: 'Notepad' })
// Attach rejection handlers before advancing timers to avoid unhandled rejections.
const firstFailed = expect(first).rejects.toMatchObject({ code: 'action_timeout' })
const queuedFailed = expect(queued).rejects.toMatchObject({
code: 'action_timeout',
message: /waiting for earlier operations/
})
await settle()
expect(children[0].requests()).toHaveLength(1)
await vi.advanceTimersByTimeAsync(1_001)
await firstFailed
await queuedFailed
children[1].respond({ ok: true, state: {} })
await expect(second).resolves.toMatchObject({ ok: true })
await settle()
expect(children.flatMap((child) => child.requests())).not.toContainEqual(
expect.objectContaining({ tool: 'click' })
)
const next = host.request({ tool: 'handshake' })
await settle()
children[1].respond({ ok: true, capabilities: {} })
await expect(next).resolves.toMatchObject({ ok: true })
host.dispose()
})
it('gives a queued request its full timeout once it reaches the helper', async () => {
vi.useFakeTimers()
const { host, children } = createHost({ requestTimeoutMs: 1_000 })
const head = host.request({ tool: 'handshake' })
const queued = host.request({ tool: 'get_app_state', app: 'Slow' })
await settle()
await vi.advanceTimersByTimeAsync(900)
children[0].respond({ ok: true, capabilities: {} })
await expect(head).resolves.toMatchObject({ ok: true })
await settle()
await vi.advanceTimersByTimeAsync(900)
children[0].respond({ ok: true, state: {} })
await expect(queued).resolves.toMatchObject({ ok: true })
host.dispose()
})
// Leave now unset to test the default monotonic clock rather than an injected substitute.
it('does not stretch the cooldown when the wall clock steps backwards', async () => {
const wallClock = vi.spyOn(Date, 'now').mockReturnValue(2_000_000_000_000)
const { host, children } = createHost({ cooldownMs: 60_000 })
const failed = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, 'The term is not recognized')
await expect(failed).rejects.toSatisfy(isRuntimeHostUnavailable)
wallClock.mockReturnValue(2_000_000_000_000 - 3_600_000)
const refused = await host.request({ tool: 'handshake' }).then(
() => null,
(error: Error) => error
)
expect(refused?.message).toMatch(/retrying the runtime host in/)
expect(remainingCooldownMs(refused)).toBeLessThanOrEqual(60_000)
host.dispose()
})
it('serves from the persistent helper again after a backwards clock step', async () => {
vi.spyOn(Date, 'now').mockReturnValue(2_000_000_000_000)
const { host, children } = createHost({ cooldownMs: 25 })
const failed = host.request({ tool: 'handshake' })
await settle()
await failEveryStart(children, 'The term is not recognized')
await expect(failed).rejects.toSatisfy(isRuntimeHostUnavailable)
const attempts = children.length
vi.mocked(Date.now).mockReturnValue(2_000_000_000_000 - 3_600_000)
// Real elapsed time, because the clock under test is the real monotonic one.
await new Promise((resolve) => setTimeout(resolve, 60))
const recovered = host.request({ tool: 'handshake' })
await settle()
expect(children).toHaveLength(attempts + 1)
children[attempts].respond({ ok: true, capabilities: {} })
await expect(recovered).resolves.toMatchObject({ ok: true })
host.dispose()
})
})