From 8eea0c0d5bf01ff3f23db4c86ad0faa90f474fd1 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sat, 24 Oct 2026 18:26:35 -0700 Subject: [PATCH] test: remove redundant cases and repair audit assertions (#33334) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../use-mobile-pr-sidebar-controller.test.ts | 6 +- ...ssion-terminal-create-agent-launch.test.ts | 31 ---- .../use-mobile-session-view-mode.test.ts | 26 --- ...ctured-agent-session-hold-refused.test.tsx | 2 +- ...agent-session-newer-approval-send.test.tsx | 10 +- ...tured-agent-session-queued.test-fixture.ts | 49 ++++-- ...e-structured-agent-session-queued.test.tsx | 27 ++++ ...ructured-agent-session-retry-rows.test.tsx | 25 +-- ...ile-structured-agent-session-send.test.tsx | 149 +++++++++--------- ...red-agent-session.cut-turn-notice.test.tsx | 24 +-- ...e-mobile-structured-agent-session.test.tsx | 140 ++++------------ ...use-mobile-structured-agent-state.test.tsx | 4 +- .../use-mobile-structured-sends.test.tsx | 6 +- ...-mobile-structured-turn-indicator.test.tsx | 26 +-- ...mobile-terminal-inventory-recovery.test.ts | 5 +- .../use-notification-pane-navigation.test.tsx | 9 +- .../mobile-branch-compare.test.ts | 15 +- mobile/tests-typecheck-baseline.txt | 2 - ...ence-terminal-option-key-migration.test.ts | 31 ++-- .../persistence-repo-lifecycle.test.ts | 116 +------------- ...pty-connection-interrupt-inference.spec.ts | 32 ---- ...ction-notification-settings-gating.spec.ts | 2 +- ...n-post-dispose-restore-termination.test.ts | 16 +- ...onnection-remote-pause-drop-notice.spec.ts | 2 +- ...ection-remote-snapshot-source-grid.test.ts | 14 +- ...ection-runtime-owner-spawn-routing.spec.ts | 5 +- ...ty-connection-spawn-deferred-sleep.test.ts | 2 +- ...connection-spawn-left-pane-unbound.test.ts | 2 +- ...-status-child-work-admission-parse.test.ts | 77 +++++---- src/shared/agent-status-child-work.test.ts | 5 +- 30 files changed, 274 insertions(+), 586 deletions(-) diff --git a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts index 621bbe18655..0e41b58d02a 100644 --- a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts +++ b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts @@ -309,12 +309,8 @@ describe('isPrSidebarDetailsPlaceholder / prSidebarDetailsNeedFetch', () => { }) describe('buildMobilePrSidebarIdentity', () => { - it('keys identity on worktree + branch, not head SHA', () => { + it('keys identity on worktree and branch', () => { expect(buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' })).toBe('w1\u0000feat') - // Head advances on commit must not form a new identity — soft refresh keeps ready UI. - expect(buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' })).toBe( - buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' }) - ) }) it('is null without a branch and changes when branch or worktree changes', () => { diff --git a/mobile/src/session/use-mobile-session-terminal-create-agent-launch.test.ts b/mobile/src/session/use-mobile-session-terminal-create-agent-launch.test.ts index de140e842ae..9411011dc08 100644 --- a/mobile/src/session/use-mobile-session-terminal-create-agent-launch.test.ts +++ b/mobile/src/session/use-mobile-session-terminal-create-agent-launch.test.ts @@ -22,7 +22,6 @@ import { } from './pending-session-selection' import { NOTES_UNCONFIRMED_MESSAGE, PROMPT_UNCONFIRMED_MESSAGE } from './new-tab-agent-host-launch' import { AGENT_PROMPT_NOT_SENT_MESSAGE } from './pr-ai-triage-launch' -import { releaseTerminalCreateLock } from './terminal-create-lock' import { useMobileSessionTerminalCreateActions } from './use-mobile-session-terminal-create-actions' vi.mock('../platform/haptics', () => ({ triggerSuccess: vi.fn(), triggerError: vi.fn() })) @@ -457,36 +456,6 @@ describe('launches that carry a prompt', () => { }) }) - it('lands on the tab and frees the + lock before a paste-after-start agent replies', async () => { - const reply = launchReply( - { kind: 'terminal', handle: 'term_7' }, - { delivery: 'submit', outcome: 'handed-to-terminal' } - ) - const state = scope(scriptedClient(reply).client) - let landedBeforeReply: PendingSessionSelection | null = null - state.client = requestPortRpcClient(async (_method, params) => { - // The host lists the tab as soon as it exists, then waits for the agent to paste into it. - const pending = state.pendingSelectionRef.current - const landed = resolveLaunchedSelection(pending, [reservedTab(params, 'term_7')]) - state.pendingSelectionRef.current = landed.selection - if (landed.landedTabId && pending?.kind === 'launched') { - releaseTerminalCreateLock(state, pending.lock) - } - landedBeforeReply = state.pendingSelectionRef.current - expect(state.creatingTerminalRef.current).toBeNull() - return reply - }) - - await create_(state, 'aider', { agentPrompt: 'run the tests' }) - - expect(landedBeforeReply).toEqual( - expect.objectContaining({ kind: 'terminal', handle: 'term_7' }) - ) - // The late reply leaves the landed pick alone. - expect(state.pendingSelectionRef.current).toBe(landedBeforeReply) - expect(state.setCreating).toHaveBeenCalledTimes(2) - }) - it("keeps a second launch's + lock when the first one's reply comes in", async () => { const reply = launchReply( { kind: 'terminal', handle: 'term_7' }, diff --git a/mobile/src/session/use-mobile-session-view-mode.test.ts b/mobile/src/session/use-mobile-session-view-mode.test.ts index c90644cd36b..d0f3d938068 100644 --- a/mobile/src/session/use-mobile-session-view-mode.test.ts +++ b/mobile/src/session/use-mobile-session-view-mode.test.ts @@ -380,32 +380,6 @@ describe('useMobileSessionViewMode', () => { expect(readSessionViewOverridesPreference).toHaveBeenCalledTimes(2) }) - it('finishes an early toggle save after the route unmounts', async () => { - const overridesLoad = deferred>() - vi.mocked(readSessionViewOverridesPreference).mockReturnValue( - overridesLoad.promise.then((overrides) => ({ overrides, loaded: true })) - ) - function Harness(): null { - controller = useMobileSessionViewMode({ hostId: 'h', worktreeId: 'w' }) - return null - } - await act(async () => { - renderer = create(createElement(Harness)) - await Promise.resolve() - }) - act(() => controller?.toggleTabChatView('new-tab')) - act(() => renderer?.unmount()) - renderer = null - - await act(async () => { - overridesLoad.resolve(new Map([['saved-tab', 'terminal']])) - await Promise.resolve() - await Promise.resolve() - }) - - expect(updateSessionViewOverride).toHaveBeenCalledWith('h', 'w', 'new-tab', 'chat') - }) - it('does not reload a failed override after the route unmounts', async () => { const failedWrite = deferred() vi.mocked(updateSessionViewOverride).mockReturnValue(failedWrite.promise) diff --git a/mobile/src/session/use-mobile-structured-agent-session-hold-refused.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-hold-refused.test.tsx index cba5ec91ba8..e632bbc0a98 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-hold-refused.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-hold-refused.test.tsx @@ -36,7 +36,7 @@ const SNAPSHOT: AgentSessionSubscribeEvent = { window: { oldest: { epoch: 'epoch-1', sequence: 1 }, newest: { epoch: 'epoch-1', sequence: 1 }, - nextCursor: { epoch: 'epoch-1', sequence: 2 } + nextCursor: { epoch: 'epoch-1', sequence: 1 } }, liveCursor: { epoch: 'epoch-1', sequence: 1 }, hasOlder: false, diff --git a/mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx index 2d7e8f07462..4b2464197e0 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx @@ -83,10 +83,7 @@ async function sentDelivery(items: AgentJournalRenderItem[]): Promise { renderer = create(createElement(Harness)) }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) - const event = snapshotEvent() - if (event.type === 'snapshot') { - event.page.items = items - } + const event = snapshotEvent({ items }) act(() => listener?.(event)) await act(async () => { await hook!.sendWithOutcome('carry on') @@ -149,10 +146,7 @@ async function afterStop(items: AgentJournalRenderItem[]): Promise { renderer = create(createElement(Harness)) }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) - const event = snapshotEvent() - if (event.type === 'snapshot') { - event.page.items = [RUNNING_TURN, ...items] - } + const event = snapshotEvent({ items: [...items, RUNNING_TURN] }) act(() => listener?.(event)) act(() => hook?.cancel()) expect(hook?.turnIndicator.stopping).toBe(true) diff --git a/mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts b/mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts index 6d42a315860..a614f9f9be1 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts +++ b/mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts @@ -1,7 +1,10 @@ // The published shapes the queued-message hook tests feed the mobile session: host answers, // drafts, submissions and stream frames. -import type { AgentJournalSubmission } from '../../../src/shared/agent-session-journal-types' +import type { + AgentJournalRenderItem, + AgentJournalSubmission +} from '../../../src/shared/agent-session-journal-types' import type { AgentSessionQueuedMessage, AgentSessionQueuePause, @@ -88,33 +91,45 @@ export function queuedDraft( export function snapshotEvent(input?: { queuedMessages?: AgentSessionQueuedMessage[] | null + items?: AgentJournalRenderItem[] + fence?: number runningTurn?: boolean submissions?: AgentJournalSubmission[] -}): AgentSessionSubscribeEvent { +}): Extract { + const items: AgentJournalRenderItem[] = + input?.items ?? + (input?.runningTurn + ? [ + { + itemId: 'turn-item-1', + revision: 1, + body: { kind: 'turn', turnId: 'turn-1', state: 'running' }, + sequence: 1, + observedAt: 1 + } + ] + : []) + const oldest = items[0] + const newest = items.at(-1) + const fence = input?.fence ?? 3 return { type: 'snapshot', sessionId: SESSION_ID, - fence: 3, + fence, page: { sessionId: SESSION_ID, epoch: 'epoch-1', - fence: 3, + fence, direction: 'tail', - items: input?.runningTurn - ? [ - { - itemId: 'turn-item-1', - revision: 1, - body: { kind: 'turn', turnId: 'turn-1', state: 'running' }, - sequence: 1, - observedAt: 1 - } - ] - : [], + items, removedItemIds: [], submissions: input?.submissions ?? [], - window: { oldest: null, newest: null, nextCursor: { epoch: 'epoch-1', sequence: 0 } }, - liveCursor: { epoch: 'epoch-1', sequence: 0 }, + window: { + oldest: oldest ? { epoch: 'epoch-1', sequence: oldest.sequence } : null, + newest: newest ? { epoch: 'epoch-1', sequence: newest.sequence } : null, + nextCursor: { epoch: 'epoch-1', sequence: oldest?.sequence ?? 0 } + }, + liveCursor: { epoch: 'epoch-1', sequence: newest?.sequence ?? 0 }, hasOlder: false, hasNewer: false }, diff --git a/mobile/src/session/use-mobile-structured-agent-session-queued.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-queued.test.tsx index 40758e7135f..ac094eb7308 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-queued.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-queued.test.tsx @@ -6,6 +6,7 @@ // Older hosts retain the copy-before-delete fallback; capable hosts edit in place. import { createElement } from 'react' +import { agentJournalSubmissionKey } from '../../../src/shared/agent-session-journal-item-key' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { structuredAgentSessionPayloadFingerprint } from '../../../src/shared/structured-agent-session-mutation' @@ -320,6 +321,19 @@ describe('mobile structured queued messages', () => { queuedDraft({ messageId: 'drained' }), queuedDraft({ messageId: 'still-waiting' }) ], + items: [ + { + itemId: agentJournalSubmissionKey('fresh-hand-off-id'), + revision: 1, + sequence: 1, + observedAt: 10, + body: { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'sent message' }] + } + } + ], submissions: [acceptedSubmission('fresh-hand-off-id', 'drained')] }) ) @@ -331,6 +345,19 @@ describe('mobile structured queued messages', () => { CAPABLE, snapshotEvent({ queuedMessages: [queuedDraft({ messageId: 'same-id' })], + items: [ + { + itemId: agentJournalSubmissionKey('same-id'), + revision: 1, + sequence: 1, + observedAt: 10, + body: { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'sent message' }] + } + } + ], submissions: [acceptedSubmission('same-id')] }) ) diff --git a/mobile/src/session/use-mobile-structured-agent-session-retry-rows.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-retry-rows.test.tsx index ee0ed68bda6..45c41a03e7e 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-retry-rows.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-retry-rows.test.tsx @@ -1,4 +1,5 @@ import { createElement } from 'react' +import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, expect, it, vi } from 'vitest' import { agentSessionFailureWords } from '../../../src/shared/agent-session-failure-words' @@ -33,29 +34,7 @@ function retry(sequence: number, attempt: number): AgentJournalRenderItem { } function snapshot(items: AgentJournalRenderItem[]): AgentSessionSubscribeEvent { - const newest = items.length - return { - type: 'snapshot', - sessionId: 'session-1', - fence: 3, - page: { - sessionId: 'session-1', - epoch: 'epoch-1', - fence: 3, - direction: 'tail', - items, - removedItemIds: [], - submissions: [], - window: { - oldest: { epoch: 'epoch-1', sequence: 1 }, - newest: { epoch: 'epoch-1', sequence: newest }, - nextCursor: { epoch: 'epoch-1', sequence: newest + 1 } - }, - liveCursor: { epoch: 'epoch-1', sequence: newest }, - hasOlder: false, - hasNewer: false - } - } + return snapshotEvent({ items }) } // Module scope: the harness assigns these, which a test body's own `let` would narrow to null. diff --git a/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx index cc3b412ae81..40a70f465dd 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx @@ -3,9 +3,12 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentJournalDispatchState } from '../../../src/shared/agent-session-journal-types' import { DISPATCH_REJECTED_CANCELLED } from '../../../src/shared/structured-agent-session-dispatch-rejection' -import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire' import type { RpcClient } from '../transport/rpc-client' -import { fieldsOf, ok } from './use-mobile-structured-agent-session-queued.test-fixture' +import { + fieldsOf, + ok, + snapshotEvent +} from './use-mobile-structured-agent-session-queued.test-fixture' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' import { structuredSendResultFixture } from './structured-agent-send-result.test-fixture' import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session' @@ -40,31 +43,6 @@ function sendResult(dispatchState: AgentJournalDispatchState, reason: string | n }) } -function snapshotEvent(): Extract { - return { - type: 'snapshot', - sessionId: 'session-1', - fence: 3, - page: { - sessionId: 'session-1', - epoch: 'epoch-1', - fence: 3, - direction: 'tail', - items: [], - removedItemIds: [], - submissions: [], - window: { - oldest: null, - newest: null, - nextCursor: { epoch: 'epoch-1', sequence: 0 } - }, - liveCursor: { epoch: 'epoch-1', sequence: 0 }, - hasOlder: false, - hasNewer: false - } - } -} - describe('mobile structured send actions', () => { let renderer: ReactTestRenderer | null = null let hook: ReturnType | null = null @@ -277,20 +255,38 @@ describe('mobile structured send actions', () => { throw new Error('expected a submission answer') } original.submission.recovered = true - const event = snapshotEvent() - event.page.submissions = [original.submission] - event.page.items = [ - { - itemId: agentJournalSubmissionKey(original.submission.clientMessageId), - revision: 1, - sequence: 1, - observedAt: 10, - body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'same text' }] } + const event = snapshotEvent({ + items: [ + { + itemId: agentJournalSubmissionKey(original.submission.clientMessageId), + revision: 1, + sequence: 1, + observedAt: 10, + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'same text' }] } + } + ], + submissions: [original.submission] + }) + + sendRequest.mockImplementation(async (method, params) => { + if (method !== 'agentSession.send') { + return ok({ models: [], current: {} }) } - ] - sendRequest.mockImplementation(async (method) => - method === 'agentSession.send' ? sendResult('accepted') : ok({ models: [], current: {} }) - ) + const id = String(fieldsOf(fieldsOf(params).envelope).clientOperationId) + const value = structuredSendResultFixture('accepted') + if (!('submission' in value)) { + throw new Error('expected a submission answer') + } + value.clientMessageId = id + value.submission.clientMessageId = id + return ok({ + ok: true, + replayed: false, + fence: 3, + cursor: { epoch: 'epoch-1', sequence: 2 }, + value + }) + }) await mountSession() act(() => listener?.(event)) const messages = hook!.session.messages @@ -313,24 +309,25 @@ describe('mobile structured send actions', () => { const newId = sentIds()[0]! expect(newId).not.toBe(original.submission.clientMessageId) followUp.submission.clientMessageId = newId - const reply = snapshotEvent() - reply.page.submissions = [original.submission, followUp.submission] - reply.page.items = [ - ...event.page.items, - { ...event.page.items[0]!, itemId: sent, sequence: 2, observedAt: 20 }, - { - itemId: 'assistant-reply', - revision: 1, - sequence: 3, - observedAt: 30, - body: { - kind: 'message', - role: 'assistant', - blocks: [{ type: 'text', text: 'Reply to the new send' }] + const reply = snapshotEvent({ + items: [ + ...event.page.items, + { ...event.page.items[0]!, itemId: sent, sequence: 2, observedAt: 20 }, + { + itemId: 'assistant-reply', + revision: 1, + sequence: 3, + observedAt: 30, + body: { + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text: 'Reply to the new send' }] + } } - } - ] - reply.page.liveCursor = { epoch: 'epoch-1', sequence: 3 } + ], + submissions: [original.submission, followUp.submission] + }) + act(() => listener?.(reply)) expect(hook!.session.messages.map((message) => message.id)).toEqual([ messages[0]?.id, @@ -356,25 +353,27 @@ describe('mobile structured send actions', () => { throw new Error('expected submission') } value.submission.rejection = fact - const event = snapshotEvent() const sentRow = agentJournalSubmissionKey(value.submission.clientMessageId) - event.page.items = [ - { - itemId: 'auth-row', - sequence: 1, - revision: 1, - observedAt: 1, - body: { kind: 'status', tone: 'error', ...words } - }, - { - itemId: sentRow, - sequence: 2, - revision: 1, - observedAt: 2, - body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'my message' }] } - } - ] - event.page.submissions = [value.submission] + const event = snapshotEvent({ + items: [ + { + itemId: 'auth-row', + sequence: 1, + revision: 1, + observedAt: 1, + body: { kind: 'status', tone: 'error', ...words } + }, + { + itemId: sentRow, + sequence: 2, + revision: 1, + observedAt: 2, + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'my message' }] } + } + ], + submissions: [value.submission] + }) + sendRequest.mockImplementation(async (method) => { if (method !== 'agentSession.send') { return ok({ models: [], current: {} }) diff --git a/mobile/src/session/use-mobile-structured-agent-session.cut-turn-notice.test.tsx b/mobile/src/session/use-mobile-structured-agent-session.cut-turn-notice.test.tsx index e356693f8e0..253ed2b7bff 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.cut-turn-notice.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session.cut-turn-notice.test.tsx @@ -1,4 +1,5 @@ import { createElement } from 'react' +import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' @@ -48,28 +49,7 @@ const ITEMS: AgentJournalRenderItem[] = [ ] function snapshot(items: AgentJournalRenderItem[]): AgentSessionSubscribeEvent { - return { - type: 'snapshot', - sessionId: 'session-1', - fence: 3, - page: { - sessionId: 'session-1', - epoch: 'epoch-1', - fence: 3, - direction: 'tail', - items, - removedItemIds: [], - submissions: [], - window: { - oldest: { epoch: 'epoch-1', sequence: 1 }, - newest: { epoch: 'epoch-1', sequence: 3 }, - nextCursor: { epoch: 'epoch-1', sequence: 4 } - }, - liveCursor: { epoch: 'epoch-1', sequence: 3 }, - hasOlder: false, - hasNewer: false - } - } + return snapshotEvent({ items }) } describe('useMobileStructuredAgentSession transcript', () => { diff --git a/mobile/src/session/use-mobile-structured-agent-session.test.tsx b/mobile/src/session/use-mobile-structured-agent-session.test.tsx index 9b0a6616157..cc8f223217d 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session.test.tsx @@ -1,4 +1,5 @@ import { createElement } from 'react' +import { snapshotEvent as publishedSnapshot } from './use-mobile-structured-agent-session-queued.test-fixture' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { @@ -6,7 +7,6 @@ import type { AgentJournalRenderItem, AgentJournalResolution } from '../../../src/shared/agent-session-journal-types' -import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire' import type { SessionOptionDescriptor } from '../../../src/shared/native-chat-session-options' import type { RpcClient } from '../transport/rpc-client' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' @@ -32,58 +32,24 @@ function ok(result: unknown) { return { ok: true, result, _meta: { runtimeId: 'runtime-1' } } } -function snapshotEvent(fence = 3): AgentSessionSubscribeEvent { - return { - type: 'snapshot', - sessionId: 'session-1', - fence, - page: { - sessionId: 'session-1', - epoch: 'epoch-1', - fence, - direction: 'tail', - items: [], - removedItemIds: [], - submissions: [], - window: { - oldest: null, - newest: null, - nextCursor: { epoch: 'epoch-1', sequence: 0 } - }, - liveCursor: { epoch: 'epoch-1', sequence: 0 }, - hasOlder: false, - hasNewer: false - } - } +function snapshotEvent(fence = 3, items: AgentJournalRenderItem[] = []) { + return publishedSnapshot({ fence, items }) } -function snapshotWithMessage(): AgentSessionSubscribeEvent { - const event = snapshotEvent() - return { - ...event, - page: { - ...event.page, - items: [ - { - itemId: 'msg-1', - revision: 1, - sequence: 1, - observedAt: 10, - body: { - kind: 'message', - role: 'user', - blocks: [{ type: 'text', text: 'sent before the blip' }] - } - } - ], - window: { - oldest: { epoch: 'epoch-1', sequence: 1 }, - newest: { epoch: 'epoch-1', sequence: 1 }, - nextCursor: { epoch: 'epoch-1', sequence: 2 } - }, - liveCursor: { epoch: 'epoch-1', sequence: 1 } +function snapshotWithMessage() { + return snapshotEvent(3, [ + { + itemId: 'msg-1', + revision: 1, + sequence: 1, + observedAt: 10, + body: { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'sent before the blip' }] + } } - } as AgentSessionSubscribeEvent + ]) } function pendingResolution(): AgentJournalResolution { @@ -322,7 +288,7 @@ describe('useMobileStructuredAgentSession', () => { ) }) - it('re-holds after a reconnect that outlives the host release grace', async () => { + it('re-holds before resubscribing after a reconnect', async () => { act(() => { renderer = create(createElement(Harness, { connected: true })) }) @@ -333,8 +299,7 @@ describe('useMobileStructuredAgentSession', () => { ) await vi.waitFor(() => expect(subscribe).toHaveBeenCalledTimes(1)) - // A transport loss retires the connection-scoped hold; after the host's 15s grace - // it may evict the provider child. Reconnect must acquire before replaying the stream. + // Reconnect must acquire the connection-scoped hold before replaying the stream. await act(async () => { renderer?.update(createElement(Harness, { connected: false })) }) @@ -402,15 +367,7 @@ describe('useMobileStructuredAgentSession', () => { await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) act(() => listener?.(snapshotEvent(3))) act(() => listener?.(snapshotEvent(3))) - act(() => - listener?.({ - ...snapshotEvent(3), - page: { - ...snapshotEvent(3).page, - items: [approvalItem(), questionItem()] - } - }) - ) + act(() => listener?.(snapshotEvent(3, [approvalItem(), questionItem()]))) if (!hook) { throw new Error('hook not ready') @@ -569,32 +526,24 @@ describe('useMobileStructuredAgentSession', () => { }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) act(() => - listener?.({ - ...snapshotEvent(3), - page: { - ...snapshotEvent(3).page, - items: [ - approvalItemWithIdentity('approval-old', 4), - questionItemWithIdentity('question-old', 8) - ] - } - }) + listener?.( + snapshotEvent(3, [ + approvalItemWithIdentity('approval-old', 4), + questionItemWithIdentity('question-old', 8) + ]) + ) ) const approvalToken = hook!.permission!.options[0]!.send const questionToken = hook!.question!.optionTokens[0]! const freeText = formatQuestionFreeTextAnswer(hook!.question!, 'old answer') act(() => - listener?.({ - ...snapshotEvent(3), - page: { - ...snapshotEvent(3).page, - items: [ - approvalItemWithIdentity('approval-new', 9), - questionItemWithIdentity('question-new', 10) - ] - } - }) + listener?.( + snapshotEvent(3, [ + approvalItemWithIdentity('approval-new', 9), + questionItemWithIdentity('question-new', 10) + ]) + ) ) sendRequest.mockClear() @@ -638,15 +587,7 @@ describe('useMobileStructuredAgentSession', () => { renderer = create(createElement(Harness)) }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) - act(() => - listener?.({ - ...snapshotEvent(3), - page: { - ...snapshotEvent(3).page, - items: [approvalItem(), questionItem()] - } - }) - ) + act(() => listener?.(snapshotEvent(3, [approvalItem(), questionItem()]))) sendRequest.mockImplementation(async (method, params) => { if (method === 'agentSession.respondToApproval') { throw markRpcDeliveryUnknown(new Error('Connection closed')) @@ -679,12 +620,7 @@ describe('useMobileStructuredAgentSession', () => { renderer = create(createElement(Harness)) }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) - act(() => - listener?.({ - ...snapshotEvent(3), - page: { ...snapshotEvent(3).page, items: [approvalItem()] } - }) - ) + act(() => listener?.(snapshotEvent(3, [approvalItem()]))) let attempts = 0 sendRequest.mockImplementation(async (method, params) => { if (method === 'agentSession.respondToApproval' && attempts++ === 0) { @@ -749,15 +685,7 @@ describe('useMobileStructuredAgentSession', () => { renderer = create(createElement(Harness)) }) await vi.waitFor(() => expect(listener).toEqual(expect.any(Function))) - act(() => - listener?.({ - ...snapshotEvent(3), - page: { - ...snapshotEvent(3).page, - items: [runningStatusItem()] - } - }) - ) + act(() => listener?.(snapshotEvent(3, [runningStatusItem()]))) sendRequest.mockImplementation(async (method, params) => { if (method === 'agentSession.cancel') { throw markRpcDeliveryUnknown(new Error('Connection closed')) diff --git a/mobile/src/session/use-mobile-structured-agent-state.test.tsx b/mobile/src/session/use-mobile-structured-agent-state.test.tsx index c7b9ad28faf..e438d52dde7 100644 --- a/mobile/src/session/use-mobile-structured-agent-state.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-state.test.tsx @@ -120,7 +120,7 @@ describe('useMobileStructuredAgentState older history', () => { type: 'snapshot', sessionId: 'session-1', fence: 1, - page: page(newest, true) + page: { ...page(newest, true), direction: 'tail' } }) }) await vi.waitFor(() => expect(hook.current!.state.status).toBe('ready')) @@ -241,7 +241,7 @@ describe('useMobileStructuredAgentState live feed', () => { type: 'snapshot', sessionId: 'session-1', fence: 1, - page: { ...page([said(58), said(59)], false), liveCursor: cursorAt(59) } + page: { ...page([said(58), said(59)], false), direction: 'tail', liveCursor: cursorAt(59) } }) ) await act(async () => { diff --git a/mobile/src/session/use-mobile-structured-sends.test.tsx b/mobile/src/session/use-mobile-structured-sends.test.tsx index 4bd4eb42179..d2af8ffd584 100644 --- a/mobile/src/session/use-mobile-structured-sends.test.tsx +++ b/mobile/src/session/use-mobile-structured-sends.test.tsx @@ -52,11 +52,7 @@ function userRow(clientMessageId: string, text: string, sequence: number): Agent } function journal(items: AgentJournalRenderItem[], submissions: AgentJournalSubmission[]) { - const event = snapshotEvent({ submissions }) - if (event.type === 'snapshot') { - event.page.items = items - } - return event + return snapshotEvent({ items, submissions }) } describe('a structured send in the open chat', () => { diff --git a/mobile/src/session/use-mobile-structured-turn-indicator.test.tsx b/mobile/src/session/use-mobile-structured-turn-indicator.test.tsx index 19fb71df6fd..a456d8231ef 100644 --- a/mobile/src/session/use-mobile-structured-turn-indicator.test.tsx +++ b/mobile/src/session/use-mobile-structured-turn-indicator.test.tsx @@ -1,4 +1,5 @@ import { createElement } from 'react' +import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest' import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' @@ -21,30 +22,7 @@ function snapshot( fence: number, activity?: AgentSessionTurnActivity ): AgentSessionSubscribeEvent { - const newest = items.length - return { - type: 'snapshot', - sessionId: 'session-1', - fence, - ...(activity ? { activity } : {}), - page: { - sessionId: 'session-1', - epoch: 'epoch-1', - fence, - direction: 'tail', - items, - removedItemIds: [], - submissions: [], - window: { - oldest: { epoch: 'epoch-1', sequence: 1 }, - newest: { epoch: 'epoch-1', sequence: newest }, - nextCursor: { epoch: 'epoch-1', sequence: newest + 1 } - }, - liveCursor: { epoch: 'epoch-1', sequence: newest }, - hasOlder: false, - hasNewer: false - } - } + return { ...snapshotEvent({ items, fence }), ...(activity ? { activity } : {}) } } /** What the one live indicator row reads, resolved off the session journal. */ diff --git a/mobile/src/session/use-mobile-terminal-inventory-recovery.test.ts b/mobile/src/session/use-mobile-terminal-inventory-recovery.test.ts index 83d568b4151..643cb87b6da 100644 --- a/mobile/src/session/use-mobile-terminal-inventory-recovery.test.ts +++ b/mobile/src/session/use-mobile-terminal-inventory-recovery.test.ts @@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RpcClient } from '../transport/rpc-client' import { + type MobileTerminalInventoryRefreshOptions, useMobileTerminalInventoryRecovery, useMobileTerminalInventoryRecoveryBridge } from './use-mobile-terminal-inventory-recovery' @@ -30,7 +31,9 @@ function deferred() { const client = { getState: () => 'connected' } as unknown as RpcClient -const fetchTerminals = vi.fn(async () => true) +const fetchTerminals = vi.fn<(options?: MobileTerminalInventoryRefreshOptions) => Promise>( + async () => true +) type RecoveryActions = ReturnType let actions: RecoveryActions | null = null diff --git a/mobile/src/session/use-notification-pane-navigation.test.tsx b/mobile/src/session/use-notification-pane-navigation.test.tsx index 7f10da56c58..73fe7f215be 100644 --- a/mobile/src/session/use-notification-pane-navigation.test.tsx +++ b/mobile/src/session/use-notification-pane-navigation.test.tsx @@ -163,7 +163,7 @@ describe('the page pane hook', () => { expect(bridge.cleared).toHaveLength(2) }) - it('takes the tap that arrived while it was applying the one before it', () => { + it('applies successive pane requests and names each value when clearing', () => { render(true) deliver(`tab-a:${LEAF}`) deliver(`tab-b:${LEAF}`) @@ -172,13 +172,6 @@ describe('the page pane hook', () => { expect(bridge.cleared.map((entry) => entry.value)).toEqual([`tab-a:${LEAF}`, `tab-b:${LEAF}`]) }) - it('takes a different pane as its own request', () => { - render(true) - deliver(`tab-a:${LEAF}`) - deliver(`tab-b:${LEAF}`) - expect(switched.map((tab) => tab.id)).toEqual(['first', 'second']) - }) - it('asks for nothing and applies nothing when the route carries no pane', () => { // Load-bearing rather than defensive: the shell's own erase comes back as a route that moved, // and so does every `ready` answered after it. diff --git a/mobile/src/source-control/mobile-branch-compare.test.ts b/mobile/src/source-control/mobile-branch-compare.test.ts index cb7a5d7e86a..9e85f3d3938 100644 --- a/mobile/src/source-control/mobile-branch-compare.test.ts +++ b/mobile/src/source-control/mobile-branch-compare.test.ts @@ -1,17 +1,11 @@ -import { describe, expect, expectTypeOf, it } from 'vitest' -import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types' +import { describe, expect, it } from 'vitest' import { buildMobileBranchCompareSection, canOpenMobileBranchCompareDiff, - formatMobileBranchCompareSummary, - type MobileGitBranchCompareResult + formatMobileBranchCompareSummary } from './mobile-branch-compare' describe('mobile branch compare helpers', () => { - it('keeps the mobile branch compare type in lockstep with the runtime contract', () => { - expectTypeOf().toEqualTypeOf() - }) - it('sorts committed branch entries by path', () => { const section = buildMobileBranchCompareSection([ { path: 'zeta.ts', status: 'modified' }, @@ -32,6 +26,7 @@ describe('mobile branch compare helpers', () => { mergeBase: 'c'.repeat(40), changedFiles: 2, commitsAhead: 1, + errorMessage: undefined, status: 'ready' }) ).toBe('2 files - 1 commit - vs origin/main') @@ -46,6 +41,8 @@ describe('mobile branch compare helpers', () => { headOid: 'b'.repeat(40), mergeBase: 'c'.repeat(40), changedFiles: 1, + commitsAhead: undefined, + errorMessage: undefined, status: 'ready' }) ).toBe(true) @@ -58,6 +55,8 @@ describe('mobile branch compare helpers', () => { headOid: null, mergeBase: null, changedFiles: 0, + commitsAhead: undefined, + errorMessage: undefined, status: 'unborn-head' }) ).toBe(false) diff --git a/mobile/tests-typecheck-baseline.txt b/mobile/tests-typecheck-baseline.txt index 44256a34643..5c55cf2cbab 100644 --- a/mobile/tests-typecheck-baseline.txt +++ b/mobile/tests-typecheck-baseline.txt @@ -58,12 +58,10 @@ src/session/use-mobile-pr-sidebar-controller.test.ts src/session/use-mobile-session-terminal-create-actions.test.ts src/session/use-mobile-structured-agent-session.test.tsx src/session/use-mobile-structured-prompt-responses.test.tsx -src/session/use-mobile-terminal-inventory-recovery.test.ts src/settings/native-notification-delivery-settings.test.tsx src/settings/notification-display-test.test.tsx src/settings/settings-screen-state.test.tsx src/settings/voice-settings-poller-refresh.test.tsx -src/source-control/mobile-branch-compare.test.ts src/source-control/mobile-create-pr-action.test.ts src/source-control/mobile-git-status.test.ts src/source-control/mobile-hosted-review-create-intent-runner.test.ts diff --git a/src/main/persistence/terminal-topology/persistence-terminal-option-key-migration.test.ts b/src/main/persistence/terminal-topology/persistence-terminal-option-key-migration.test.ts index 5cf72d1647e..6d23634a682 100644 --- a/src/main/persistence/terminal-topology/persistence-terminal-option-key-migration.test.ts +++ b/src/main/persistence/terminal-topology/persistence-terminal-option-key-migration.test.ts @@ -201,20 +201,23 @@ describe('Store', () => { expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true) }) - it('preserves explicit "left" / "right" terminalMacOptionAsAlt through migration', async () => { - writeDataFile({ - schemaVersion: 1, - repos: [], - worktreeMeta: {}, - settings: { terminalMacOptionAsAlt: 'left' }, - ui: {}, - githubCache: { pr: {}, issue: {} }, - workspaceSession: {} - }) - const store = await createStore() - expect(store.getSettings().terminalMacOptionAsAlt).toBe('left') - expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true) - }) + it.each(['left', 'right'] as const)( + 'preserves explicit "%s" terminalMacOptionAsAlt through migration', + async (side) => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { terminalMacOptionAsAlt: side }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + const store = await createStore() + expect(store.getSettings().terminalMacOptionAsAlt).toBe(side) + expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true) + } + ) it('respects already-migrated settings with explicit "true"', async () => { writeDataFile({ diff --git a/src/main/persistence/tracking-repos/persistence-repo-lifecycle.test.ts b/src/main/persistence/tracking-repos/persistence-repo-lifecycle.test.ts index be96e7d208a..050e7ab3559 100644 --- a/src/main/persistence/tracking-repos/persistence-repo-lifecycle.test.ts +++ b/src/main/persistence/tracking-repos/persistence-repo-lifecycle.test.ts @@ -8,10 +8,7 @@ import { makeRepo, makeWorktreeLineage } from '../persistence-test-harness' -import { - makeTerminalTab, - makeTerminalTab as makeSessionTerminalTab -} from '../persistence-session-fixtures' +import { makeTerminalTab } from '../persistence-session-fixtures' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { rmSync, mkdtempSync } from 'node:fs' import { join } from 'node:path' @@ -86,9 +83,6 @@ describe('Store', () => { await closeTestStores() rmSync(testState.dir, { recursive: true, force: true }) }) - it('keeps the persistence harness terminal-tab fixture as the same public function', () => { - expect(makeTerminalTab).toBe(makeSessionTerminalTab) - }) it('addRepo stores a repo retrievable by getRepo', async () => { const store = await createStore() @@ -170,57 +164,6 @@ describe('Store', () => { expect(store.getRepo('sibling')?.projectGroupId).toBe(sibling.id) }) - it('adapts flat folder-scan groups into sparse nested folder scopes on load', async () => { - writeDataFile({ - schemaVersion: 1, - repos: [ - makeRepo({ id: 'api', path: '/workspace/platform/api', projectGroupId: 'root' }), - makeRepo({ id: 'web', path: '/workspace/platform/web', projectGroupId: 'root' }), - makeRepo({ - id: 'repo1', - path: '/workspace/platform/packages/shared/repo1', - projectGroupId: 'root' - }), - makeRepo({ - id: 'repo2', - path: '/workspace/platform/packages/shared/repo2', - projectGroupId: 'root' - }) - ], - worktreeMeta: {}, - settings: {}, - ui: {}, - githubCache: { pr: {}, issue: {} }, - projectGroups: [ - { - id: 'root', - name: 'Platform', - parentPath: '/workspace/platform', - parentGroupId: null, - createdFrom: 'folder-scan', - tabOrder: 0, - isCollapsed: false, - color: null, - createdAt: 1, - updatedAt: 1 - } - ] - }) - - const store = await createStore() - const groups = store.getProjectGroups() - const shared = groups.find((group) => group.name === 'packages/shared') - - expect(groups.map((group) => [group.name, group.parentGroupId, group.parentPath])).toEqual([ - ['Platform', null, '/workspace/platform'], - ['packages/shared', 'root', '/workspace/platform/packages/shared'] - ]) - expect(store.getRepo('api')?.projectGroupId).toBe('root') - expect(store.getRepo('web')?.projectGroupId).toBe('root') - expect(store.getRepo('repo1')?.projectGroupId).toBe(shared?.id) - expect(store.getRepo('repo2')?.projectGroupId).toBe(shared?.id) - }) - it('creates a project group when persisted group history is very large', async () => { const projectGroups: ProjectGroup[] = Array.from({ length: 130_000 }, (_, index) => ({ id: `group-${index}`, @@ -466,40 +409,6 @@ describe('Store', () => { ]) }) - it('removeProjectForHost keeps the surviving host session for a shared repo id + path', async () => { - const store = await createStore() - // Identical repo IDs and paths require host-scoped session pruning. - store.addRepo(makeRepo({ id: 'shared', path: '/repo' })) - store.addRepo( - makeRepo({ - id: 'shared', - path: '/repo', - connectionId: 'ssh-a', - executionHostId: 'ssh:ssh-a' - }) - ) - store.setWorktreeMeta(at('shared::/repo', 'local'), { displayName: 'local' }) - - store.setWorkspaceSession({ - ...getDefaultWorkspaceSession(), - lastVisitedAtByWorktreeId: { 'shared::/repo': 111 } - }) - store.setWorkspaceSession( - { - ...getDefaultWorkspaceSession(), - lastVisitedAtByWorktreeId: { 'shared::/repo': 222 } - }, - 'ssh:ssh-a' - ) - - store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-a' }) - - expect( - store.getWorkspaceSession('ssh:ssh-a').lastVisitedAtByWorktreeId?.['shared::/repo'] - ).toBeUndefined() - expect(store.getWorkspaceSession().lastVisitedAtByWorktreeId?.['shared::/repo']).toBe(111) - }) - it('removeProjectForHost on the local host keeps a surviving SSH host session', async () => { const store = await createStore() store.addRepo(makeRepo({ id: 'shared', path: '/repo' })) @@ -665,29 +574,6 @@ describe('Store', () => { expect(store.getRepos().map((repo) => repo.path)).toEqual(originalPaths) }) - it('removeProjectForHost prunes the SSH host meta (tagged hostId) for a shared id', async () => { - const store = await createStore() - store.addRepo(makeRepo({ id: 'shared', path: '/local/repo' })) - store.addRepo( - makeRepo({ - id: 'shared', - path: '/remote/repo', - connectionId: 'ssh-old', - executionHostId: 'ssh:ssh-old' - }) - ) - store.setWorktreeMeta(at('shared::/remote/repo/wt', 'ssh:ssh-old'), { - displayName: 'remote-wt' - }) - // Hostless legacy metadata belongs to local; remote removal must preserve it. - store.setWorktreeMeta(at('shared::/local/repo/wt', 'local'), { displayName: 'local-wt' }) - - store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-old' }) - - expect(store.getWorktreeMeta('shared::/remote/repo/wt')).toBeUndefined() - expect(store.getWorktreeMeta('shared::/local/repo/wt')).toBeDefined() - }) - it('removeProjectForHost prunes everything when the id exists on no other host', async () => { const store = await createStore() store.addRepo(makeRepo({ id: 'only', connectionId: 'ssh-x', executionHostId: 'ssh:ssh-x' })) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.spec.ts index e6fb73fb282..c82e35ef4bc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.spec.ts @@ -707,38 +707,6 @@ export function registerPtyConnectionInterruptInferenceTests(): void { } }) - it('does not request interrupt inference from a selection-copy keydown without terminal input', async () => { - const { connectPanePty } = await import('./pty-connection') - const transport = createMockTransport() - transportFactoryQueue.push(transport) - vi.useFakeTimers() - vi.setSystemTime(1_100) - const paneKey = makePaneKey('tab-1', LEAF_1) - mockStoreState.agentStatusByPaneKey[paneKey] = { - state: 'working', - prompt: 'copy selection', - updatedAt: 1_000, - stateStartedAt: 900, - agentType: 'codex', - paneKey, - terminalTitle: 'Codex', - stateHistory: [] - } - const terminalTarget = createKeyboardEventTarget() - const pane = createPane(1) - ;(pane.terminal as { element?: unknown; hasSelection: ReturnType }).element = - terminalTarget.target - ;(pane.terminal as { hasSelection: ReturnType }).hasSelection.mockReturnValue( - true - ) - - connectPanePty(pane as never, createManager(1) as never, createDeps() as never) - terminalTarget.dispatch(keyEvent({ key: 'c', ctrlKey: true })) - vi.advanceTimersByTime(500) - - expect(window.api.agentStatus.inferInterrupt).not.toHaveBeenCalled() - }) - it('removes the terminal key listener on dispose before a remount adds another', async () => { const { connectPanePty } = await import('./pty-connection') const firstTransport = createMockTransport() diff --git a/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.spec.ts index 0ae7228b1f4..20d1e54304b 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.spec.ts @@ -157,7 +157,7 @@ export function registerPtyConnectionNotificationSettingsGatingTests(): void { ) }) - it('raises terminal attention for agent completion when OS completion notifications are disabled', async () => { + it('forwards agent completion to notification policy when desktop banners are disabled', async () => { const { connectPanePty } = await import('./pty-connection') const transport = createMockTransport('pty-codex') transportFactoryQueue.push(transport) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts index 952e6376bbc..ad3bfb56a39 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts @@ -113,7 +113,7 @@ describe('connectPanePty', () => { }) const pane = createPane(1) - captureCallbackTerminalWrites(pane) + const { writes } = captureCallbackTerminalWrites(pane) // Why a counting getter: the restore task's `.finally` reads isVisibleRef.current // on every re-arm, so read count is a direct, deterministic measure of how many @@ -143,12 +143,16 @@ describe('connectPanePty', () => { }) await flushAsyncTicks(20) + expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1) + expect(writes).toContain('snapshot-state\r\n') disposable.dispose() const readsAtDispose = visibilityReads - await flushAsyncTicks(200) - - // A terminated chain settles in a handful of turns; the self-feeding loop grew - // once per microtask turn and would blow past this by orders of magnitude. - expect(visibilityReads - readsAtDispose).toBeLessThan(50) + try { + await flushAsyncTicks(200) + expect(visibilityReads - readsAtDispose).toBeLessThan(50) + } finally { + // Stop a regressed re-arm loop after measuring it so failures can finish. + visible = false + } }) }) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts index 40307366d2e..aac178c3ec9 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts @@ -131,7 +131,7 @@ export function registerPtyConnectionRemotePauseDropNoticeTests(): void { binding.dispose() }) - it('still restores on every reveal when the host cannot report drops', async () => { + it('restores on reveal when the host cannot report drops', async () => { const { transport, binding, setVisible } = await connectRemotePane(false) await setVisible(false) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts index 03a225f8528..b40d29a6fab 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts @@ -1,6 +1,6 @@ import type * as React from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { flushAsyncTicks, renderHeadlessBuffer } from './pty-connection-test-async' +import { flushAsyncTicks } from './pty-connection-test-async' import { terminalWriteText, createMockTransport, @@ -233,16 +233,4 @@ describe('pushed remote snapshot replay grid', () => { expect(session.pane.terminal.resize).not.toHaveBeenCalledWith(HOST_COLS, HOST_ROWS) session.dispose() }) - - it('only reproduces the host frame when it is parsed at the host grid', async () => { - const atHostGrid = await renderHeadlessBuffer([HOST_FRAME], HOST_COLS, HOST_ROWS) - const atPaneGrid = await renderHeadlessBuffer([HOST_FRAME], PANE_COLS, HOST_ROWS - 4) - - // Why this is the user-visible failure: the alternate screen has no - // scrollback, so rows scrolled off by a shorter grid are gone for good and - // an idle TUI never repaints them. - expect(atHostGrid.filter((line) => line.startsWith('host row'))).toHaveLength(HOST_ROWS) - expect(atPaneGrid.filter((line) => line.startsWith('host row')).length).toBeLessThan(HOST_ROWS) - expect(atPaneGrid).not.toContain('host row 1') - }) }) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.spec.ts index 785c3717f49..2f4acf9d3da 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.spec.ts @@ -271,7 +271,7 @@ export function registerPtyConnectionRuntimeOwnerSpawnRoutingTests(): void { expect(deps.syncPanePtyLayoutBinding).toHaveBeenCalledWith(2, 'remote:env-1@@terminal-2') }) - it('persists a restarted pane PTY id and uses it on the next remount', async () => { + it('publishes a restarted pane PTY id and reuses its restored hint on remount', async () => { const { connectPanePty } = await import('./pty-connection') const restartedTransport = createMockTransport() @@ -325,8 +325,7 @@ export function registerPtyConnectionRuntimeOwnerSpawnRoutingTests(): void { expect(remountDeps.syncPanePtyLayoutBinding).toHaveBeenCalledWith(1, 'pty-restarted') }) - // Why: BEL (0x07) is the attention signal — onBell raises the bell event + an OS notification. - it('wires onBell to the bell attention event and OS notification', async () => { + it('forwards onBell to pane attention and notification policy', async () => { const { connectPanePty } = await import('./pty-connection') const transport = createMockTransport() transportFactoryQueue.push(transport) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts index 5431c1af990..19caaa24f28 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts @@ -322,7 +322,7 @@ describe('connectPanePty: a restore the host kept asleep', () => { binding.dispose() }) - it('stops the background retry when the pane is opened or closed', async () => { + it('stops the background retry when the pane is disposed', async () => { const { connectPanePty } = await import('./pty-connection') const transport = deferringTransport() transportFactoryQueue.push(transport) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts index 5e61b65f656..40605cda45d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts @@ -138,7 +138,7 @@ describe('fresh spawn leaves a local pane unbound', () => { return buildPaneConnectionDeps(() => mockStoreState, overrides) } - it('remounts the pane when the spawn resolves without a PTY id', async () => { + it('requests pane recovery when the spawn resolves without a PTY id', async () => { const { connectPanePty } = await import('./pty-connection') const transport = createMockTransport() // A spawn that produced nothing: no id returned and nothing bound after it. diff --git a/src/shared/agent-status-child-work-admission-parse.test.ts b/src/shared/agent-status-child-work-admission-parse.test.ts index d4988e22d39..24214ed66fe 100644 --- a/src/shared/agent-status-child-work-admission-parse.test.ts +++ b/src/shared/agent-status-child-work-admission-parse.test.ts @@ -75,24 +75,30 @@ const TEXT_FIELDS: Record = { lastMessage: { cap: 512, carry: (lastMessage) => ({ lastMessage }), read: (c) => c?.lastMessage } } -function hostileText(cap: number): [string, string][] { +function hostileText(cap: number): [string, string, string | undefined][] { return [ - ['a tab', 'col1\tcol2'], - ['a CRLF', 'line one\r\nline two'], - ['a line separator', 'one\u2028two'], - ['a paragraph separator', 'one\u2029two'], - ['a next-line control', 'one\u0085two'], - ['a no-break space inside and around', '\u00a0keep\u00a0this\u00a0'], - ['an escape sequence', 'plain \u001b[31mred'], - ['a delete', 'rub\u007fout'], - ['whitespace only', ' \t\r\n\u2028 '], - // A space at character cap - 1, cap and cap + 1: just inside the cut, on it, and past it. - ...[-1, 0, 1].map((offset): [string, string] => [ - `a space at character cap${offset < 0 ? '' : '+'}${offset}`, - `${'x'.repeat(cap + offset - 1)} tail` - ]), - ['a surrogate pair split by the cut', `${'x'.repeat(cap - 1)}\u{1f600}tail`], - ['a surrogate pair ending at the cut', `${'x'.repeat(cap - 2)}\u{1f600}tail`] + ['a tab', 'col1\tcol2', 'col1 col2'], + ['a CRLF', 'line one\r\nline two', 'line one line two'], + ['a line separator', 'one\u2028two', 'one two'], + ['a paragraph separator', 'one\u2029two', 'one two'], + ['a next-line control', 'one\u0085two', 'one two'], + ['a no-break space inside and around', '\u00a0keep\u00a0this\u00a0', 'keep\u00a0this'], + ['an escape sequence', 'plain \u001b[31mred', 'plain [31mred'], + ['a delete', 'rub\u007fout', 'rub out'], + ['whitespace only', ' \t\r\n\u2028 ', undefined], + ['a space at character cap-1', `${'x'.repeat(cap - 2)} tail`, `${'x'.repeat(cap - 2)} t`], + ['a space at character cap+0', `${'x'.repeat(cap - 1)} tail`, 'x'.repeat(cap - 1)], + ['a space at character cap+1', `${'x'.repeat(cap)} tail`, 'x'.repeat(cap)], + [ + 'a surrogate pair split by the cut', + `${'x'.repeat(cap - 1)}\u{1f600}tail`, + 'x'.repeat(cap - 1) + ], + [ + 'a surrogate pair ending at the cut', + `${'x'.repeat(cap - 2)}\u{1f600}tail`, + `${'x'.repeat(cap - 2)}\u{1f600}` + ] ] } @@ -105,25 +111,30 @@ function breaksOneLine(text: string): boolean { describe('child-work admission parses provider text into what the codec stores', () => { const cases = Object.entries(TEXT_FIELDS).flatMap(([field, spec]) => - hostileText(spec.cap).map(([label, raw]) => [field, label, raw, spec] as const) + hostileText(spec.cap).map( + ([label, raw, expected]) => [field, label, raw, spec, expected] as const + ) ) - it.each(cases)('%s with %s is admitted at a fixed point', (_field, _label, raw, spec) => { - const { store, admission } = setup() - expect(admission.announce(observation(spec.carry(raw)))).toMatchObject({ accepted: true }) - const stored = spec.read(store.getChild('child-1')) - expect(stored).toBe(normalizeChildWorkText(raw, spec.cap)) - if (stored === undefined) { - return + it.each(cases)( + '%s with %s is admitted at a fixed point', + (_field, _label, raw, spec, expected) => { + const { store, admission } = setup() + expect(admission.announce(observation(spec.carry(raw)))).toMatchObject({ accepted: true }) + const stored = spec.read(store.getChild('child-1')) + expect(stored).toBe(expected) + if (stored === undefined) { + return + } + expect(normalizeChildWorkText(stored, spec.cap)).toBe(stored) + // Independent of the normalizer: one line, trimmed, within the cap, no half pair at the cut. + expect(breaksOneLine(stored)).toBe(false) + expect(stored).toBe(stored.trim()) + expect(stored.length).toBeLessThanOrEqual(spec.cap) + const last = stored.charCodeAt(stored.length - 1) + expect(last >= 0xd800 && last <= 0xdbff).toBe(false) } - expect(normalizeChildWorkText(stored, spec.cap)).toBe(stored) - // Independent of the normalizer: one line, trimmed, within the cap, no half pair at the cut. - expect(breaksOneLine(stored)).toBe(false) - expect(stored).toBe(stored.trim()) - expect(stored.length).toBeLessThanOrEqual(spec.cap) - const last = stored.charCodeAt(stored.length - 1) - expect(last >= 0xd800 && last <= 0xdbff).toBe(false) - }) + ) it('keeps a surrogate pair whole when it ends exactly at the cap', () => { const { store, admission } = setup() diff --git a/src/shared/agent-status-child-work.test.ts b/src/shared/agent-status-child-work.test.ts index 8b11888ae56..87522cf6023 100644 --- a/src/shared/agent-status-child-work.test.ts +++ b/src/shared/agent-status-child-work.test.ts @@ -93,10 +93,11 @@ describe('AgentChildWorkRecord', () => { }) it('parses a copied record with outcome, tokens, timing and bounded invocation history', () => { - const parsed = parseAgentChildWorkRecord({ ...child(), revision: 7 }) + const value = { ...child(), revision: 7 } + const parsed = parseAgentChildWorkRecord(value) expect(parsed).toEqual({ ...child(), revision: 7 }) - expect(parsed).not.toBe(child()) + expect(parsed).not.toBe(value) expect(parsed?.parent).not.toBe(parent) })