diff --git a/src/main/automations/automation-launch-delivery.test.ts b/src/main/automations/automation-launch-delivery.test.ts index ff7c0d97676..4a7493b52da 100644 --- a/src/main/automations/automation-launch-delivery.test.ts +++ b/src/main/automations/automation-launch-delivery.test.ts @@ -83,24 +83,14 @@ describe('headless dispatch prompt delivery', () => { } ) - it('records launch evidence once the agent terminal exists, and keeps it after retention clears the pane', async () => { - const { store, service, run, row, settle } = await heldLaunch('delivery') + it('records launch evidence once the agent terminal exists even when delivery fails', async () => { + const { service, row, settle } = await heldLaunch('delivery') await vi.waitFor(() => expect(row()?.terminalPaneKey).toBe(OPENED.terminalPaneKey)) await settle("Aider started, but Orca could not send this run's prompt.") const launchedAt = row()?.launchedAt expect(launchedAt).toEqual(expect.any(Number)) expect(row()?.dispatchedAt).toBeNull() - // Dead-PTY retention forgets the terminal but must not forget that the agent launched. - store.updateAutomationRun({ - runId: run.id, - status: 'dispatch_failed', - error: row()?.error ?? null, - terminalSessionId: null, - terminalPaneKey: null, - terminalPtyId: null - }) - expect(row()).toMatchObject({ terminalPaneKey: null, launchedAt }) service.stop() }) }) diff --git a/src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-refresh-targets.test.ts b/src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-refresh-targets.test.ts deleted file mode 100644 index 9f149237b69..00000000000 --- a/src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-refresh-targets.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -// @vitest-environment happy-dom -import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id' - -import { createWorkspaceReferenceRefreshTargetsSelector } from './workspace-reference-refresh-targets' -import { - referenceAttachment, - referenceRepo, - referenceWorkspace -} from './workspace-reference-fixtures.test-support' -vi.mock('@/store', async () => { - const browserWindow = globalThis.window - const { createTestStore } = await import('@/store/github/github-slice-test-harness') - globalThis.window = browserWindow - return { useAppStore: createTestStore() } -}) -const read = vi.hoisted(() => vi.fn(async () => null)) -vi.mock('./workspace-reference-detail-read', () => ({ readWorkspaceReferenceDetails: read })) -const workspace = { ...referenceWorkspace, linkedPR: null, linkedItems: [referenceAttachment()] } -const update = () => createWorkspaceReferenceRefreshTargetsSelector()(useAppStore.getState(), true) -beforeEach(() => { - vi.useFakeTimers() - vi.clearAllMocks() - useAppStore.setState({ - settings: null, - repos: [referenceRepo], - worktreesByRepo: { repo: [workspace] }, - detectedWorktreesByRepo: {}, - visibleReviewWorktreeIds: [workspace.id], - activeWorktreeId: null, - activeWorkspaceExecutionHostId: null, - visibleWorkspaceHostIds: null, - workspaceHostScope: 'all', - hostedReviewCache: {}, - prCache: {}, - tabsByWorktree: {}, - ptyIdsByTabId: {}, - browserTabsByWorktree: { - [workspace.id]: [ - { - id: 'browser', - worktreeId: workspace.id, - url: '', - title: '', - loading: false, - faviconUrl: null, - canGoBack: false, - canGoForward: false, - loadError: null, - createdAt: 1 - } - ] - }, - unifiedTabsByWorktree: {}, - agentStatusByPaneKey: {}, - agentStatusEpoch: 0, - sshConnectionStates: new Map() - }) -}) -afterEach(() => { - vi.useRealTimers() -}) -it('retains both explicit owners when equal visible workspace IDs exist on two hosts', () => { - const remoteRepo = { - ...referenceRepo, - executionHostId: 'ssh:remote' as const, - connectionId: 'remote' - } - useAppStore.setState({ - repos: [referenceRepo, remoteRepo], - worktreesByRepo: { - local: [{ ...workspace, hostId: 'local' }], - remote: [{ ...workspace, hostId: 'ssh:remote' }] - }, - sshConnectionStates: new Map([ - ['remote', { targetId: 'remote', status: 'connected', error: null, reconnectAttempt: 0 }] - ]) - }) - const targets = update() - expect(targets).toHaveLength(2) - expect(new Set(targets.map((target) => target.key)).size).toBe(2) - useAppStore.setState({ visibleWorkspaceHostIds: ['local'] }) - expect(update()).toHaveLength(1) - useAppStore.setState({ - activeWorktreeId: workspace.id, - activeWorkspaceExecutionHostId: 'ssh:remote' - }) - expect( - createWorkspaceReferenceRefreshTargetsSelector({ selectedOnly: true })( - useAppStore.getState(), - true - ) - ).toHaveLength(1) -}) - -it('adds and removes exact review targets when a headless agent wakes and sleeps its workspace', () => { - useAppStore.setState({ browserTabsByWorktree: {} }) - const select = createWorkspaceReferenceRefreshTargetsSelector() - expect(select(useAppStore.getState(), true)).toEqual([]) - const paneKey = makePaneKey('headless', '11111111-1111-4111-8111-111111111111') - useAppStore.setState({ - agentStatusByPaneKey: { - [paneKey]: { - paneKey, - worktreeId: workspace.id, - agentType: 'codex', - state: 'working', - prompt: '', - updatedAt: Date.now(), - stateStartedAt: Date.now(), - stateHistory: [] - } - } - }) - expect(select(useAppStore.getState(), false)).toMatchObject([ - { selected: false, fetchedAt: null } - ]) - useAppStore.setState({ agentStatusByPaneKey: {} }) - expect(select(useAppStore.getState(), false)).toEqual([]) -}) diff --git a/src/renderer/src/store/slices/worktrees/teardown/removed-worktree-slot-pane-index.test.ts b/src/renderer/src/store/slices/worktrees/teardown/removed-worktree-slot-pane-index.test.ts index c45e9b124f3..ef6ee05527a 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/removed-worktree-slot-pane-index.test.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/removed-worktree-slot-pane-index.test.ts @@ -9,7 +9,6 @@ import { } from '@/components/sidebar/worktree-card-slot-pane-index' import { createTestStore } from '../../../testing/app-store-test-fixture' import { makeWorktree } from '../worktree.test-fixture' -import { applyRemoveWorktreeSuccessState } from './remove-worktree-store-cleanup' import { buildWorktreePurgeState } from './worktree-purge-state' function displayers(paneKey: string): string[] { @@ -18,51 +17,6 @@ function displayers(paneKey: string): string[] { return ids } -it('retires displayed pane owners after explicit workspace removal and identity reuse', () => { - const store = createTestStore() - const identity = composeWorktreeHostIdentity('local', 'removed-slot') - store.setState({ - workspaceUnreadRaisedByMarkers: { - [identity]: true, - [composeWorktreeHostIdentity('runtime:box', 'removed-slot')]: true, - [composeWorktreeHostIdentity('local', 'surviving-slot')]: true - }, - workspacesMarkedUnreadByUser: { - [identity]: true, - [composeWorktreeHostIdentity('runtime:box', 'removed-slot')]: true, - [composeWorktreeHostIdentity('local', 'surviving-slot')]: true - } - }) - recordDisplayedPanes(identity, ['old-pane', 'reused-pane']) - applyRemoveWorktreeSuccessState(store.setState, 'removed-slot', new Set()) - expect(displayers('old-pane')).toEqual([]) - expect(displayers('reused-pane')).toEqual([]) - expect(store.getState().workspaceUnreadRaisedByMarkers).toEqual({ 'local|surviving-slot': true }) - expect(store.getState().workspacesMarkedUnreadByUser).toEqual({ 'local|surviving-slot': true }) - - recordDisplayedPanes(identity, ['new-pane']) - expect(displayers('new-pane')).toEqual(['removed-slot']) - applyRemoveWorktreeSuccessState(store.setState, 'removed-slot', new Set()) - expect(displayers('new-pane')).toEqual([]) -}) - -it('retires historical owners during repeated bulk workspace churn', () => { - const store = createTestStore() - for (let index = 0; index < 32; index += 1) { - const id = `churn-slot-${index}` - recordDisplayedPanes(composeWorktreeHostIdentity('local', id), ['churn-pane']) - recordDisplayedPanes(composeWorktreeHostIdentity('runtime:box', id), ['churn-pane']) - store.setState({ - workspaceUnreadRaisedByMarkers: { [composeWorktreeHostIdentity('local', id)]: true }, - workspacesMarkedUnreadByUser: { [composeWorktreeHostIdentity('runtime:box', id)]: true } - }) - store.setState(buildWorktreePurgeState(store.getState(), [id])) - } - expect(displayers('churn-pane')).toEqual([]) - expect(store.getState().workspaceUnreadRaisedByMarkers).toEqual({}) - expect(store.getState().workspacesMarkedUnreadByUser).toEqual({}) -}) - it('preserves the surviving host twin when retiring a pane owner', () => { const store = createTestStore() store.setState({ diff --git a/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts b/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts deleted file mode 100644 index efe8a71d50f..00000000000 --- a/src/shared/agent-launch/choices/agent-launch-settings-auto-projection.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { agentLaunchSettingsMutationUpdates } from './agent-launch-settings' -import { applyAgentPermissionMode } from '../permissions/tui-agent-permissions' - -describe('agent launch permission projection compatibility', () => { - it('changes only agents Auto does not cover', () => { - const automatic = applyAgentPermissionMode({ - mode: 'auto', - ...applyAgentPermissionMode({ mode: 'yolo' }) - }) - const updates = agentLaunchSettingsMutationUpdates( - automatic, - { type: 'permissions', mode: 'manual', exceptAuto: true }, - 'darwin' - ) - expect(updates.agentDefaultArgs?.gemini).toBe('') - expect(() => - agentLaunchSettingsMutationUpdates( - automatic, - { type: 'permissions', mode: 'auto', exceptAuto: true }, - 'darwin' - ) - ).toThrow('Auto cannot apply to agents without automatic permission review') - }) -}) diff --git a/src/shared/coalescing-keyed-runner.test.ts b/src/shared/coalescing-keyed-runner.test.ts index 0b9d6c33adc..c996a9b6ab0 100644 --- a/src/shared/coalescing-keyed-runner.test.ts +++ b/src/shared/coalescing-keyed-runner.test.ts @@ -12,35 +12,6 @@ function deferred() { } describe('createCoalescingKeyedRunner', () => { - it('queues a caller with a different share key behind the run in flight, never beside it', async () => { - const run = createCoalescingKeyedRunner() - const first = deferred() - let active = 0 - let maxActive = 0 - const tracked = (work: () => Promise) => async () => { - active += 1 - maxActive = Math.max(maxActive, active) - try { - return await work() - } finally { - active -= 1 - } - } - const leadingWork = tracked(() => first.promise) - const leading = run('repo', 'origin', leadingWork) - const other = vi.fn(async () => 'upstream result') - const queued = run('repo', 'upstream', tracked(other)) - await Promise.resolve() - expect(other).not.toHaveBeenCalled() - - first.resolve('origin result') - await expect(Promise.all([leading, queued])).resolves.toEqual([ - 'origin result', - 'upstream result' - ]) - expect(maxActive).toBe(1) - }) - it('answers every caller from a run of its own share key, in arrival order', async () => { const run = createCoalescingKeyedRunner() const first = deferred() @@ -83,18 +54,4 @@ describe('createCoalescingKeyedRunner', () => { held.resolve('held') await expect(heldResult).resolves.toBe('held') }) - - it('starts fresh once a key has settled, instead of joining a finished run', async () => { - const run = createCoalescingKeyedRunner() - let runs = 0 - const work = async () => ++runs - - await expect(run('repo', 'target', work)).resolves.toBe(1) - await expect(run('repo', 'target', work)).resolves.toBe(2) - - const leading = run('repo', 'target', work) - const trailing = run('repo', 'target', work) - await expect(Promise.all([leading, trailing])).resolves.toEqual([3, 4]) - await expect(run('repo', 'target', work)).resolves.toBe(5) - }) }) diff --git a/src/shared/queued-message-edit-lease.test.ts b/src/shared/queued-message-edit-lease.test.ts index 21791738b5b..7497fa30901 100644 --- a/src/shared/queued-message-edit-lease.test.ts +++ b/src/shared/queued-message-edit-lease.test.ts @@ -26,79 +26,6 @@ const message = { } afterEach(() => vi.useRealTimers()) -it.each(['initial', 'renew', 'reacquire'] as const)( - 'end during pending %s suppresses delivery and releases exactly after owned work', - async (phase) => { - vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) - const host = queuedEditHost('session', message) - const onLost = vi.fn() - const events: string[] = [] - let finish!: () => void - let count = 0 - const request = vi.fn( - async ( - params: AgentSessionQueuedMessageEditHoldParams - ): Promise => { - events.push(`start:${params.action}`) - const key = { callerKey: 'client', editId: params.editId } - if (params.action === 'release') { - host.leases.release(key, params.messageId) - events.push('end:release') - return { status: 'released' } - } - count++ - if (count === (phase === 'initial' ? 1 : phase === 'renew' ? 2 : 3)) { - await new Promise((resolve) => { - finish = resolve - }) - } - const answer = - params.action === 'acquire' - ? host.acquire(params.editId, params.expectedBodyFingerprint ?? '') - : host.leases.renew(key, params.messageId) - events.push(`end:${params.action}:${answer.status}`) - return answer - } - ) - const lease = startQueuedEditLease({ - sessionId: 'session', - messageId: 'card', - editId: 'edit', - expectedBodyFingerprint: host.row!.fingerprint, - request, - onLost - }) - if (phase !== 'initial') { - await lease.acquired - if (phase === 'reacquire') { - host.leases.retire('card') - } - await vi.advanceTimersByTimeAsync(QUEUED_EDIT_LEASE_RENEW_MS) - } - expect(finish).toBeTypeOf('function') - lease.end() - lease.end() - expect(events.at(-1)).toBe(`start:${phase === 'renew' ? 'renew' : 'acquire'}`) - expect(request.mock.calls.some(([params]) => params.action === 'release')).toBe(false) - host.depart() - finish() - await vi.advanceTimersByTimeAsync(0) - expect(events.slice(-2)).toEqual(['start:release', 'end:release']) - expect(onLost).not.toHaveBeenCalled() - expect(request.mock.calls.filter(([params]) => params.action === 'release')).toHaveLength(1) - expect(request.mock.calls.at(-1)?.[0]).toEqual({ - sessionId: 'session', - messageId: 'card', - editId: 'edit', - action: 'release' - }) - const calls = request.mock.calls.length - await vi.advanceTimersByTimeAsync(4 * QUEUED_EDIT_LEASE_RENEW_MS) - expect(request).toHaveBeenCalledTimes(calls) - host.leases.dispose() - } -) - it('keeps the existing held/failure/expired-to-held request protocol without terminal delivery', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/src/shared/sidebar-list/feed/sidebar-feed-rows.test.ts b/src/shared/sidebar-list/feed/sidebar-feed-rows.test.ts index e1235c31fc7..9fba6b9a975 100644 --- a/src/shared/sidebar-list/feed/sidebar-feed-rows.test.ts +++ b/src/shared/sidebar-list/feed/sidebar-feed-rows.test.ts @@ -71,55 +71,6 @@ function outline(rows: readonly HostSectionRow[]): string[] { }) } -it('retires recorded deletions through paired feed, pins and lineage', () => { - const rows = [ - normalizePairedServerRow('runtime:a', { - worktreeId: 'r::/p', - repoId: 'r', - worktreeInstanceId: 'p', - isPinned: true, - removing: true, - removalRecorded: true - }), - normalizePairedServerRow('runtime:a', { - worktreeId: 'r::/c', - repoId: 'r', - worktreeInstanceId: 'c', - parentWorktreeId: 'r::/p', - lineageWorktreeInstanceId: 'c', - parentWorktreeInstanceId: 'p' - }), - normalizePairedServerRow('runtime:a', { - worktreeId: 'r::/legacy', - repoId: 'r', - removing: true - }), - normalizePairedServerRow('runtime:a', { - worktreeId: 'r::/failed', - repoId: 'r', - removalError: 'Cleanup failed. Retry deletion.' - }), - normalizePairedServerRow('runtime:b', { worktreeId: 'r::/p', repoId: 'r' }) - ].flatMap((row) => row ?? []) - const repos = [ - repo('r', { executionHostId: 'runtime:a' }), - repo('r', { executionHostId: 'runtime:b' }) - ] - const inputs = domains({ - pairedServerHosts: new Map( - ['runtime:a', 'runtime:b'].map((hostId) => [ - hostId, - { freshness: 'live' as const, host: null } - ]) - ), - pairedServerRows: new Map(rows.map((row) => [row.key, row])) - }) - const settings = view({ pinnedDisplayPolicy: 'duplicate-in-groups' }) - const built = buildSidebarFeedRows(inputs, catalog(repos), settings) - expect(outline(built.sectionRows)).toContain('runtime:a|r::/c@0') - expect(outline(built.sectionRows)).not.toContain('runtime:a|r::/p@0') -}) - describe('buildSidebarFeedRows nests each paired server’s own lineage', () => { const serverRows = (hostId: 'runtime:a' | 'runtime:b', recentAt: number) => [ diff --git a/src/shared/sidebar-list/worktree-lineage-range.test.ts b/src/shared/sidebar-list/worktree-lineage-range.test.ts deleted file mode 100644 index f08ee9622f8..00000000000 --- a/src/shared/sidebar-list/worktree-lineage-range.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { WorktreeOccurrence, WorktreeOccurrencePlan } from '../worktree-occurrence-plan' -import type { Worktree } from '../worktree/types' -import { buildWorktreeLineageRanges } from './worktree-lineage-range' - -function row(id: string, overrides: Partial = {}): Worktree { - return { - id, - repoId: 'repo', - hostId: 'local', - path: `/tmp/${id}`, - head: 'abc123', - branch: `refs/heads/${id}`, - isBare: false, - isMainWorktree: false, - displayName: id, - comment: '', - linkedIssue: null, - linkedPR: null, - linkedLinearIssue: null, - isArchived: false, - isUnread: false, - isPinned: false, - sortOrder: 0, - lastActivityAt: 0, - ...overrides - } -} - -/** One section: a parent at depth 0 with two children at depth 1. */ -function plan(rows: readonly Worktree[], depths = [0, 1, 1]): WorktreeOccurrencePlan { - const occurrences = rows.map((worktree, index): WorktreeOccurrence => ({ - key: `section:${worktree.id}`, - row: worktree, - sectionKey: 'section', - placement: 'natural', - sectionPath: { key: 'section' }, - depth: depths[index]!, - childCount: index === 0 ? rows.length - 1 : 0, - isLastChild: index === rows.length - 1, - showProjectName: false - })) - return { bySection: new Map([['section', occurrences]]), preferredByIdentity: new Map() } -} - -describe('buildWorktreeLineageRanges', () => { - it('keeps the tree and ranges when a row is replaced without moving', () => { - const rows = [row('parent'), row('a'), row('b')] - const before = buildWorktreeLineageRanges(plan(rows)) - const replaced = row('a', { isUnread: true }) - const after = buildWorktreeLineageRanges(plan([rows[0]!, replaced, rows[2]!])) - - const parent = after.get('section:parent')! - expect(parent).toBe(before.get('section:parent')) - // The reused tree carries the new row so fallback reads see it, and names the index. - expect(parent.tree.worktrees[1]).toBe(replaced) - expect(parent.tree.replacedRows).toEqual([[1]]) - }) - - it('rebuilds the tree when depth changes', () => { - const rows = [row('parent'), row('a'), row('b')] - const before = buildWorktreeLineageRanges(plan(rows)) - const after = buildWorktreeLineageRanges(plan([...rows], [0, 1, 2])) - expect(after.get('section:parent')!.tree).not.toBe(before.get('section:parent')!.tree) - expect(after.get('section:a')!.length).toBe(1) - }) -})