test: strengthen hook, release, and mobile transport coverage (#33031)

* test: exercise hook and decorative title limits

* test: reject non-string launch preferences

* test: preserve live builds on release rejection

* test: verify restored child and expiry session state

* test: strengthen mobile transport lifecycle coverage

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-23 21:53:27 -07:00
committed by GitHub
co-authored by OrcaWin
parent 5dc197e5ec
commit 2eb0bb637d
11 changed files with 156 additions and 80 deletions
@@ -1,3 +1,4 @@
import { createHash } from 'node:crypto'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
@@ -97,7 +98,9 @@ describe('release JavaScript configuration', () => {
const { root, env } = configurationFixture(version)
const result = releaseJavascriptConfiguration(root, { ...env, ORCA_BUILD_IDENTITY: identity })
expect(result.identity).toBe(identity)
expect(result.telemetryKeySha256).toMatch(/^[a-f0-9]{64}$/)
expect(result.telemetryKeySha256).toBe(
createHash('sha256').update(env.ORCA_POSTHOG_WRITE_KEY).digest('hex')
)
expect(result.sourceSha).toMatch(/^[a-f0-9]{40}$/)
})
@@ -200,15 +203,16 @@ describe('release JavaScript transfer', () => {
])('rejects a different %s before replacing the current build', (field) => {
const options = fixture()
packReleaseJavascript(options)
writeFileSync(join(options.root, 'out', 'main', 'index.js'), 'current build')
writeFileSync(join(options.root, 'out', 'current-only.js'), 'current only')
const current = javascriptInventory(join(options.root, 'out'))
expect(() =>
restoreReleaseJavascript({
...options,
configuration: { ...configuration, [field]: 'different' }
})
).toThrow('JavaScript build configuration differs')
expect(readFileSync(join(options.root, 'out', 'main', 'index.js'), 'utf8')).toBe(
'main/index.js'
)
expect(javascriptInventory(join(options.root, 'out'))).toEqual(current)
})
it('rejects a damaged archive before replacing the current build', () => {
@@ -224,9 +228,13 @@ describe('release JavaScript transfer', () => {
it('rejects a file inventory that no longer describes the archive', () => {
const options = fixture()
const manifest = packReleaseJavascript(options)
writeFileSync(join(options.root, 'out', 'main', 'index.js'), 'current build')
writeFileSync(join(options.root, 'out', 'current-only.js'), 'current only')
const current = javascriptInventory(join(options.root, 'out'))
manifest.files[0].sha256 = 'c'.repeat(64)
writeFileSync(join(options.artifactDir, 'manifest.json'), JSON.stringify(manifest))
expect(() => restoreReleaseJavascript(options)).toThrow('JavaScript file inventory differs')
expect(javascriptInventory(join(options.root, 'out'))).toEqual(current)
})
it.each([
@@ -248,10 +256,9 @@ describe('release JavaScript transfer', () => {
'rejects symlinks instead of following files outside the build',
() => {
const options = fixture()
symlinkSync(
join(options.root, 'out', 'main', 'index.js'),
join(options.root, 'out', 'linked.js')
)
const outside = join(options.root, 'outside.js')
writeFileSync(outside, 'outside the build')
symlinkSync(outside, join(options.root, 'out', 'linked.js'))
expect(() => packReleaseJavascript(options)).toThrow('contains a symlink')
}
)
@@ -1,7 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import {
dispatchReleaseMacBuildWorkflow,
executeReleaseMacBuildMode,
expectedReleaseMacBuildRunTitle,
readReleaseMacBuildWorkflowOptions,
@@ -22,13 +21,15 @@ const baseOptions = {
describe('release mac build workflow dispatch', () => {
it('passes the shared JavaScript commit only when the parent produced a bundle', async () => {
const request = vi.fn(async () => ({}))
const api = { owner: 'stablyai', repo: 'orca', request }
await dispatchReleaseMacBuildWorkflow(api, {
...baseOptions,
javascriptSourceSha: 'a'.repeat(40)
})
expect(request.mock.calls[0][2].inputs).toEqual({
const { fetch, requests } = createGitHubFetch([
jsonResponse(200, { workflow_run_id: 123 }),
jsonResponse(200, { id: 123, status: 'completed', conclusion: 'success' })
])
await runReleaseMacBuildWorkflow(
{ ...baseOptions, javascriptSourceSha: 'a'.repeat(40) },
{ fetch, now: () => Date.parse('2026-06-30T12:00:00Z'), sleep: vi.fn() }
)
expect(requests[0].body.inputs).toEqual({
release_run_id: '777',
tag: 'v1.2.3-rc.4',
javascript_source_sha: 'a'.repeat(40)
+15 -2
View File
@@ -1,5 +1,7 @@
import { createElement, type ReactElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { createElement } from 'react'
import type { ReactElement } from 'react'
import { act, create } from 'react-test-renderer'
import type { ReactTestRenderer } from 'react-test-renderer'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ConnectionState } from './types'
import type { RpcClient } from './rpc-client'
@@ -180,6 +182,7 @@ describe('useHostClient', () => {
connectMock.mockReturnValueOnce(client1).mockReturnValueOnce(client2)
loadHostsMock.mockResolvedValue([HOST, host2])
const selections: { hostId: string; value: ReturnType<typeof useHostClient> }[] = []
let selectedHostId = HOST.id
let selectedClient: RpcClient | null = null
let selectedClientId: string | null = null
@@ -187,6 +190,7 @@ describe('useHostClient', () => {
let renderer: ReactTestRenderer | null = null
function Probe(): null {
const selected = useHostClient(selectedHostId)
selections.push({ hostId: selectedHostId, value: selected })
selectedClient = selected.client
selectedClientId = selected.clientId
selectedState = selected.state
@@ -213,6 +217,15 @@ describe('useHostClient', () => {
expect(selectedClient).toBe(client2)
expect(selectedClientId).toBe(host2.deviceToken)
expect(selectedState).toBe('disconnected')
for (const { value } of selections.filter(({ hostId }) => hostId === host2.id)) {
expect(value.state).toBe('disconnected')
if (value.client === null) {
expect(value.clientId).toBeNull()
} else {
expect(value.client).toBe(client2)
expect(value.clientId).toBe(host2.deviceToken)
}
}
expect(connectMock).toHaveBeenCalledTimes(2)
} finally {
act(() => renderer?.unmount())
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react'
import { act, create } from 'react-test-renderer'
import type { ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-envelope'
import { BRIDGE_PAGE_CLIENT_ID } from '../mobile-web-shell/bridge/bridge-page-client-identity'
@@ -47,6 +48,15 @@ function Screen(): null {
return null
}
const trees: ReactTestRenderer[] = []
const clients: BridgeRpcClient[] = []
function mount(client: BridgeRpcClient): ReactTestRenderer {
const tree = create(render(client))
trees.push(tree)
return tree
}
function render(client: BridgeRpcClient): ReactElement {
return (
<RpcClientProvider client={client}>
@@ -73,12 +83,16 @@ function installChannel(): { deliver: (frame: unknown) => void } {
}
/** What the entry hands the provider: one client, already holding a session. */
function createReadyClient(deliver: (frame: unknown) => void): BridgeRpcClient {
function createReadyClient(
deliver: (frame: unknown) => void,
init: unknown = INIT
): BridgeRpcClient {
const client = createShellPageClient()
if (client === null) {
throw new Error('no channel installed')
}
deliver(INIT)
clients.push(client)
deliver(init)
return client
}
@@ -97,6 +111,15 @@ beforeEach(() => {
})
afterEach(() => {
act(() => {
for (const tree of trees.splice(0)) {
tree.unmount()
}
})
for (const client of clients.splice(0)) {
client.close()
}
vi.restoreAllMocks()
vi.useRealTimers()
Reflect.deleteProperty(globalThis, 'orcaBridge')
})
@@ -106,7 +129,7 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
expect(screen.mounts).toBe(1)
@@ -124,7 +147,7 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
const context = readContext()
@@ -138,7 +161,7 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
// What `terminal.subscribe` carries as `client.id` and what the send gate reads. Null was not a
@@ -152,7 +175,7 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
const first = readContext().getClientId('host-a')
@@ -160,8 +183,14 @@ describe('the page provider', () => {
// The composer's send journal refuses a retained operation whose caller fingerprint moved, and
// it has no expiry, so that would make "send it again" a permanent refusal for that message.
act(() => {
channel.deliver({ ...INIT, sessionId: 'session-b' })
create(render(client))
for (const tree of trees.splice(0)) {
tree.unmount()
}
})
client.close()
const nextChannel = installChannel()
act(() => {
mount(createReadyClient(nextChannel.deliver, { ...INIT, sessionId: 'session-b' }))
})
expect(readContext().getClientId('host-a')).toBe(first)
@@ -172,7 +201,7 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
const listener = vi.fn()
readContext().subscribeHostState('host-a', listener)
@@ -193,10 +222,10 @@ describe('the page provider', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
const listener = vi.fn()
readContext().subscribeAllHosts(listener)
const unsubscribe = readContext().subscribeAllHosts(listener)
act(() => {
channel.deliver({
@@ -207,13 +236,22 @@ describe('the page provider', () => {
})
expect(listener).toHaveBeenCalledTimes(1)
unsubscribe()
act(() => {
channel.deliver({
v: BRIDGE_PROTOCOL_VERSION,
type: 'state',
connection: { ...INIT.connection, state: 'disconnected' }
})
})
expect(listener).toHaveBeenCalledTimes(1)
})
it('stops listening when the screen that asked goes away', () => {
const channel = installChannel()
const client = createReadyClient(channel.deliver)
act(() => {
create(render(client))
mount(client)
})
const listener = vi.fn()
const unsubscribe = readContext().subscribeHostState('host-a', listener)
@@ -235,10 +273,11 @@ describe('the page provider', () => {
const client = createReadyClient(channel.deliver)
const close = vi.spyOn(client, 'close')
act(() => {
create(render(client))
mount(client)
})
const context = readContext()
expect(context.acquire('host-a', {})).toBe(client)
context.release('host-a', {})
context.releaseAndCloseIfUnused('host-a', {})
context.closeIfUnused('host-a')
@@ -255,7 +294,7 @@ describe('the page provider', () => {
// renders, and an inert one painted controls whose only effect on the page was nothing.
const channel = installChannel()
act(() => {
create(render(createReadyClient(channel.deliver)))
mount(createReadyClient(channel.deliver))
})
expect(readContext().forceReconnect).toBeNull()
})
@@ -35,10 +35,13 @@ describe('executionHost on the envelope', () => {
streams.subscribe('terminal.subscribe', params, () => {})
await vi.waitFor(() => expect(sent).toHaveLength(2))
disposeServer()
expect(sent.at(-1)).toMatchObject({
expect(sent.at(-1)).toEqual({
id: 'relay-3',
method: 'terminal.unsubscribe',
params: { subscriptionId: 't1:phone', client: { id: 'phone' }, requestId: 'relay-1' },
envelope: { executionHost: SERVER }
})
streams.clear()
})
})
@@ -59,13 +62,21 @@ describe('a relay stream slot', () => {
envelope: { executionHost: SERVER, trace: 't' }
})
await vi.waitFor(() => expect(sent).toHaveLength(1))
streams.subscribe('terminal.subscribe', params, () => {}, {
const disposeNewer = streams.subscribe('terminal.subscribe', params, () => {}, {
envelope: { trace: 't', executionHost: SERVER }
})
await vi.waitFor(() => expect(sent).toHaveLength(2))
// The newer sibling already holds the slot on the server, so the older one sends no unsubscribe.
disposeOlder()
expect(sent.map((frame) => frame.method)).toEqual(['terminal.subscribe', 'terminal.subscribe'])
disposeNewer()
expect(sent.at(-1)).toEqual({
id: 'relay-3',
method: 'terminal.unsubscribe',
params: { subscriptionId: 't1:phone', client: { id: 'phone' }, requestId: 'relay-2' },
envelope: { trace: 't', executionHost: SERVER }
})
streams.clear()
})
})
@@ -81,18 +92,28 @@ describe('a server workspace call on the direct wire', () => {
streams.subscribe(method, params, onData, options)
}
const server = scopeRpcClientToExecutionHost(direct, SERVER)
void server.sendRequest('worktree.ps', { limit: 1 })
await vi.waitFor(() => expect(socket.sent).toHaveLength(1))
server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
// What #26501 wrote with its own `executionHost` option; the desktop parses JSON, so key order is not read.
expect(socket.frames()).toStrictEqual([
JSON.parse(
'{"id":"rpc-1","deviceToken":"device-token","method":"worktree.ps","params":{"limit":1},"executionHost":"runtime:env-1"}'
),
JSON.parse(
'{"id":"rpc-2","deviceToken":"device-token","method":"terminal.subscribe","params":{"terminal":"t1"},"executionHost":"runtime:env-1"}'
)
])
const request = server.sendRequest('worktree.ps', { limit: 1 })
let dispose: (() => void) | undefined
try {
await vi.waitFor(() => expect(socket.sent).toHaveLength(1))
dispose = server.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
// What #26501 wrote with its own `executionHost` option; the desktop parses JSON, so key order is not read.
expect(socket.frames()).toStrictEqual([
JSON.parse(
'{"id":"rpc-1","deviceToken":"device-token","method":"worktree.ps","params":{"limit":1},"executionHost":"runtime:env-1"}'
),
JSON.parse(
'{"id":"rpc-2","deviceToken":"device-token","method":"terminal.subscribe","params":{"terminal":"t1"},"executionHost":"runtime:env-1"}'
)
])
const reply: RpcResponse = { id: 'rpc-1', ok: true, result: { worktrees: [] } }
expect(requests.resolve(reply)).toBe(true)
await expect(request).resolves.toBe(reply)
} finally {
dispose?.()
requests.rejectAll('test cleanup')
await request.catch(() => {})
}
})
})
@@ -124,7 +145,11 @@ describe('scopeRpcClientToExecutionHost', () => {
const client = fakeClient()
const view = scopeRpcClientToExecutionHost(client, SERVER)
void view.sendRequest('worktree.ps', { limit: 1 }, { timeoutMs: 5, envelope: { trace: 't' } })
view.subscribe('terminal.subscribe', { terminal: 't1' }, () => {})
const onBinaryFrame = vi.fn()
view.subscribe('terminal.subscribe', { terminal: 't1' }, () => {}, {
onBinaryFrame,
envelope: { trace: 'stream', executionHost: 'runtime:wrong' }
})
view.close()
expect(client.sendRequest).toHaveBeenCalledWith(
'worktree.ps',
@@ -135,7 +160,7 @@ describe('scopeRpcClientToExecutionHost', () => {
'terminal.subscribe',
{ terminal: 't1' },
expect.any(Function),
{ envelope: { executionHost: SERVER } }
{ onBinaryFrame, envelope: { trace: 'stream', executionHost: SERVER } }
)
expect(client.close).not.toHaveBeenCalled()
expect(scopeRpcClientToExecutionHost(client, SERVER)).toBe(view)
@@ -1,14 +1,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT,
getDecorativeAgentTitleSignature,
isDecorativeAgentTitleFrameChange
} from './agent-decorative-title-signature'
afterEach(() => {
vi.restoreAllMocks()
})
const BRAILLE_SPINNER_FRAME_A = String.fromCharCode(0x280b)
const BRAILLE_SPINNER_FRAME_B = String.fromCharCode(0x2819)
@@ -26,27 +22,20 @@ describe('agent decorative title signatures', () => {
expect(getDecorativeAgentTitleSignature('vim src/index.ts')).toBeNull()
})
it('folds title whitespace without full whitespace replacement', () => {
const replace = vi.spyOn(String.prototype, 'replace')
it('folds title whitespace in the decorative signature', () => {
const title = ` \t${BRAILLE_SPINNER_FRAME_A}\t Codex is\nthinking ${' pasted text '.repeat(40)}`
expect(getDecorativeAgentTitleSignature(title)).toBe(
`working:Codex is thinking ${'pasted text '.repeat(40).trimEnd()}`
)
expect(
replace.mock.calls.filter(
([pattern]) => pattern instanceof RegExp && pattern.source === '\\s+'
)
).toHaveLength(0)
})
it('does not normalize oversized title text on the hot path', () => {
const replace = vi.spyOn(String.prototype, 'replace')
const title = `${BRAILLE_SPINNER_FRAME_A} Codex is thinking ${'x'.repeat(
DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT + 1
)}`
it('accepts the scan limit and returns null for longer decorative titles', () => {
const prefix = `${BRAILLE_SPINNER_FRAME_A} Codex is thinking `
const tail = 'x'.repeat(DECORATIVE_AGENT_TITLE_SIGNATURE_SOURCE_SCAN_LIMIT - prefix.length)
const title = `${prefix}${tail}`
expect(getDecorativeAgentTitleSignature(title)).toBeNull()
expect(replace).not.toHaveBeenCalled()
expect(getDecorativeAgentTitleSignature(title)).toBe(`working:Codex is thinking ${tail}`)
expect(getDecorativeAgentTitleSignature(`${title}x`)).toBeNull()
})
})
@@ -598,10 +598,7 @@ describe('shared agent-hook-listener', () => {
})
it('does not resurrect persisted idle child rows after a restart', () => {
// Why: the roster tracks only working children now. A persisted idle
// snapshot (from a build that kept idle rows) is a finished child, so
// hydration must drop it — otherwise restart would re-pile the exact
// squatting rows this fix removes.
// Idle snapshots have no live confirmation after restart.
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, [
{
id: 'aprobe2-6d3cb5b52120b7bf',
@@ -610,7 +607,8 @@ describe('shared agent-hook-listener', () => {
agentType: 'security-reviewer'
}
])
claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'after restart' })
const resumed = claudeEvent({ hook_event_name: 'UserPromptSubmit', prompt: 'after restart' })
expect(resumed?.payload.subagents).toBeUndefined()
const stop = claudeEvent({
hook_event_name: 'Stop',
background_tasks: [
@@ -408,7 +408,7 @@ describe('Claude owed task notifications that never arrive', () => {
wallClock.mockRestore()
})
it("restates under the stored row's launch token and owner, as a row with no hook event", () => {
it("restates under the stored row's launch token and session, as a row with no hook event", () => {
const { post, restated } = host()
const tokened = (payload: Record<string, unknown>) =>
post(payload, PANE_KEY, { launchToken: 'launch-1' })
@@ -421,7 +421,12 @@ describe('Claude owed task notifications that never arrive', () => {
vi.advanceTimersByTime(LEASE)
expect(restated[0]).toMatchObject({ launchToken: 'launch-1', payload: { state: 'done' } })
expect(restated[0]).toMatchObject({
launchToken: 'launch-1',
source: 'claude',
providerSession: { key: 'session_id', id: SESSION },
payload: { state: 'done' }
})
expect(restated[0].hookEventName).toBeUndefined()
})
@@ -12,6 +12,7 @@ import {
normalizeAndAccept,
PANE_KEY
} from './agent-hook-listener-test-harness'
import { makePaneKey } from './stable-pane-id'
describe('shared agent-hook-listener', () => {
let state: HookListenerState
@@ -154,7 +155,7 @@ describe('shared agent-hook-listener', () => {
state,
'claude',
{
paneKey: 'x'.repeat(300),
paneKey: makePaneKey('x'.repeat(300), '11111111-1111-4111-8111-111111111111'),
payload: { hook_event_name: 'UserPromptSubmit', prompt: 'hi' }
},
'production'
-4
View File
@@ -39,10 +39,6 @@ describe('agentKindToTuiAgent', () => {
}
})
it("reverses Claude's product id back to the TuiAgent id", () => {
expect(agentKindToTuiAgent('claude-code')).toBe('claude')
})
it('returns null for the catch-all and missing kinds', () => {
expect(agentKindToTuiAgent('other')).toBeNull()
expect(agentKindToTuiAgent(null)).toBeNull()
+3 -1
View File
@@ -14,7 +14,9 @@ describe('toAgentLaunchPreferences', () => {
})
it('answers nothing when no preference survives', () => {
expect(toAgentLaunchPreferences({ model: ' ', fastMode: true })).toBeUndefined()
expect(
toAgentLaunchPreferences({ model: ' ', effort: 123, mode: false, fastMode: true })
).toBeUndefined()
expect(toAgentLaunchPreferences(undefined)).toBeUndefined()
})
})