test: remove 158 low-value test declarations (#38177)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-04 09:09:09 -08:00
committed by GitHub
co-authored by OrcaWin
parent bd44338ebe
commit e15bd083bb
50 changed files with 35 additions and 3604 deletions
+1 -1
View File
@@ -1074,7 +1074,7 @@
2 test-no-wall-clock:src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts
2 test-no-wall-clock:src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx
2 test-no-wall-clock:src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx
2 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/surface/WorktreeCard.single-reference-details.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/surface/WorktreeCard.single-reference-details.test.tsx
2 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx
2 test-no-wall-clock:src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts
2 test-no-wall-clock:src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts
File diff suppressed because one or more lines are too long
-9
View File
@@ -7101,7 +7101,6 @@
"src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx": 1178,
"src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts": 3346,
"src/renderer/src/components/dashboard/useRetainedAgents.test.ts": 2028,
"src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts": 1736,
"src/renderer/src/components/dictation/DictationIndicator.test.tsx": 386,
"src/renderer/src/components/dictation/dictation-insertion-target.test.ts": 323,
"src/renderer/src/components/dictation/dictation-meter-store.test.tsx": 270,
@@ -7341,7 +7340,6 @@
"src/renderer/src/components/editor/rich-markdown/clipboard/rich-markdown-paste-fallback.test.ts": 1931,
"src/renderer/src/components/editor/rich-markdown/code-blocks/rich-markdown-plain-code.test.ts": 307,
"src/renderer/src/components/editor/rich-markdown/review/rich-markdown-range-bounds.test.ts": 70,
"src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-rail-blocks.test.ts": 2561,
"src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-text-ranges.test.ts": 53,
"src/renderer/src/components/editor/rich-markdown/search/rich-markdown-search-focus.adversarial.test.ts": 301,
"src/renderer/src/components/editor/rich-markdown/search/rich-markdown-search-focus.test.ts": 238,
@@ -7481,7 +7479,6 @@
"src/renderer/src/components/linear-issue-text-draft-state.test.ts": 49,
"src/renderer/src/components/linear-issue-text-save-plan.test.ts": 47,
"src/renderer/src/components/linear-issue-view-storage.test.ts": 207,
"src/renderer/src/components/linear-project-view-surfaces.test.tsx": 781,
"src/renderer/src/components/linear-scope-selector.test.ts": 281,
"src/renderer/src/components/linear-workspace-picker.test.tsx": 693,
"src/renderer/src/components/link-actions/LinkActionPopover.subscriptions.test.tsx": 4808,
@@ -7501,7 +7498,6 @@
"src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 240,
"src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 4173,
"src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 117,
"src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts": 1930,
"src/renderer/src/components/native-chat-resume-result-view.test.tsx": 2723,
"src/renderer/src/components/native-chat-resume-run.test.ts": 65,
"src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 3009,
@@ -7941,7 +7937,6 @@
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-original-pane-index.test.ts": 86,
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-original-pane.test.ts": 64,
"src/renderer/src/components/right-sidebar/ai-vault/scanning/ai-vault-panel-session-list-request.test.tsx": 3044,
"src/renderer/src/components/right-sidebar/ai-vault/scanning/ai-vault-scan-issue-state.test.ts": 222,
"src/renderer/src/components/right-sidebar/ai-vault/search/ai-vault-search-session.test.ts": 201,
"src/renderer/src/components/right-sidebar/ai-vault/accounts/ai-vault-session-account-render.test.tsx": 2885,
"src/renderer/src/components/right-sidebar/ai-vault/scanning/ai-vault-session-conditional-refresh-hook.test.tsx": 2370,
@@ -7953,7 +7948,6 @@
"src/renderer/src/components/right-sidebar/ai-vault/scanning/ai-vault-session-refresh.test.ts": 2034,
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-session-resume-in-chat-launch.test.ts": 1260,
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-session-resume-in-chat-workspace.test.ts": 746,
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-session-resume-in-chat.test.ts": 98,
"src/renderer/src/components/right-sidebar/ai-vault/session-actions/ai-vault-session-resume.test.ts": 1753,
"src/renderer/src/components/right-sidebar/ai-vault/session-list/ai-vault-session-worktree-map.test.tsx": 2000,
"src/renderer/src/components/right-sidebar/ai-vault/session-list/ai-vault-session-worktree.test.ts": 307,
@@ -8081,7 +8075,6 @@
"src/renderer/src/components/settings/appearance/chat/AppearanceChatSection.test.tsx": 1633,
"src/renderer/src/components/settings/appearance/AppearancePane.test.tsx": 2066,
"src/renderer/src/components/settings/artifacts/ArtifactsSettingsPane.test.tsx": 862,
"src/renderer/src/components/settings/browser/BrowserPane.search-catalog.test.tsx": 774,
"src/renderer/src/components/settings/browser/profiles/BrowserProfileRow.google-cookie-clear.test.tsx": 27636,
"src/renderer/src/components/settings/browser/BrowserSitePermissionsSetting.test.tsx": 739,
"src/renderer/src/components/settings/browser/BrowserUserAgentSetting.test.tsx": 2325,
@@ -9195,7 +9188,6 @@
"src/renderer/src/hooks/agent-hook-completion-background-turn-notifications.test.ts": 2279,
"src/renderer/src/hooks/agent-hook-completion-grok-notifications.test.ts": 679,
"src/renderer/src/hooks/agent-hook-completion-notifications.test.ts": 1131,
"src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts": 52,
"src/renderer/src/hooks/automation-dispatch-folder-loss.test.ts": 575,
"src/renderer/src/hooks/automation-dispatch-legacy-owner.test.ts": 1914,
"src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts": 182,
@@ -9334,7 +9326,6 @@
"src/renderer/src/hooks/useVirtualizedScrollAnchor.test.tsx": 311,
"src/renderer/src/hooks/virtualized-hidden-row-measurement.test.ts": 199,
"src/renderer/src/hooks/worktree-change-refresh-queue.test.ts": 59,
"src/renderer/src/hooks/zoom-routing.test.ts": 842,
"src/renderer/src/i18n/I18nProvider.test.tsx": 2101,
"src/renderer/src/i18n/language-catalog-timeout.test.tsx": 2025,
"src/renderer/src/i18n/language-detection-reconciliation.test.tsx": 2927,
@@ -125,39 +125,10 @@ function runRename(state: AppShortcutState = shortcutState()): boolean | undefin
describe('tab.rename shortcut', () => {
beforeEach(() => vi.clearAllMocks())
it('leaves activeTabId on a background terminal while a structured tab is active', () => {
// Prove the stale-terminal premise before testing structured-tab rename.
mocks.store = storeForActiveTab(CHAT_UNIFIED_ID)
expect(mocks.store.activeTabType).toBe('agent-session')
expect(mocks.store.activeTabId).toBe(TERMINAL_ENTITY_ID)
})
it('renames the structured chat tab, not the stale background terminal', () => {
mocks.store = storeForActiveTab(CHAT_UNIFIED_ID)
expect(runRename()).toBe(true)
expect(mocks.requestTerminalTabRename).toHaveBeenCalledWith(CHAT_UNIFIED_ID)
expect(mocks.requestTerminalTabRename).not.toHaveBeenCalledWith(TERMINAL_ENTITY_ID)
})
it('still renames the terminal tab by its backing terminal id', () => {
mocks.store = storeForActiveTab(TERMINAL_UNIFIED_ID)
expect(mocks.store.activeTabType).toBe('terminal')
expect(runRename()).toBe(true)
expect(mocks.requestTerminalTabRename).toHaveBeenCalledWith(TERMINAL_ENTITY_ID)
})
it('does not claim the chord for a tab type that has no inline rename', () => {
mocks.store = {
...storeForActiveTab(CHAT_UNIFIED_ID),
activeTabType: 'browser'
} as AppState
expect(runRename()).toBe(false)
expect(mocks.requestTerminalTabRename).not.toHaveBeenCalled()
})
it('does not claim the chord for a structured tab with no active worktree', () => {
mocks.store = storeForActiveTab(CHAT_UNIFIED_ID)
expect(runRename({ ...shortcutState(), activeWorktreeId: null })).toBe(false)
expect(mocks.requestTerminalTabRename).not.toHaveBeenCalled()
})
})
@@ -10,15 +10,9 @@ import { WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/work
import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save'
import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness'
import { createTestStore } from '../store/slices/store-test-helpers'
import {
ACCEPTED_DIFFERENCES,
GIT,
KNOWN_DIFFERENCES,
sweptAtStartup
} from './startup-window-parity-differences.test-fixture'
import { GIT, sweptAtStartup } from './startup-window-parity-differences.test-fixture'
import {
bootWindowFromPartitions,
diffStartupWindowViews,
projectStartupWindowView
} from './startup-window-view.test-fixture'
@@ -55,48 +49,6 @@ function normalize(hostId: ExecutionHostId, session: WorkspaceSessionState): Wor
const PROFILES = [...WORKSPACE_LAYOUT_PROFILES, ...LEGACY_WORKSPACE_LAYOUT_PROFILES]
describe('the window shows the same session before and after the shared Loader rewrites it', () => {
it.each(PROFILES.map((profile) => [profile.name, profile] as const))(
'%s',
async (name, { hostId, build }) => {
const stored = build()
const storedView = projectStartupWindowView(
await bootWindowFromPartitions(createTestStore(), [{ hostId, session: stored }])
)
const normalizedView = projectStartupWindowView(
await bootWindowFromPartitions(createTestStore(), [
{ hostId, session: normalize(hostId, stored) }
])
)
// The window must show the stored workspaces at all, or an empty view hides differences.
for (const [key, rows] of Object.entries(stored.tabsByWorktree)) {
if (rows.length > 0) {
expect(storedView, key).toHaveProperty([`${key} group 0 tabs`])
}
}
const differences = diffStartupWindowViews(storedView, normalizedView)
const known = KNOWN_DIFFERENCES.filter((entry) => entry.fixture === name)
const accepted = ACCEPTED_DIFFERENCES.filter((entry) => entry.fixture === name)
expect(
differences.map((difference) => difference.path).sort(),
`window differences (stored vs normalized):\n${JSON.stringify(differences, null, 2)}`
).toEqual(
[
...known.flatMap((entry) => entry.paths),
...accepted.flatMap((entry) => Object.keys(entry.values))
].sort()
)
// An accepted difference stays accepted only while it shows exactly the recorded values.
const acceptedValues = Object.assign({}, ...accepted.map((entry) => entry.values))
expect(
Object.fromEntries(
differences
.filter((difference) => difference.path in acceptedValues)
.map(({ path, stored, normalized }) => [path, { stored, normalized }])
)
).toEqual(acceptedValues)
}
)
it('keeps the tab-bar twin selected when a selected legacy copy is dropped', async () => {
const profile = PROFILES.find(
(entry) => entry.name === 'legacy row repeating a tab-bar tab’s terminal, selected'
@@ -122,11 +74,4 @@ describe('the window shows the same session before and after the shared Loader r
expect(view[`${GIT} group 0 tabs`]).toEqual(['tab-a', 'tab-b'])
expect(view).not.toHaveProperty([`${GIT} tab /Users/dev/orca/src/app.ts`])
})
it('lists differences only for fixtures that exist', () => {
const names = new Set(PROFILES.map((profile) => profile.name))
expect(
[...KNOWN_DIFFERENCES, ...ACCEPTED_DIFFERENCES].filter((entry) => !names.has(entry.fixture))
).toEqual([])
})
})
@@ -135,31 +135,4 @@ describe('NewWorkspaceComposerCard set location', () => {
expect(storeMocks.openModal).not.toHaveBeenCalled()
expect(storeMocks.openSettingsPage).not.toHaveBeenCalled()
})
// Flush independently so this test cannot rely on an earlier test warming the chunk.
it('closes the nested dialog before publishing the ready run target', async () => {
const nestedOpenChanges: boolean[] = []
const setupChanges: string[] = []
container = await renderCard({
onNestedDialogOpenChange: (open) => nestedOpenChanges.push(open),
onProjectHostSetupChange: (setupId) => setupChanges.push(setupId)
})
act(() => {
container?.querySelector<HTMLElement>('div[data-run-target-combobox-root="true"]')?.click()
})
const setLocation = [...document.body.querySelectorAll<HTMLButtonElement>('button')].find(
(button) => button.textContent?.includes('Set project location')
)
act(() => setLocation?.click())
await act(async () => {})
const complete = [...document.body.querySelectorAll<HTMLButtonElement>('button')].find(
(button) => button.textContent === 'Complete location'
)
act(() => complete?.click())
expect(nestedOpenChanges).toEqual([true, false])
expect(setupChanges).toEqual(['setup-remote'])
expect(document.body.querySelector('[data-testid="set-project-location-dialog"]')).toBeNull()
})
})
@@ -135,20 +135,6 @@ afterEach(() => {
vi.clearAllMocks()
})
it('preserves the pending pagehide flush through HMR and removes the listener when drained', () => {
const remove = vi.spyOn(domWindow, 'removeEventListener')
try {
clearCompletedActivity([doneThread])
disposePendingClearCompletedEvictionListener()
expect(remove).not.toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions)
domWindow.dispatchEvent(new Event('pagehide'))
expect(drop).toHaveBeenCalledOnce()
expect(remove).toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions)
} finally {
remove.mockRestore()
}
})
it('preserves Undo after HMR without dropping the restored persisted row', () => {
clearCompletedActivity([doneThread])
disposePendingClearCompletedEvictionListener()
@@ -159,20 +145,3 @@ it('preserves Undo after HMR without dropping the restored persisted row', () =>
expect(drop).not.toHaveBeenCalled()
expect(mockStore.retainedAgentsByPaneKey['t-done:1']).toBeDefined()
})
it('releases the retired listener when its fallback settles and supports a late stale handler', () => {
const remove = vi.spyOn(domWindow, 'removeEventListener')
try {
clearCompletedActivity([doneThread])
disposePendingClearCompletedEvictionListener()
vi.advanceTimersByTime(CLEAR_COMPLETED_EVICTION_FALLBACK_MS)
expect(drop).toHaveBeenCalledOnce()
expect(remove).toHaveBeenCalledWith('pagehide', flushPendingClearCompletedEvictions)
mockStore.retainedAgentsByPaneKey = { 't-done:1': makeRetained('t-done:1') }
clearCompletedActivity([doneThread])
domWindow.dispatchEvent(new Event('pagehide'))
expect(drop).toHaveBeenCalledTimes(2)
} finally {
remove.mockRestore()
}
})
@@ -3,8 +3,6 @@ import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/cont
import type { DashboardCard, DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
import { patchDashboardSnapshotFromAgentStatus } from './dashboard-agent-status-patch'
import { dashboardCardDisplayState } from '../../../../shared/dashboard-snapshot'
function card(overrides: Partial<DashboardCard> = {}): DashboardCard {
return {
paneKey: 'tab-1:leaf-1',
@@ -46,118 +44,6 @@ function snapshot(cards: DashboardCard[] = [card()]): DashboardSnapshot {
}
describe('patchDashboardSnapshotFromAgentStatus', () => {
it.each(['cancellation', 'failure', 'unconfirmed'] as const)(
'projects a newer %s verdict and clears it on the next full status',
(outcome) => {
const patched = patchDashboardSnapshotFromAgentStatus(
snapshot(),
event({
state: 'done',
mainAgent: { state: 'done', outcome, stateStartedAt: 250 }
})
).snapshot
expect(dashboardCardDisplayState(patched.cards[0])).toBe(
outcome === 'cancellation'
? 'interrupted'
: outcome === 'failure'
? 'failed'
: 'unconfirmed'
)
expect(patched.cards[0].bucket).toBe('done')
for (const interrupted of [undefined, false]) {
const cleared = patchDashboardSnapshotFromAgentStatus(
patched,
event({ state: 'working', interrupted, receivedAt: 400, stateStartedAt: 350 })
).snapshot
expect(dashboardCardDisplayState(cleared.cards[0])).toBe('working')
}
expect(
patchDashboardSnapshotFromAgentStatus(
patched,
event({ receivedAt: 400, programStatusOnly: true })
).snapshot
).toBe(patched)
expect(
patchDashboardSnapshotFromAgentStatus(
patched,
event({ receivedAt: 400, providerSessionOnly: true })
).snapshot
).toBe(patched)
}
)
it('patches one known card without rebuilding the dashboard topology', () => {
const original = snapshot([card(), card({ paneKey: 'tab-2:leaf-2' })])
const result = patchDashboardSnapshotFromAgentStatus(original, event())
expect(result.matched).toBe(true)
expect(result.snapshot.cards[0]).toMatchObject({
bucket: 'attention',
dotState: 'blocked',
task: 'Need a decision',
lastUserMessage: 'Need a decision',
askSummary: '{"question":"Continue?"}',
stateChangedAt: 250,
statusUpdatedAt: 300,
unseen: true
})
expect(result.snapshot.cards[1]).toBe(original.cards[1])
})
it('preserves cached fields omitted from same-state hook pings', () => {
const original = snapshot([
card({
bucket: 'attention',
dotState: 'waiting',
askSummary: 'Pick one',
lastAgentMessage: 'Waiting',
stateChangedAt: 250,
unseen: true
})
])
const result = patchDashboardSnapshotFromAgentStatus(
original,
event({ state: 'waiting', prompt: '', interactivePrompt: undefined, stateStartedAt: 250 })
)
expect(result.snapshot.cards[0]).toMatchObject({
task: 'old task',
askSummary: 'Pick one',
lastAgentMessage: 'Waiting',
unseen: true
})
})
it('patches same-state working into passive monitoring', () => {
const result = patchDashboardSnapshotFromAgentStatus(
snapshot(),
event({ state: 'working', workingMode: 'monitoring', stateStartedAt: 100 })
)
expect(result.snapshot.cards[0]).toMatchObject({
bucket: 'working',
dotState: 'working',
workingMode: 'monitoring'
})
})
it('keeps an unverifiable child compatible with the pop-out wire vocabulary', () => {
const result = patchDashboardSnapshotFromAgentStatus(
snapshot(),
event({
subagents: [{ id: 'child-1', state: 'unverifiable', startedAt: 100 }]
})
)
expect(result.snapshot.cards[0].subagents).toEqual([
{
id: 'tab-1:leaf-1\u0000subagent:child-1',
name: 'unknown',
dotState: 'idle'
}
])
})
it('ignores stale, wrong-workspace, and session-only events', () => {
const original = snapshot()
expect(
@@ -170,14 +56,4 @@ describe('patchDashboardSnapshotFromAgentStatus', () => {
patchDashboardSnapshotFromAgentStatus(original, event({ providerSessionOnly: true })).snapshot
).toBe(original)
})
it('asks the caller for topology only when the pane is unknown', () => {
const original = snapshot()
const result = patchDashboardSnapshotFromAgentStatus(
original,
event({ paneKey: 'tab-new:leaf-new' })
)
expect(result).toEqual({ matched: false, snapshot: original })
})
})
@@ -1,180 +0,0 @@
import { describe, expect, it } from 'vitest'
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status/contracts/agent-status-freshness'
import type {
AgentStatusEntry,
AgentStatusState
} from '../../../../shared/agent-status/contracts/agent-status-types'
import type { FolderWorkspace } from '../../../../shared/folder-workspaces/folder-workspace-types'
import type { Repo } from '../../../../shared/repo-types'
import type { TerminalTab } from '../../../../shared/workspace-layout/terminal-tab-types'
import type { Worktree } from '../../../../shared/worktree/types'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
import { buildRetainedAgentsSyncSnapshot } from './useRetainedAgents'
const ACTIVE_PANE_KEY = makePaneKey('tab-active', '22222222-2222-4222-8222-222222222222')
const ARCHIVED_PANE_KEY = makePaneKey('tab-archived', '33333333-3333-4333-8333-333333333333')
const FOLDER_PANE_KEY = makePaneKey('tab-folder', '44444444-4444-4444-8444-444444444444')
const ARCHIVED_FOLDER_PANE_KEY = makePaneKey(
'tab-folder-archived',
'55555555-5555-4555-8555-555555555555'
)
function makeFolderWorkspace(overrides?: Partial<FolderWorkspace>): FolderWorkspace {
return {
id: 'folder-1',
projectGroupId: 'group-1',
name: 'Folder',
folderPath: '/repo/folder',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1,
...overrides
}
}
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/repo',
displayName: 'Repo',
badgeColor: '#000',
addedAt: 1
}
}
function makeWorktree(overrides?: Partial<Worktree>): Worktree {
return {
id: 'wt-1',
repoId: 'repo-1',
path: '/repo/wt-1',
head: 'abc123',
branch: 'feature',
isBare: false,
isMainWorktree: false,
displayName: 'feature',
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
...overrides
}
}
function makeTab(overrides?: Partial<TerminalTab>): TerminalTab {
return {
id: 'tab-1',
ptyId: null,
worktreeId: 'wt-1',
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
...overrides
}
}
function makeEntry(args: {
paneKey: string
state: AgentStatusState
updatedAt: number
stateStartedAt?: number
prompt?: string
toolName?: string
}): AgentStatusEntry {
return {
state: args.state,
prompt: args.prompt ?? 'Fix it',
updatedAt: args.updatedAt,
stateStartedAt: args.stateStartedAt ?? args.updatedAt,
paneKey: args.paneKey,
terminalTitle: 'Claude',
stateHistory: [],
agentType: 'claude',
toolName: args.toolName
}
}
describe('buildRetainedAgentsSyncSnapshot', () => {
it('builds live rows for non-archived worktrees and stale-decays active states', () => {
const repo = makeRepo()
const activeWorktree = makeWorktree({ id: 'wt-active' })
const archivedWorktree = makeWorktree({ id: 'wt-archived', isArchived: true })
const activeTab = makeTab({ id: 'tab-active', worktreeId: 'wt-active' })
const archivedTab = makeTab({ id: 'tab-archived', worktreeId: 'wt-archived' })
const snapshot = buildRetainedAgentsSyncSnapshot({
repos: [repo],
worktreesByRepo: { [repo.id]: [activeWorktree, archivedWorktree] },
folderWorkspaces: [],
tabsByWorktree: {
[activeWorktree.id]: [activeTab],
[archivedWorktree.id]: [archivedTab]
},
agentStatusByPaneKey: {
[ACTIVE_PANE_KEY]: makeEntry({
paneKey: ACTIVE_PANE_KEY,
state: 'working',
updatedAt: 10_000,
stateStartedAt: 10_000
}),
[ARCHIVED_PANE_KEY]: makeEntry({
paneKey: ARCHIVED_PANE_KEY,
state: 'done',
updatedAt: 20_000,
stateStartedAt: 20_000
})
},
now: 10_000 + AGENT_STATUS_STALE_AFTER_MS + 1
})
expect([...snapshot.existingWorktreeIds]).toEqual(['wt-active'])
expect(snapshot.currentAgents.get(ACTIVE_PANE_KEY)?.row.state).toBe('idle')
expect(snapshot.currentAgents.get(ARCHIVED_PANE_KEY)).toBeUndefined()
})
it('builds live rows for non-archived folder workspaces and skips archived ones', () => {
const folderWorkspace = makeFolderWorkspace()
const archivedFolderWorkspace = makeFolderWorkspace({ id: 'folder-2', isArchived: true })
const workspaceKey = folderWorkspaceKey(folderWorkspace.id)
const archivedWorkspaceKey = folderWorkspaceKey(archivedFolderWorkspace.id)
const tab = makeTab({ id: 'tab-folder', worktreeId: workspaceKey })
const archivedTab = makeTab({ id: 'tab-folder-archived', worktreeId: archivedWorkspaceKey })
const snapshot = buildRetainedAgentsSyncSnapshot({
repos: [],
worktreesByRepo: {},
folderWorkspaces: [folderWorkspace, archivedFolderWorkspace],
tabsByWorktree: { [workspaceKey]: [tab], [archivedWorkspaceKey]: [archivedTab] },
agentStatusByPaneKey: {
[FOLDER_PANE_KEY]: makeEntry({
paneKey: FOLDER_PANE_KEY,
state: 'done',
updatedAt: 10_000
}),
[ARCHIVED_FOLDER_PANE_KEY]: makeEntry({
paneKey: ARCHIVED_FOLDER_PANE_KEY,
state: 'done',
updatedAt: 10_000
})
},
now: 10_000
})
expect([...snapshot.existingWorktreeIds]).toEqual([workspaceKey])
expect(snapshot.currentAgents.get(FOLDER_PANE_KEY)?.worktreeId).toBe(workspaceKey)
expect(snapshot.currentAgents.get(ARCHIVED_FOLDER_PANE_KEY)).toBeUndefined()
})
})
@@ -62,25 +62,6 @@ afterEach(() => {
})
describe('DiagramViewerModal', () => {
it('shows the diagram fitted and centered, and Escape returns focus to the opener', async () => {
const user = userEvent.setup()
render(<Host />)
const opener = screen.getByRole('button', { name: 'open' })
await user.click(opener)
expect(screen.getByRole('dialog', { name: 'Diagram' })).toBeInTheDocument()
sizeCanvas()
const image = screen.getByRole('img', { name: 'Diagram' })
expect(image.getAttribute('src')).toMatch(/^data:image\/svg\+xml/)
expect(image.style.width).toBe('400px')
expect(image.style.transform).toBe('translate(300px, 200px)')
expect(screen.getByRole('button', { name: 'Fit to screen' })).toBeDisabled()
await user.keyboard('{Escape}')
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
await waitFor(() => expect(document.activeElement).toBe(opener))
})
it('zooms from the toolbar, pans on Alt-drag, and fits again', async () => {
const user = userEvent.setup()
render(<Host />)
@@ -134,17 +115,6 @@ describe('DiagramViewerModal', () => {
await waitFor(() => expect(document.activeElement).toBe(opener))
})
it('pans by the wheel distance whatever unit the device reports', async () => {
const user = userEvent.setup()
render(<Host />)
await user.click(screen.getByRole('button', { name: 'open' }))
const canvas = sizeCanvas()
const image = screen.getByRole('img', { name: 'Diagram' })
fireEvent.wheel(canvas, { deltaY: 3, deltaMode: WheelEvent.DOM_DELTA_LINE })
expect(image.style.transform).toBe('translate(300px, 152px)')
})
it('leaves focus alone when another modal replaced the viewer', async () => {
const user = userEvent.setup()
render(<Host />)
@@ -51,25 +51,6 @@ const expectedTasks = [
]
describe('task children of ordered list items', () => {
it.each([
['4. Parent', 3, '-'],
['10) Parent', 4, '-']
] as const)('keeps sibling task states under %s at indent %s', (parent, indent, marker) => {
const source = [
parent,
`${' '.repeat(indent)}${marker} [ ] First`,
`${' '.repeat(indent)}${marker} [x] Middle`,
`${' '.repeat(indent)}${marker} [X] Last`
].join('\n')
const editor = openMarkdown(source)
try {
editor.state.doc.check()
expect(taskItems(editor)).toEqual(expectedTasks)
} finally {
editor.destroy()
}
})
it('preserves checked siblings when Enter splits a task before save and reopen', () => {
const source = '4. Parent\n - [ ] First\n - [x] Middle\n - [x] Last'
const editor = openMarkdown(source)
@@ -119,21 +100,6 @@ describe('task children of ordered list items', () => {
}
})
it('keeps a four-space continuation paragraph free of leftover indent', () => {
const editor = openMarkdown('1. Parent\n - [ ] a\n more text')
try {
const paragraphs: string[] = []
editor.state.doc.descendants((node) => {
if (node.type.name === 'paragraph') {
paragraphs.push(node.textContent)
}
})
expect(paragraphs).toEqual(['Parent', 'a', 'more text'])
} finally {
editor.destroy()
}
})
it('keeps an intentionally nested task under its owning sibling', () => {
const editor = openMarkdown(
'4. Parent\n - [ ] First\n - [x] Child\n - [x] Middle\n - [x] Last'
@@ -149,25 +115,4 @@ describe('task children of ordered list items', () => {
editor.destroy()
}
})
it('keeps following plain bullets and the next ordered item in their own lists', () => {
const editor = openMarkdown(
'4. Parent\n - [ ] First\n - Plain\n - [x] Last\n5. Following'
)
try {
editor.state.doc.check()
expect(taskItems(editor)).toEqual([expectedTasks[0], expectedTasks[2]])
expect(editor.state.doc.firstChild?.childCount).toBe(2)
expect(editor.state.doc.firstChild?.lastChild?.textContent).toBe('Following')
const plainItems: string[] = []
editor.state.doc.descendants((node) => {
if (node.type.name === 'listItem' && node.firstChild?.textContent === 'Plain') {
plainItems.push(node.type.name)
}
})
expect(plainItems).toEqual(['listItem'])
} finally {
editor.destroy()
}
})
})
@@ -1,143 +0,0 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Editor, type JSONContent } from '@tiptap/core'
import type { DiffComment } from '../../../../../../shared/diff-comment-types'
import { createRichMarkdownExtensions } from '../schema/rich-markdown-extensions'
import { createRichMarkdownEditorCodec } from '../document/rich-markdown-source-transport'
import './rich-markdown-review-annotations'
import {
buildRichMarkdownCommentBlocks,
getRichMarkdownCommentBlocks as getRichMarkdownReviewRailBlocks
} from './rich-markdown-comment-blocks'
import { measureRichMarkdownReviewNotePositions } from './rich-markdown-review-note-positioning'
const editors: Editor[] = []
function createEditor(
content: string | JSONContent = '# Heading\n\nFirst paragraph\n\n- One\n- Two'
) {
const editor = new Editor({
element: document.createElement('div'),
extensions: createRichMarkdownExtensions({ codec: createRichMarkdownEditorCodec() }),
content,
...(typeof content === 'string' ? { contentType: 'markdown' as const } : {})
})
editors.push(editor)
// Settle the trailing-node plugin before measuring selection-only transactions.
editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false))
return editor
}
afterEach(() => {
for (const editor of editors.splice(0)) {
editor.destroy()
}
vi.restoreAllMocks()
})
describe('review rail source block reuse', () => {
it.each([
'```ts\nconst value = 1\n\nvalue++\n```\n\nAfter code',
'| First | Second |\n| --- | --- |\n| one | two |\n\nAfter table',
'<details><summary>Toggle</summary>\n\nInside\n\n</details>\n\nAfter toggle'
])('preserves multiline block boundaries for %s', (source) => {
const editor = createEditor(source)
expect(getRichMarkdownReviewRailBlocks(editor)).toEqual(buildRichMarkdownCommentBlocks(editor))
})
it('preserves block lines and reuses them after selection-only transactions', () => {
const editor = createEditor()
const expected = buildRichMarkdownCommentBlocks(editor)
const serialize = vi.spyOn(editor.markdown!, 'serialize')
const blocks = getRichMarkdownReviewRailBlocks(editor)
expect(blocks).toEqual(expected)
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
serialize.mockClear()
editor.commands.setTextSelection(3)
expect(getRichMarkdownReviewRailBlocks(editor)).toBe(blocks)
expect(serialize).not.toHaveBeenCalled()
})
it('rebuilds after edits and undo while keeping editors isolated', () => {
const editor = createEditor()
const original = getRichMarkdownReviewRailBlocks(editor)
editor.commands.insertContentAt(1, 'changed\ntext')
const changed = getRichMarkdownReviewRailBlocks(editor)
expect(changed).not.toBe(original)
expect(changed).toEqual(buildRichMarkdownCommentBlocks(editor))
editor.commands.undo()
expect(getRichMarkdownReviewRailBlocks(editor)).toEqual(original)
expect(getRichMarkdownReviewRailBlocks(createEditor())).not.toBe(original)
})
it('invalidates an in-place serializer replacement and a manager replacement', () => {
const editor = createEditor()
const original = getRichMarkdownReviewRailBlocks(editor)
const serialize = editor.markdown!.serialize.bind(editor.markdown)
vi.spyOn(editor.markdown!, 'serialize').mockImplementation(
(content) => `${serialize(content)}\nextra line`
)
const changed = getRichMarkdownReviewRailBlocks(editor)
expect(changed).not.toEqual(original)
expect(changed).toEqual(buildRichMarkdownCommentBlocks(editor))
editor.markdown = createEditor().markdown
expect(getRichMarkdownReviewRailBlocks(editor)).toEqual(original)
})
it('does not reuse fallback lines after a missing serializer becomes available', () => {
const editor = createEditor()
const markdown = editor.markdown
editor.markdown = undefined
const fallback = getRichMarkdownReviewRailBlocks(editor)
expect(fallback).toEqual(buildRichMarkdownCommentBlocks(editor))
editor.markdown = markdown
expect(getRichMarkdownReviewRailBlocks(editor)).not.toEqual(fallback)
expect(getRichMarkdownReviewRailBlocks(editor)).toEqual(buildRichMarkdownCommentBlocks(editor))
})
it('avoids serialization for every comment and repeated scroll while refreshing geometry', () => {
const editor = createEditor()
let sourceTop = 100
const coords = vi.spyOn(editor.view, 'coordsAtPos').mockImplementation(() => ({
top: sourceTop,
bottom: sourceTop + 20,
left: 0,
right: 10
}))
const container = document.createElement('div')
const comment: DiffComment = {
id: 'note',
worktreeId: 'workspace',
filePath: 'notes.md',
source: 'markdown',
lineNumber: 1,
body: 'Review',
createdAt: 1,
side: 'modified'
}
const markdownComments = Array.from({ length: 5 }, (_, index) => ({
...comment,
id: `note-${index}`,
selectedText: index === 0 ? 'Heading' : undefined
}))
const serialize = vi.spyOn(editor.markdown!, 'serialize')
const measure = () =>
measureRichMarkdownReviewNotePositions({
editor,
container,
markdownComments,
markdownSourceLineOffset: 0
})
expect(measure()[0]?.top).toBe(100)
expect(serialize).toHaveBeenCalledTimes(2 * editor.state.doc.childCount - 1)
serialize.mockClear()
sourceTop = 200
container.scrollTop = 30
for (let index = 0; index < 60; index++) {
expect(measure()[0]?.top).toBe(230)
}
expect(serialize).not.toHaveBeenCalled()
expect(coords).toHaveBeenCalledTimes(61 * markdownComments.length)
})
})
@@ -1,13 +1,12 @@
// @vitest-environment happy-dom
import { Editor } from '@tiptap/core'
import { expect, it, vi } from 'vitest'
import { expect, it } from 'vitest'
import { encodeRawMarkdownHtmlForRichEditor } from '../raw-source/raw-markdown-html'
import { createRichMarkdownEditorCodec } from '../document/rich-markdown-source-transport'
import { createRichMarkdownExtensions } from './rich-markdown-extensions'
import { getMarkdownRichModeUnsupportedReason } from '../../markdown/render-mode/markdown-rich-mode'
import { commitRichMarkdownSerialization } from '../document/rich-markdown-serialization-commit'
import { createRichMarkdownHtmlSuperscriptLinkContext } from '../citations/rich-markdown-html-superscript-link-context'
import * as fenceScanner from '../../markdown/source-scanning/markdown-fence-scanner'
function createEditor(source: string): Editor {
const codec = createRichMarkdownEditorCodec()
@@ -81,17 +80,6 @@ it.each([200, 50_000, 50_001])(
}
)
it.each([
' <span>indented</span>',
'[link](https://example.com "<span>title</span>")',
'[link](<span>)'
])('blocks markup that the parser keeps as transport text: %s', (markup) => {
for (const length of [200, 50_000, 50_001, 120_000]) {
const source = `${'a'.repeat(length)}\n\n${markup}\n`
expect(getMarkdownRichModeUnsupportedReason(source)).toBe('html-or-jsx')
}
})
it('preserves markup in the edited paragraph through successive saves and reopening', () => {
const markup = '<span title="keep">text</span> <sup><a href="https://example.com">1</a></sup>'
const source = `${'a'.repeat(50_001)}\n\nBefore ${markup} after\n`
@@ -142,14 +130,3 @@ it('preserves markup in the edited paragraph through successive saves and reopen
editor.destroy()
}
})
it('scans whole-document fence ranges once for thousands of passthrough details blocks', () => {
const source = '<details class="custom"><summary>Toggle</summary>Body</details>\n\n'.repeat(4_000)
const scan = vi.spyOn(fenceScanner, 'getMarkdownFenceRanges')
try {
expect(getMarkdownRichModeUnsupportedReason(source)).toBeNull()
expect(scan.mock.calls.filter(([content]) => content === source)).toHaveLength(1)
} finally {
scan.mockRestore()
}
})
@@ -79,28 +79,6 @@ describe('rich markdown search focus handoff', () => {
expect(event.defaultPrevented).toBe(false)
})
it.each(['button', 'input', 'textarea', 'select', 'noneditable', 'task-label'])(
'leaves embedded %s controls in charge of focus',
(kind) => {
const { editorDom, findInput, focus } = createSurface()
const control = document.createElement(
kind === 'noneditable' ? 'div' : kind === 'task-label' ? 'label' : kind
)
if (kind === 'noneditable' || kind === 'task-label') {
control.setAttribute('contenteditable', 'false')
}
const child = document.createElement('span')
control.append(child)
editorDom.append(control)
findInput.focus()
child.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }))
expect(document.activeElement).toBe(findInput)
expect(focus).not.toHaveBeenCalled()
}
)
it('does not steal focus from a different editor find widget or unrelated field', () => {
const first = createSurface()
const second = createSurface()
@@ -116,36 +94,6 @@ describe('rich markdown search focus handoff', () => {
expect(first.focus).not.toHaveBeenCalled()
})
it('leaves ordinary editor clicks and blank surface clicks untouched', () => {
const { root, editorDom, paragraph, findInput, focus } = createSurface()
editorDom.focus()
focus.mockClear()
paragraph.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }))
findInput.focus()
root.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }))
expect(focus).not.toHaveBeenCalled()
expect(document.activeElement).toBe(findInput)
})
it('leaves canceled and non-primary presses untouched', () => {
const { paragraph, findInput, focus } = createSurface()
findInput.focus()
const canceled = new MouseEvent('mousedown', { bubbles: true, cancelable: true, button: 0 })
canceled.preventDefault()
for (const event of [
canceled,
new MouseEvent('mousedown', { bubbles: true, button: 1 }),
new MouseEvent('mousedown', { bubbles: true, button: 2 })
]) {
paragraph.dispatchEvent(event)
}
expect(focus).not.toHaveBeenCalled()
expect(document.activeElement).toBe(findInput)
})
it('allows editable content nested inside an unrelated noneditable ancestor', () => {
const { root, paragraph, findInput, focus } = createSurface()
root.setAttribute('contenteditable', 'false')
@@ -155,8 +103,4 @@ describe('rich markdown search focus handoff', () => {
expect(focus).toHaveBeenCalledExactlyOnceWith({ preventScroll: true })
})
it('does nothing when the editor has not mounted', () => {
focusRichMarkdownEditorFromSearch(new MouseEvent('mousedown', { button: 0 }), null)
})
})
@@ -1,53 +1,11 @@
import { describe, expect, it } from 'vitest'
import {
buildWheelGesturePoints,
mapClientPointToSimulatorScreen,
resolveEmulatorWheelDelta,
resolveEmulatorPointerAction
} from './emulator-screen-gesture'
import { resolveVisualStreamGeometry } from './emulator-device-frame-layout'
const rect = {
left: 10,
top: 20,
width: 200,
height: 400
}
describe('emulator screen gestures', () => {
it.each([
{ orientation: 'portrait', display: { x: 0.2, y: 0.7 } },
{ orientation: 'landscape_left', display: { x: 0.3, y: 0.2 } },
{ orientation: 'landscape_right', display: { x: 0.7, y: 0.8 } },
{ orientation: 'portrait_upside_down', display: { x: 0.8, y: 0.3 } }
] as const)(
'round-trips a raw panel point through $orientation display geometry',
({ orientation, display }) => {
const geometry = resolveVisualStreamGeometry(
{ width: 2007, height: 2853 },
'portrait',
orientation
)
const displayRect = {
left: 10,
top: 20,
width: geometry.size?.width ?? 0,
height: geometry.size?.height ?? 0
}
const point = mapClientPointToSimulatorScreen(
{
clientX: displayRect.left + display.x * displayRect.width,
clientY: displayRect.top + display.y * displayRect.height
},
displayRect,
geometry.size,
geometry.streamRotation
)
expect(point?.x).toBeCloseTo(0.2)
expect(point?.y).toBeCloseTo(0.7)
}
)
it('inverse rotates fallback taps, drag phases and the home edge into raw panel space', () => {
const displayRect = { left: 0, top: 0, width: 400, height: 200 }
expect(
@@ -77,123 +35,6 @@ describe('emulator screen gestures', () => {
})
})
it.each([
{ rotation: 90, x: -0.2, y: 0.1 },
{ rotation: -90, x: 0.2, y: -0.1 },
{ rotation: 180, x: 0.1, y: 0.2 }
] as const)('inverse rotates wheel vectors for $rotation degrees', ({ rotation, x, y }) => {
const delta = resolveEmulatorWheelDelta(
{ clientX: 110, clientY: 220, deltaX: 20, deltaY: 80 },
rect,
null,
1,
rotation
)
expect(delta?.delta.x).toBeCloseTo(x)
expect(delta?.delta.y).toBeCloseTo(y)
})
it('maps client coordinates into normalized simulator coordinates', () => {
expect(mapClientPointToSimulatorScreen({ clientX: 110, clientY: 220 }, rect, null)).toEqual({
x: 0.5,
y: 0.5
})
})
it('keeps small movement as a tap', () => {
const action = resolveEmulatorPointerAction(
[
{ clientX: 100, clientY: 200 },
{ clientX: 103, clientY: 204 }
],
rect,
null
)
expect(action).toEqual({
kind: 'tap',
point: { x: 0.465, y: 0.46 }
})
})
it('turns a drag into a begin/move/end gesture sequence', () => {
const action = resolveEmulatorPointerAction(
[
{ clientX: 110, clientY: 320 },
{ clientX: 110, clientY: 260 },
{ clientX: 110, clientY: 180 }
],
rect,
null
)
expect(action).toEqual({
kind: 'gesture',
points: [
{ type: 'begin', x: 0.5, y: 0.75 },
{ type: 'move', x: 0.5, y: 0.6 },
{ type: 'end', x: 0.5, y: 0.4 }
]
})
})
it('marks drags that start at the home indicator as bottom-edge gestures', () => {
const action = resolveEmulatorPointerAction(
[
{ clientX: 110, clientY: 396 },
{ clientX: 110, clientY: 260 },
{ clientX: 110, clientY: 120 }
],
rect,
null
)
expect(action).toEqual({
kind: 'gesture',
points: [
{ type: 'begin', x: 0.5, y: 0.94, edge: 3 },
{ type: 'move', x: 0.5, y: 0.6, edge: 3 },
{ type: 'end', x: 0.5, y: 0.25, edge: 3 }
]
})
})
it('maps wheel deltas into opposite-direction touch movement', () => {
const delta = resolveEmulatorWheelDelta(
{ clientX: 110, clientY: 220, deltaX: 0, deltaY: 80 },
rect,
null,
1
)
expect(delta?.start).toEqual({ x: 0.5, y: 0.5 })
expect(delta?.delta.x).toBeCloseTo(0)
expect(delta?.delta.y).toBeCloseTo(-0.2)
})
it('builds a wheel gesture with a move point', () => {
expect(buildWheelGesturePoints({ x: 0.5, y: 0.5 }, { x: 0.5, y: 0.25 })).toEqual([
{ type: 'begin', x: 0.5, y: 0.5 },
{ type: 'move', x: 0.5, y: 0.375 },
{ type: 'end', x: 0.5, y: 0.25 }
])
})
it('accounts for letterboxed stream content', () => {
expect(
mapClientPointToSimulatorScreen({ clientX: 110, clientY: 220 }, rect, {
width: 100,
height: 100
})
).toEqual({ x: 0.5, y: 0.5 })
expect(
mapClientPointToSimulatorScreen({ clientX: 110, clientY: 30 }, rect, {
width: 100,
height: 100
})
).toBeNull()
})
it('maps the full landscape screen while the stream canvas shape settles', () => {
const landscapeRect = {
left: 0,
@@ -4,7 +4,6 @@ import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { notifyWorkItemDetailsMutation } from './github-work-item-comment-mutations'
import { runGHEditLabelToggle } from '../github-item-dialog/edit-item-fields/gh-edit-section-mutations'
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: vi.fn() }))
vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn(() => ({ repos: [] })) } }))
@@ -100,24 +99,6 @@ describe('issue edits retain the displayed repository', () => {
})
expectProjectUpdate(sourceContext, { body: 'Project edit' })
})
it.each([
{ state: 'closed' as const },
{ addLabels: ['bug'] },
{ addAssignees: ['upstream-assignee'] }
])('keeps the Project field edit on the same exact actor: %j', async (updates) => {
vi.mocked(window.api.gh.updateIssueBySlug).mockResolvedValue({ ok: true })
await runIssueUpdate({
repoPath: '/home/fixture/fork-widgets',
repoId: item.repoId,
sourceContext,
projectOrigin,
issueRepo: UPSTREAM,
number: 12,
updates
})
expectProjectUpdate(sourceContext, updates)
})
})
it.each([
@@ -143,36 +124,4 @@ describe('issue edits retain the displayed repository', () => {
)
expect(window.api.gh.updateIssueBySlug).not.toHaveBeenCalled()
})
it.each([
{ localLabels: [], updates: { addLabels: ['bug'] } },
{ localLabels: ['bug'], updates: { removeLabels: ['bug'] } }
])(
'retains the target for both label toggle directions: %j',
async ({ localLabels, updates }) => {
let mutation: Promise<unknown> = Promise.resolve()
runGHEditLabelToggle({
item,
itemNumber: item.number,
itemRepoId: item.repoId,
repoPath: 'C:\\workspace\\widgets',
projectOrigin: undefined,
issueRepo: ORIGIN,
label: 'bug',
localLabels,
run: async (_key, options) => {
mutation = options.mutate()
await mutation
},
onLabelsChange: vi.fn(),
patchProjectRowIfNeeded: vi.fn(),
onMutated: vi.fn()
})
await mutation
expect(window.api.gh.updateIssue).toHaveBeenCalledWith(
expect.objectContaining({ ownerRepo: ORIGIN, updates })
)
}
)
})
@@ -1,172 +0,0 @@
// @vitest-environment happy-dom
import { credentialDecryptionMessage } from '../../../shared/integration-credential-errors'
import React, { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import type {
LinearCustomViewSummary,
LinearProjectSummary
} from '../../../shared/linear/project-types'
import {
LinearCollectionNotice,
LinearCustomViewTable,
LinearProjectOverview,
LinearProjectTable
} from './linear-project-view-surfaces'
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
const roots: Root[] = []
afterEach(() => {
roots.splice(0).forEach((root) => act(() => root.unmount()))
document.body.replaceChildren()
})
function render(element: React.ReactNode): HTMLElement {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
roots.push(root)
act(() => root.render(<TooltipProvider>{element}</TooltipProvider>))
return container
}
const project: LinearProjectSummary = {
id: 'project-1',
workspaceId: 'workspace-1',
workspaceName: 'Acme',
name: 'Compiler',
url: 'https://linear.app/project/project-1',
labels: [
{ id: 'label-1', name: 'Alpha' },
{ id: 'label-2', name: 'Beta' },
{ id: 'label-3', name: 'Hidden' }
],
progress: 0.5,
priority: 2,
targetDate: 'invalid-date'
}
describe('Linear project view surfaces', () => {
it.each([
['Reconnect Linear', 'Unable to load Linear issues. Try again.'],
[
credentialDecryptionMessage('Linear'),
'Unable to read saved Linear login. Reconnect it in Settings.'
]
])('renders safe collection failure %s and controlled pagination', (message, expected) => {
const onLoadMore = vi.fn()
const container = render(
<LinearCollectionNotice
errors={[
{
workspaceId: 'workspace-1',
workspaceName: 'Acme',
type: 'auth',
message
}
]}
hasMore
count={50}
kind="projects"
onLoadMore={onLoadMore}
/>
)
expect(container.textContent).toContain(expected)
const button = container.querySelector('button')
act(() => button?.dispatchEvent(new MouseEvent('click', { bubbles: true })))
expect(onLoadMore).toHaveBeenCalledOnce()
})
it('keeps project row keyboard selection and nested actions independent', () => {
const onSelectProject = vi.fn()
const onOpenProject = vi.fn()
const onUseProjectIssues = vi.fn()
const container = render(
<LinearProjectTable
projects={[project]}
loading={false}
selectedProjectId="project-1"
workspaceSelection="all"
onSelectProject={onSelectProject}
onOpenProject={onOpenProject}
onUseProjectIssues={onUseProjectIssues}
/>
)
const row = container.querySelector('[role="button"]') as HTMLElement
expect(row.getAttribute('aria-current')).toBe('true')
expect(row.dataset.current).toBe('true')
expect(container.textContent).toContain('Acme')
expect(container.textContent).toContain('Alpha')
expect(container.textContent).toContain('Beta')
expect(container.textContent).not.toContain('Hidden')
act(() => row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
expect(onSelectProject).toHaveBeenCalledWith(project)
const issuesButton = container.querySelector('button[aria-label="Open Compiler issues"]')
act(() => issuesButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })))
expect(onUseProjectIssues).toHaveBeenCalledWith(project)
expect(onSelectProject).toHaveBeenCalledTimes(1)
const openButton = container.querySelector('button[aria-label="Open Compiler in Linear"]')
act(() => openButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })))
expect(onOpenProject).toHaveBeenCalledWith(project)
expect(onSelectProject).toHaveBeenCalledTimes(1)
})
it('preserves custom-view metadata and overview action callbacks', () => {
const view: LinearCustomViewSummary = {
id: 'view-1',
name: 'Triage',
model: 'issue',
shared: true,
owner: { id: 'user-1', displayName: 'Ada' }
}
const onSelectView = vi.fn()
const onOpenView = vi.fn()
const views = render(
<LinearCustomViewTable
views={[view]}
loading={false}
onSelectView={onSelectView}
onOpenView={onOpenView}
/>
)
expect(views.textContent).toContain('Shared')
expect(views.textContent).toContain('Ada')
const row = views.querySelector('[role="button"]') as HTMLElement
act(() => row.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })))
expect(onSelectView).toHaveBeenCalledWith(view)
const onBack = vi.fn()
const onRefresh = vi.fn()
const onOpenIssues = vi.fn()
const onOpenProject = vi.fn()
const overview = render(
<LinearProjectOverview
project={project}
loading={false}
onBack={onBack}
onRefresh={onRefresh}
onOpenIssues={onOpenIssues}
onOpenProject={onOpenProject}
/>
)
expect(overview.textContent).toContain('Compiler')
act(() =>
overview
.querySelector('button[aria-label="Back to projects"]')
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
)
expect(onBack).toHaveBeenCalledOnce()
const refresh = [...overview.querySelectorAll('button')].find((button) =>
button.textContent?.includes('Refresh')
)
act(() => refresh?.dispatchEvent(new MouseEvent('click', { bubbles: true })))
expect(onRefresh).toHaveBeenCalledOnce()
})
})
@@ -86,36 +86,6 @@ afterEach(() => {
vi.useRealTimers()
})
it('re-reads once when the user replies in a failed chat, then lets the stream go', async () => {
await listFailure()
expect(mocks.subscribeStatus).toHaveBeenCalledOnce()
const later = Date.now() + 10_000
hostEmit()({ type: 'status', session: summary('working', 'Carry on please', later) })
hostEmit()({ type: 'status', session: summary('idle', 'Carry on please', later + 1) })
expect(offerReads()).toBe(1)
await vi.advanceTimersByTimeAsync(500)
expect(offerReads()).toBe(2)
expect(getNativeChatRestartOffer().failed).toEqual([])
expect(mocks.unsubscribe).toHaveBeenCalledOnce()
})
it('does not re-read for an agent streaming in a failed chat or for news older than the list', async () => {
await listFailure({ failed: [failure] })
const later = Date.now() + 10_000
// Already reflected in the listing the store holds.
hostEmit()({ type: 'snapshot', sessions: [summary('working', 'Replayed note', 1)] })
hostEmit()({ type: 'status', session: summary('working', 'Replayed note', later) })
await vi.advanceTimersByTimeAsync(500)
expect(offerReads()).toBe(1)
hostEmit()({ type: 'status', session: summary('idle', 'Replayed note', later + 1) })
await vi.advanceTimersByTimeAsync(500)
expect(offerReads()).toBe(2)
expect(mocks.unsubscribe).not.toHaveBeenCalled()
})
// Known chat changes are news despite older host timestamps because list and status delivery can race.
it('re-reads for a known failed chat that changes even when its stamp predates the list', async () => {
await listFailure()
@@ -1,157 +0,0 @@
import { toast } from 'sonner'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { ResumeCandidate } from './native-chat-resume-on-restart-grouping'
import { lastToastShow } from './native-chat-resume-toast.test-support'
import {
consumeNativeChatResumeOnRestartDialogRequest,
getNativeChatResumeOnRestartDialogRequest
} from './native-chat-resume-on-restart-dialog'
import {
_resetNativeChatRestartOffer,
continueNativeChatRestartOffer,
getNativeChatRestartOffer,
refreshNativeChatRestartOffer
} from './native-chat-resume-on-restart-store'
const rpc = vi.hoisted(() => vi.fn())
vi.mock('@/runtime/structured-agent-session-client', () => ({
callStructuredAgentSession: rpc,
// A failed row opens the status feed; these cases never drive it.
subscribeStructuredAgentSessionStatus: () => new Promise(() => {})
}))
vi.mock('sonner', async () =>
(await import('./native-chat-resume-sonner-mock.test-support')).resumeSonnerMock()
)
const offered: ResumeCandidate[] = ['a', 'b'].map((sessionId) => ({
sessionId,
workspaceId: 'workspace',
agent: 'codex',
trigger: 'quit',
latestPrompt: `Prompt ${sessionId}`,
recordedAt: 1_800_000_000_000
}))
beforeEach(() => {
rpc.mockReset()
_resetNativeChatRestartOffer()
consumeNativeChatResumeOnRestartDialogRequest()
vi.mocked(toast).mockClear()
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => {
vi.mocked(console.warn).mockRestore()
_resetNativeChatRestartOffer()
})
/** The host lists both chats, then loses every later call. */
function hostLostAfterListing(): void {
let reachable = true
rpc.mockImplementation(async (_target, method) => {
if (method === 'agentSession.restartResumable' && reachable) {
return { sessions: offered, failed: [] }
}
reachable = false
throw new Error('host unreachable')
})
}
const resumes = {
click: () => continueNativeChatRestartOffer(['a', 'b']),
launch: () => continueNativeChatRestartOffer(offered.map((candidate) => candidate.sessionId))
}
// An unavailable host cannot settle delivery; Review still opens the retained action summary.
it.each(['click', 'launch'] as const)(
'keeps every chat of a lost %s resume unconfirmed when the host cannot be read either',
async (caller) => {
hostLostAfterListing()
await refreshNativeChatRestartOffer()
await resumes[caller]()
expect(vi.mocked(toast).mock.calls).toEqual([
[
'Couldn’t confirm 2 chats were resumed',
expect.objectContaining({ action: expect.objectContaining({ label: 'Review' }) })
]
])
expect(lastToastShow()).toBeDefined()
lastToastShow()?.()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(getNativeChatResumeOnRestartDialogRequest()).toBe(true)
}
)
// A fresh host offer is an observation, so the lost request still needs Review.
it('answers an opted-in launch that loses its resume request with one unconfirmed toast', async () => {
rpc.mockImplementation(async (_target, method) => {
if (method === 'agentSession.restartResumable') {
return { sessions: offered, failed: [] }
}
throw new Error('response lost')
})
await refreshNativeChatRestartOffer()
await resumes.launch()
expect(getNativeChatRestartOffer().failed.map((entry) => entry.sessionId)).toEqual(['a', 'b'])
expect(vi.mocked(toast).mock.calls.map(([title]) => title)).toEqual([
'Couldn’t confirm 2 chats were resumed'
])
expect(lastToastShow()).toBeDefined()
})
it('answers a mixed opted-in launch with one toast', async () => {
const failure = { ...offered[1]!, failedAt: 1, outcome: 'refused', reason: 'unknown' }
let acted = false
rpc.mockImplementation(async (_target, method, params: { sessionIds: string[] } | undefined) => {
const failed = acted ? [failure] : []
if (method === 'agentSession.restartResumable') {
return { sessions: acted ? [] : offered, failed }
}
acted = true
return {
continued: (params?.sessionIds ?? []).map((sessionId) => ({
sessionId,
outcome: sessionId === 'b' ? 'refused' : 'continued'
})),
sessions: [],
failed: [failure]
}
})
await refreshNativeChatRestartOffer()
await resumes.launch()
expect(vi.mocked(toast).mock.calls).toEqual([
['1 chat couldn’t be resumed', expect.objectContaining({ description: 'Resumed 1 chat' })]
])
expect(lastToastShow()).toBeDefined()
})
it('raises no toast when the host had nothing left for an opted-in launch to resume', async () => {
rpc.mockImplementation(async (_target, method) =>
method === 'agentSession.restartResumable'
? { sessions: offered, failed: [] }
: { continued: [], sessions: [], failed: [] }
)
await refreshNativeChatRestartOffer()
await resumes.launch()
expect(toast).not.toHaveBeenCalled()
})
it('opens the retained summary from Show after the host withdraws a failure', async () => {
let failed = [{ ...offered[0]!, failedAt: 1, outcome: 'unconfirmed', reason: 'unknown' }]
rpc.mockImplementation(async (_target, method) =>
method === 'agentSession.restartResumable'
? { sessions: [], failed }
: { continued: [{ sessionId: 'a', outcome: 'unknown' }], sessions: [], failed }
)
await refreshNativeChatRestartOffer()
await continueNativeChatRestartOffer(['a'])
expect(vi.mocked(toast).mock.calls.map(([title]) => title)).toEqual([
'Couldn’t confirm 1 chat was resumed'
])
failed = []
lastToastShow()?.()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(rpc.mock.calls.at(-1)?.[1]).toBe('agentSession.restartResumable')
expect(getNativeChatRestartOffer().failed).toEqual([])
expect(getNativeChatResumeOnRestartDialogRequest()).toBe(true)
})
@@ -3,8 +3,8 @@
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { loadSyntaxLanguage } from '@/lib/syntax-highlighting/syntax-highlighter'
import { NativeChatCodeBlock, NativeChatPlainCodeBlock } from './NativeChatCodeBlock'
import { NativeChatCodeBlock } from './NativeChatCodeBlock'
vi.mock('@/lib/syntax-highlighting/oniguruma', async () => ({
loadOniguruma: (await import('@/lib/syntax-highlighting/oniguruma-test-harness'))
@@ -38,64 +38,6 @@ describe('NativeChatCodeBlock', () => {
expect(second.container.querySelector('pre')?.textContent).toBe(code)
})
it('leaves the code of a dimmed row uncolored', async () => {
const code = <code>{'const dimmed = 1\n'}</code>
await loadSyntaxLanguage('typescript')
const colored = render(<NativeChatCodeBlock language="typescript">{code}</NativeChatCodeBlock>)
expect(colored.container.querySelector('pre span')).not.toBeNull()
const plain = render(
<NativeChatPlainCodeBlock language="typescript">{code}</NativeChatPlainCodeBlock>
)
expect(plain.container.querySelector('pre span')).toBeNull()
expect(plain.container.querySelector('pre')?.textContent).toBe('const dimmed = 1\n')
})
it('leaves a fence in an unknown language as plain text', async () => {
const { container } = render(
<NativeChatCodeBlock language="not-a-real-language">
<code>{'plain words\n'}</code>
</NativeChatCodeBlock>
)
const pre = container.querySelector('pre')
await waitFor(() => expect(pre?.textContent).toBe('plain words\n'))
expect(pre?.querySelector('span')).toBeNull()
})
it.each([
['with a language header', 'typescript'],
['without a language header', undefined]
])('wraps long lines only after the reader turns wrapping on (%s)', (_name, language) => {
const { container } = render(
<NativeChatCodeBlock language={language}>
<code>{'const longLine = 1\n'}</code>
</NativeChatCodeBlock>
)
const pre = container.querySelector('pre')
const wrapOn = screen.getByRole('button', { name: 'Wrap lines' })
expect(wrapOn).toHaveAttribute('aria-pressed', 'false')
expect(pre).toHaveClass('overflow-x-auto')
expect(pre).not.toHaveClass('whitespace-pre-wrap')
fireEvent.click(wrapOn)
const wrapOff = screen.getByRole('button', { name: 'Disable line wrap' })
expect(wrapOff).toHaveAttribute('aria-pressed', 'true')
expect(pre).toHaveClass('whitespace-pre-wrap', '[overflow-wrap:anywhere]')
expect(pre).not.toHaveClass('overflow-x-auto')
fireEvent.click(wrapOff)
expect(screen.getByRole('button', { name: 'Wrap lines' })).toHaveAttribute(
'aria-pressed',
'false'
)
expect(pre).toHaveClass('overflow-x-auto')
})
it('colors and wraps fenced code, copies its text, and confirms success', async () => {
const writeClipboardText = vi.fn().mockResolvedValue(undefined)
Object.assign(window, { api: { ui: { writeClipboardText } } })
@@ -120,28 +62,4 @@ describe('NativeChatCodeBlock', () => {
await waitFor(() => expect(writeClipboardText).toHaveBeenCalledWith(code))
expect(await screen.findByRole('button', { name: 'Copied' })).toBeInTheDocument()
})
it('keeps wrapping to the block the reader toggled', () => {
render(
<>
<NativeChatCodeBlock language="typescript">
<code>{'first\n'}</code>
</NativeChatCodeBlock>
<NativeChatCodeBlock language="typescript">
<code>{'second\n'}</code>
</NativeChatCodeBlock>
</>
)
fireEvent.click(screen.getAllByRole('button', { name: 'Wrap lines' })[0])
expect(screen.getByRole('button', { name: 'Disable line wrap' })).toBeInTheDocument()
expect(screen.getAllByRole('button', { name: 'Wrap lines' })).toHaveLength(1)
})
it('shows no wrap control for an empty block', () => {
render(<NativeChatCodeBlock language="typescript" />)
expect(screen.queryByRole('button', { name: 'Wrap lines' })).toBeNull()
})
})
@@ -1,6 +1,6 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { getDefaultSettings } from '../../../../shared/constants'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { useAppStore } from '@/store'
@@ -82,60 +82,3 @@ it('offers chat actions without a duplicate close button and targets Resume at t
})
)
})
it('changes the saved chat width from the standalone chat toolbar', async () => {
const settings = {
...getDefaultSettings('/tmp'),
nativeChatAppearance: { fontSize: 16, width: 'wide' as const }
}
const updateSettings = vi.fn(async (updates) => {
useAppStore.setState({ settings: { ...settings, ...updates } })
})
useAppStore.setState({ settings, updateSettings })
renderHeader()
fireEvent.keyDown(screen.getByRole('button', { name: 'Chat settings' }), { key: 'Enter' })
expect(screen.getByRole('menuitemradio', { name: 'Wide' }).getAttribute('aria-checked')).toBe(
'true'
)
fireEvent.click(screen.getByRole('menuitemradio', { name: 'Full' }))
await waitFor(() =>
expect(updateSettings).toHaveBeenCalledWith({
nativeChatAppearance: { fontSize: 16, width: 'full' }
})
)
})
async function splitTooltipText(): Promise<string | null> {
const trigger = screen.getByRole('button', { name: 'Split Chat Right' })
fireEvent.pointerMove(trigger, { pointerType: 'mouse' })
return (await screen.findByRole('tooltip')).textContent
}
it('shows the effective split-right shortcut in the Split Chat Right tooltip', async () => {
useAppStore.setState({ keybindings: { 'terminal.splitRight': ['Mod+Alt+K'] } })
renderHeader()
const shortcut = navigator.userAgent.includes('Mac') ? '⌘⌥K' : 'Ctrl+Alt+K'
expect(await splitTooltipText()).toBe(`Split Chat Right: ${shortcut}`)
expect(screen.getByRole('button', { name: 'Split Chat Right' })).toBeTruthy()
})
it.each([
['Mac', '⌘D, ⌘K'],
['Windows', 'Ctrl+D, Ctrl+K'],
['Linux', 'Ctrl+D, Ctrl+K']
])('separates alternative split shortcuts on %s', async (userAgent, shortcuts) => {
const userAgentSpy = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(userAgent)
try {
useAppStore.setState({ keybindings: { 'terminal.splitRight': ['Mod+D', 'Mod+K'] } })
renderHeader()
expect(await splitTooltipText()).toBe(`Split Chat Right: ${shortcuts}`)
} finally {
userAgentSpy.mockRestore()
}
})
it('omits the shortcut from the Split Chat Right tooltip when split-right is unbound', async () => {
useAppStore.setState({ keybindings: { 'terminal.splitRight': [] } })
renderHeader()
expect(await splitTooltipText()).toBe('Split Chat Right')
})
@@ -1,146 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { useMemo } from 'react'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import type { AgentSessionStatusSummary } from '../../../../shared/agent-session/agent-session-wire'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import { NATIVE_CHAT_USAGE_LIMIT_MAX_OVERDUE_MS } from '../../../../shared/native-chat/native-chat-usage-limit-cut'
import { NativeChatNoticeRow } from './NativeChatNoticeRow'
import { NativeChatOrcaStopContext } from './native-chat-orca-stop-context'
import { useNativeChatUsageLimitAutoContinue } from './use-native-chat-usage-limit-auto-continue'
const mocks = vi.hoisted(() => {
const state: {
capability: string
summary: Pick<AgentSessionStatusSummary, 'usageLimitContinue'> | undefined
} = { capability: 'supported', summary: undefined }
return Object.assign(state, {
call: vi.fn(async () => ({ ok: true })),
feed: { activate: () => () => {}, subscribe: () => () => {}, getSnapshot: () => new Map() }
})
})
vi.mock('@/runtime/structured-agent-session-host-capability', () => ({
useStructuredAgentSessionHostCapabilityState: () => mocks.capability
}))
vi.mock('@/runtime/structured-agent-session-client', () => ({
callStructuredAgentSession: mocks.call
}))
vi.mock('@/runtime/structured-agent-session-status-feed', () => ({
getStructuredAgentSessionStatusFeed: () => mocks.feed
}))
vi.mock('./NativeChatUsageLimitActions', () => ({ NativeChatUsageLimitActions: () => null }))
const reset = 1_800_000_000_000
beforeEach(() => {
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(reset - 60_000)
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
vi.useRealTimers()
})
const LOCAL: RuntimeClientTarget = { kind: 'local' }
function Row({ target = LOCAL, known = true }: { target?: RuntimeClientTarget; known?: boolean }) {
const state = useNativeChatUsageLimitAutoContinue(target, 'chat', {
turnItemId: 'cut',
turnEndedAt: reset - 120_000,
...(known ? { resetsAt: reset } : {})
})
const value = useMemo(
() => ({
hostLabel: null,
remoteHost: target.kind !== 'local',
continueAvailable: false,
offeredTurnItemId: null,
continueNow: () => {},
usageLimitTurnItemId: 'cut',
continueUsageLimit: () => {},
usageLimitAutoContinue: state
}),
[state, target.kind]
)
return (
<TooltipProvider>
<NativeChatOrcaStopContext.Provider value={value}>
<NativeChatNoticeRow
block={{
type: 'text',
text: 'Usage limit',
usageLimitTurnItemId: 'cut',
failure: { kind: 'usageLimitReached', usageLimit: { resetsAt: reset } }
}}
/>
</NativeChatOrcaStopContext.Provider>
</TooltipProvider>
)
}
function setState(state: NonNullable<AgentSessionStatusSummary['usageLimitContinue']>['state']) {
mocks.capability = 'supported'
mocks.summary = { usageLimitContinue: { turnItemId: 'cut', resetsAt: reset, state } }
const snapshot = new Map([['chat', mocks.summary]])
mocks.feed.getSnapshot = () => snapshot
}
it('shows the setting link, scheduled cancel action, and cancellation in the usage-limit row', async () => {
setState('off')
const view = render(<Row />)
expect(
screen.getByRole('button', { name: 'Continue automatically when limits reset' })
).toBeInTheDocument()
setState('scheduled')
view.rerender(<Row />)
expect(screen.getByText(/Orca will continue this chat at/)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: "Don't continue" }))
await waitFor(() =>
expect(mocks.call).toHaveBeenCalledWith(
{ kind: 'local' },
'agentSession.cancelUsageLimitContinue',
{ sessionId: 'chat', turnItemId: 'cut', resetsAt: reset }
)
)
setState('cancelled')
view.rerender(<Row />)
expect(screen.getByText("Orca won't continue this chat automatically.")).toBeInTheDocument()
setState('failed')
view.rerender(<Row />)
expect(screen.getByText("Orca won't continue this chat automatically.")).toBeInTheDocument()
})
it('hides new controls on old hosts, missing resets, stale summaries, and remote off states', () => {
setState('scheduled')
mocks.capability = 'unsupported'
const view = render(<Row />)
expect(screen.queryByText(/Orca will continue/)).toBeNull()
mocks.capability = 'supported'
view.rerender(<Row known={false} />)
expect(screen.queryByText(/Orca will continue/)).toBeNull()
setState('off')
view.rerender(<Row target={{ kind: 'environment', environmentId: 'server' }} />)
expect(
screen.queryByRole('button', { name: 'Continue automatically when limits reset' })
).toBeNull()
mocks.summary!.usageLimitContinue!.turnItemId = 'old-cut'
view.rerender(<Row />)
expect(
screen.queryByRole('button', { name: 'Continue automatically when limits reset' })
).toBeNull()
})
it('shows only the manual actions once a cut is more than a day past its reset, setting on or off', () => {
vi.setSystemTime(reset + NATIVE_CHAT_USAGE_LIMIT_MAX_OVERDUE_MS + 1_000)
// A summary from before the cutoff: 'off' with the setting off, 'scheduled' with it on.
for (const state of ['off', 'scheduled'] as const) {
setState(state)
const view = render(<Row />)
expect(
screen.queryByRole('button', { name: 'Continue automatically when limits reset' })
).toBeNull()
expect(screen.queryByText(/Orca will continue/)).toBeNull()
expect(screen.queryByRole('button', { name: "Don't continue" })).toBeNull()
expect(screen.getByRole('button', { name: 'Continue Now' })).toBeInTheDocument()
view.unmount()
}
})
@@ -1,180 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { AiVaultListResult } from '../../../../../../shared/ai-vault/contracts/ai-vault-types'
import {
aiVaultScanNoticeIssues,
blockingAiVaultScanIssue,
skippedAiVaultTranscriptCount,
skippedAiVaultTranscriptReasons
} from './ai-vault-scan-issue-state'
describe('blockingAiVaultScanIssue', () => {
it('surfaces the cause when a scan returns no sessions', () => {
const issue = {
executionHostId: 'ssh:dev-box' as const,
agent: 'codex' as const,
kind: 'host' as const,
path: 'dev-box',
message: 'Remote connection dropped. Reconnect the SSH target.'
}
expect(blockingAiVaultScanIssue(result([], [issue]))).toEqual(issue)
})
it('leaves partial-result issues as a skipped transcript count', () => {
expect(
blockingAiVaultScanIssue(
result(
[{ id: 'session' }],
[{ agent: 'codex', path: '/bad.jsonl', message: 'Malformed transcript' }]
)
)
).toBeNull()
})
it('does not block an empty scan for a skipped transcript', () => {
expect(
blockingAiVaultScanIssue(
result([], [{ agent: 'codex', path: '/bad.jsonl', message: 'Malformed transcript' }])
)
).toBeNull()
})
})
describe('aiVaultScanNoticeIssues', () => {
it('surfaces scope truncation without counting it as a skipped transcript', () => {
const scopeIssue = {
agent: 'codex' as const,
kind: 'scope' as const,
path: '/home/ada',
message: 'Only the first 64 project paths were scanned.'
}
const truncated = result([], [scopeIssue])
expect(blockingAiVaultScanIssue(truncated)).toBeNull()
expect(aiVaultScanNoticeIssues(truncated)).toEqual([scopeIssue])
expect(skippedAiVaultTranscriptCount(truncated)).toBe(0)
})
it('keeps kinded issues as notices and counts only transcripts as skipped', () => {
const hostIssue = {
executionHostId: 'ssh:dev-box' as const,
agent: 'codex' as const,
kind: 'host' as const,
path: 'dev-box',
message: 'Remote connection dropped.'
}
const scopeIssue = {
agent: 'codex' as const,
kind: 'scope' as const,
path: '/home/ada',
message: 'Only the first 64 project paths were scanned.'
}
const partial = result(
[{ id: 'session' }],
[hostIssue, scopeIssue, { agent: 'codex', path: '/bad.jsonl', message: 'Malformed' }]
)
expect(aiVaultScanNoticeIssues(partial)).toEqual([hostIssue, scopeIssue])
expect(skippedAiVaultTranscriptCount(partial)).toBe(1)
})
// An unreadable OpenCode database hides every session; report the source failure, not one skipped transcript (#15036).
it('gives an unreadable OpenCode database its own row instead of a skipped count', () => {
const sourceIssue = {
agent: 'opencode' as const,
kind: 'scope' as const,
path: '\\\\wsl.localhost\\Ubuntu\\home\\ada\\.local\\share\\opencode\\opencode.db',
message:
'OpenCode is writing to opencode.db right now, so its history was skipped. It is read again on the next refresh.'
}
const empty = result([], [sourceIssue])
expect(skippedAiVaultTranscriptCount(empty)).toBe(0)
expect(skippedAiVaultTranscriptReasons(empty)).toEqual([])
expect(aiVaultScanNoticeIssues(empty)).toEqual([sourceIssue])
})
it('does not repeat the blocking issue as a notice row', () => {
const hostIssue = {
executionHostId: 'ssh:dev-box' as const,
agent: 'codex' as const,
kind: 'host' as const,
path: 'dev-box',
message: 'Remote connection dropped.'
}
expect(aiVaultScanNoticeIssues(result([], [hostIssue]))).toEqual([])
})
it('reports nothing before the first scan', () => {
expect(aiVaultScanNoticeIssues(null)).toEqual([])
expect(skippedAiVaultTranscriptCount(null)).toBe(0)
})
})
describe('skippedAiVaultTranscriptReasons', () => {
it('surfaces the file-too-large reason behind a skipped transcript', () => {
expect(
skippedAiVaultTranscriptReasons(
result(
[{ id: 'session' }],
[
{
agent: 'claude',
path: '/home/dev/.claude/projects/a/huge.jsonl',
message: 'File too large: 12.4MB exceeds 10MB limit'
}
]
)
)
).toEqual(['File too large: 12.4MB exceeds 10MB limit'])
})
it('leaves host and scope notices to their own rows', () => {
expect(
skippedAiVaultTranscriptReasons(
result(
[],
[
{
agent: 'codex',
kind: 'scope',
path: '/home/dev',
message: 'Only the first 64 project paths were scanned.'
},
{ agent: 'codex', kind: 'host', path: 'dev-box', message: 'Reconnect the SSH target.' }
]
)
)
).toEqual([])
})
it('dedupes repeats and caps the list so a 500-issue scan stays readable', () => {
const issues = Array.from({ length: 40 }, (_unused, index) => ({
agent: 'codex' as const,
path: `/transcripts/${index}.jsonl`,
message: `Unreadable transcript ${index % 5}`
}))
expect(skippedAiVaultTranscriptReasons(result([], issues))).toEqual([
'Unreadable transcript 0',
'Unreadable transcript 1',
'Unreadable transcript 2'
])
})
it('reports nothing before the first scan', () => {
expect(skippedAiVaultTranscriptReasons(null)).toEqual([])
})
})
function result(
sessions: { id: string }[],
issues: AiVaultListResult['issues']
): AiVaultListResult {
return {
sessions: sessions as AiVaultListResult['sessions'],
issues,
scannedAt: '2026-07-26T00:00:00.000Z'
}
}
@@ -1,177 +0,0 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import type { AiVaultSession } from '../../../../../../shared/ai-vault/contracts/ai-vault-types'
import { resolveAiVaultSessionResumeInChatEligibility } from './ai-vault-session-resume-in-chat'
type ResumeInChatSession = Parameters<
typeof resolveAiVaultSessionResumeInChatEligibility
>[0]['session']
const WORKSPACE_PATH = '/repo/orca'
function session(overrides: Partial<ResumeInChatSession> = {}): ResumeInChatSession {
return {
agent: 'claude',
cwd: WORKSPACE_PATH,
filePath: '/home/dev/.claude/projects/-repo-orca/session-1.jsonl',
executionHostId: 'local',
messageCount: 12,
previewMessages: [],
...overrides
}
}
function eligibility(
overrides: Partial<Parameters<typeof resolveAiVaultSessionResumeInChatEligibility>[0]> = {}
) {
return resolveAiVaultSessionResumeInChatEligibility({
session: session(),
targetWorkspaceId: 'repo-1::/repo/orca',
targetWorkspacePath: WORKSPACE_PATH,
structuredRouteAvailable: true,
...overrides
})
}
describe('resolveAiVaultSessionResumeInChatEligibility', () => {
it('refuses a row already adopted into a chat before any other check', () => {
expect(
eligibility({
session: {
...session(),
structuredSession: { sessionId: 'claude_1', workspaceId: 'repo-1::/repo/orca' }
}
})
).toEqual({ available: false, reason: 'already-structured' })
})
it.each(['ssh:build-box', 'runtime:unresolved-owner'] as const)(
'refuses a row recorded on %s',
(executionHostId) => {
expect(eligibility({ session: session({ executionHostId }) })).toEqual({
available: false,
reason: 'remote'
})
}
)
it.each<AiVaultSession['agent']>(['claude', 'codex'])(
'offers a paired-runtime %s conversation when its route is admitted',
(agent) => {
expect(
eligibility({ session: session({ agent, executionHostId: 'runtime:server-1' }) })
).toMatchObject({ available: true })
}
)
it('rejects WSL transcripts on a paired runtime too', () => {
expect(
eligibility({
session: session({
executionHostId: 'runtime:server-1',
filePath: '//wsl.localhost/Ubuntu/home/dev/.claude/projects/p/session.jsonl'
})
})
).toEqual({ available: false, reason: 'remote' })
})
it('refuses when the same pair could not take the structured route for a fresh chat', () => {
expect(eligibility({ structuredRouteAvailable: false })).toEqual({
available: false,
reason: 'workspace'
})
})
})
describe('workspace matching, which binds the agents that store conversations per folder', () => {
it('accepts host correspondence for cwd from recorded Claude output', () => {
const capture = readFileSync(
new URL(
'../../../../../../main/claude/__fixtures__/claude-adapter-capture-early-steer.jsonl',
import.meta.url
),
'utf8'
)
const cwds = capture
.trim()
.split('\n')
.flatMap((line) => {
const event: unknown = JSON.parse(line)
if (typeof event !== 'object' || !event || !('frame' in event)) {
return []
}
const frame = event.frame
return typeof frame === 'object' && frame && 'cwd' in frame && typeof frame.cwd === 'string'
? [frame.cwd]
: []
})
expect(cwds).toEqual(['/tmp/scrubbed-cwd'])
const cwd = cwds[0]
expect(
eligibility({
session: session({ cwd, cwdAliases: [`/private${cwd}`] }),
targetWorkspacePath: `/private${cwd}`
}).available
).toBe(true)
})
it.each<AiVaultSession['agent']>(['claude', 'grok', 'omp', 'opencode', 'opencode2', 'pi'])(
'offers a %s row when its host proves the registered spelling of cwd',
(agent) => {
const row = {
...session({ agent, cwd: '/private/var/example' }),
cwdAliases: ['/var/example']
}
expect(eligibility({ session: row, targetWorkspacePath: '/var/example' })).toMatchObject({
available: true
})
expect(eligibility({ session: row, targetWorkspacePath: '/var' })).toEqual({
available: false,
reason: 'workspace'
})
expect(eligibility({ session: row, structuredRouteAvailable: false })).toEqual({
available: false,
reason: 'workspace'
})
}
)
it.each<AiVaultSession['agent']>(['claude', 'grok', 'omp', 'opencode', 'opencode2', 'pi'])(
'refuses a %s row whose conversation was recorded in another workspace',
(agent) => {
// These key conversations by launch cwd, so resuming elsewhere finds or loads nothing.
expect(
eligibility({
session: session({ agent, cwd: '/repo/other' }),
targetWorkspacePath: WORKSPACE_PATH
})
).toEqual({ available: false, reason: 'workspace' })
}
)
it('refuses a Claude row that recorded no cwd', () => {
expect(eligibility({ session: session({ cwd: null }) })).toEqual({
available: false,
reason: 'workspace'
})
})
it('keeps Codex available in a different workspace, and with no recorded cwd', () => {
// Codex is handed the rollout file and a cwd, so it resumes anywhere.
expect(eligibility({ session: session({ agent: 'codex', cwd: '/repo/other' }) })).toMatchObject(
{ available: true }
)
expect(eligibility({ session: session({ agent: 'codex', cwd: null }) })).toMatchObject({
available: true
})
})
it('treats Windows spellings of one directory as the same workspace', () => {
expect(
eligibility({
session: session({ cwd: 'C:\\Users\\Dev\\repo\\Orca\\' }),
targetWorkspacePath: 'c:/users/dev/repo/orca'
})
).toMatchObject({ available: true })
})
})
@@ -44,59 +44,6 @@ function fixture(root = '/repo') {
afterEach(() => vi.useRealTimers())
describe('linked-directory watcher invalidation', () => {
it.each(['/repo', 'C:\\Repo', '\\\\host\\share\\repo'])(
'refreshes a collapsed cached alias even when canonical target is not cached (%s)',
(root) => {
const f = fixture(root)
f.process([{ kind: 'create', absolutePath: joinPath(root, 'target/new.ts') }])
expect(f.refreshTree).toHaveBeenCalledOnce()
expect(f.refreshDir).not.toHaveBeenCalled()
expect(f.cache[f.link].children).toHaveLength(1)
expect(f.selected()).toBe(joinPath(f.link, 'old.ts'))
}
)
it.each([
{ kind: 'update' as const, absolutePath: '/repo/target/new.ts', isDirectory: false },
{ kind: 'update' as const, absolutePath: '/repo/target', isDirectory: true },
{ kind: 'update' as const, absolutePath: '/repo/link', isDirectory: false },
{ kind: 'delete' as const, absolutePath: '/repo/target/deleted.ts' },
{
kind: 'rename' as const,
absolutePath: '/repo/target/new.ts',
oldAbsolutePath: '/repo/target/old.ts'
}
])('refreshes structural uncertainty including link retarget: %j', (event) => {
const f = fixture()
f.process([event])
expect(f.refreshTree).toHaveBeenCalledOnce()
})
it('keeps known regular content edits cheap and honors node directory/symlink flags', () => {
const f = fixture('C:\\Repo')
const target = 'C:\\Repo\\Target'
f.cache[target] = { children: [node(`${target}\\file.ts`), node(`${target}\\dir`, true)] }
f.process([{ kind: 'update', absolutePath: 'c:/repo/target/FILE.ts', isDirectory: false }])
expect(f.refreshTree).not.toHaveBeenCalled()
expect(f.refreshDir).not.toHaveBeenCalled()
f.process([{ kind: 'update', absolutePath: 'c:/repo/target/DIR', isDirectory: false }])
expect(f.refreshTree).toHaveBeenCalledOnce()
})
it('keeps cached historical aliases refreshed with the Quick Open preference disabled', () => {
const f = fixture()
const event = { kind: 'create' as const, absolutePath: '/repo/target/new.ts' }
f.process([event], false)
expect(f.refreshTree).toHaveBeenCalledOnce()
f.refreshTree.mockClear()
f.process([{ ...event, absolutePath: '/other/new.ts' }])
f.process([event], true, '/other')
expect(f.refreshTree).not.toHaveBeenCalled()
delete f.cache[f.link]
f.process([event])
expect(f.refreshTree).not.toHaveBeenCalled()
})
it('still clears a lexical deleted selection after a prior event requested full refresh', () => {
const f = fixture()
f.process([
@@ -1,143 +0,0 @@
// @vitest-environment happy-dom
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../../shared/constants'
import { BrowserPane } from './BrowserPane'
import { getBrowserPaneSearchEntries } from './browser-search'
import type * as BrowserSearch from './browser-search'
const fake = vi.hoisted(() => ({
query: '',
locale: { language: 'en' },
translate: vi.fn((_key: string, fallback: string) => fallback),
setHost: vi.fn(),
profiles: [],
repos: [],
map: new Map(),
environments: [],
hostOptions: [{ id: 'local', kind: 'local', label: 'Local' }]
}))
vi.mock('@/i18n/i18n', () => ({ i18n: fake.locale, translate: fake.translate }))
vi.mock('./browser-search', async (importOriginal) => {
const original = await importOriginal<typeof BrowserSearch>()
return { ...original, getBrowserPaneSearchEntries: vi.fn(original.getBrowserPaneSearchEntries) }
})
vi.mock('@/store/index', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
settingsSearchQuery: fake.query,
browserDefaultUrl: '',
browserSessionProfiles: fake.profiles,
repos: fake.repos,
sshTargetLabels: fake.map,
sshConnectionStates: fake.map,
runtimeEnvironments: fake.environments,
runtimeStatusByEnvironmentId: fake.map,
browserSessionHostIdOverride: null,
setBrowserSessionHostId: fake.setHost
})
}))
vi.mock('@/components/terminal-pane/pane-helpers', () => ({ isMacUserAgent: () => false }))
vi.mock('../../sidebar/worktree-list/controls/sidebar-host-options', () => ({
buildSidebarHostOptions: () => fake.hostOptions
}))
vi.mock('./BrowserHomePageSetting', () => ({
BrowserHomePageSetting: ({
value,
onChange
}: {
value: string
onChange: (value: string) => void
}) => (
<input
aria-label="Home page draft"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
)
}))
vi.mock('./use/BrowserUsePane', () => ({ BrowserUseSetup: () => <span>Browser use row</span> }))
vi.mock('./BrowserDefaultZoomSetting', () => ({
BrowserDefaultZoomSetting: () => <span>Zoom row</span>
}))
vi.mock('./BrowserSearchEngineSetting', () => ({
BrowserSearchEngineSetting: () => <span>Search row</span>
}))
vi.mock('./routing/BrowserLinkRoutingSetting', () => ({
BrowserLinkRoutingSetting: () => <span>Routing row</span>
}))
vi.mock('./routing/BrowserLinkRoutingModifierSetting', () => ({
BrowserLinkRoutingModifierSetting: () => <span>Modifier row</span>
}))
vi.mock('./routing/BrowserLinkRoutingBackgroundSetting', () => ({
BrowserLinkRoutingBackgroundSetting: () => <span>Background links row</span>
}))
vi.mock('./routing/BrowserTerminalLinkActionsSetting', () => ({
BrowserTerminalLinkActionsSetting: () => <span>Terminal links row</span>
}))
vi.mock('./routing/BrowserLocalhostWorktreeLabelsSetting', () => ({
BrowserLocalhostWorktreeLabelsSetting: () => <span>Localhost row</span>
}))
vi.mock('./BrowserSitePermissionsSetting', () => ({
BrowserSitePermissionsSetting: () => <span>Site permissions row</span>
}))
vi.mock('./routing/BrowserClientHostedRemoteSetting', () => ({
BrowserClientHostedRemoteSetting: () => <span>Remote row</span>
}))
vi.mock('./routing/BrowserSshWorkspaceRoutingSetting', () => ({
BrowserSshWorkspaceRoutingSetting: () => <span>SSH row</span>
}))
vi.mock('./profiles/BrowserProfileScopeSetting', () => ({
BrowserProfileScopeSetting: () => <span>Profile scope row</span>
}))
vi.mock('./BrowserUserAgentSetting', () => ({
BrowserUserAgentSetting: () => <span>Identity row</span>
}))
vi.mock('../form-controls/SettingsFormControls', () => ({ SettingsSubsectionHeader: () => null }))
vi.mock('./profiles/BrowserSessionCookiesSection', () => ({
BrowserSessionCookiesSection: () => <span>Cookies row</span>
}))
vi.mock('./profiles/BrowserNewProfileDialog', () => ({ BrowserNewProfileDialog: () => null }))
beforeEach(() => {
vi.clearAllMocks()
fake.query = ''
fake.locale.language = 'en'
fake.translate.mockImplementation((_key, fallback) => fallback)
})
afterEach(cleanup)
it('builds one catalog per render while typing a home page draft', () => {
render(<BrowserPane settings={getDefaultSettings('/synthetic')} updateSettings={vi.fn()} />)
vi.clearAllMocks()
for (let index = 1; index <= 20; index++) {
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'x'.repeat(index) } })
}
expect(getBrowserPaneSearchEntries).toHaveBeenCalledTimes(20)
})
it('keeps targeted search visibility', () => {
fake.query = 'Default Home Page'
render(<BrowserPane settings={getDefaultSettings('/synthetic')} updateSettings={vi.fn()} />)
expect(screen.getByRole('textbox')).toBeTruthy()
expect(screen.queryByText('Cookies row')).toBeNull()
fake.query = 'google'
cleanup()
render(<BrowserPane settings={getDefaultSettings('/synthetic')} updateSettings={vi.fn()} />)
expect(screen.getByText('Cookies row')).toBeTruthy()
})
it('rebuilds metadata in the next render after the language changes', () => {
fake.query = 'Default Home Page'
const settings = getDefaultSettings('/synthetic')
const view = render(<BrowserPane settings={settings} updateSettings={vi.fn()} />)
expect(screen.getByRole('textbox')).toBeTruthy()
fake.locale.language = 'synthetic-locale'
fake.query = 'Translated home'
fake.translate.mockImplementation((_key, fallback) =>
fallback === 'Default Home Page' ? 'Translated home' : fallback
)
vi.mocked(getBrowserPaneSearchEntries).mockClear()
view.rerender(<BrowserPane settings={settings} updateSettings={vi.fn()} />)
expect(screen.getByRole('textbox')).toBeTruthy()
expect(screen.queryByText('Cookies row')).toBeNull()
})
@@ -83,18 +83,6 @@ function openRepoCard(repoId: string) {
fireEvent.click(screen.getByRole('button', { name: 'Configure Codex' }))
}
it('a local repository card lists the models this machine reports when its picker opens', async () => {
useAppStore.setState({ repos: [TEST_REPO] })
openRepoCard(TEST_REPO.id)
expect(await screen.findByRole('option', { name: 'GPT-6.1 Sol' })).toBeDefined()
expect(discover).toHaveBeenCalledWith(
expect.objectContaining({ target: { kind: 'local' }, worktreeId: null }),
'codex'
)
const saved = useAppStore.getState().settings?.sourceControlAi
expect(saved?.discoveredModelsByAgentByHost?.local?.codex).toEqual(SOL)
})
it('a local repository card in WSL refreshes the local catalog generation reads, as the global card does', async () => {
discover.mockImplementation(actualGenerationClient.discoverRuntimeCommitMessageModels)
const list = vi.fn(async (args: { worktreePath?: string }) => ({
@@ -1,38 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
getFocusedDropRuntimeId,
getSidebarProjectDropAffordance,
isSidebarProjectDropRefusedByFocus,
resolveSidebarProjectDropPath,
routeSidebarProjectDrop
} from './sidebar-project-drop'
import { routeSidebarProjectDrop } from './sidebar-project-drop'
import { wslDistroWindowsPathAccess } from '../../../../../../shared/windows-path-access'
describe('resolveSidebarProjectDropPath', () => {
it('accepts exactly one dropped path', () => {
expect(resolveSidebarProjectDropPath(['/Users/alice/repo'])).toEqual({
status: 'ready',
path: '/Users/alice/repo'
})
})
it('rejects empty and multi-path drops before routing', () => {
expect(resolveSidebarProjectDropPath([])).toEqual({ status: 'empty' })
expect(resolveSidebarProjectDropPath(['/repo/a', '/repo/b'])).toEqual({
status: 'multiple',
count: 2
})
})
})
describe('getFocusedDropRuntimeId', () => {
it('distinguishes local runtime from active server runtime', () => {
expect(getFocusedDropRuntimeId(null)).toBeNull()
expect(getFocusedDropRuntimeId({ activeRuntimeEnvironmentId: ' ' })).toBeNull()
expect(getFocusedDropRuntimeId({ activeRuntimeEnvironmentId: 'server-1' })).toBe('server-1')
})
})
describe('routeSidebarProjectDrop', () => {
const wslHosts = [
{ environmentId: 'wsl:Ubuntu', access: wslDistroWindowsPathAccess('Ubuntu') },
@@ -67,17 +36,6 @@ describe('routeSidebarProjectDrop', () => {
).toEqual({ kind: 'windows-drive-on-wsl-host', distro: 'Ubuntu', path: 'C:\\src\\app' })
})
it('with WSL hosts off, routes a drvfs UNC drop exactly as before', () => {
const path = '\\\\wsl.localhost\\Ubuntu\\mnt\\c\\proj'
expect(routeSidebarProjectDrop({ path, focusedRuntimeId: null, wslHosts: [] })).toEqual({
kind: 'local',
path
})
expect(routeSidebarProjectDrop({ path, focusedRuntimeId: 'server', wslHosts: [] })).toEqual({
kind: 'runtime-focused'
})
})
it.each([
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\src\\app',
// Any distro's drvfs mount, as the picker treats it.
@@ -94,73 +52,4 @@ describe('routeSidebarProjectDrop', () => {
path: 'C:\\src\\app'
})
})
it('keeps refusing other drops while a server is focused', () => {
expect(
routeSidebarProjectDrop({ path: 'C:\\src', focusedRuntimeId: 'paired-server', wslHosts })
).toEqual({ kind: 'runtime-focused' })
expect(
routeSidebarProjectDrop({
path: '\\\\wsl.localhost\\Arch\\home',
focusedRuntimeId: 'wsl:Ubuntu',
wslHosts
})
).toEqual({ kind: 'runtime-focused' })
expect(
routeSidebarProjectDrop({ path: '/Users/me/app', focusedRuntimeId: 'server', wslHosts: [] })
).toEqual({ kind: 'runtime-focused' })
})
it('accepts the drag over a focused server only when a WSL host could own the folder', () => {
expect(isSidebarProjectDropRefusedByFocus(null, [])).toBe(false)
expect(isSidebarProjectDropRefusedByFocus('server', [])).toBe(true)
expect(isSidebarProjectDropRefusedByFocus('server', wslHosts)).toBe(false)
})
})
describe('getSidebarProjectDropAffordance', () => {
it('hides when the sidebar is not in a drop interaction', () => {
expect(
getSidebarProjectDropAffordance({
isDragOver: false,
isHandlingDrop: false,
remoteRuntimeActive: false
})
).toEqual({ visible: false })
})
it('shows ready, busy, and blocked states', () => {
expect(
getSidebarProjectDropAffordance({
isDragOver: true,
isHandlingDrop: false,
remoteRuntimeActive: false
})
).toMatchObject({ visible: true, tone: 'ready' })
expect(
getSidebarProjectDropAffordance({
isDragOver: false,
isHandlingDrop: true,
remoteRuntimeActive: false
})
).toMatchObject({ visible: true, tone: 'busy' })
expect(
getSidebarProjectDropAffordance({
isDragOver: true,
isHandlingDrop: false,
remoteRuntimeActive: true
})
).toMatchObject({ visible: true, tone: 'blocked' })
expect(
getSidebarProjectDropAffordance({
isDragOver: true,
isHandlingDrop: false,
remoteRuntimeActive: false,
wslDropsOnly: true
})
).toMatchObject({ visible: true, tone: 'ready', label: 'Drop a WSL folder to add project' })
})
})
@@ -1,12 +1,9 @@
import { describe, expect, it } from 'vitest'
import type { Tab } from '../../../../../../shared/tab-types'
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { structuredAgentSessionPaneKey } from '../../../../../../shared/agent-session/view/structured-agent-session-projection'
import {
getWorkspaceLineageMenuActions,
hasSleepableWorkspaceActivity
} from './workspace-lineage-menu-actions'
import { getWorkspaceLineageMenuActions } from './workspace-lineage-menu-actions'
function makeWorktree(id: string): Worktree {
return {
@@ -44,42 +41,6 @@ function makeLineage(child: Worktree, parent: Worktree): WorktreeLineage {
}
describe('workspace lineage menu actions', () => {
it('collects recursive descendants and only targets workspaces with active panels for sleep', () => {
const parent = makeWorktree('parent')
const child = makeWorktree('child')
const grandchild = makeWorktree('grandchild')
const actions = getWorkspaceLineageMenuActions({
parent,
worktrees: [parent, child, grandchild],
lineageById: {
[child.id]: makeLineage(child, parent),
[grandchild.id]: makeLineage(grandchild, child)
},
activity: {
tabsByWorktree: {
[parent.id]: [{ id: 'parent-tab' }],
[child.id]: [{ id: 'child-tab' }],
[grandchild.id]: [{ id: 'grandchild-tab' }]
},
ptyIdsByTabId: {
'parent-tab': [],
'child-tab': ['child-pty'],
'grandchild-tab': []
},
browserTabsByWorktree: {
[grandchild.id]: [{ id: 'grandchild-browser' }]
},
unifiedTabsByWorktree: {},
agentStatusByPaneKey: {},
structuredChatSleptAtByPaneKey: {}
}
})
expect(actions.descendants.map((target) => target.id)).toEqual(['child', 'grandchild'])
expect(actions.targets.map((target) => target.id)).toEqual(['parent', 'child', 'grandchild'])
expect(actions.sleepableTargets.map((target) => target.id)).toEqual(['child', 'grandchild'])
})
it('does not expose stale descendants through the recursive action scope', () => {
const parent = makeWorktree('parent')
const child = makeWorktree('child')
@@ -107,74 +68,3 @@ describe('workspace lineage menu actions', () => {
expect(actions.sleepableTargets).toEqual([])
})
})
function hostHeld(tab: Tab): Record<string, { structuredHostOwned: true }> {
return { [structuredAgentSessionPaneKey(tab.id, tab.entityId)]: { structuredHostOwned: true } }
}
describe('hasSleepableWorkspaceActivity', () => {
it('treats preserved empty PTY arrays as slept, not live', () => {
expect(
hasSleepableWorkspaceActivity('wt-1', {
tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
ptyIdsByTabId: { 'tab-1': [] },
browserTabsByWorktree: {},
unifiedTabsByWorktree: {},
agentStatusByPaneKey: {},
structuredChatSleptAtByPaneKey: {}
})
).toBe(false)
})
it('detects live terminal and browser activity', () => {
expect(
hasSleepableWorkspaceActivity('wt-1', {
tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
ptyIdsByTabId: { 'tab-1': ['pty-1'] },
browserTabsByWorktree: {},
unifiedTabsByWorktree: {},
agentStatusByPaneKey: {},
structuredChatSleptAtByPaneKey: {}
})
).toBe(true)
expect(
hasSleepableWorkspaceActivity('wt-1', {
tabsByWorktree: {},
ptyIdsByTabId: {},
browserTabsByWorktree: { 'wt-1': [{ id: 'browser-1' }] },
unifiedTabsByWorktree: {},
agentStatusByPaneKey: {},
structuredChatSleptAtByPaneKey: {}
})
).toBe(true)
})
it('offers Sleep for a chat-only workspace while its host holds the chat', () => {
const chat: Tab = {
id: 'chat-1',
entityId: 'session-1',
groupId: 'group-1',
worktreeId: 'wt-1',
contentType: 'agent-session',
agentSessionAgent: 'claude',
label: 'Claude',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
const activity = (agentStatusByPaneKey: Record<string, { structuredHostOwned?: true }>) =>
hasSleepableWorkspaceActivity('wt-1', {
tabsByWorktree: {},
ptyIdsByTabId: {},
browserTabsByWorktree: {},
unifiedTabsByWorktree: { 'wt-1': [chat] },
agentStatusByPaneKey,
structuredChatSleptAtByPaneKey: {
[structuredAgentSessionPaneKey(chat.id, chat.entityId)]: null
}
})
expect(activity(hostHeld(chat))).toBe(true)
expect(activity({})).toBe(true)
})
})
@@ -1,5 +1,5 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { cleanup, fireEvent, render } from '@testing-library/react'
import React, { createRef } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../../../shared/repo-types'
@@ -134,16 +134,6 @@ function mountedStatusIds(container: HTMLElement): string[] {
)
}
function flushNextAnimationFrame(): void {
const next = animationFrames.entries().next().value as [number, FrameRequestCallback] | undefined
expect(next).toBeDefined()
if (!next) {
return
}
animationFrames.delete(next[0])
act(() => next[1](performance.now()))
}
beforeEach(() => {
animationFrames.clear()
nextAnimationFrameId = 1
@@ -181,16 +171,4 @@ describe('WorkspaceKanbanLaneGrid', () => {
'state-21'
])
})
it('hydrates at most one mounted lane per animation frame', () => {
const { container } = renderGrid()
const renderedLaneCount = (): number =>
container.querySelectorAll('[data-render-cards="true"]').length
expect(renderedLaneCount()).toBe(0)
flushNextAnimationFrame()
expect(renderedLaneCount()).toBe(1)
flushNextAnimationFrame()
expect(renderedLaneCount()).toBe(2)
})
})
@@ -2,14 +2,11 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { createVisibleHostedReviewRefreshScheduler } from '@/store/github/visible-hosted-review-refresh-scheduler'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity'
import { getWorkspaceReferenceRefreshTargets } from './workspace-reference-refresh-targets'
import {
referenceAttachment,
referenceRepo,
referenceWorkspace,
referenceReview
referenceWorkspace
} from './workspace-reference-fixtures.test-support'
vi.mock('@/store', async () => {
const browserWindow = globalThis.window
@@ -63,87 +60,6 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers()
})
it('leaves the detached primary review to its visible target while retaining sibling reviews', () => {
useAppStore.setState({
worktreesByRepo: {
repo: [
{
...workspace,
branch: '',
linkedPR: 7,
linkedItems: [referenceAttachment(7), referenceAttachment(8)]
}
]
}
})
const targets = update()
expect(targets).toHaveLength(1)
expect(targets[0]?.key).toContain('8')
})
it('pauses disconnected SSH owners and only admits the owning reconnect', () => {
useAppStore.setState({ repos: [{ ...referenceRepo, connectionId: 'ssh-a' }] })
expect(update()).toEqual([])
for (const [targetId, status, count] of [
['other', 'connected', 0],
['ssh-a', 'connected', 1],
['ssh-a', 'connecting', 0]
] as const) {
useAppStore
.getState()
.sshConnectionStates.set(targetId, { targetId, status, error: null, reconnectAttempt: 0 })
expect(update()).toHaveLength(count)
}
})
it('shares one scheduler timer and removes queued work when the workspace leaves the viewport', async () => {
useAppStore.setState({
worktreesByRepo: {
repo: [
{
...workspace,
linkedItems: Array.from({ length: 12 }, (_, i) => referenceAttachment(i + 1))
}
]
}
})
const scheduler = createVisibleHostedReviewRefreshScheduler()
try {
scheduler.update(update())
scheduler.setVisible(true)
await vi.advanceTimersByTimeAsync(0)
expect(read).toHaveBeenCalledTimes(12)
expect(vi.getTimerCount()).toBe(1)
useAppStore.setState({ visibleReviewWorktreeIds: [] })
scheduler.update(update())
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(600_000)
expect(read).toHaveBeenCalledTimes(12)
} finally {
scheduler.dispose()
}
})
it('uses the known branch provider for sparse enterprise references', () => {
const repo = {
...referenceRepo,
gitRemoteIdentity: {
canonicalKey: 'enterprise.test/acme/orca',
remoteName: 'origin',
remoteUrl: 'https://enterprise.test/acme/orca.git'
}
}
const key = getHostedReviewCacheKey(repo.path, workspace.branch, null, repo.id, null, null, true)
useAppStore.setState({
repos: [repo],
worktreesByRepo: {
repo: [{ ...workspace, linkedItems: [{ provider: 'github', type: 'pr', number: 42 }] }]
}
})
expect(update()).toHaveLength(0)
useAppStore.setState({
hostedReviewCache: { [key]: { data: referenceReview(), fetchedAt: Date.now() } }
})
expect(update()).toHaveLength(1)
})
it('retains both explicit owners when equal visible workspace IDs exist on two hosts', () => {
const remoteRepo = {
...referenceRepo,
@@ -10,7 +10,7 @@ import {
referenceRepo,
referenceLinearIssue
} from '../review/workspace-reference-fixtures.test-support'
import { getWorkspaceReferenceRequest } from '../review/workspace-reference-details'
import { normalizeTaskSourceContext } from '../../../../../../shared/task-source-context'
import { getLinearReadScope, scopedLinearCacheKey } from '@/store/slices/linear/linear-slice-scope'
@@ -43,62 +43,6 @@ vi.mock('../../workspace-actions/deletion/workspace-delete-quick-action', () =>
afterEach(cleanup)
describe('single linked reference rich hover details', () => {
it.each([
{
data: {
provider: 'gitlab' as const,
number: 8,
title: 'Fix detached review',
state: 'open' as const,
status: 'success' as const,
updatedAt: '',
mergeable: 'MERGEABLE' as const,
url: 'https://gitlab.example.com/team/repo/-/merge_requests/8'
},
title: 'Fix detached review'
},
{ data: null, title: 'MR details unavailable' }
])(
'reads detached MR details from the same exact cache as Linked work: $title',
({ data, title }) => {
const repo = {
...referenceRepo,
gitRemoteIdentity: {
canonicalKey: 'gitlab.example.com/team/repo',
remoteName: 'origin',
remoteUrl: 'https://gitlab.example.com/team/repo.git'
}
}
const item = {
provider: 'gitlab' as const,
type: 'mr' as const,
number: 8,
url: 'https://gitlab.example.com/team/repo/-/merge_requests/8'
}
const workspace = {
...referenceWorkspace,
branch: '',
linkedPR: null,
linkedGitLabMR: 8,
linkedItems: [item]
}
const key = getWorkspaceReferenceRequest(item, workspace, repo).key
cache.hosted = { [key]: { data, fetchedAt: Date.now(), linkedReviewHintKey: 'gitlab:8' } }
const { result } = renderHook(() =>
useWorktreeCardReviewDetails({
worktree: workspace,
repo,
settings: null,
projectGroups: [],
cardProps: ['pr'],
newCardStyle: false
})
)
expect(result.current.hostedReviewCacheKey).toBe(key)
expect(result.current.prDisplay?.title).toBe(title)
}
)
it('reuses source-bound Linear details when an exact workspace read is outside the legacy all-workspaces cache slot', () => {
const source = normalizeTaskSourceContext({
provider: 'linear',
@@ -1,4 +1,3 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import {
CARD_TEMPLATE_LIMITS,
@@ -23,66 +22,7 @@ function template(root: CardTemplate['root'], extra: Partial<CardTemplate> = {})
return { version: 1, facts: 1, root, ...extra }
}
function draw(t: CardTemplate, facts: Record<string, unknown> = {}): string {
const { content } = compileCardTemplate(t, registry).render(facts, '')
return renderToStaticMarkup(<>{content}</>)
}
describe('card template compiler', () => {
it('draws layout as host elements and evaluates props, vars and when', () => {
const t = template(
{
el: 'Row',
props: { gap: 'sm', align: '=vars.wide ? "center" : "start"' },
children: [
{ el: 'Echo', props: { text: '=ws.name' } },
{ el: 'Echo', when: 'ws.hidden', props: { text: 'never' } }
]
},
{ vars: { wide: 'len(ws.name) > 3' } }
)
expect(draw(t, { ws: { name: 'card plugins', hidden: false } })).toBe(
'<div class="flex min-w-0 gap-1 items-center"><i>card plugins</i></div>'
)
expect(draw(t, { ws: { name: 'abc' } })).toContain('items-start')
})
it('expands defs and reports frame answers', () => {
const t = template(
{ el: 'Stack', children: [{ el: 'Label' }, { el: 'Label', when: 'false' }] },
{
defs: { Label: { el: 'Echo', props: { text: 'hi' } } },
frame: { singleLine: '=true', statusLane: false }
}
)
const compiled = compileCardTemplate(t, registry)
const { frame, content } = compiled.render({}, '!')
expect(frame).toEqual({ singleLine: true, statusLane: false })
expect(renderToStaticMarkup(<>{content}</>)).toBe(
'<div class="flex min-w-0 flex-col"><i>hi!</i></div>'
)
})
it('lets the frame add attributes to the root', () => {
const { content } = compileCardTemplate(template({ el: 'Row' }), registry).render(
{},
'',
() => ({
style: { marginLeft: '-4px' }
})
)
expect(renderToStaticMarkup(<>{content}</>)).toContain('margin-left:-4px')
})
it('passes frame attributes through a def used as the root, and honours its when', () => {
const t = template({ el: 'Body', when: 'ws.show' }, { defs: { Body: { el: 'Row' } } })
const compiled = compileCardTemplate(t, registry)
const attrs = (): Record<string, unknown> => ({ style: { marginLeft: '-4px' } })
const shown = compiled.render({ ws: { show: true } }, '', attrs).content
expect(renderToStaticMarkup(<>{shown}</>)).toContain('margin-left:-4px')
expect(compiled.render({ ws: { show: false } }, '', attrs).content).toBeNull()
})
it('bounds the tree defs expand to', () => {
// Each def holds `width` copies of the next: tiny as written, width^levels once inlined.
const fanOut = (levels: number, width: number): CardTemplate => {
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { computeVisibleWorktreeIds } from '../listing/compute-visible-worktrees'
import { isDefaultBranchWorkspace } from '../../../../../../shared/worktree/workspace-kinds'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host'
@@ -59,25 +59,6 @@ function visibleOptions(overrides: Partial<VisibleOptions> = {}): VisibleOptions
}
}
describe('#8873 default-branch workspace under "Hide sleeping"', () => {
it('is genuinely the default-branch row the "Hide default branch" toggle targets', () => {
expect(isDefaultBranchWorkspace(makeDefaultBranchWorktree(), repoMap.get('repo1'))).toBe(true)
})
it('stays in the sidebar when it is sleeping and "Hide sleeping" is on', () => {
const worktree = makeDefaultBranchWorktree()
const visible = computeVisibleWorktreeIds({ repo1: [worktree] }, [worktree.id], {
...visibleOptions({
showSleepingWorkspaces: false,
hideDefaultBranchWorkspace: false
})
})
expect(visible).toEqual([worktree.id])
})
})
describe('the "Hide sleeping" exemption for project entry-point rows', () => {
function visible(worktrees: Worktree[], overrides: Partial<VisibleOptions>): string[] {
return computeVisibleWorktreeIds(
@@ -93,34 +74,6 @@ describe('the "Hide sleeping" exemption for project entry-point rows', () => {
expect(visible([worktree], { alwaysShowDefaultBranchWorkspace: false })).toEqual([])
})
it('keeps the sleeping default branch when the option is explicitly on', () => {
const worktree = makeDefaultBranchWorktree()
expect(visible([worktree], { alwaysShowDefaultBranchWorkspace: true })).toEqual([worktree.id])
})
it('lets an explicit "Hide default branch" still win over the exemption', () => {
const worktree = makeDefaultBranchWorktree()
expect(
visible([worktree], {
hideDefaultBranchWorkspace: true,
alwaysShowDefaultBranchWorkspace: true
})
).toEqual([])
})
it('still sweeps sleeping non-main workspaces', () => {
const feature: Worktree = {
...makeDefaultBranchWorktree(),
id: 'wt-feature',
branch: 'refs/heads/feature',
isMainWorktree: false
}
expect(visible([feature], { alwaysShowDefaultBranchWorkspace: true })).toEqual([])
})
it('keeps a sleeping folder workspace, which has no sibling row to fall back to', () => {
// Folder roots have no head/branch; sweeping the sole root hides the entire project.
const folder: Worktree = {
@@ -132,14 +85,6 @@ describe('the "Hide sleeping" exemption for project entry-point rows', () => {
expect(visible([folder], { alwaysShowDefaultBranchWorkspace: true })).toEqual([folder.id])
})
it('keeps a sleeping detached-HEAD main worktree', () => {
const detachedMain: Worktree = { ...makeDefaultBranchWorktree(), id: 'wt-detached', branch: '' }
expect(visible([detachedMain], { alwaysShowDefaultBranchWorkspace: true })).toEqual([
detachedMain.id
])
})
it('lets "Hide detached HEAD" still win over the exemption', () => {
const detachedMain: Worktree = { ...makeDefaultBranchWorktree(), id: 'wt-detached', branch: '' }
@@ -150,20 +95,4 @@ describe('the "Hide sleeping" exemption for project entry-point rows', () => {
})
).toEqual([])
})
it('keeps every project’s entry point, not just the first', () => {
const mainA = makeDefaultBranchWorktree()
const mainB: Worktree = { ...makeDefaultBranchWorktree(), id: 'wt-main-b' }
expect(visible([mainA, mainB], {})).toEqual([mainA.id, mainB.id])
})
it('does not flicker out while an SSH host is offline', () => {
// Offline SSH synthesis preserves main-worktree identity but empties head/branch and loses live PTYs.
const awake = makeDefaultBranchWorktree()
const offline: Worktree = { ...awake, head: '', branch: '' }
expect(visible([offline], {})).toEqual([offline.id])
expect(visible([awake], {})).toEqual([awake.id])
})
})
@@ -121,21 +121,3 @@ it.each([
)
}
)
it('disables sprint filtering in linked mode while retaining its checked preference', () => {
const current = model(source)
current.jiraHasWorkspace = true
const { rerender } = render(<TaskPageJiraContent model={current} />)
expect(screen.getByRole('switch', { name: 'Current sprint' }).hasAttribute('disabled')).toBe(true)
expect(screen.getByRole('switch', { name: 'Current sprint' }).getAttribute('aria-checked')).toBe(
'true'
)
current.jiraHasWorkspace = false
rerender(<TaskPageJiraContent model={current} />)
expect(screen.getByRole('switch', { name: 'Current sprint' }).hasAttribute('disabled')).toBe(
false
)
expect(screen.getByRole('switch', { name: 'Current sprint' }).getAttribute('aria-checked')).toBe(
'true'
)
})
@@ -120,28 +120,6 @@ describe('remote runtime recovery input hold', () => {
expect(sent).toEqual([])
})
it('accepts new input right after a release drained the previous hold', async () => {
const hold = createRemoteRuntimeRecoveryInputHold()
const { sent, writer } = createWriter()
hold.enqueue(endpoint, 'a', 'driving')
hold.release(endpoint, writer)
await vi.waitFor(() => expect(sent).toEqual(['a']))
expect(hold.enqueue(endpoint, 'b', 'driving')).toBe(true)
hold.release(endpoint, writer)
await vi.waitFor(() => expect(sent).toEqual(['a', 'b']))
})
it('resolves held acknowledged input as undelivered on discard', async () => {
const hold = createRemoteRuntimeRecoveryInputHold()
const accepted = hold.enqueueAccepted(endpoint, 'x', 'driving')
hold.discard()
await expect(accepted).resolves.toBe(false)
expect(hold.isHolding()).toBe(false)
})
it('lets only the latest connect settle typing held before the pane bound a terminal', async () => {
const hold = createRemoteRuntimeRecoveryInputHold({ holdBeforeFirstConnect: true })
const { sent, writer } = createWriter()
@@ -130,44 +130,4 @@ describe('terminal surface ownership', () => {
).resolves.toBe(false)
expect(mocks.remountTerminalTabForRecovery).not.toHaveBeenCalled()
})
// Round-trip through disk/Zod: undeclared fields vanish only at load, invisible to same-process assertions.
it('still refuses a chat-owned tab after a real persist/parse round trip', async () => {
const loaded = parseWorkspaceSession({
activeRepoId: null,
activeWorktreeId: WORKTREE_ID,
activeTabId: 'tab-1',
tabsByWorktree: {
[WORKTREE_ID]: [
{
id: 'tab-1',
ptyId: null,
worktreeId: WORKTREE_ID,
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0,
viewMode: 'chat'
}
]
},
terminalLayoutsByTabId: {}
})
expect(loaded.ok).toBe(true)
setTerminalTabs(
(loaded.ok ? loaded.value.tabsByWorktree[WORKTREE_ID] : []) as StoredTerminalTab[]
)
// Blind on purpose: only the reloaded row can refuse this.
mocks.getTab.mockReturnValue(null)
await expect(
requestTerminalPaneRecovery({
tabId: 'tab-1',
ptyId: 'pty-1',
reason: 'write-stalled'
})
).resolves.toBe(false)
expect(mocks.remountTerminalTabForRecovery).not.toHaveBeenCalled()
})
})
@@ -1,177 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
_measureAgentHookCompletionStoreSyncForTest,
shouldSyncAgentHookCompletionForStoreUpdate,
type AgentHookCompletionStoreSnapshot
} from './agent-hook-completion-store-sync'
function createState(
overrides: Partial<AgentHookCompletionStoreSnapshot> = {}
): AgentHookCompletionStoreSnapshot {
return {
settings: {
experimentalTerminalAttention: false,
notifications: { enabled: true, agentTaskComplete: true }
},
tabsByWorktree: {
'wt-1': [{ id: 'tab-1', ptyId: 'pty-1', title: 'Terminal 1' }]
},
ptyIdsByTabId: { 'tab-1': ['pty-1'] },
terminalLayoutsByTabId: {},
suppressedPtyExitIds: {},
...overrides
}
}
describe('agent hook completion store sync', () => {
it('ignores unrelated and title-only writes at accumulated-workspace scale', () => {
const tabCount = 100
const coordinatorCount = 100
const updateCount = 600
const tabsByWorktree = Object.fromEntries(
Array.from({ length: tabCount }, (_, index) => [
`wt-${index}`,
[{ id: `tab-${index}`, ptyId: `pty-${index}`, title: `Terminal ${index}` }]
])
)
let previous = createState({ tabsByWorktree })
let syncPasses = 0
let tabVisits = 0
for (let index = 0; index < updateCount; index += 1) {
const current = { ...previous }
const measurement = _measureAgentHookCompletionStoreSyncForTest(current, previous)
syncPasses += Number(measurement.shouldSync)
tabVisits += measurement.tabVisits
previous = current
}
for (let index = 0; index < updateCount; index += 1) {
const targetTabs = previous.tabsByWorktree['wt-99']
if (!targetTabs) {
throw new Error('Expected title-update fixture tab')
}
const current = createState({
...previous,
tabsByWorktree: {
...previous.tabsByWorktree,
'wt-99': targetTabs.map((tab) => ({ ...tab, title: `Agent frame ${index}` }))
}
})
const measurement = _measureAgentHookCompletionStoreSyncForTest(current, previous)
syncPasses += Number(measurement.shouldSync)
tabVisits += measurement.tabVisits
previous = current
}
const previousFullPassCost = {
tabVisits: updateCount * 2 * tabCount,
coordinatorVisits: updateCount * 2 * coordinatorCount
}
const gatedCost = {
tabVisits,
coordinatorVisits: syncPasses * coordinatorCount
}
expect(previousFullPassCost).toEqual({ tabVisits: 120_000, coordinatorVisits: 120_000 })
expect(gatedCost).toEqual({ tabVisits: 600, coordinatorVisits: 0 })
})
it('keeps every coordinator-liveness input reactive', () => {
const previous = createState()
const titleOnly = createState({
...previous,
tabsByWorktree: {
'wt-1': [{ id: 'tab-1', ptyId: 'pty-1', title: 'Codex working' }]
}
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(titleOnly, previous)).toBe(false)
const tabCreated = createState({
...previous,
tabsByWorktree: {
...previous.tabsByWorktree,
'wt-2': [{ id: 'tab-2', ptyId: null }]
}
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(tabCreated, previous)).toBe(true)
const tabRemoved = createState({ ...previous, tabsByWorktree: {} })
expect(shouldSyncAgentHookCompletionForStoreUpdate(tabRemoved, previous)).toBe(true)
const tabPtyChanged = createState({
...previous,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'pty-2' }] }
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(tabPtyChanged, previous)).toBe(true)
expect(
shouldSyncAgentHookCompletionForStoreUpdate(
createState({ ...previous, ptyIdsByTabId: { 'tab-1': ['pty-2'] } }),
previous
)
).toBe(true)
expect(
shouldSyncAgentHookCompletionForStoreUpdate(
createState({ ...previous, terminalLayoutsByTabId: { 'tab-1': { root: null } } }),
previous
)
).toBe(true)
expect(
shouldSyncAgentHookCompletionForStoreUpdate(
createState({ ...previous, suppressedPtyExitIds: { 'pty-1': true } }),
previous
)
).toBe(true)
const trackingDisabled = createState({
...previous,
settings: {
experimentalTerminalAttention: false,
notifications: { enabled: false, agentTaskComplete: false }
}
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(trackingDisabled, previous)).toBe(false)
expect(
shouldSyncAgentHookCompletionForStoreUpdate(
createState({ ...previous, settings: null }),
previous
)
).toBe(true)
})
it('treats tab order and duplicate-id worktree precedence as liveness inputs', () => {
const previous = createState({
tabsByWorktree: {
'wt-1': [{ id: 'tab-shared', ptyId: 'pty-first' }],
'wt-2': [{ id: 'tab-shared', ptyId: 'pty-second' }]
}
})
const reorderedWorktrees = createState({
...previous,
tabsByWorktree: {
'wt-2': previous.tabsByWorktree['wt-2'] ?? [],
'wt-1': previous.tabsByWorktree['wt-1'] ?? []
}
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(reorderedWorktrees, previous)).toBe(true)
const twoTabPrevious = createState({
tabsByWorktree: {
'wt-1': [
{ id: 'tab-1', ptyId: 'pty-1' },
{ id: 'tab-2', ptyId: 'pty-2' }
]
}
})
const reorderedTabs = createState({
...twoTabPrevious,
tabsByWorktree: {
'wt-1': [
{ id: 'tab-2', ptyId: 'pty-2' },
{ id: 'tab-1', ptyId: 'pty-1' }
]
}
})
expect(shouldSyncAgentHookCompletionForStoreUpdate(reorderedTabs, twoTabPrevious)).toBe(true)
})
})
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from '../../../shared/constants'
import { resolveAutoAckTabTargets } from './agent-auto-ack-targets'
import { acknowledgeViewedAutoAckTarget } from './agent-auto-ack-surfaces'
import {
@@ -128,34 +128,3 @@ describe('the on-screen read in a workspace holding both a terminal and a struct
expect(workspaceStillHeld(store)).toBe(true)
})
})
function markChatGroupUnread(store: TestStore, groupId: string): void {
// Why: tab markers for completions sit behind the experimental attention setting.
store.setState({
settings: { ...getDefaultSettings('/home/test'), experimentalTerminalAttention: true }
})
store.getState().markAgentAttentionUnread({
workspaceId: WORKSPACE,
subjectKey: CHAT_SUBJECT,
groupId,
reason: 'agent-completion'
})
}
describe('tab marker container addressing', () => {
it('accepts a structured chat addressed by its unified tab id', () => {
const { store } = seedMixedWorkspace({ visible: 'chat', unreadSubjectKeys: [] })
markChatGroupUnread(store, CHAT_TAB)
expect(store.getState().unreadTerminalTabs[CHAT_TAB]).toBe('agent-completion')
})
it('still refuses an id no open tab owns', () => {
const { store } = seedMixedWorkspace({ visible: 'chat', unreadSubjectKeys: [] })
markChatGroupUnread(store, 'tab-that-never-existed')
expect(store.getState().unreadTerminalTabs['tab-that-never-existed']).toBeUndefined()
})
})
-176
View File
@@ -1,176 +0,0 @@
import type { NativeChatAppearanceSettings } from '../../../shared/native-chat/native-chat-appearance-settings'
import { beforeEach, describe, expect, it, vi } from 'vitest'
function makeTarget(args: {
hasXtermClass?: boolean
editorClosest?: boolean
chatClosest?: boolean
browserPageId?: string
/** The page's <webview> sits in a viewport shell outside its pane. */
viewportPageId?: string
}): {
classList: { contains: (token: string) => boolean }
closest: (selector: string) => object | null
} {
const {
hasXtermClass = false,
editorClosest = false,
chatClosest = false,
browserPageId,
viewportPageId
} = args
return {
classList: {
contains: (token: string) => hasXtermClass && token === 'xterm-helper-textarea'
},
closest: (selector) => {
if (selector === '[data-browser-page-pane-id]') {
return browserPageId ? { getAttribute: () => browserPageId } : null
}
if (selector === '[data-browser-page-viewport-id]') {
return viewportPageId ? { getAttribute: () => viewportPageId } : null
}
return (selector === '[data-native-chat-root]' ? chatClosest : editorClosest) ? {} : null
}
}
}
describe('registerZoomIpcBridge', () => {
beforeEach(() => {
vi.resetModules()
vi.unstubAllGlobals()
})
// Test the zoom bridge directly: useIpcEvents adds an unrelated import graph that timed out under parallel load.
async function mountZoomBridge(
args: {
activeView?: string
activeTabType?: string
activeElement?: ReturnType<typeof makeTarget>
uiZoomLevel?: number
nativeChatAppearance?: NativeChatAppearanceSettings
editorFontZoomLevel?: number
viewerTakesZoom?: boolean
} = {}
) {
const {
activeView = 'terminal',
activeTabType = 'browser',
activeElement = makeTarget({ editorClosest: true, hasXtermClass: true }),
uiZoomLevel = 0,
editorFontZoomLevel = 0,
nativeChatAppearance,
viewerTakesZoom = false
} = args
const applyUIZoom = vi.fn()
const dispatchZoomLevelChanged = vi.fn()
const setEditorFontZoomLevel = vi.fn()
const setUI = vi.fn()
const settings = { terminalFontSize: 13, nativeChatAppearance }
const updateSettings = vi.fn(async (updates: Partial<typeof settings>) => {
Object.assign(settings, updates)
})
const requestViewerZoom = vi.fn(() => viewerTakesZoom)
const dispatchBrowserPageZoom = vi.fn()
vi.doMock('@/components/browser-pane/host-guest/browser-page-zoom', () => ({
dispatchBrowserPageZoom
}))
vi.doMock('@/components/editor/viewer-zoom-request', () => ({
requestViewerZoom,
requestFrontDialogViewerZoom: () => false
}))
vi.doMock('@/lib/ui-zoom', () => ({ applyUIZoom }))
vi.doMock('@/lib/zoom-events', () => ({ dispatchZoomLevelChanged }))
vi.doMock('../store', () => ({
useAppStore: {
getState: () => ({
activeView,
activeTabType,
editorFontZoomLevel,
setEditorFontZoomLevel,
settings,
updateSettings
})
}
}))
const listenerRef: { current: ((direction: 'in' | 'out' | 'reset') => void) | null } = {
current: null
}
vi.stubGlobal('document', { activeElement })
vi.stubGlobal('window', {
api: {
ui: {
onTerminalZoom: (listener: (direction: 'in' | 'out' | 'reset') => void) => {
listenerRef.current = listener
return () => {}
},
getZoomLevel: () => uiZoomLevel,
set: setUI
}
}
})
const { registerZoomIpcBridge } = await import('./ipc-events/zoom-ipc-bridge')
const unsubs: (() => void)[] = []
registerZoomIpcBridge(unsubs)
expect(unsubs).toHaveLength(1)
const fire = listenerRef.current
if (!fire) {
throw new Error('Expected the terminal-zoom listener to be registered')
}
return {
fire,
dispatchBrowserPageZoom,
requestViewerZoom,
applyUIZoom,
dispatchZoomLevelChanged,
setEditorFontZoomLevel,
setUI,
settings,
updateSettings
}
}
it('routes keyboard and menu zoom directions to persisted chat size, never app or editor zoom', async () => {
const zoom = await mountZoomBridge({
activeTabType: 'terminal',
activeElement: makeTarget({ chatClosest: true }),
nativeChatAppearance: { fontSize: 19, codeFontSize: 16, width: 'wide' }
})
zoom.fire('in')
zoom.fire('in')
zoom.fire('out')
await vi.waitFor(() => expect(zoom.updateSettings).toHaveBeenCalledTimes(2))
expect(zoom.settings.nativeChatAppearance).toEqual({
fontSize: 19,
codeFontSize: 16,
width: 'wide'
})
zoom.fire('reset')
await vi.waitFor(() =>
expect(zoom.settings.nativeChatAppearance).toEqual({ codeFontSize: 16, width: 'wide' })
)
expect(zoom.applyUIZoom).not.toHaveBeenCalled()
expect(zoom.setUI).not.toHaveBeenCalled()
expect(zoom.setEditorFontZoomLevel).not.toHaveBeenCalled()
})
it('consumes menu chat zoom while matching without changing saved size or app zoom', async () => {
const zoom = await mountZoomBridge({
activeTabType: 'terminal',
activeElement: makeTarget({ chatClosest: true }),
nativeChatAppearance: { fontSize: 18, matchTerminalInterface: true }
})
zoom.fire('in')
zoom.fire('out')
zoom.fire('reset')
await vi.waitFor(() => expect(zoom.settings.nativeChatAppearance?.fontSize).toBe(18))
expect(zoom.updateSettings).not.toHaveBeenCalled()
expect(zoom.applyUIZoom).not.toHaveBeenCalled()
expect(zoom.setEditorFontZoomLevel).not.toHaveBeenCalled()
})
})
@@ -23,7 +23,6 @@ import {
revealStructuredSession
} from './activate-ai-vault-structured-session'
import { STRUCTURED_AGENT_SESSION_REVEAL_RUNTIME_CAPABILITY } from '../../../shared/agent-session/structured-agent-session-surface-capabilities'
import { RUNTIME_COMPAT_BLOCK_CODE } from '@/runtime/runtime-protocol-compat'
const target = { worktreeId: 'workspace-1', sessionId: 'session-1' }
@@ -40,37 +39,6 @@ beforeEach(() => {
})
describe('revealStructuredSession', () => {
it('asks a local host without a capability round trip', async () => {
// The local host shares this build; probing it would only add a round trip.
await expect(revealStructuredSession(target)).resolves.toEqual({ ...target, agent: 'codex' })
expect(mocks.supports).not.toHaveBeenCalled()
expect(mocks.call).toHaveBeenCalledWith(
{ kind: 'local' },
'agentSession.reveal',
{ sessionId: 'session-1' },
expect.objectContaining({ timeoutMs: expect.any(Number) })
)
})
it('negotiates against the host that owns the workspace, not the local one', async () => {
mocks.environmentIdFor.mockReturnValue('env-1')
await expect(revealStructuredSession(target)).resolves.toEqual({ ...target, agent: 'codex' })
expect(mocks.supports).toHaveBeenCalledWith(
'env-1',
STRUCTURED_AGENT_SESSION_REVEAL_RUNTIME_CAPABILITY,
expect.any(Number)
)
expect(mocks.call).toHaveBeenCalledWith(
{ kind: 'environment', environmentId: 'env-1' },
'agentSession.reveal',
{ sessionId: 'session-1' },
expect.objectContaining({ timeoutMs: expect.any(Number) })
)
})
it('keeps a known recipient target when the active workspace would resolve locally', async () => {
const host = { kind: 'environment', environmentId: 'recipient-host' } as const
await expect(revealStructuredSession({ ...target, target: host })).resolves.toEqual({
@@ -99,17 +67,6 @@ describe('revealStructuredSession', () => {
expect(mocks.call).not.toHaveBeenCalled()
})
it('reads a version block as a host too old, not as a lost connection', async () => {
mocks.environmentIdFor.mockReturnValue('env-1')
const blocked = Object.assign(new Error('Update the Orca server'), {
code: RUNTIME_COMPAT_BLOCK_CODE
})
mocks.supports.mockRejectedValue(blocked)
await expect(revealStructuredSession(target)).resolves.toBe('host-cannot-open')
expect(mocks.call).not.toHaveBeenCalled()
})
})
it('returns the actual revealed identity/workspace/provider for reconciliation', async () => {
@@ -34,25 +34,6 @@ beforeEach(() => {
})
describe('doc preview grant lifetime', () => {
it('mints once for repeated mounts of the same preview tab', async () => {
const request = {
owner: { kind: 'ssh' as const, connectionId: 'ssh-1' },
requestBase: '/d',
root: '/d',
entryRelativePath: 'a.html'
}
const [first, second] = await Promise.all([
ensureDocPreviewGrant('preview-1', request),
ensureDocPreviewGrant('preview-1', request)
])
// StrictMode effect replay must neither mint twice nor revoke the surviving webview’s grant.
expect(mocks.mintGrant).toHaveBeenCalledOnce()
expect(first).toBe(second)
expect(mocks.revokeGrant).not.toHaveBeenCalled()
})
// Why: Share reads and keys from the handle's location, so a later host change must not move it.
it('keeps the minted location on the handle until the grant is released', async () => {
const minted = {
@@ -85,22 +66,6 @@ describe('doc preview grant lifetime', () => {
})
})
it('revokes on release and mints fresh afterwards', async () => {
const request = {
owner: { kind: 'ssh' as const, connectionId: 'ssh-1' },
requestBase: '/d',
root: '/d',
entryRelativePath: 'a.html'
}
await ensureDocPreviewGrant('preview-2', request)
releaseDocPreviewGrant('preview-2')
await vi.waitFor(() => expect(mocks.revokeGrant).toHaveBeenCalledWith('grant-1'))
await ensureDocPreviewGrant('preview-2', request)
expect(mocks.mintGrant).toHaveBeenCalledTimes(2)
})
// Late rejection from a released tab must not evict its reopened tab’s new, otherwise unrevokable grant.
it('leaves the entry of a later mint alone when an earlier one rejects', async () => {
const request = {
@@ -129,21 +94,6 @@ describe('doc preview grant lifetime', () => {
releaseDocPreviewGrant('preview-4')
await vi.waitFor(() => expect(mocks.revokeGrant).toHaveBeenCalledWith('grant-2'))
})
it('does not cache a failed mint', async () => {
mocks.mintGrant.mockRejectedValueOnce(new Error('runtime offline'))
const request = {
owner: { kind: 'ssh' as const, connectionId: 'ssh-1' },
requestBase: '/d',
root: '/d',
entryRelativePath: 'a.html'
}
await expect(ensureDocPreviewGrant('preview-3', request)).rejects.toThrow('runtime offline')
await expect(ensureDocPreviewGrant('preview-3', request)).resolves.toMatchObject({
grantId: 'grant-1'
})
})
})
describe('outside-workspace document grants', () => {
@@ -98,28 +98,6 @@ describe('useImeEnterGestureOwnership', () => {
}
})
// Only bare confirm redispatch is suppressed; held Cmd/Ctrl still requests deliberate submit.
it.each([
['meta', { metaKey: true }],
['ctrl', { ctrlKey: true }],
['alt', { altKey: true }]
])('lets the %s+Enter chord through the carried redispatch', (_label, modifier) => {
const { result } = renderHook(() => useImeEnterGestureOwnership())
result.current.setComposing(true)
// The IME still owns the confirming keydown even with the modifier held.
expect(
result.current.ownsKeyDown(
gestureEvent({ key: 'Enter', keyCode: 13, isComposing: true, ...modifier })
)
).toBe(true)
result.current.setComposing(false)
const chord = gestureEvent({ key: 'Enter', keyCode: 13, ...modifier })
expect(result.current.ownsKeyDown(chord)).toBe(false)
expect(chord.prevented).toBe(false)
})
// Chorded redispatch must spend carry without suppressing submit or the next Enter would be swallowed.
it('spends the carry on the chorded redispatch so the next Enter is not eaten', () => {
const { result } = renderHook(() => useImeEnterGestureOwnership())
@@ -71,23 +71,6 @@ describe('openMobileEmulatorTab', () => {
vi.unstubAllGlobals()
})
it('does not start a duplicate attach while a manual launch is already pending', async () => {
let resolveAttach: (value: typeof mockAttachResult) => void = () => {}
vi.mocked(callRuntimeRpc).mockImplementation(
() =>
new Promise((resolve) => {
resolveAttach = resolve
})
)
const firstLaunch = openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBe('sim-1')
expect(callRuntimeRpc).toHaveBeenCalledTimes(1)
resolveAttach(mockAttachResult)
await firstLaunch
})
it('shuts down the managed emulator if the tab closes before attach resolves', async () => {
let resolveAttach: (value: typeof mockAttachResult) => void = () => {}
vi.mocked(callRuntimeRpc).mockImplementation(async (_target, method) => {
@@ -115,23 +98,6 @@ describe('openMobileEmulatorTab', () => {
expect(consumePrelaunchedSimulatorSession('wt-1')).toBeNull()
})
it('allows a successful retry after simulator tab creation returns null', async () => {
vi.mocked(ensureSimulatorTab).mockReturnValueOnce(null).mockReturnValueOnce('sim-retry')
vi.mocked(callRuntimeRpc).mockResolvedValue(mockAttachResult)
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBeNull()
await expect(openMobileEmulatorTab('wt-1', { targetGroupId: 'group-1' })).resolves.toBe(
'sim-retry'
)
expect(ensureSimulatorTab).toHaveBeenCalledTimes(2)
const attachCalls = vi
.mocked(callRuntimeRpc)
.mock.calls.filter(([, method]) => method === 'emulator.attach')
expect(attachCalls).toHaveLength(1)
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
})
it('does not release another in-flight launch when duplicate tab creation throws', async () => {
let resolveAttach: (value: typeof mockAttachResult) => void = () => {}
vi.mocked(callRuntimeRpc).mockImplementation(
@@ -1,10 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ManagedPaneInternal, ScrollState } from './pane-manager-types'
import {
attachPaneFitResizeObserver,
detachPaneFitResizeObserver,
requestStablePaneFit
} from './pane-fit-resize-observer'
import { requestStablePaneFit } from './pane-fit-resize-observer'
import {
beginTerminalScrollIntentBufferRebuild,
endTerminalScrollIntentBufferRebuild
@@ -113,21 +109,6 @@ describe('attachPaneFitResizeObserver', () => {
vi.restoreAllMocks()
})
it('coalesces repeated observer callbacks and fits once the grid is stable', () => {
const pane = createPane()
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
mockResizeObservers[0]?.trigger()
expect(requestAnimationFrame).toHaveBeenCalledTimes(1)
expect(pane.fitAddon.fit).not.toHaveBeenCalled()
flushAnimationFrames()
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
})
it('releases a hidden reattach continuation when the stable grid already matches', async () => {
const pane = createPane()
vi.mocked(pane.fitAddon.proposeDimensions).mockReturnValue(undefined)
@@ -160,38 +141,4 @@ describe('attachPaneFitResizeObserver', () => {
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledTimes(1)
})
it('throttles an endlessly unstable grid instead of fitting every frame', () => {
let cols = 80
const pane = createPane(() => {
cols = cols === 80 ? 81 : 80
return { cols, rows: 24 }
})
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
for (let i = 0; i < 10; i += 1) {
flushAnimationFrames()
}
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
expect(pane.pendingObservedFitRafId).toBeNull()
})
it('disconnects the observer and cancels any queued fit', () => {
const pane = createPane()
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
const scheduledRafId = pane.pendingObservedFitRafId
detachPaneFitResizeObserver(pane)
flushAnimationFrames()
expect(mockResizeObservers[0]?.disconnect).toHaveBeenCalledTimes(1)
expect(cancelAnimationFrame).toHaveBeenCalledWith(scheduledRafId)
expect(pane.fitAddon.fit).not.toHaveBeenCalled()
expect(pane.pendingObservedFitRafId).toBeNull()
})
})
@@ -57,22 +57,6 @@ afterEach(() => {
})
describe('terminal scroll-intent input resync', () => {
it('does not apply input resync after tracking is disposed', () => {
vi.stubGlobal('requestAnimationFrame', () => 0)
vi.stubGlobal('Element', TestElement)
const { terminal, capturedInput, capturedUserInput } = createTerminalWithInputCapture(42, 100)
const host = new TestElement() as unknown as HTMLElement
const disposable = attachTerminalScrollIntentTracking(terminal, host)
markTerminalPinnedViewport(terminal)
capturedUserInput.listener?.()
disposable.dispose()
terminal.buffer.active.viewportY = terminal.buffer.active.baseY
capturedInput.listener?.('a')
expect(getTerminalScrollIntentKind(terminal)).toBe('pinnedViewport')
})
it('defers real typing intent until a snapshot rebuild completes', () => {
vi.stubGlobal('requestAnimationFrame', () => 0)
vi.stubGlobal('Element', TestElement)
@@ -61,40 +61,6 @@ describe('a finished Claude turn reaches the hibernation planner', () => {
resetHibernationPaneAgeForTests()
})
// Pin planner/anchor seam: broad resumable-agent recovery must match sleep exemption, formerly Pi-only (#10238).
it('writes a live resume anchor that no longer blocks it', () => {
const store = createTestStore()
seedWorkspace(store)
store
.getState()
.setAgentStatus(
PANE,
{ state: 'working', prompt: 'summarize the diff', agentType: 'claude' },
'Claude',
{ updatedAt: DONE_AT - 5_000, stateStartedAt: DONE_AT - 5_000 },
ROUTING,
{ providerSession: PROVIDER_SESSION }
)
store
.getState()
.setAgentStatus(
PANE,
{ state: 'done', prompt: 'summarize the diff', agentType: 'claude' },
'Claude',
{ updatedAt: DONE_AT, stateStartedAt: DONE_AT },
ROUTING,
{ providerSession: PROVIDER_SESSION }
)
const state = store.getState()
expect(state.agentStatusByPaneKey[PANE]?.state).toBe('done')
expect(state.sleepingAgentSessionsByPaneKey[PANE]).toMatchObject({
agent: 'claude',
origin: 'live'
})
expect(planFrom(state, DONE_AT)).toEqual([PANE])
})
it('is not restarted by a done→done repaint carrying a fresh updatedAt', () => {
const store = createTestStore()
seedWorkspace(store)
@@ -35,30 +35,6 @@ async function createAnnotatedPage() {
afterEach(() => vi.unstubAllGlobals())
describe('saved browser note lifetime and marker eligibility', () => {
it('retires same-URL geometry without changing the saved array or object references', async () => {
const { store, pageId } = await createAnnotatedPage()
const saved = store.getState().browserAnnotationsByPageId[pageId]
const note = saved[0]
expect(store.getState().browserAnnotationMarkerIdsByPageId[pageId]).toEqual([note.id])
store.getState().setBrowserPageUrl(pageId, 'https://example.com')
expect(store.getState().browserAnnotationMarkerIdsByPageId[pageId]).toEqual([note.id])
store.getState().invalidateBrowserPageAnnotationGeometry(pageId)
expect(store.getState().browserAnnotationsByPageId[pageId]).toBe(saved)
expect(store.getState().browserAnnotationsByPageId[pageId][0]).toBe(note)
expect(store.getState().browserAnnotationMarkerIdsByPageId[pageId]).toBeUndefined()
const listener = vi.fn()
const unsubscribe = store.subscribe(listener)
store.getState().invalidateBrowserPageAnnotationGeometry(pageId)
expect(listener).not.toHaveBeenCalled()
unsubscribe()
store.getState().addBrowserPageAnnotation(makeAnnotation(pageId, 'fresh'))
expect(store.getState().browserAnnotationsByPageId[pageId][0]).toBe(note)
expect(store.getState().browserAnnotationMarkerIdsByPageId[pageId]).toEqual(['fresh'])
})
it('removes an accepted delivery snapshot after navigation while preserving later edits and additions', async () => {
const { store, pageId } = await createAnnotatedPage()
store.getState().addBrowserPageAnnotation(makeAnnotation(pageId, 'edited'))
@@ -1,17 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import { replaceRuntimeEnvironmentRevisions } from '@/runtime/runtime-environment-revision'
import { assertEditorBufferInvariant } from '../../../editor-buffer-record-lifecycle'
import { makeWorktree } from '../../worktrees-slice-test-fixtures'
import type { OpenFile } from '../types/open-file'
import type * as identityModule from './editor-buffer-identity'
import { editorBufferIdentityOf } from './editor-buffer-identity'
// Counts identity resolutions, so the scale test can prove the index never re-resolves per keystroke.
vi.mock('./editor-buffer-identity', async (importOriginal) => {
const actual = await importOriginal<typeof identityModule>()
return { ...actual, editorBufferIdentityOf: vi.fn(actual.editorBufferIdentityOf) }
})
import type { OpenFile } from '../types/open-file'
function tab(id: string, patch: Partial<OpenFile> = {}): OpenFile {
return {
@@ -37,7 +29,6 @@ beforeEach(() => {
afterEach(() => {
replaceRuntimeEnvironmentRevisions([])
vi.mocked(editorBufferIdentityOf).mockClear()
})
describe('a retired runtime', () => {
@@ -95,59 +86,3 @@ describe('SSH identity through the record index', () => {
expect(state().editorDrafts.w2).toBe('edits')
})
})
describe('scale', () => {
const FILES = 20
const WORKSPACES = 10
it('indexes 200 tabs across 20 files with one resolution per tab, and none per keystroke', () => {
const worktrees = Array.from({ length: WORKSPACES }, (_, w) =>
makeWorktree({ id: `wt-${w}`, repoId: 'repo-1', path: '/repo', hostId: 'local' })
)
const tabs = Array.from({ length: FILES * WORKSPACES }, (_, i) => {
const file = i % FILES
return tab(`t${i}`, {
filePath: `/repo/f${file}.ts`,
relativePath: `f${file}.ts`,
worktreeId: `wt-${Math.floor(i / FILES)}`
})
})
const resolutions = vi.mocked(editorBufferIdentityOf)
useAppStore.setState({ worktreesByRepo: { 'repo-1': worktrees } })
resolutions.mockClear()
expect(() => useAppStore.setState({ openFiles: tabs })).not.toThrow()
expect(resolutions).toHaveBeenCalledTimes(tabs.length)
expect(Object.keys(state().editorBuffers)).toHaveLength(FILES)
for (const viewIds of Object.values(state().editorBufferViews)) {
expect(viewIds).toHaveLength(WORKSPACES)
}
expect(() => assertEditorBufferInvariant(state())).not.toThrow()
// Going dirty rewrites only the record's own views.
resolutions.mockClear()
state().setEditorDraft('t0', 'e')
state().markFileDirty('t0', true)
expect(resolutions).toHaveBeenCalledTimes(WORKSPACES)
resolutions.mockClear()
state().setEditorDraft('t0', 'edits')
expect(resolutions).not.toHaveBeenCalled()
const dirty = state().openFiles.filter((file) => file.isDirty)
expect(dirty.map((file) => file.id)).toEqual(
Array.from({ length: WORKSPACES }, (_, w) => `t${w * FILES}`)
)
// One more tab resolves only itself.
useAppStore.setState((s) => ({
openFiles: [...s.openFiles, tab('late', { worktreeId: 'wt-3', filePath: '/repo/f0.ts' })]
}))
expect(resolutions).toHaveBeenCalledTimes(1)
expect(recordOf('late')).toBe(recordOf('t0'))
expect(view('late')?.isDirty).toBe(true)
// A route change re-resolves each tab once.
resolutions.mockClear()
state().reconcileEditorBufferIdentities()
expect(resolutions).toHaveBeenCalledTimes(tabs.length + 1)
})
})
@@ -12,10 +12,8 @@ import {
import { createTestStore, makeWorktree } from './store-test-helpers'
import { buildWorktreePurgeState } from './worktrees/teardown/worktree-purge-state'
import { applyRemoveWorktreeSuccessState } from './worktrees/teardown/remove-worktree-store-cleanup'
import { buildWorktreeRenameState } from './worktrees/session/worktree-identity-rename-state'
const removed = 'repo::/workspace/deleted'
const kept = 'repo::/workspace/kept'
function mountDiff(worktreeId: string): { key: string; lease: CombinedDiffViewLease } {
const key = `${worktreeId}::all-diffs::uncommitted::pane`
@@ -59,44 +57,6 @@ afterEach(() => {
})
describe('removed workspace diff memory', () => {
it('preserves cached bodies and scroll through rename, then releases them on deletion', () => {
const key = seedDiff(removed)
const keptKey = seedDiff(kept)
const renamed = 'repo::/workspace/renamed'
const store = createTestStore()
const sections = readCombinedDiffViewMemory(key)?.view?.sections
store.setState(buildWorktreeRenameState(store.getState(), removed, renamed))
expect(readCombinedDiffViewMemory(key)).toMatchObject({
worktreeId: renamed,
scrollTop: 42,
scrollAnchor: { key: 'source', offset: 0 }
})
expect(readCombinedDiffViewMemory(key)?.view?.sections).toBe(sections)
buildWorktreePurgeState(store.getState(), [removed])
expect(readCombinedDiffViewMemory(key)).toBeDefined()
buildWorktreePurgeState(store.getState(), [renamed])
expect(readCombinedDiffViewMemory(key)).toBeUndefined()
expect(readCombinedDiffViewMemory(keptKey)).toBeDefined()
})
it.each(['listing', 'explicit'] as const)(
'releases cached file bodies on %s removal and preserves another workspace',
(path) => {
const removedKey = seedDiff(removed)
const keptKey = seedDiff(kept)
const overlappingKey = seedDiff(`${removed}::all-diffs::nested-directory`)
const store = createTestStore()
if (path === 'listing') {
store.setState(buildWorktreePurgeState(store.getState(), [removed]))
} else {
applyRemoveWorktreeSuccessState(store.setState, removed, new Set())
}
expect(readCombinedDiffViewMemory(removedKey)).toBeUndefined()
expect(readCombinedDiffViewMemory(keptKey)).toBeDefined()
expect(readCombinedDiffViewMemory(overlappingKey)).toBeDefined()
}
)
it('ignores the unmount write of a viewer whose workspace was already removed', () => {
const { key, lease } = mountDiff(removed)
const store = createTestStore()
@@ -109,23 +69,6 @@ describe('removed workspace diff memory', () => {
expect(readCombinedDiffViewMemory(key)).toEqual({ worktreeId: removed, scrollTop: 7 })
})
it('protects a mounted viewer from LRU eviction until release', () => {
const { key, lease } = mountDiff(kept)
const memory = readCombinedDiffViewMemory(key)
expect(memory).toBeDefined()
const oldestInactiveKey = seedDiff('repo::/workspace/other-0')
for (let index = 1; index < 20; index += 1) {
seedDiff(`repo::/workspace/other-${index}`)
}
expect(readCombinedDiffViewMemory(key)).toBe(memory)
expect(readCombinedDiffViewMemory(oldestInactiveKey)).toBeUndefined()
releaseCombinedDiffView(lease)
seedDiff('repo::/workspace/other-20')
expect(readCombinedDiffViewMemory(key)).toBeUndefined()
rememberCombinedDiffView(lease, { scrollTop: 9 })
expect(readCombinedDiffViewMemory(key)).toEqual({ worktreeId: kept, scrollTop: 9 })
})
it('keeps cached views when a different host still owns the same workspace id', () => {
const key = seedDiff(removed)
const store = createTestStore()