fix(mobile): degrade a null ui.get result the way the settings leg now does

One host answers both legs of useNewWorkspaceRuntimeContext, so fixing only
settings.get left the likelier failure in place: a null or absent ui.get result
still threw `reading 'ui'` out of the effect, skipping the provider commit.

Review follow-ups on the same files: reply() returns the literal uncast and the
stub client is FakeSession, dropping two assertions and their SAFETY disables;
the degradation cases now assert absolute state instead of comparing mounts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-14 02:06:18 -04:00
parent 54f3e6363a
commit edbdfe4218
2 changed files with 50 additions and 44 deletions
@@ -1,35 +1,41 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it } from 'vitest'
import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
import { useNewWorkspaceRuntimeContext } from './use-new-workspace-runtime-context'
type RuntimeContext = ReturnType<typeof useNewWorkspaceRuntimeContext>
type PublishedState = Pick<
RuntimeContext,
'runtimeSettings' | 'trustedOrcaHooks' | 'availableProviders'
>
const TRUSTED_HOOKS = { '/repo/orca.yaml': 'sha-1' }
const UI_WITH_TRUST = { ui: { trustedOrcaHooks: TRUSTED_HOOKS } }
const SETTINGS = { defaultTuiAgent: 'codex', visibleTaskProviders: ['github', 'linear'] }
function reply(result: unknown): RpcResponse {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the test scripts raw host replies, not validated payloads.
return { id: 'r', ok: true, result, _meta: { runtimeId: 'runtime-1' } } as RpcResponse
return { id: 'r', ok: true, result, _meta: { runtimeId: 'runtime-1' } }
}
/** Every prerequisite answers normally; only the settings result varies. */
function clientAnsweringSettingsWith(settingsResult: unknown): RpcClient {
const sendRequest = vi.fn(async (method: string) => {
/** Every prerequisite answers normally; only the two reads under test vary. */
function clientAnswering(settingsResult: unknown, uiResult: unknown): RpcClient {
const client = new FakeSession('connected')
client.sendRequest.mockImplementation(async (method: string) => {
switch (method) {
case 'settings.get':
return reply(settingsResult)
case 'ui.get':
return reply({ ui: { trustedOrcaHooks: TRUSTED_HOOKS } })
return reply(uiResult)
case 'preflight.check':
return reply({ glab: { installed: false } })
default:
return reply({ connected: false })
}
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reaches only sendRequest on the client.
return { sendRequest } as unknown as RpcClient
return client
}
describe('useNewWorkspaceRuntimeContext', () => {
@@ -40,9 +46,9 @@ describe('useNewWorkspaceRuntimeContext', () => {
renderer = null
})
async function mount(settingsResult: unknown): Promise<RuntimeContext> {
async function mount(settingsResult: unknown, uiResult: unknown): Promise<PublishedState> {
// One client for the whole mount: the hook keys its effect on client identity.
const client = clientAnsweringSettingsWith(settingsResult)
const client = clientAnswering(settingsResult, uiResult)
let context!: RuntimeContext
function Harness(): null {
context = useNewWorkspaceRuntimeContext(client, true)
@@ -52,48 +58,43 @@ describe('useNewWorkspaceRuntimeContext', () => {
renderer = create(createElement(Harness))
})
await act(async () => {})
return context
const { runtimeSettings, trustedOrcaHooks, availableProviders } = context
return { runtimeSettings, trustedOrcaHooks, availableProviders }
}
it('degrades a null result the way a reply without a settings member degrades', async () => {
const absent = await mount({})
const absentState = {
runtimeSettings: absent.runtimeSettings,
trustedOrcaHooks: absent.trustedOrcaHooks,
availableProviders: absent.availableProviders
}
act(() => renderer?.unmount())
renderer = null
const nullResult = await mount(null)
expect({
runtimeSettings: nullResult.runtimeSettings,
trustedOrcaHooks: nullResult.trustedOrcaHooks,
availableProviders: nullResult.availableProviders
}).toEqual(absentState)
// The state the property-read TypeError used to skip on its way out of the effect.
expect(absentState).toEqual({
// A null result used to throw the `settings` property read out of the effect, skipping the
// provider commit the absent case still reached.
it.each([
['null', null],
['absent', undefined],
['without a settings member', {}]
])('degrades a %s settings result to absent settings', async (_label, settingsResult) => {
expect(await mount(settingsResult, UI_WITH_TRUST)).toEqual({
runtimeSettings: null,
trustedOrcaHooks: TRUSTED_HOOKS,
availableProviders: ['github']
})
})
it('degrades an absent result the same way', async () => {
const context = await mount(undefined)
expect(context.runtimeSettings).toBeNull()
expect(context.trustedOrcaHooks).toEqual(TRUSTED_HOOKS)
expect(context.availableProviders).toEqual(['github'])
// Same defect on the sibling leg: `reading 'ui'` threw after the settings commit and before
// the provider commit.
it.each([
['null', null],
['absent', undefined],
['without a ui member', {}]
])('degrades a %s ui result to untrusted hooks', async (_label, uiResult) => {
expect(await mount({ settings: SETTINGS }, uiResult)).toEqual({
runtimeSettings: SETTINGS,
trustedOrcaHooks: {},
availableProviders: ['github']
})
})
it('publishes the settings a host does send', async () => {
const context = await mount({
settings: { defaultTuiAgent: 'codex', visibleTaskProviders: ['github', 'linear'] }
it('publishes the settings and trust a host does send', async () => {
expect(await mount({ settings: SETTINGS }, UI_WITH_TRUST)).toEqual({
runtimeSettings: SETTINGS,
trustedOrcaHooks: TRUSTED_HOOKS,
availableProviders: ['github']
})
expect(context.runtimeSettings).toEqual({
defaultTuiAgent: 'codex',
visibleTaskProviders: ['github', 'linear']
})
expect(context.availableProviders).toEqual(['github'])
})
})
@@ -60,7 +60,12 @@ export function useNewWorkspaceRuntimeContext(
}
const uiResult = settledSuccess(uiRes)
if (uiResult) {
const ui = (uiResult.result as { ui?: { trustedOrcaHooks?: PersistedTrustedOrcaHooks } }).ui
const ui = (
uiResult.result as
| { ui?: { trustedOrcaHooks?: PersistedTrustedOrcaHooks } }
| null
| undefined
)?.ui
setTrustedOrcaHooks(ui?.trustedOrcaHooks ?? {})
}