From cc929aaadbf8b170bbab6f377d93346a39e21f2e Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 23 Oct 2026 21:22:38 -0700 Subject: [PATCH] test: strengthen hook, navigation, and release coverage (#33028) * test: strengthen hook identity and transport assertions * test: strengthen mobile handoff and release policy coverage --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../scripts/regenerate-xterm-patches.test.mjs | 34 ++++++---- .../relay-era-ref-refusal-workflow.test.mjs | 3 + .../release-cut-token-permissions.test.mjs | 19 ++++-- .../route-handoff-host-area.web.test.tsx | 55 ++++++++++++---- .../src/navigation/route-handoff.web.test.tsx | 64 +++++++++++++++---- .../navigation/use-back-claim.web.test.tsx | 14 +++- .../workspace-route-handoff.test.tsx | 49 +++++++++----- .../src/navigation/workspace-route.test.tsx | 33 ++++++++-- ...gent-hook-listener-grok-completion.test.ts | 2 +- src/shared/agent-hook-listener-grok.test.ts | 25 ++++---- ...tener-opencode-session-correlation.test.ts | 7 ++ ...ook-listener-opencode-tui-identity.test.ts | 4 +- src/shared/agent-hook-relay.test.ts | 23 ++----- .../agent-hook-transport-interference.test.ts | 6 +- src/shared/agent-notification-id.test.ts | 8 ++- 15 files changed, 245 insertions(+), 101 deletions(-) diff --git a/config/scripts/regenerate-xterm-patches.test.mjs b/config/scripts/regenerate-xterm-patches.test.mjs index 1bc1c3387ea..c39eeadc710 100644 --- a/config/scripts/regenerate-xterm-patches.test.mjs +++ b/config/scripts/regenerate-xterm-patches.test.mjs @@ -260,20 +260,26 @@ describe('source derivation agreement', () => { ).toThrow(/disagree on a source file/) }) - it('diffs the checkout with pnpm formatting so the two halves stay comparable', () => { - expect(CHECKOUT_DIFF_FLAGS.filter((flag) => flag !== '--relative')).toEqual( - PNPM_DIFF_FLAGS.filter((flag) => flag !== '--no-index') - ) - expect(CHECKOUT_DIFF_FLAGS).toContain('--full-index') - expect(CHECKOUT_DIFF_FLAGS).not.toContain('--no-index') - }) + it('diffs a nested checkout with package-relative paths and full object IDs', async () => { + const root = await createDirectory() + const checkout = path.join(root, 'checkout') + const packageRoot = path.join(checkout, 'addons', 'widget') + const pristine = path.join(root, 'pristine') + await writeTree(packageRoot, PRISTINE) + await writeTree(pristine, PRISTINE) + execFileSync('git', ['init', '--quiet', checkout], { env: pnpmDiffEnvironment() }) + execFileSync('git', ['add', '.'], { cwd: checkout, env: pnpmDiffEnvironment() }) + await writeTree(packageRoot, PATCHED) - it('passes --relative as a flag, not as a pathspec', () => { - // After `--` git reads it as a path, silently leaving addon diffs rooted at the - // repo instead of the package, which drops every source hunk from the patch. - expect(CHECKOUT_DIFF_FLAGS.indexOf('--relative')).toBeLessThan( - CHECKOUT_DIFF_FLAGS.indexOf('--') - ) + const patch = execFileSync('git', [...CHECKOUT_DIFF_FLAGS, 'src/'], { + cwd: packageRoot, + encoding: 'utf8', + env: pnpmDiffEnvironment() + }) + + expect(patch).toBe(sourceHunks(diffFolders(pristine, packageRoot))) + expect(patch).toContain('diff --git a/src/Widget.ts b/src/Widget.ts') + expect(patch).toMatch(/^index [0-9a-f]{40}\.\.[0-9a-f]{40} 100644$/m) }) }) @@ -455,7 +461,7 @@ describe('committed xterm patch artifacts', () => { } }) - it('pins a full upstream commit and a buildable package entry', async () => { + it('requires a full upstream commit, a package, and allowed build steps', async () => { const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8')) expect(manifest.upstream.commit).toMatch(/^[0-9a-f]{40}$/) expect(manifest.packages.length).toBeGreaterThan(0) diff --git a/config/scripts/relay-era-ref-refusal-workflow.test.mjs b/config/scripts/relay-era-ref-refusal-workflow.test.mjs index 30df8a74908..47ed8f35cda 100644 --- a/config/scripts/relay-era-ref-refusal-workflow.test.mjs +++ b/config/scripts/relay-era-ref-refusal-workflow.test.mjs @@ -47,6 +47,9 @@ describe.each(refusalTargets)( ({ jobSteps, index, laterStep }) => { it('checks the ref before it builds or tags anything', () => { expect(index).toBeGreaterThan(0) + expect(jobSteps[index].if).toBeUndefined() + expect(jobSteps[index]['continue-on-error'] ?? false).toBe(false) + expect(jobSteps[index].shell).toBe('bash') expect(jobSteps[index - 1].uses).toMatch(/^actions\/checkout@/) const sparse = jobSteps[index - 1].with['sparse-checkout'] expect(sparse?.split('\n').includes('/config/tsconfig.relay.json') ?? true).toBe(true) diff --git a/config/scripts/release-cut-token-permissions.test.mjs b/config/scripts/release-cut-token-permissions.test.mjs index 1f5b410fba4..494c50a2e29 100644 --- a/config/scripts/release-cut-token-permissions.test.mjs +++ b/config/scripts/release-cut-token-permissions.test.mjs @@ -140,7 +140,9 @@ function normalizePermissions(permissions) { ) } -function intersectPermissions(granted, requested) { +const permissionEscalations = [] + +function intersectPermissions(granted, requested, identity) { const rank = { none: 0, read: 1, write: 2 } const scopes = new Set([...Object.keys(granted), ...Object.keys(requested)]) return Object.fromEntries( @@ -148,6 +150,14 @@ function intersectPermissions(granted, requested) { .map((scope) => { const grantedLevel = granted[scope] ?? 'none' const requestedLevel = requested[scope] ?? 'none' + if (rank[requestedLevel] > rank[grantedLevel]) { + permissionEscalations.push({ + identity, + scope, + granted: grantedLevel, + requested: requestedLevel + }) + } return [scope, rank[grantedLevel] < rank[requestedLevel] ? grantedLevel : requestedLevel] }) .filter(([, level]) => level !== 'none') @@ -165,12 +175,12 @@ function resolveWorkflowMatrix(workflow, workflowPath, inheritedPermissions, pre if (!requested) { throw new Error(`${workflowPath}#${jobName} has no resolvable permissions`) } - const effective = inheritedPermissions - ? intersectPermissions(inheritedPermissions, requested) - : requested const identity = prefix ? `${prefix} -> ${workflowPath}#${jobName}` : `${workflowPath}#${jobName}` + const effective = inheritedPermissions + ? intersectPermissions(inheritedPermissions, requested, identity) + : requested matrix[identity] = effective if (job.uses?.startsWith('./.github/workflows/')) { @@ -249,6 +259,7 @@ describe('release-cut token permissions', () => { ) it('keeps the complete effective job and reusable-workflow matrix least-privileged', () => { + expect(permissionEscalations).toEqual([]) expect(matrix).toEqual(EXPECTED_MATRIX) }) diff --git a/mobile/src/navigation/route-handoff-host-area.web.test.tsx b/mobile/src/navigation/route-handoff-host-area.web.test.tsx index 6e3bb452f01..6084a109e73 100644 --- a/mobile/src/navigation/route-handoff-host-area.web.test.tsx +++ b/mobile/src/navigation/route-handoff-host-area.web.test.tsx @@ -3,11 +3,13 @@ * the session screen inside the same document, decided by the page's real handoff rule against the * `init` the shell's real policy builds. */ -import { act, create } from 'react-test-renderer' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createShellPageClient } from '../mobile-web-shell/bridge/page-bootstrap' import { createBridgeInitFrame } from '../mobile-web-shell/bridge/bridge-init-frame' import { routeViewOf, type MobileWebPageRoute } from '../mobile-web-shell/page-route-policy' +import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-envelope' +import type { BridgeRpcClient } from '../mobile-web-shell/bridge/bridge-rpc-client' import type { RouteHandoff } from './route-handoff' const router = vi.hoisted(() => ({ @@ -65,6 +67,9 @@ const ROUTES: MobileWebPageRoute[] = [ } ] +const trees: ReactTestRenderer[] = [] +const clients: BridgeRpcClient[] = [] + const held: { handoff: RouteHandoff | null } = { handoff: null } function Screen(): null { @@ -76,7 +81,7 @@ function Screen(): null { function mountPage( pathname: string, wide: boolean -): { handoff: RouteHandoff; navigations: () => number } { +): { handoff: RouteHandoff; navigations: () => unknown[] } { const posted: string[] = [] const channel: { postMessage: (json: string) => void @@ -87,6 +92,7 @@ function mountPage( if (client === null) { throw new Error('no channel installed') } + clients.push(client) const view = routeViewOf(ROUTES, pathname, wide) const init = createBridgeInitFrame({ sessionId: 'session-a', @@ -108,10 +114,12 @@ function mountPage( }) channel.onmessage?.({ data: JSON.stringify(init) }) act(() => { - create( - - - + trees.push( + create( + + + + ) ) }) if (held.handoff === null) { @@ -121,7 +129,7 @@ function mountPage( return { handoff, navigations: () => - posted.map((json) => JSON.parse(json)).filter((frame) => frame.name === 'navigate').length + posted.map((json) => JSON.parse(json)).filter((frame) => frame.name === 'navigate') } } @@ -130,26 +138,45 @@ beforeEach(() => { vi.clearAllMocks() }) +afterEach(() => { + act(() => { + for (const tree of trees.splice(0)) { + tree.unmount() + } + }) + for (const client of clients.splice(0)) { + client.close() + } + Reflect.deleteProperty(globalThis, 'orcaBridge') +}) + describe('a worktree row in the host-area page', () => { it('opens the session inside this document, without a native push', () => { const page = mountPage('/h/host-1', true) page.handoff.push('/h/host-1/session/wt-1?name=main') expect(router.push).toHaveBeenCalledWith('/h/host-1/session/wt-1?name=main', undefined) - expect(page.navigations()).toBe(0) + expect(page.navigations()).toEqual([]) }) it('switches workspaces in place too, which the embedded sidebar does with a replace', () => { const page = mountPage('/h/host-1', true) page.handoff.replace('/h/host-1/session/wt-2') expect(router.replace).toHaveBeenCalledWith('/h/host-1/session/wt-2', undefined) - expect(page.navigations()).toBe(0) + expect(page.navigations()).toEqual([]) }) it('is still handed to the shell from a phone host page, whose grants do not cover it', () => { const page = mountPage('/h/host-1', false) page.handoff.push('/h/host-1/session/wt-1') expect(router.push).not.toHaveBeenCalled() - expect(page.navigations()).toBe(1) + expect(page.navigations()).toEqual([ + { + v: BRIDGE_PROTOCOL_VERSION, + type: 'notify', + name: 'navigate', + href: '/h/host-1/session/wt-1' + } + ]) }) }) @@ -161,7 +188,9 @@ describe('a session page opened on its own, leaving for the host route', () => { const page = mountPage('/h/host-1/session/wt-1', true) page.handoff.replace(hop) expect(router.replace).not.toHaveBeenCalled() - expect(page.navigations()).toBe(1) + expect(page.navigations()).toEqual([ + { v: BRIDGE_PROTOCOL_VERSION, type: 'notify', name: 'navigate', href: hop } + ]) expect(routeViewOf(ROUTES, '/h/host-1', true).ownsHostArea).toBe(true) }) @@ -169,6 +198,6 @@ describe('a session page opened on its own, leaving for the host route', () => { const page = mountPage('/h/host-1/session/wt-1', false) page.handoff.replace(hop) expect(router.replace).toHaveBeenCalledWith(hop, undefined) - expect(page.navigations()).toBe(0) + expect(page.navigations()).toEqual([]) }) }) diff --git a/mobile/src/navigation/route-handoff.web.test.tsx b/mobile/src/navigation/route-handoff.web.test.tsx index dc777e07fb0..c8e8fc3892b 100644 --- a/mobile/src/navigation/route-handoff.web.test.tsx +++ b/mobile/src/navigation/route-handoff.web.test.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react' -import { act, create } from 'react-test-renderer' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { BRIDGE_MAX_ROUTE_HREF_CHARS } from '../mobile-web-shell/bridge/bridge-caps' import { @@ -36,7 +36,7 @@ vi.mock('../transport/host-client-hooks', () => ({ })) import { RpcClientProvider } from '../transport/client-context.web' -import { useRouteHandoff, WRAPPED_HREF_MEMBERS } from './route-handoff.web' +import { useRouteHandoff } from './route-handoff.web' const INIT = { v: BRIDGE_PROTOCOL_VERSION, @@ -55,6 +55,9 @@ const INIT = { pageRoutes: ['/h/[hostId]'] } +const trees: ReactTestRenderer[] = [] +const clients: BridgeRpcClient[] = [] + const held: { handoff: RouteHandoff | null } = { handoff: null } function Screen(): null { @@ -62,13 +65,21 @@ function Screen(): null { return null } -function installChannel(): { posted: string[]; deliver: (frame: unknown) => void } { +function installChannel(): { + posted: string[] + deliver: (frame: unknown) => void + failPosts: () => void +} { + let failed = false const posted: string[] = [] const channel: { postMessage: (json: string) => void onmessage: ((event: { data: string }) => void) | null } = { postMessage: (json) => { + if (failed) { + throw new Error('page port unavailable') + } posted.push(json) }, onmessage: null @@ -76,6 +87,9 @@ function installChannel(): { posted: string[]; deliver: (frame: unknown) => void Object.defineProperty(globalThis, 'orcaBridge', { value: channel, configurable: true }) return { posted, + failPosts: () => { + failed = true + }, deliver: (frame) => { channel.onmessage?.({ data: JSON.stringify(frame) }) } @@ -83,21 +97,22 @@ function installChannel(): { posted: string[]; deliver: (frame: unknown) => void } /** The page as the entry leaves it: one client, already holding a session. */ -function mount(init: unknown): { posted: string[]; handoff: RouteHandoff } { +function mount(init: unknown): { posted: string[]; handoff: RouteHandoff; failPosts: () => void } { const channel = installChannel() const client = createShellPageClient() if (client === null) { throw new Error('no channel installed') } + clients.push(client) channel.deliver(init) act(() => { - create(render(client)) + trees.push(create(render(client))) }) const handoff = held.handoff if (handoff === null) { throw new Error('no screen mounted') } - return { posted: channel.posted, handoff } + return { posted: channel.posted, handoff, failPosts: channel.failPosts } } function render(client: BridgeRpcClient): ReactElement { @@ -147,6 +162,14 @@ beforeEach(() => { }) afterEach(() => { + act(() => { + for (const tree of trees.splice(0)) { + tree.unmount() + } + }) + for (const client of clients.splice(0)) { + client.close() + } restoreWarn?.() restoreWarn = null vi.useRealTimers() @@ -346,8 +369,12 @@ describe('a back this document cannot serve', () => { }) it('does not throw out of a tap handler when there is nowhere to post', () => { - const { handoff } = mount({ ...INIT, grants: { ...INIT.grants, native: [] } }) + const { posted, handoff, failPosts } = mount(INIT) + failPosts() expect(() => handoff.back()).not.toThrow() + expect(backs(posted)).toEqual([]) + expect(router.back).toHaveBeenCalledTimes(1) + expect(JSON.stringify(warned)).toContain('send-failed') }) }) @@ -360,10 +387,25 @@ describe('a back this document cannot serve', () => { */ describe('every member that takes a target', () => { it('is replaced rather than handed through, all of them', () => { - const { handoff } = mount(INIT) - for (const member of WRAPPED_HREF_MEMBERS) { - expect(typeof handoff[member], member).toBe('function') - expect(handoff[member], member).not.toBe(router[member]) + const options = { withAnchor: false } + for (const member of ['push', 'replace', 'navigate', 'dismissTo'] as const) { + router[member].mockClear() + const { posted, handoff } = mount(INIT) + handoff[member]('/h/host-b', options) + expect(router[member], member).toHaveBeenCalledExactlyOnceWith('/h/host-b', options) + router[member].mockClear() + handoff[member]('/h/host-a/session/wt-1') + expect(navigations(posted), member).toEqual([ + { + v: BRIDGE_PROTOCOL_VERSION, + type: 'notify', + name: 'navigate', + href: '/h/host-a/session/wt-1' + } + ]) + handoff[member]('/h/host-a/tasks#top') + expect(navigations(posted), member).toHaveLength(1) + expect(router[member], member).not.toHaveBeenCalled() } }) diff --git a/mobile/src/navigation/use-back-claim.web.test.tsx b/mobile/src/navigation/use-back-claim.web.test.tsx index ab63b2366a0..6fc66048dc4 100644 --- a/mobile/src/navigation/use-back-claim.web.test.tsx +++ b/mobile/src/navigation/use-back-claim.web.test.tsx @@ -1,6 +1,6 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' -import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { createPageBackConsumers, type PageBackConsumers @@ -38,11 +38,14 @@ function Screen({ claim }: { claim: (() => boolean) | null }): null { return null } +const trees: ReactTestRenderer[] = [] + function render(claim: (() => boolean) | null): ReactTestRenderer { let tree!: ReactTestRenderer act(() => { tree = create(createElement(Screen, { claim })) }) + trees.push(tree) return tree } @@ -55,6 +58,15 @@ beforeEach(() => { }) }) +afterEach(() => { + act(() => { + for (const tree of trees.splice(0)) { + tree.unmount() + } + }) + page.consumers = null +}) + /** * The page half of the seam every sheet takes. There is no hardware key inside a WebView: * react-native-web answers `BackHandler.addEventListener` with a console warning and an inert diff --git a/mobile/src/navigation/workspace-route-handoff.test.tsx b/mobile/src/navigation/workspace-route-handoff.test.tsx index 47669eb1727..2a30994865e 100644 --- a/mobile/src/navigation/workspace-route-handoff.test.tsx +++ b/mobile/src/navigation/workspace-route-handoff.test.tsx @@ -1,13 +1,13 @@ import { createElement } from 'react' -import { act, create } from 'react-test-renderer' -import { describe, expect, it, vi } from 'vitest' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' const route = vi.hoisted(() => { const opened: unknown[] = [] const router = { - push: (href: unknown) => opened.push(['push', href]), - replace: (href: unknown) => opened.push(['replace', href]), - navigate: (href: unknown) => opened.push(['navigate', href]), + push: (href: unknown, options?: unknown) => opened.push(['push', href, options]), + replace: (href: unknown, options?: unknown) => opened.push(['replace', href, options]), + navigate: (href: unknown, options?: unknown) => opened.push(['navigate', href, options]), back: () => opened.push(['back']) } const state: { opened: unknown[]; router: typeof router } = { @@ -23,6 +23,16 @@ import { useWorkspaceRouteHandoff } from './workspace-route-handoff' import { normalizeExecutionHostId } from '../../../src/shared/execution-host' import { WorkspaceExecutionHostContext } from './workspace-execution-host' +const trees: ReactTestRenderer[] = [] + +afterEach(() => { + act(() => { + for (const tree of trees.splice(0)) { + tree.unmount() + } + }) +}) + async function workspaceRouterFor( params: Record ): Promise> { @@ -33,11 +43,13 @@ async function workspaceRouterFor( return null } await act(async () => { - create( - createElement( - WorkspaceExecutionHostContext.Provider, - { value: normalizeExecutionHostId(params.executionHost) ?? undefined }, - createElement(Probe) + trees.push( + create( + createElement( + WorkspaceExecutionHostContext.Provider, + { value: normalizeExecutionHostId(params.executionHost) ?? undefined }, + createElement(Probe) + ) ) ) }) @@ -50,19 +62,24 @@ async function workspaceRouterFor( describe('a workspace screen’s router', () => { it('keeps every screen it opens on the workspace’s server', async () => { const router = await workspaceRouterFor({ executionHost: 'runtime:vm' }) - router.push('/h/host-1/review/wt?name=api') - router.replace('/h/host-1/session/wt') - router.navigate({ pathname: '/h/[hostId]/files/[worktreeId]', params: { hostId: 'host-1' } }) + const options = { withAnchor: false } + router.push('/h/host-1/review/wt?name=api', options) + router.replace('/h/host-1/session/wt', options) + router.navigate( + { pathname: '/h/[hostId]/files/[worktreeId]', params: { hostId: 'host-1' } }, + options + ) router.back() expect(route.opened).toEqual([ - ['push', '/h/host-1/review/wt?name=api&executionHost=runtime%3Avm'], - ['replace', '/h/host-1/session/wt?executionHost=runtime%3Avm'], + ['push', '/h/host-1/review/wt?name=api&executionHost=runtime%3Avm', options], + ['replace', '/h/host-1/session/wt?executionHost=runtime%3Avm', options], [ 'navigate', { pathname: '/h/[hostId]/files/[worktreeId]', params: { hostId: 'host-1', executionHost: 'runtime:vm' } - } + }, + options ], ['back'] ]) diff --git a/mobile/src/navigation/workspace-route.test.tsx b/mobile/src/navigation/workspace-route.test.tsx index b29cc2c2bf4..4fe7718840a 100644 --- a/mobile/src/navigation/workspace-route.test.tsx +++ b/mobile/src/navigation/workspace-route.test.tsx @@ -2,7 +2,7 @@ import { readdirSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { createElement, type ReactElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' const route = vi.hoisted(() => { const state: { @@ -155,6 +155,22 @@ const WORKSPACE_ROUTE_FILES = readdirSync(HOST_ROUTES, { recursive: true, encodi .filter((entry) => /(^|\/)\[worktreeId\](\.web)?\.tsx$/.test(entry)) .sort() +const trees: ReactTestRenderer[] = [] +const pairs: ReturnType[] = [] + +afterEach(async () => { + await act(async () => { + for (const tree of trees.splice(0)) { + tree.unmount() + } + }) + for (const pair of pairs.splice(0)) { + pair.client.close() + await pair.flush() + pair.host.dispose() + } +}) + async function render(element: ReactElement, gates: HostStatusGates): Promise { route.seen.length = 0 route.shellRoutes.length = 0 @@ -170,6 +186,7 @@ async function render(element: ReactElement, gates: HostStatusGates): Promise { it('is found on disk', () => { - expect(WORKSPACE_ROUTE_FILES.length).toBeGreaterThanOrEqual(14) + expect(WORKSPACE_ROUTE_FILES.length).toBeGreaterThan(0) }) it.each(WORKSPACE_ROUTE_FILES)('keeps %s on the server its route names', async (file) => { @@ -225,9 +242,10 @@ describe('a server workspace', () => { it('gates its features on that server’s own status, not the desktop’s', async () => { route.params = { hostId: 'host-1', executionHost: 'runtime:vm' } - const session = desktop([]) + const serverCapability = 'server-feature.v1' + const session = desktop([serverCapability]) await render(screen, desktopGates([...RELAYS, VAULT])) - await vi.waitFor(() => expect(below.capabilities).toEqual([])) + await vi.waitFor(() => expect(below.capabilities).toEqual([serverCapability])) expect(session.sendRequest).toHaveBeenCalledWith('status.get', undefined, { envelope: { executionHost: 'runtime:vm' } }) @@ -273,8 +291,10 @@ describe('a server workspace', () => { desktop() serverReport.current = FAILED await act(async () => { - create( - createElement(HostStatusGatesContext.Provider, { value: desktopGates(RELAYS) }, screen) + trees.push( + create( + createElement(HostStatusGatesContext.Provider, { value: desktopGates(RELAYS) }, screen) + ) ) }) expect(below.state).toBe('disconnected') @@ -299,6 +319,7 @@ describe('a server workspace', () => { rpc: shell, rewriteToPage: (json) => json.replace(',"rpcEnvelope"', '') }) + pairs.push(pair) await pair.flush() expect(pair.client.carriesRpcEnvelope?.()).toBe(false) route.client = pair.client diff --git a/src/shared/agent-hook-listener-grok-completion.test.ts b/src/shared/agent-hook-listener-grok-completion.test.ts index 263c9e01a03..af37cb05685 100644 --- a/src/shared/agent-hook-listener-grok-completion.test.ts +++ b/src/shared/agent-hook-listener-grok-completion.test.ts @@ -237,7 +237,7 @@ describe('Grok completion observations', () => { ).toBe('done') }) - it('keeps a starting finite task working until a later terminal observation', () => { + it('keeps a starting finite task working', () => { expect( normalize({ hookEventName: 'Stop', diff --git a/src/shared/agent-hook-listener-grok.test.ts b/src/shared/agent-hook-listener-grok.test.ts index 9fa6f844114..a3ea8080d98 100644 --- a/src/shared/agent-hook-listener-grok.test.ts +++ b/src/shared/agent-hook-listener-grok.test.ts @@ -289,9 +289,8 @@ describe('shared agent-hook-listener', () => { expect(event?.payload.prompt).toBe('Find recent PR') }) - it('strips newline-heavy Grok user_query wrappers without regex matching', () => { - const matchSpy = vi.spyOn(String.prototype, 'match') - const promptText = 'Find recent PR\n'.repeat(300) + it('preserves both ends of newline-heavy Grok user_query prompts', () => { + const promptText = `Find recent PR${'\n'.repeat(300)}Keep the final requirement` const event = normalizeHookPayload( state, 'grok', @@ -305,16 +304,18 @@ describe('shared agent-hook-listener', () => { 'production' ) - expect(event?.payload.prompt).toContain('Find recent PR') - expect(event?.payload.prompt).not.toContain('') - expect(event?.payload.prompt).not.toContain('') - const usedGrokWrapperMatch = matchSpy.mock.calls.some( - ([pattern]) => - pattern instanceof RegExp && - pattern.source.startsWith('^') && - pattern.source.includes('[\\s\\S]') + expect(event?.payload.prompt).toBe('Find recent PR Keep the final requirement') + + const tool = normalizeHookPayload( + state, + 'grok', + { + paneKey: PANE_KEY, + payload: { hookEventName: 'pre_tool_use', toolName: 'web_search' } + }, + 'production' ) - expect(usedGrokWrapperMatch).toBe(false) + expect(tool?.payload.prompt).toBe('Find recent PR Keep the final requirement') }) it('maps Grok feedback notifications to waiting without overwriting the prompt', () => { diff --git a/src/shared/agent-hook-listener-opencode-session-correlation.test.ts b/src/shared/agent-hook-listener-opencode-session-correlation.test.ts index c0bc1d0ef37..2a1914dc669 100644 --- a/src/shared/agent-hook-listener-opencode-session-correlation.test.ts +++ b/src/shared/agent-hook-listener-opencode-session-correlation.test.ts @@ -138,6 +138,13 @@ describe('correlateOpenCodeSessionOwners', () => { knownOwners: new Map() }) expect(results).toEqual([]) + const expired = correlateOpenCodeSessionOwners({ + sessions: [session('ses_1', NOW - 60_000)], + panes: [{ paneKey: 'pane-a', directory: DIR }], + clients: [client('pane-a', NOW - 120_000, NOW - 90_000)], + knownOwners: new Map() + }) + expect(expired).toEqual([]) }) it('breaks a same-directory tie by client evidence', () => { diff --git a/src/shared/agent-hook-listener-opencode-tui-identity.test.ts b/src/shared/agent-hook-listener-opencode-tui-identity.test.ts index 8195dde52f8..8e31013b96f 100644 --- a/src/shared/agent-hook-listener-opencode-tui-identity.test.ts +++ b/src/shared/agent-hook-listener-opencode-tui-identity.test.ts @@ -74,7 +74,7 @@ describe('legacy structural TUI identity at the execution-host boundary', () => bindOpenCodeSession(state, 'ses_b', { paneKey: PANE_A, boundAt: 1, basis: 'argv' }) trackOpenCodePaneLaunchToken(state, PANE_A, 'creator-live') const tokens = [...state.lastLaunchTokenByPaneKey] - const owner = lookupOpenCodeSessionPane(state, 'ses_b') + const owner = { ...lookupOpenCodeSessionPane(state, 'ses_b') } const admitOpenCodeTui = vi.fn(() => false) expect(normalizeHookPayload(state, 'opencode', body(), 'test', { admitOpenCodeTui })).toBeNull() expect(admitOpenCodeTui).toHaveBeenCalledExactlyOnceWith({ @@ -84,7 +84,7 @@ describe('legacy structural TUI identity at the execution-host boundary', () => hasExplicitPrompt: false }) expect([...state.lastLaunchTokenByPaneKey]).toEqual(tokens) - expect(lookupOpenCodeSessionPane(state, 'ses_b')).toBe(owner) + expect(lookupOpenCodeSessionPane(state, 'ses_b')).toEqual(owner) expect(state.lastStatusByPaneKey.size).toBe(0) expect(state.lastPromptByPaneKey.size).toBe(0) }) diff --git a/src/shared/agent-hook-relay.test.ts b/src/shared/agent-hook-relay.test.ts index 8587bc9020a..41d75f6f029 100644 --- a/src/shared/agent-hook-relay.test.ts +++ b/src/shared/agent-hook-relay.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' import { - AGENT_HOOK_INSTALL_PLUGINS_METHOD, - AGENT_HOOK_NOTIFICATION_METHOD, - AGENT_HOOK_REQUEST_REPLAY_METHOD, - AGENT_HOOK_SHED_FIELDS_KEY, ORCA_FEATURE_REMOTE_AGENT_HOOKS_ENV, createShedSubagentsField, isAgentHookSource, @@ -13,12 +9,6 @@ import { import type { ParsedAgentStatusPayload } from './agent-status-types' describe('agent-hook-relay wire shape', () => { - it('exposes stable JSON-RPC method names', () => { - expect(AGENT_HOOK_NOTIFICATION_METHOD).toBe('agent.hook') - expect(AGENT_HOOK_REQUEST_REPLAY_METHOD).toBe('agent_hook.requestReplay') - expect(AGENT_HOOK_INSTALL_PLUGINS_METHOD).toBe('agent_hook.installPlugins') - }) - it('validates hook sources crossing persisted and relay trust boundaries', () => { expect(isAgentHookSource('claude')).toBe(true) expect(isAgentHookSource('kimi')).toBe(true) @@ -69,7 +59,6 @@ describe('restoreShedStatusFields', () => { }) it('does not restore a shed roster across a prompt identity change', () => { - expect(AGENT_HOOK_SHED_FIELDS_KEY).toBe('shedFields') const nextTurn = { ...shed, prompt: 'next prompt' } const restored = restoreShedStatusFields( nextTurn, @@ -110,11 +99,13 @@ describe('restoreShedStatusFields', () => { expect(restoreShedStatusFields(shed, [{ toString: () => 'subagents' }], cached)).toBe(shed) }) - it('does not overwrite a value the envelope still carries', () => { - const fresh: ParsedAgentStatusPayload = { ...shed, lastAssistantMessage: 'fresh' } - expect( - restoreShedStatusFields(fresh, ['lastAssistantMessage'], cached).lastAssistantMessage - ).toBe('fresh') + it('does not overwrite a roster the envelope still carries', () => { + const freshRoster = [ + { id: 'child-fresh', agentType: 'reviewer', state: 'working' as const, startedAt: 2 } + ] + const fresh: ParsedAgentStatusPayload = { ...shed, subagents: freshRoster } + const restored = restoreShedStatusFields(fresh, [createShedSubagentsField(roster)], cached) + expect(restored.subagents).toEqual(freshRoster) }) it('is a no-op with no cached payload for the pane', () => { diff --git a/src/shared/agent-hook-transport-interference.test.ts b/src/shared/agent-hook-transport-interference.test.ts index 95b4c97c055..4234a39882c 100644 --- a/src/shared/agent-hook-transport-interference.test.ts +++ b/src/shared/agent-hook-transport-interference.test.ts @@ -30,9 +30,9 @@ describe('classifyTruncatedHookRequest', () => { }) it('reads the first value when the header arrives duplicated', () => { - expect(classifyTruncatedHookRequest(['4096', '4096'], 12)).toBeInstanceOf( - HookRequestTruncatedError - ) + const error = classifyTruncatedHookRequest(['4096', '8'], 12) + expect(error).toBeInstanceOf(HookRequestTruncatedError) + expect(error).toMatchObject({ bytesRead: 12, contentLength: 4096 }) }) }) diff --git a/src/shared/agent-notification-id.test.ts b/src/shared/agent-notification-id.test.ts index c167750094d..bd32a616cdb 100644 --- a/src/shared/agent-notification-id.test.ts +++ b/src/shared/agent-notification-id.test.ts @@ -2,14 +2,18 @@ import { describe, expect, it } from 'vitest' import { buildAgentNotificationId } from './agent-notification-id' describe('buildAgentNotificationId', () => { - it('builds a stable id for the same agent event metadata', () => { + it('builds a stable escaped id for the same agent event metadata', () => { const args = { worktreeId: 'repo::/Users/me/orca/workspaces/feature', paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', stateStartedAt: 1780000000123 } - expect(buildAgentNotificationId(args)).toBe(buildAgentNotificationId(args)) + const id = buildAgentNotificationId(args) + expect(id).toBe( + 'agent:repo%3A%3A%2FUsers%2Fme%2Forca%2Fworkspaces%2Ffeature:tab-1%3A11111111-1111-4111-8111-111111111111:1780000000123' + ) + expect(buildAgentNotificationId(args)).toBe(id) }) it('changes when the agent state start time changes', () => {