From edbdfe4218a6782d2607a9126eab056a6fbbe2b4 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 14 Sep 2026 02:06:18 -0400 Subject: [PATCH] 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 --- .../use-new-workspace-runtime-context.test.ts | 87 ++++++++++--------- .../use-new-workspace-runtime-context.ts | 7 +- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/mobile/src/components/use-new-workspace-runtime-context.test.ts b/mobile/src/components/use-new-workspace-runtime-context.test.ts index 377e76a5083..cc98330b4ed 100644 --- a/mobile/src/components/use-new-workspace-runtime-context.test.ts +++ b/mobile/src/components/use-new-workspace-runtime-context.test.ts @@ -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 +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 { + async function mount(settingsResult: unknown, uiResult: unknown): Promise { // 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']) }) }) diff --git a/mobile/src/components/use-new-workspace-runtime-context.ts b/mobile/src/components/use-new-workspace-runtime-context.ts index c5cc21224a7..a6e078a4d75 100644 --- a/mobile/src/components/use-new-workspace-runtime-context.ts +++ b/mobile/src/components/use-new-workspace-runtime-context.ts @@ -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 ?? {}) }