test: remove redundant cases and strengthen persistence, terminal and mobile checks (#33318)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-24 17:35:35 -07:00
committed by GitHub
co-authored by OrcaWin
parent 7ffcb7c7b4
commit e8a18a03d5
18 changed files with 128 additions and 210 deletions
@@ -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())
})
@@ -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<boolean> | undefined
let second: Promise<boolean> | 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
@@ -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.
@@ -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<ReturnType<RpcClient['sendRequest']>>) => void = () => {}
const betaReply = new Promise<Awaited<ReturnType<RpcClient['sendRequest']>>>((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 () => {
@@ -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()
})
})
@@ -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()
@@ -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)
})
@@ -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<keyof WorkspaceSessionState>([
'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<string, unknown>)[field]).toEqual({
[RETAINED_OWNER_KEY]: 'retained-value'
})
}
)
expect((session as Record<string, unknown>)[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 = {
@@ -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'])
})
@@ -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}`
@@ -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()
@@ -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 () => {
@@ -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'
@@ -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(
@@ -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 () => {
@@ -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()
})
@@ -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' }],
@@ -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', () => {