diff --git a/mobile/src/session/use-missing-worktree-bounce.test.ts b/mobile/src/session/use-missing-worktree-bounce.test.ts index df1bccc5678..a697bbfb905 100644 --- a/mobile/src/session/use-missing-worktree-bounce.test.ts +++ b/mobile/src/session/use-missing-worktree-bounce.test.ts @@ -61,6 +61,8 @@ describe('useMissingWorktreeBounce', () => { // Why: navigation lands after the render, so the pre-unmount renders must not re-fire. render('repo::wt', 'missing') + render('repo::wt', 'unknown') + render('repo::wt', 'missing') expect(bounce).toHaveBeenCalledTimes(1) act(() => renderer?.unmount()) }) diff --git a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts index 33e630c5624..743488bac9f 100644 --- a/mobile/src/session/use-mobile-native-chat-answer-send.test.ts +++ b/mobile/src/session/use-mobile-native-chat-answer-send.test.ts @@ -587,40 +587,6 @@ describe('useMobileNativeChatAnswerSend', () => { expect(sendRequest).toHaveBeenCalledTimes(2) }) - it('does not write a successor after the prior delivery becomes ambiguous', async () => { - let rejectFirst: (error: Error) => void = () => undefined - const sendRequest = vi - .fn() - .mockImplementationOnce( - () => - new Promise((_resolve, reject) => { - rejectFirst = reject - }) - ) - .mockResolvedValue(acceptedResponse()) - await mount({ sendRequest } as unknown as RpcClient, vi.fn()) - - let first: Promise | undefined - let second: Promise | undefined - await act(async () => { - first = answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [0] }]) - await Promise.resolve() - }) - await act(async () => { - second = answerSend?.answerAsk(TABS_OR_SPACES, [{ indices: [1] }]) - await Promise.resolve() - }) - await act(async () => { - rejectFirst(markRpcDeliveryUnknown(new Error('Connection closed'))) - await Promise.resolve() - }) - - // The key may have landed; a blind successor could double-step the selector. - await expect(first).resolves.toBe(false) - await expect(second).resolves.toBe(false) - expect(sendRequest).toHaveBeenCalledTimes(1) - }) - it('tells the user when a queued answer is fenced instead of dropping it silently', async () => { const onSendError = vi.fn() let rejectFirst: (error: Error) => void = () => undefined diff --git a/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts b/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts index 5b95a74f60c..2d2f202e4e1 100644 --- a/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts +++ b/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts @@ -149,17 +149,6 @@ describe('useMobileNativeChatDrafts glued pending sends', () => { expect(renderedPendingTexts(history, state)).toEqual(['run the tests', 'again']) }) - // FP2: an older turn reads like the concatenation of two much later sends. - it('keeps and renders both bubbles when an older turn spells them out', async () => { - const history = [userTurn('m1', 'fix the bug', 1000), assistantTurn('m2', 'fixed', 1100)] - await mount(history) - rapidSend('fix the', 'bug') - - await update([...history]) - expect(pendingTexts()).toEqual(['fix the', 'bug']) - expect(renderedPendingTexts(history, state)).toEqual(['fix the', 'bug']) - }) - // A send of the same text as a message a Stop took back is resent as a new message: its bubble // waits beside the stopped row for that new row, never for the stopped one. it('keeps a resend bubble beside a stopped row of the same text until its own row lands', async () => { @@ -292,26 +281,6 @@ describe('useMobileNativeChatDrafts glued pending sends', () => { expect(state?.pending).toEqual([]) }) - // The same read seen one reconnect later: the send left, the agent echoed it, - // and only then did the first authoritative read land. - it('retires a hydration-time send after a reconnect delivered the read late', async () => { - await mount([], true) - act(() => send('run the tests')) - // Still loading: the first read never arrived before the drop. - await update([], true) - - const afterReconnect = [ - userTurn('m1', 'run the tests', 5000), - assistantTurn('m2', 'ok', 5100) - ] - await update(afterReconnect) - expect(state?.pending).toEqual([]) - - // Nothing later resurrects it, and the run stays glue-capable. - await update([...afterReconnect, assistantTurn('m3', 'all green', 6000)]) - expect(state?.pending).toEqual([]) - }) - // A reconnect leaves this session's own retained history on screen, so sends // made across it own a real boundary already. Moving it onto the read that // follows would push it past their own glued row and strand both bubbles. diff --git a/mobile/src/session/use-mobile-native-chat-file-search.test.ts b/mobile/src/session/use-mobile-native-chat-file-search.test.ts index e42a5989840..15eb7010a37 100644 --- a/mobile/src/session/use-mobile-native-chat-file-search.test.ts +++ b/mobile/src/session/use-mobile-native-chat-file-search.test.ts @@ -105,8 +105,12 @@ describe('useMobileNativeChatFileSearch', () => { }) it('cancels an in-flight query on a cache hit so a stale result cannot clobber it', async () => { - const sendRequest = vi.fn(async (_method: string, params: { query: string }) => - rpcSuccess(params.query === 'app' ? ['src/app.ts'] : ['src/beta.ts']) + let resolveBeta: (reply: Awaited>) => void = () => {} + const betaReply = new Promise>>((resolve) => { + resolveBeta = resolve + }) + const sendRequest = vi.fn((_method: string, params: { query: string }) => + params.query === 'app' ? Promise.resolve(rpcSuccess(['src/app.ts'])) : betaReply ) await mount(fakeClient({ sendRequest })) @@ -132,6 +136,18 @@ describe('useMobileNativeChatFileSearch', () => { expect( sendRequest.mock.calls.filter(([, params]) => (params as { query: string }).query === 'beta') ).toHaveLength(0) + + act(() => state?.loadNativeChatFiles('beta')) + await act(async () => { + await vi.advanceTimersByTimeAsync(120) + }) + expect(sendRequest).toHaveBeenCalledTimes(2) + act(() => state?.loadNativeChatFiles('app')) + await act(async () => { + resolveBeta(rpcSuccess(['src/beta.ts'])) + await betaReply + }) + expect(state?.nativeChatFilePaths).toEqual(['src/app.ts']) }) it('reloads the legacy inventory when the logical authority epoch advances', async () => { diff --git a/mobile/src/session/use-mobile-pr-actions.test.ts b/mobile/src/session/use-mobile-pr-actions.test.ts index dd022bb25f7..893c80ca049 100644 --- a/mobile/src/session/use-mobile-pr-actions.test.ts +++ b/mobile/src/session/use-mobile-pr-actions.test.ts @@ -198,12 +198,15 @@ describe('PrActionsEngine — auto-merge optimistic revert', () => { expect(engine.error).toBe('connection lost') }) - it('keeps the optimistic value through success until refetch', async () => { - const mutations = fakeMutations() + it('shows the optimistic toggle while pending and clears it on success', async () => { + const pending = deferred<{ ok: true }>() + const mutations = fakeMutations({ setPRAutoMerge: vi.fn(() => pending.promise) }) const { engine, refetch } = makeEngine(mutations) - await engine.setAutoMerge(true) - // After success the optimism clears to authoritative (refetch supplies truth). - expect(engine.resolveAutoMerge(true)).toBe(true) + const operation = engine.setAutoMerge(true) + expect(engine.resolveAutoMerge(false)).toBe(true) + pending.resolve({ ok: true }) + await operation + expect(engine.resolveAutoMerge(false)).toBe(false) expect(refetch).toHaveBeenCalledOnce() }) }) diff --git a/mobile/src/session/use-mobile-pr-branch-context.test.ts b/mobile/src/session/use-mobile-pr-branch-context.test.ts index 8b93921c74a..6973292d725 100644 --- a/mobile/src/session/use-mobile-pr-branch-context.test.ts +++ b/mobile/src/session/use-mobile-pr-branch-context.test.ts @@ -65,11 +65,6 @@ describe('deriveMobilePrBranchContext', () => { expect(result.headSha).toBeNull() }) - it('derives branch from status.branch', () => { - const result = deriveMobilePrBranchContext(status({ branch: 'topic' }), null) - expect(result.branch).toBe('topic') - }) - it('returns null branch when status.branch is absent', () => { const result = deriveMobilePrBranchContext(status({ branch: undefined }), null) expect(result.branch).toBeNull() diff --git a/src/main/persistence/restoring-sessions/pane-key-remapping.test.ts b/src/main/persistence/restoring-sessions/pane-key-remapping.test.ts index 2c73045d901..5bb9254b425 100644 --- a/src/main/persistence/restoring-sessions/pane-key-remapping.test.ts +++ b/src/main/persistence/restoring-sessions/pane-key-remapping.test.ts @@ -9,14 +9,15 @@ import { const STABLE_LEAF_ID = '00000000-0000-4000-8000-000000000001' describe('remapManuallyUnreadTurnPaneKeys', () => { - it('promotes legacy pane keys to the restored stable leaf like clear-completed cutoffs', () => { + it('promotes legacy pane keys and keeps the newest value on collision', () => { const remap = new Map([['tab-1', new Map([['pane:1', STABLE_LEAF_ID]])]]) - const turns = { 'tab-1:pane:1': 42, 'tab-2:pane:9': 7 } + const stableKey = makePaneKey('tab-1', STABLE_LEAF_ID) + const turns = { 'tab-1:pane:1': 42, [stableKey]: 50, 'tab-2:pane:9': 7 } const result = remapManuallyUnreadTurnPaneKeys(turns, remap) expect(result.changed).toBe(true) - expect(result.turns).toEqual({ [makePaneKey('tab-1', STABLE_LEAF_ID)]: 42, 'tab-2:pane:9': 7 }) + expect(result.turns).toEqual({ [stableKey]: 50, 'tab-2:pane:9': 7 }) // Same remap contract as the cutoffs so the two never drift after a session restore. expect(remapActivityClearedAtPaneKeys(turns, remap).cutoffs).toEqual(result.turns) }) diff --git a/src/main/persistence/restoring-sessions/session-owner-fields.test.ts b/src/main/persistence/restoring-sessions/session-owner-fields.test.ts index 5ff4e2692cf..a517006d2fb 100644 --- a/src/main/persistence/restoring-sessions/session-owner-fields.test.ts +++ b/src/main/persistence/restoring-sessions/session-owner-fields.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest' import { getDefaultWorkspaceSession } from '../../../shared/constants' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' -import { - deleteOwnerKeyedSessionFields, - OWNER_KEYED_SESSION_FIELDS_DELETED_WITH_THEIR_OWNER -} from './session-owner-fields' +import { deleteOwnerKeyedSessionFields } from './session-owner-fields' const REMOVED_OWNER_KEY = 'repo-1::/tmp/worktree-a' const RETAINED_OWNER_KEY = 'repo-1::/tmp/worktree-b' @@ -19,18 +16,27 @@ function sessionSeededWith(field: keyof WorkspaceSessionState): WorkspaceSession } describe('deleting a workspace owner session fields', () => { - it.each(OWNER_KEYED_SESSION_FIELDS_DELETED_WITH_THEIR_OWNER)( - 'deletes %s for the removed owner and keeps its sibling', - (field) => { - const session = sessionSeededWith(field) + it.each([ + 'openFilesByWorktree', + 'activeFileIdByWorktree', + 'activeBrowserTabIdByWorktree', + 'clientHostedBrowserPagesByWorktree', + 'activeTabTypeByWorktree', + 'activeTabIdByWorktree', + 'unifiedTabs', + 'tabGroups', + 'tabGroupLayouts', + 'activeGroupIdByWorktree', + 'defaultTerminalTabsAppliedByWorktreeId' + ])('deletes %s for the removed owner and keeps its sibling', (field) => { + const session = sessionSeededWith(field) - deleteOwnerKeyedSessionFields(session, REMOVED_OWNER_KEY, new Set()) + deleteOwnerKeyedSessionFields(session, REMOVED_OWNER_KEY, new Set()) - expect((session as Record)[field]).toEqual({ - [RETAINED_OWNER_KEY]: 'retained-value' - }) - } - ) + expect((session as Record)[field]).toEqual({ + [RETAINED_OWNER_KEY]: 'retained-value' + }) + }) it('advances the repo topology revision rather than deleting it', () => { const session: WorkspaceSessionState = { @@ -45,25 +51,7 @@ describe('deleting a workspace owner session fields', () => { expect(session.terminalTopologyRevisionByRepoId).toEqual({ 'repo-1': 5 }) }) - // Why spelled out rather than derived: the cases above iterate this list, so a field quietly - // reclassified in the census loses its delete and its test case together. - it('deletes exactly these fields with their owner', () => { - expect(OWNER_KEYED_SESSION_FIELDS_DELETED_WITH_THEIR_OWNER).toEqual([ - 'openFilesByWorktree', - 'activeFileIdByWorktree', - 'activeBrowserTabIdByWorktree', - 'clientHostedBrowserPagesByWorktree', - 'activeTabTypeByWorktree', - 'activeTabIdByWorktree', - 'unifiedTabs', - 'tabGroups', - 'tabGroupLayouts', - 'activeGroupIdByWorktree', - 'defaultTerminalTabsAppliedByWorktreeId' - ]) - }) - - // Why not in the list above: this map also holds host-qualified keys, so it is scanned rather + // Why not in the table above: this map also holds host-qualified keys, so it is scanned rather // than indexed by the owner key, and the scan is the only thing that reaches a remote row. it('deletes the recency entry under a bare and a host-qualified key alike', () => { const session: WorkspaceSessionState = { diff --git a/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts b/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts index 9cebe041544..d7a2e132dd7 100644 --- a/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts +++ b/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts @@ -127,7 +127,7 @@ describe('workspace pane normalization tab index', () => { expect(lookup.get('first')).toBe(first) }) - it('resumes scanning and makes later misses constant-time', () => { + it('resumes scanning and reuses cached hits and misses', () => { const reads: string[] = [] const tabs = ['first', 'second', 'third'].map((id) => { const tab = terminalTab(id, 'worktree', null) @@ -146,9 +146,12 @@ describe('workspace pane normalization tab index', () => { expect(reads).toEqual(['first']) expect(lookup.get('second')).toBe(tabs[1]) expect(reads).toEqual(['first', 'second']) + expect(lookup.get('first')).toBe(tabs[0]) + expect(reads).toEqual(['first', 'second']) expect(lookup.get('missing')).toBeUndefined() expect(reads).toEqual(['first', 'second', 'third']) expect(lookup.get('missing')).toBeUndefined() + expect(lookup.get('first')).toBe(tabs[0]) expect(reads).toEqual(['first', 'second', 'third']) }) diff --git a/src/main/persistence/terminal-topology/persistence-split-pane-incarnation.test.ts b/src/main/persistence/terminal-topology/persistence-split-pane-incarnation.test.ts index fb4ce908616..37115235cbb 100644 --- a/src/main/persistence/terminal-topology/persistence-split-pane-incarnation.test.ts +++ b/src/main/persistence/terminal-topology/persistence-split-pane-incarnation.test.ts @@ -159,43 +159,6 @@ describe('Store', () => { } }) - // Legacy sessions lack incarnation IDs; source-pane ownership must still permit splitting. - it('admits a split whose source pane has no persisted incarnation', async () => { - const store = await createStore() - store.setWorkspaceSession( - { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - wt1: [makeTerminalTab({ id: 'tab1', worktreeId: 'wt1', ptyId: 'pty-source' })] - }, - terminalLayoutsByTabId: { - tab1: { - root: { type: 'leaf', leafId: TEST_LEAF_1 }, - activeLeafId: TEST_LEAF_1, - expandedLeafId: null, - ptyIdsByLeafId: { [TEST_LEAF_1]: 'pty-source' } - } - } - }, - undefined - ) - - expect( - await store.persistPtyBinding({ - worktreeId: 'wt1', - tabId: 'tab1', - leafId: TEST_LEAF_2, - ptyId: 'pty-split', - expectedSourceBinding: { - worktreeId: 'wt1', - tabId: 'tab1', - leafId: TEST_LEAF_1, - ptyId: 'pty-source' - } - }) - ).toBe(true) - }) - it('rejects competing PTY and incarnation changes during reconciliation', async () => { const store = await createStore() const paneKey = `tab1:${TEST_LEAF_1}` diff --git a/src/main/persistence/terminal-topology/persistence-ssh-remote-pty-leases.test.ts b/src/main/persistence/terminal-topology/persistence-ssh-remote-pty-leases.test.ts index 2b35c12c709..0e1fba9c127 100644 --- a/src/main/persistence/terminal-topology/persistence-ssh-remote-pty-leases.test.ts +++ b/src/main/persistence/terminal-topology/persistence-ssh-remote-pty-leases.test.ts @@ -553,24 +553,6 @@ describe('Store', () => { ).toThrow('belongs to SSH connection "ssh-2"') }) - it('updates SSH remote PTY leases when callers pass scoped app ids', async () => { - const store = createStoreWithSshRemotePtyLeases([ - { - targetId: 'ssh-1', - ptyId: 'remote-pty', - state: 'attached' - } - ]) - - store.markSshRemotePtyLease( - 'ssh-1', - 'ssh:ssh-1@@remote-pty', - PtyExitProof.from('exit-observed', undefined) - ) - - expect(store.getSshRemotePtyLeases('ssh-1')).toEqual([]) - }) - it('clears workspace bindings when marking an SSH remote PTY lease terminated', async () => { const store = await storeWithBoundSshPane() diff --git a/src/main/persistence/terminal-topology/persistence-ssh-targets-and-pane-keys.test.ts b/src/main/persistence/terminal-topology/persistence-ssh-targets-and-pane-keys.test.ts index 29b5796deae..5a451180858 100644 --- a/src/main/persistence/terminal-topology/persistence-ssh-targets-and-pane-keys.test.ts +++ b/src/main/persistence/terminal-topology/persistence-ssh-targets-and-pane-keys.test.ts @@ -427,12 +427,26 @@ describe('Store', () => { ui: {}, githubCache: { pr: {}, issue: {} }, workspaceSession: {}, - migrationUnsupportedPtyEntries: {} + migrationUnsupportedPtyEntries: { + 'stale-pty': { + ptyId: 'stale-pty', + tabId: 'tab-stale', + paneKey: makePaneKey('tab-stale', TEST_LEAF_1), + reason: 'legacy-numeric-pane-key', + source: 'local', + updatedAt: 123 + } + } }) const store = await createStore() expect(store.getRepos()).toHaveLength(1) + store.flush() + expect(readDataFile()).toMatchObject({ + migrationUnsupportedPtyEntries: [], + legacyPaneKeyAliasEntries: [] + }) }) it('remaps persisted agent acknowledgement pane keys when terminal leaves migrate to UUIDs', async () => { diff --git a/src/main/persistence/terminal-topology/persistence-tab-terminal-pane-binding-load.test.ts b/src/main/persistence/terminal-topology/persistence-tab-terminal-pane-binding-load.test.ts index f3d6a650107..46c14809c6f 100644 --- a/src/main/persistence/terminal-topology/persistence-tab-terminal-pane-binding-load.test.ts +++ b/src/main/persistence/terminal-topology/persistence-tab-terminal-pane-binding-load.test.ts @@ -71,12 +71,6 @@ describe("main's Store load binds a tab's own terminal to its unbound pane", () rmSync(testState.dir, { recursive: true, force: true }) }) - it('in a layout-owner build', () => { - expect(loadStored().terminalLayoutsByTabId['tab-a']?.ptyIdsByLeafId).toEqual({ - [LEAF]: 'pty-a' - }) - }) - it("after the load purge, so a dropped workspace's pane cannot hold the terminal", () => { const session = tabLevelOnly() const gone = 'legacy-bare-key' diff --git a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.spec.ts index 5d3a4b15204..4e21646a02e 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.spec.ts @@ -98,7 +98,9 @@ export function registerPtyConnectionColdRestoreRepaintTests(): void { `${RESET_GRAPHIC_RENDITION}snapshot-payload`, expect.any(Function) ) - expect(pane.terminal.write).not.toHaveBeenCalledWith('replay-payload', expect.any(Function)) + expect( + pane.terminal.write.mock.calls.map(([data]) => terminalWriteText(data)).join('') + ).not.toContain('replay-payload') }) it('paints only relay replay when reattach result has replay and coldRestore but no snapshot', async () => { @@ -136,7 +138,9 @@ export function registerPtyConnectionColdRestoreRepaintTests(): void { `${RESET_GRAPHIC_RENDITION}replay-payload`, expect.any(Function) ) - expect(pane.terminal.write).not.toHaveBeenCalledWith('cold-payload', expect.any(Function)) + expect( + pane.terminal.write.mock.calls.map(([data]) => terminalWriteText(data)).join('') + ).not.toContain('cold-payload') // Why: the replay branch supersedes cold-restore but must still ack, or the daemon redelivers the cold-restore payload next reattach. // A layout-owner window names the pane it painted, so main releases only its current terminal. expect(window.api.pty.ackColdRestore).toHaveBeenCalledWith( diff --git a/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.spec.ts index c1495a3ee63..074e9d43e3e 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.spec.ts @@ -214,31 +214,43 @@ export function registerPtyConnectionDeliberateSleepGuardTests(): void { }) it('drops the wake listener when a waiting pane is disposed', async () => { - const { connectPanePty } = await import('./pty-connection') - const { clearWorktreeSleepIntent, markWorktreeSleepIntent } = - await import('@/lib/worktree-sleep-intent') - const transport = createMockTransport() - transportFactoryQueue.push(transport) - markWorktreeSleepIntent('wt-1') - const deps = createDeps({ tabId: 'tab-disposed', isVisibleRef: { current: false } }) + const sleepIntent = await import('@/lib/worktree-sleep-intent') + const subscribe = sleepIntent.onWorktreeSleepIntentCleared + const paneWakeListener = vi.fn() + const subscribeSpy = vi + .spyOn(sleepIntent, 'onWorktreeSleepIntentCleared') + .mockImplementation((worktreeId, listener) => + subscribe(worktreeId, () => { + paneWakeListener() + listener() + }) + ) + try { + const { connectPanePty } = await import('./pty-connection') + const transport = createMockTransport() + transportFactoryQueue.push(transport) + sleepIntent.markWorktreeSleepIntent('wt-1') + const deps = createDeps({ tabId: 'tab-disposed', isVisibleRef: { current: false } }) - const binding = connectPanePty( - createPane(1) as never, - createManager(1) as never, - deps as never - ) - await flushAsyncTicks() - binding.dispose() - // Why: the connect body already refuses a disposed session, so prove the - // listener itself is gone: a wake after dispose reaches no subscriber. - const wakeCalls: number[] = [] - const { onWorktreeSleepIntentCleared } = await import('@/lib/worktree-sleep-intent') - onWorktreeSleepIntentCleared('wt-1', () => wakeCalls.push(1)) - clearWorktreeSleepIntent('wt-1') - await flushAsyncTicks() + const binding = connectPanePty( + createPane(1) as never, + createManager(1) as never, + deps as never + ) + await flushAsyncTicks() + expect(subscribeSpy).toHaveBeenCalledTimes(1) + binding.dispose() + const wakeCalls: number[] = [] + subscribe('wt-1', () => wakeCalls.push(1)) + sleepIntent.clearWorktreeSleepIntent('wt-1') + await flushAsyncTicks() - expect(transport.connect).not.toHaveBeenCalled() - expect(wakeCalls).toHaveLength(1) + expect(transport.connect).not.toHaveBeenCalled() + expect(wakeCalls).toHaveLength(1) + expect(paneWakeListener).not.toHaveBeenCalled() + } finally { + subscribeSpy.mockRestore() + } }) it('still connects a slept pane that carries a queued startup', async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts index 62985db0f8e..789ecbb4753 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.spec.ts @@ -516,14 +516,9 @@ export function registerPtyConnectionHiddenAtlasRecoveryTests(): void { capturedDataCallback.current?.('\x1b]10;?\x1b\\\x1b]11;?\x1b\\startup frame\r\n') - expect(transport.sendInput).not.toHaveBeenCalledWith( - '\x1b]10;rgb:1111/1111/1111\x1b\\', - expect.anything() - ) - expect(transport.sendInput).not.toHaveBeenCalledWith( - '\x1b]11;rgb:1111/1111/1111\x1b\\', - expect.anything() - ) + const replies = transport.sendInput.mock.calls.map(([data]) => data).join('') + expect(replies).not.toContain('\x1b]10;rgb:') + expect(replies).not.toContain('\x1b]11;rgb:') binding.dispose() }) diff --git a/src/shared/agent-status-child-work-admission-activity.test.ts b/src/shared/agent-status-child-work-admission-activity.test.ts index 66328bbc422..2a6ecf0fa5b 100644 --- a/src/shared/agent-status-child-work-admission-activity.test.ts +++ b/src/shared/agent-status-child-work-admission-activity.test.ts @@ -118,7 +118,10 @@ describe('child-work admission of what a child is doing', () => { it('carries owner and residency through, and clears the operation of a parked child', () => { const { store, admission } = setup() - admission.announce(observation()) + admission.announce( + observation({ operation: { toolName: 'Bash', basis: 'open', observedAt: 10 } }) + ) + expect(store.getChild('child-1')?.operation).toMatchObject({ toolName: 'Bash' }) admission.announce( observation({ aliases: [{ segmentId: 'segment-1', aliasKind: 'task_id', alias: 'shell-1' }], diff --git a/src/shared/agent-status-child-work-admission.test.ts b/src/shared/agent-status-child-work-admission.test.ts index 091405d9aec..62fc8f20e45 100644 --- a/src/shared/agent-status-child-work-admission.test.ts +++ b/src/shared/agent-status-child-work-admission.test.ts @@ -118,6 +118,14 @@ describe('agent child-work admission', () => { ).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false }) expect(store.getChildren(parent)).toMatchObject([{ childWorkId: 'child-1', kind: 'agent' }]) expect(store.getSnapshot().aliases.every((alias) => alias.kind === 'agent')).toBe(true) + expect( + admission.announce( + announce(parent, { + aliases: [{ segmentId: 'segment-1', aliasKind: 'tool_use_id', alias: 'tool-1' }], + observedAt: 13 + }) + ) + ).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false }) }) it('preserves the logical id and prior outcome across an explicit resume fence', () => {