diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 23d273eccc6..02af414642f 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -2321,10 +2321,7 @@ }, { "file": "src/renderer/src/lib/workspace-session-host-contention.test.ts", - "assertions": [ - "keeps an SSH claimant out of the rotating runtime partition", - "does not strand the runtime co-claimant when the SSH row is written" - ] + "assertions": ["does not strand the runtime co-claimant when the SSH row is written"] }, { "file": "src/shared/workspace-session-partition-owner.test.ts", @@ -5781,7 +5778,7 @@ "providers": ["remote-runtime", "ssh"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "A headed macOS desktop host and separate paired Electron client exercise a real mirrored terminal link, owner-asserted host browser creation, exact host/client page identity, rendered DOM, explicit browser-tab activation, client external-browser suppression, exact host/client close convergence, and continued original-PTY liveness. Unit contracts cover OSC, WebLinksAddon, click fallback, explicit System Browser routing, failed creation, incapable runtimes, and unchanged direct SSH behavior.", + "coverageNotes": "A headed macOS desktop host and separate paired Electron client exercise a real mirrored terminal link, owner-asserted host browser creation, exact host/client page identity, rendered DOM, explicit browser-tab activation, client external-browser suppression, exact host/client close convergence, and continued original-PTY liveness. Unit contracts cover OSC, WebLinksAddon, click fallback, explicit System Browser routing, failed creation, and unchanged direct SSH behavior. The test-value audit removed separate supplied owner/capability wrapper examples; their coverage loss is accepted.", "motivatingLinks": [ "https://stablygroup.slack.com/archives/C0AK2T3JEF4/p1786567689897109", "https://github.com/stablyai/orca/issues/12418" @@ -5811,8 +5808,7 @@ { "file": "src/renderer/src/lib/workspace-browser-tab-open.test.ts", "assertions": [ - "an asserted runtime cannot fall back to a client browser", - "mismatched and incapable runtime owners fail closed" + "false or rejected runtime creation does not fall back to a client browser" ] }, { diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 7183ef526fd..2f4b06d466e 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -10903,36 +10903,18 @@ "src/renderer/src/lib/windows-terminal-capabilities-race.test.ts": 450, "src/renderer/src/lib/windows-terminal-capabilities.test.ts": 585, "src/renderer/src/lib/windows-terminal-capability-reprobe.test.ts": 198, - "src/renderer/src/lib/work-item-lookup-text.test.ts": 65, - "src/renderer/src/lib/work-item-repo-row.test.ts": 43, "src/renderer/src/lib/worker-auto-close.test.ts": 387, "src/renderer/src/lib/workspace-activation-path-gate.test.ts": 2107, "src/renderer/src/lib/workspace-activation-terminal-focus.test.ts": 532, - "src/renderer/src/lib/workspace-attachment-observed-origin.test.ts": 112, - "src/renderer/src/lib/workspace-browser-tab-close.test.ts": 290, "src/renderer/src/lib/workspace-browser-tab-open-placement.test.ts": 1996, "src/renderer/src/lib/workspace-browser-tab-open.test.ts": 605, - "src/renderer/src/lib/workspace-directory.test.ts": 303, "src/renderer/src/lib/workspace-doc-address-input.test.ts": 470, - "src/renderer/src/lib/workspace-editor-ssh-target.test.ts": 337, - "src/renderer/src/lib/workspace-emoji-picker-defaults.test.ts": 125, - "src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts": 85, - "src/renderer/src/lib/workspace-emoji-shortcodes.test.ts": 90, - "src/renderer/src/lib/workspace-file-browser-open-target.test.ts": 340, - "src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts": 67, "src/renderer/src/lib/workspace-file-drag.test.ts": 70, - "src/renderer/src/lib/workspace-ports/workspace-port-groups.test.ts": 46, - "src/renderer/src/lib/workspace-ports/workspace-port-host-scoped-scan.test.ts": 529, "src/renderer/src/lib/workspace-ports/workspace-port-scan-debounce.test.ts": 57, - "src/renderer/src/lib/workspace-ports/workspace-port-scan-publish.test.ts": 595, "src/renderer/src/lib/workspace-session-browser-history.test.ts": 419, - "src/renderer/src/lib/workspace-session-editor-drafts.test.ts": 324, - "src/renderer/src/lib/workspace-session-folder-host-twin.test.ts": 95, "src/renderer/src/lib/workspace-session-host-contention.test.ts": 265, "src/renderer/src/lib/workspace-session-host-emptied-fields.test.ts": 279, "src/renderer/src/lib/workspace-session-host-persistence.test.ts": 313, - "src/renderer/src/lib/workspace-session-host-split.test.ts": 261, - "src/renderer/src/lib/workspace-session-hydration-keys.test.ts": 269, "src/renderer/src/lib/workspace-session-liveness.test.ts": 298, "src/renderer/src/lib/workspace-session-patch.test.ts": 336, "src/renderer/src/lib/workspace-session-persistence-gate.test.ts": 314, diff --git a/src/main/runtime/folder-workspace-mixed-host-routing.test.ts b/src/main/runtime/folder-workspace-mixed-host-routing.test.ts index cb729423dcf..b9691240cec 100644 --- a/src/main/runtime/folder-workspace-mixed-host-routing.test.ts +++ b/src/main/runtime/folder-workspace-mixed-host-routing.test.ts @@ -81,7 +81,6 @@ describe('a folder whose project group mixes local and SSH repos (layout-owner b }) describe('every folder kind routes where the window saves it (layout-owner build)', () => { - // The window's side of each case runs in workspace-session-folder-host-twin.test.ts. it.each(FOLDER_HOST_CASES)('$name', async ({ folder, groups, expected }) => { const { runtime, applyLayoutCommand } = runtimeWithMixedFolder(folder, groups) await runtime.applyLayoutCommand(RATIOS) diff --git a/src/renderer/src/lib/work-item-lookup-text.test.ts b/src/renderer/src/lib/work-item-lookup-text.test.ts deleted file mode 100644 index fbdf4779eab..00000000000 --- a/src/renderer/src/lib/work-item-lookup-text.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { isWorkItemLookupText } from './work-item-lookup-text' - -describe('isWorkItemLookupText', () => { - it('detects GitHub PR and issue URLs', () => { - expect(isWorkItemLookupText('https://github.com/stablyai/orca/pull/4900')).toBe(true) - expect(isWorkItemLookupText('https://github.com/stablyai/orca/issues/123')).toBe(true) - expect(isWorkItemLookupText(' https://www.github.com/stablyai/orca/pull/1 ')).toBe(true) - }) - - it('detects hash-number shorthand', () => { - expect(isWorkItemLookupText('#4900')).toBe(true) - }) - - it('detects GitLab issue and MR URLs on any host', () => { - expect(isWorkItemLookupText('https://gitlab.com/group/project/-/merge_requests/7')).toBe(true) - expect(isWorkItemLookupText('https://gitlab.example.com/group/sub/project/-/issues/42')).toBe( - true - ) - }) - - it('detects modern /-/work_items/ GitLab URLs', () => { - expect(isWorkItemLookupText('https://gitlab.com/group/project/-/work_items/42')).toBe(true) - expect( - isWorkItemLookupText('https://gitlab.example.com:8443/group/sub/project/-/work_items/9') - ).toBe(true) - }) - - it('detects Linear issue URLs', () => { - expect(isWorkItemLookupText('https://linear.app/acme/issue/STA-123/fix-the-bug')).toBe(true) - }) - - it('rejects deliberate workspace names', () => { - expect(isWorkItemLookupText('')).toBe(false) - expect(isWorkItemLookupText(' ')).toBe(false) - expect(isWorkItemLookupText('my-feature')).toBe(false) - expect(isWorkItemLookupText('fix-2')).toBe(false) - expect(isWorkItemLookupText('terminal scrollbar polish')).toBe(false) - expect(isWorkItemLookupText('https://example.com/some/page')).toBe(false) - expect(isWorkItemLookupText('https://linear.app/acme/project/mobile')).toBe(false) - }) -}) diff --git a/src/renderer/src/lib/work-item-repo-row.test.ts b/src/renderer/src/lib/work-item-repo-row.test.ts deleted file mode 100644 index 601912ffe49..00000000000 --- a/src/renderer/src/lib/work-item-repo-row.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../shared/repo-types' -import { getWorkItemRepo } from './work-item-repo-row' - -const localRow: Repo = { - id: 'twin', - path: '/laptop/twin', - displayName: 'Twin', - badgeColor: '#000', - addedAt: 1 -} -const sshRow: Repo = { ...localRow, path: '/remote/twin', connectionId: 'builder' } -const runtimeRow: Repo = { ...localRow, executionHostId: 'runtime:env-1' } - -describe('getWorkItemRepo (GitHub and GitLab items)', () => { - it('picks the twin on the stamped host', () => { - expect(getWorkItemRepo([sshRow, localRow], { repoId: 'twin', repoHostId: 'ssh:builder' })).toBe( - sshRow - ) - }) - - it('refuses a twin whose stamped host has no row', () => { - expect( - getWorkItemRepo([sshRow, localRow], { repoId: 'twin', repoHostId: 'runtime:gone' }) - ).toBeUndefined() - }) - - it('keeps a lone row even when the fetch stamped the source host', () => { - expect(getWorkItemRepo([runtimeRow], { repoId: 'twin', repoHostId: 'local' })).toBe(runtimeRow) - }) - - it('keeps the by-id row for an unstamped item', () => { - expect(getWorkItemRepo([sshRow, localRow], { repoId: 'twin' })).toBe(localRow) - }) -}) diff --git a/src/renderer/src/lib/workspace-activation-path-gate.test.ts b/src/renderer/src/lib/workspace-activation-path-gate.test.ts index 08374ba8e20..79f8e94781a 100644 --- a/src/renderer/src/lib/workspace-activation-path-gate.test.ts +++ b/src/renderer/src/lib/workspace-activation-path-gate.test.ts @@ -3,7 +3,7 @@ import type { FolderWorkspace } from '../../../shared/folder-workspace-types' import type { ProjectGroup } from '../../../shared/project-group-types' import type { FolderWorkspacePathStatusReason } from '../../../shared/folder-workspace-path-status' import { folderWorkspaceKey } from '../../../shared/workspace-scope' -import { getDefaultSettings } from '../../../shared/constants' +import '../../../shared/constants' import { getFolderWorkspaceStatusRequestSnapshot } from '@/store/folder-workspaces/folder-path-status' import { makeGroup, @@ -99,15 +99,6 @@ describe('Cmd/Ctrl+1-9 folder-workspace path gate (#10716)', () => { expect(mocks.toastError).toHaveBeenCalledTimes(1) }) - it('blocks the number shortcut when the SSH connection owning the folder is ambiguous', async () => { - await seedFolderWorkspace({ exists: false, reason: 'ambiguous-connection' }) - - activateAndRevealWorkspace(folderWorkspaceKey(folderWorkspace.id)) - - expect(store.getState().activeWorkspaceKey).toBeNull() - expect(mocks.toastError).toHaveBeenCalledTimes(1) - }) - it('still activates the number shortcut when the folder path is present', async () => { await seedFolderWorkspace({ exists: true }) @@ -117,58 +108,6 @@ describe('Cmd/Ctrl+1-9 folder-workspace path gate (#10716)', () => { expect(mocks.toastError).not.toHaveBeenCalled() }) - it.each(['local', 'runtime:builder'] as const)( - 'keeps the selected %s host on a folder sidebar reveal', - async (executionHostId) => { - await seedFolderWorkspace({ exists: true }) - store.setState({ - folderWorkspaces: [ - { - ...folderWorkspace, - executionHostId: executionHostId === 'local' ? 'runtime:builder' : 'local' - }, - { ...folderWorkspace, executionHostId } - ] - }) - - activateAndRevealWorkspace(folderWorkspaceKey(folderWorkspace.id), { - executionHostId, - providesInitialSurface: true - }) - - expect(store.getState().pendingRevealWorktree).toMatchObject({ - worktreeId: folderWorkspaceKey(folderWorkspace.id), - executionHostId - }) - } - ) - - it.each([ - ['local', null], - ['ssh:builder', 'builder'] - ] as const)( - 'reveals the %s catalog owner when an unrelated server is focused', - async (executionHostId, connectionId) => { - await seedFolderWorkspace({ exists: true }) - store.setState({ - projectGroups: [{ ...projectGroup, connectionId }], - settings: { - ...getDefaultSettings('/tmp'), - activeRuntimeEnvironmentId: 'focused-server' - } - }) - - activateAndRevealWorkspace(folderWorkspaceKey(folderWorkspace.id), { - providesInitialSurface: true - }) - - expect(store.getState().pendingRevealWorktree).toMatchObject({ - worktreeId: folderWorkspaceKey(folderWorkspace.id), - executionHostId - }) - } - ) - it.each(['local', 'runtime:builder'] as const)( 'checks the selected %s folder when the same id is active on another host', async (executionHostId) => { diff --git a/src/renderer/src/lib/workspace-activation-terminal-focus.test.ts b/src/renderer/src/lib/workspace-activation-terminal-focus.test.ts index 2c68f13b475..8f27a1ed677 100644 --- a/src/renderer/src/lib/workspace-activation-terminal-focus.test.ts +++ b/src/renderer/src/lib/workspace-activation-terminal-focus.test.ts @@ -229,9 +229,4 @@ describe('workspace activation focus', () => { flushFrame() expect(mocks.focus).not.toHaveBeenCalled() }) - - it('declines a failed activation', () => { - expect(queueWorkspaceActivationTerminalFocus('wt-1', false)).toBe(false) - expect(mocks.subscribe).not.toHaveBeenCalled() - }) }) diff --git a/src/renderer/src/lib/workspace-attachment-observed-origin.test.ts b/src/renderer/src/lib/workspace-attachment-observed-origin.test.ts deleted file mode 100644 index b637835b5bb..00000000000 --- a/src/renderer/src/lib/workspace-attachment-observed-origin.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { AppState } from '@/store/types' -import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures' -import { singlePaneLayoutSnapshot } from '../store/slices/terminal-helpers' -import { makePaneKey } from '../../../shared/stable-pane-id' -import { - getWorkspaceAttachmentObservedOrigin, - getWorkspaceAttachmentTerminalOrigin -} from './workspace-attachment-observed-origin' - -const workspaceId = 'repo::/feature' -const leaf = '11111111-1111-4111-8111-111111111111' -const otherLeaf = '22222222-2222-4222-8222-222222222222' -const context = { tabId: 'tab', paneKey: makePaneKey('tab', leaf), ptyId: 'pty' } -function state(): Pick< - AppState, - 'tabsByWorktree' | 'terminalLayoutsByTabId' | 'agentStatusByPaneKey' -> { - return { - tabsByWorktree: { - [workspaceId]: [makeTerminalTab({ id: 'tab', worktreeId: workspaceId, ptyId: 'pty' })] - }, - terminalLayoutsByTabId: { tab: singlePaneLayoutSnapshot(leaf, 'pty') }, - agentStatusByPaneKey: { - [context.paneKey]: { - state: 'working', - prompt: '', - updatedAt: 1, - stateStartedAt: 1, - paneKey: context.paneKey, - stateHistory: [], - worktreeId: workspaceId, - terminalHandle: 'pty', - agentType: 'codex', - providerSession: { id: 'provider-session', key: 'session_id' } - } - } - } -} -describe('reference evidence terminal identity', () => { - it('records the currently bound provider session', () => { - expect(getWorkspaceAttachmentTerminalOrigin(state(), workspaceId, context, 'local')).toEqual( - expect.objectContaining({ - tabId: 'tab', - paneKey: context.paneKey, - hostId: 'local', - sessionId: 'provider-session', - agent: 'codex', - kind: 'observed' - }) - ) - }) - it('rejects removed leaves even if a stale PTY mapping is retained', () => { - const current = state() - current.terminalLayoutsByTabId.tab = { - ...singlePaneLayoutSnapshot(otherLeaf, 'another-pty'), - ptyIdsByLeafId: { [leaf]: 'pty', [otherLeaf]: 'another-pty' } - } - expect( - getWorkspaceAttachmentTerminalOrigin(current, workspaceId, context, 'local') - ).toBeUndefined() - }) - it('rejects a PTY rebound in the same pane', () => { - const current = state() - current.terminalLayoutsByTabId.tab = singlePaneLayoutSnapshot(leaf, 'replacement-pty') - expect( - getWorkspaceAttachmentTerminalOrigin(current, workspaceId, context, 'local') - ).toBeUndefined() - }) - it('retains terminal evidence without attributing a session from a different PTY', () => { - const current = state() - const entry = current.agentStatusByPaneKey[context.paneKey] - if (!entry) { - throw new Error('Expected source session') - } - entry.terminalHandle = 'previous-pty' - const origin = getWorkspaceAttachmentTerminalOrigin(current, workspaceId, context, 'local') - expect(origin).toMatchObject({ tabId: 'tab', kind: 'observed' }) - expect(origin?.sessionId).toBeUndefined() - expect(origin?.agent).toBeUndefined() - }) - it('does not record an observation before a PTY is running', () => { - const current = state() - current.terminalLayoutsByTabId.tab = singlePaneLayoutSnapshot(leaf) - current.tabsByWorktree[workspaceId][0].ptyId = null - current.agentStatusByPaneKey = {} - expect( - getWorkspaceAttachmentTerminalOrigin( - current, - workspaceId, - { tabId: 'tab', paneKey: context.paneKey }, - 'local' - ) - ).toBeUndefined() - }) -}) - -describe('reference evidence native chat identity', () => { - it('records the chat tab and its session without a terminal pane', () => { - const chat = { - ...state(), - unifiedTabsByWorktree: { - [workspaceId]: [ - { - id: 'chat-tab', - entityId: 'chat-session', - groupId: 'group', - worktreeId: workspaceId, - contentType: 'agent-session' as const, - agentSessionAgent: 'claude' as const, - label: 'Plan refactor', - customLabel: null, - color: null, - sortOrder: 0, - createdAt: 0 - } - ] - } - } - expect( - getWorkspaceAttachmentObservedOrigin( - chat, - workspaceId, - { kind: 'agent-session', tabId: 'chat-tab' }, - 'local' - ) - ).toEqual({ - kind: 'observed', - tabId: 'chat-tab', - hostId: 'local', - label: 'Plan refactor', - agent: 'claude', - sessionId: 'chat-session' - }) - - const renamed = { - ...chat, - unifiedTabsByWorktree: { - [workspaceId]: chat.unifiedTabsByWorktree[workspaceId].map((tab) => ({ - ...tab, - customLabel: 'Pinned chat' - })) - } - } - expect( - getWorkspaceAttachmentObservedOrigin( - renamed, - workspaceId, - { kind: 'agent-session', tabId: 'chat-tab' }, - 'local' - ) - ).toMatchObject({ label: 'Pinned chat' }) - }) -}) diff --git a/src/renderer/src/lib/workspace-browser-tab-close.test.ts b/src/renderer/src/lib/workspace-browser-tab-close.test.ts deleted file mode 100644 index ac9fe97ad7c..00000000000 --- a/src/renderer/src/lib/workspace-browser-tab-close.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => { - const state: Record = {} - return { - state, - closeOnHosts: vi.fn(), - destroyWorkspaceWebviews: vi.fn(), - closeBrowserTab: vi.fn(), - closeUnifiedTab: vi.fn() - } -}) - -vi.mock('@/store', () => ({ useAppStore: { getState: () => mocks.state } })) -vi.mock('@/store/slices/browser-webview-cleanup', () => ({ - destroyWorkspaceWebviews: mocks.destroyWorkspaceWebviews -})) -vi.mock('@/runtime/browser-workspace-tab-close', () => ({ - closeBrowserWorkspaceTabOnHosts: mocks.closeOnHosts -})) - -import { closeWorkspaceBrowserTab } from './workspace-browser-tab-close' - -const WORKTREE_ID = 'repo::/w' -type OwnerHost = 'local' | 'runtime:env-1' - -function stateOwnedBy(hostIds: OwnerHost[], pages: { id: string }[] = []) { - return { - repos: hostIds.map((hostId) => ({ id: 'repo', executionHostId: hostId })), - worktreesByRepo: { - repo: hostIds.map((hostId) => ({ - id: WORKTREE_ID, - repoId: 'repo', - hostId, - ...(hostId === 'local' ? {} : { runtimeOwnerEnvironmentId: 'env-1' }) - })) - }, - browserPagesByWorkspace: { 'browser-ws': pages }, - closeBrowserTab: mocks.closeBrowserTab, - closeUnifiedTab: mocks.closeUnifiedTab - } -} - -describe('closeWorkspaceBrowserTab owner', () => { - beforeEach(() => { - vi.clearAllMocks() - mocks.closeOnHosts.mockReturnValue({ - hostEnvironmentIds: ['env-1'], - closesLocally: false, - removesVisibleTab: true - }) - }) - - it("closes a pageless mirror on its runtime owner's host", () => { - mocks.state = stateOwnedBy(['runtime:env-1']) - - closeWorkspaceBrowserTab(WORKTREE_ID, 'browser-ws', 'visible-tab') - - expect(mocks.closeOnHosts).toHaveBeenCalledWith( - expect.objectContaining({ focusedEnvironmentId: 'env-1', visibleTabId: 'visible-tab' }) - ) - expect(mocks.closeUnifiedTab).toHaveBeenCalledWith('visible-tab', undefined) - }) - - it('refuses a pageless mirror of a local twin: no host close, no local teardown', () => { - mocks.state = stateOwnedBy(['local', 'runtime:env-1']) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - - const plan = closeWorkspaceBrowserTab(WORKTREE_ID, 'browser-ws', 'visible-tab') - - expect(plan).toEqual({ hostEnvironmentIds: [], closesLocally: false, removesVisibleTab: false }) - expect(mocks.closeOnHosts).not.toHaveBeenCalled() - expect(mocks.closeBrowserTab).not.toHaveBeenCalled() - expect(mocks.closeUnifiedTab).not.toHaveBeenCalled() - expect(mocks.destroyWorkspaceWebviews).not.toHaveBeenCalled() - expect(warn.mock.calls[0]).toContain(WORKTREE_ID) - warn.mockRestore() - }) -}) diff --git a/src/renderer/src/lib/workspace-browser-tab-open.test.ts b/src/renderer/src/lib/workspace-browser-tab-open.test.ts index fedd65a19e0..87f5730d445 100644 --- a/src/renderer/src/lib/workspace-browser-tab-open.test.ts +++ b/src/renderer/src/lib/workspace-browser-tab-open.test.ts @@ -1,18 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - LOCAL_EXECUTION_HOST_ID, - toRuntimeExecutionHostId, - toSshExecutionHostId -} from '../../../shared/execution-host' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' +import { toRuntimeExecutionHostId } from '../../../shared/execution-host' +import '../../../shared/constants' import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' -import { BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES } from '../../../shared/browser-client-hosting-eligibility' -import * as clientCreationActionPolicy from './client-creation-action-policy' -import { - canOpenWorkspaceBrowserTabOnRuntime, - canOpenWorkspaceBrowserTabOnSsh, - openWorkspaceBrowserTab -} from './workspace-browser-tab-open' +import '../../../shared/browser-client-hosting-eligibility' +import './client-creation-action-policy' +import { openWorkspaceBrowserTab } from './workspace-browser-tab-open' function createBrowserTabMock() { return vi.fn((_worktreeId: string, _url: string, _options: Record) => ({ @@ -75,345 +67,6 @@ beforeEach(() => { }) describe('openWorkspaceBrowserTab', () => { - it('opens client-owned searches with a safe title and the desktop default profile', async () => { - const createBrowserTab = createBrowserTabMock() - const sshHost = toSshExecutionHostId('ssh-target') - mocks.state = { - ...ownerState(sshHost), - createBrowserTab, - settings: { browserDefaultProfileIdByHostId: { local: 'local-profile' } } - } - - expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-target')).toBe( - true - ) - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - targetGroupId: 'group-1', - url: 'https://www.google.com/search?q=private%20query', - intent: { kind: 'search', engine: 'google' }, - expectedSshConnectionId: 'ssh-target' - }) - - expect(createBrowserTab).toHaveBeenCalledWith( - WORKSPACE_ID, - 'https://www.google.com/search?q=private%20query', - { - activate: true, - browserRuntimeEnvironmentId: null, - executionHostId: 'ssh:ssh-target', - focusAddressBar: false, - sessionProfileId: 'local-profile', - targetGroupId: 'group-1', - title: 'Search Google' - } - ) - expect(mocks.createRemote).not.toHaveBeenCalled() - }) - - it('creates a client tab without activating it when requested', async () => { - const createBrowserTab = createBrowserTabMock() - const sshHost = toSshExecutionHostId('ssh-target') - mocks.state = { - ...ownerState(sshHost), - createBrowserTab - } - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://github.com/acme/orca/pull/456', - afterTabId: 'source-unified-tab', - intent: { kind: 'url' }, - focusOnCreate: false, - selectWorktree: false - }) - - expect(createBrowserTab).toHaveBeenCalledWith( - WORKSPACE_ID, - 'https://github.com/acme/orca/pull/456', - expect.objectContaining({ activate: false, afterTabId: 'source-unified-tab' }) - ) - }) - - it('fails closed when the asserted SSH browser route is opted out or belongs to another host', async () => { - const sshHost = toSshExecutionHostId('ssh-target') - mocks.state = { - ...ownerState(sshHost), - settings: { browserSshWorkspaceRoutingDisabledTargetIds: ['ssh-target'] }, - createBrowserTab: createBrowserTabMock() - } - - expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-target')).toBe( - false - ) - expect(canOpenWorkspaceBrowserTabOnSsh(mocks.state as never, WORKSPACE_ID, 'ssh-other')).toBe( - false - ) - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://0.0.0.0:8000/', - intent: { kind: 'url' }, - expectedSshConnectionId: 'ssh-target' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it('surfaces the opening workspace and titles runtime-owned URL tabs by target', async () => { - const createBrowserTab = createBrowserTabMock() - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab - } - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.state as never, WORKSPACE_ID, 'hub-a')).toBe( - true - ) - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/docs?token=secret', - targetGroupId: 'group-1', - intent: { kind: 'url' } - }) - - expect(mocks.createRemote).toHaveBeenCalledWith({ - worktreeId: WORKSPACE_ID, - environmentId: 'hub-a', - url: 'https://example.com/docs?token=secret', - targetGroupId: 'group-1', - clientTargetGroupId: 'group-1', - selectWorktree: true, - stagedTitle: 'example.com/docs', - stagedFocusAddressBar: false, - failureLogMode: 'operation-only' - }) - expect(createBrowserTab).not.toHaveBeenCalled() - }) - - it('stages a runtime tab without selecting the worktree or focusing the browser', async () => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://gitlab.com/acme/orca/-/merge_requests/77', - afterTabId: 'source-unified-tab', - intent: { kind: 'url' }, - focusOnCreate: false, - selectWorktree: false - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ - clientAfterTabId: 'source-unified-tab', - focusOnCreate: false, - selectWorktree: false, - url: 'https://gitlab.com/acme/orca/-/merge_requests/77' - }) - ) - }) - - it('waits for host registration before reconciling an asserted runtime link', async () => { - const createBrowserTab = createBrowserTabMock() - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab - } - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/pinned', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ - environmentId: 'hub-a', - waitForRegistration: true, - worktreeId: WORKSPACE_ID - }) - ) - expect(createBrowserTab).not.toHaveBeenCalled() - }) - - it('keeps an asserted runtime link on its owner when policy selects the local client', async () => { - const createBrowserTab = createBrowserTabMock() - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab - } - const policySpy = vi - .spyOn(clientCreationActionPolicy, 'getClientCreationActionPolicy') - .mockReturnValue({ - 'managed-browser': { state: 'enabled', provider: 'local-client' }, - 'mobile-emulator': { state: 'enabled', provider: 'local-client' } - }) - - try { - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/pinned', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ - environmentId: 'hub-a', - waitForRegistration: true, - worktreeId: WORKSPACE_ID - }) - ) - expect(createBrowserTab).not.toHaveBeenCalled() - } finally { - policySpy.mockRestore() - } - }) - - it('forwards an explicit server placement for owner-pinned remote panes', async () => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/pinned', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a', - placementPreference: 'server' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ placementPreference: 'server' }) - ) - }) - - it('fails closed when the workspace route swaps away from the pane runtime before opening', async () => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - const paneRuntimeEnvironmentId = 'hub-a' - - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-b')), - ...browserCapableRuntime('hub-b'), - createBrowserTab: createBrowserTabMock() - } - - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: paneRuntimeEnvironmentId - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it('fails closed for a blank asserted runtime owner', async () => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: ' ' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it('routes an asserted paired-HUB SSH owner through that runtime', async () => { - const sshHost = toSshExecutionHostId('ssh-target') - mocks.state = { - ...ownerState(sshHost, 'hub-a'), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ environmentId: 'hub-a', worktreeId: WORKSPACE_ID }) - ) - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it('does not fall back to a client browser for an incapable asserted runtime', async () => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - createBrowserTab: createBrowserTabMock() - } - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.state as never, WORKSPACE_ID, 'hub-a')).toBe( - false - ) - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://example.com/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it.each(['runtime', 'ssh'] as const)( - 'rejects another owner for an opted-in %s preview client provider', - async (kind) => { - const hostId = - kind === 'runtime' ? toRuntimeExecutionHostId('source') : toSshExecutionHostId('source') - const createBrowserTab = createBrowserTabMock() - mocks.state = { - ...ownerState(hostId), - settings: { browserSshWorkspaceRoutingEnabled: false }, - createBrowserTab - } - const request = { - workspaceId: WORKSPACE_ID, - url: 'https://example.com/', - intent: { kind: 'url' as const }, - allowClientBrowserFallback: true - } - const expectedOwner = (id: string) => - kind === 'runtime' ? { expectedRuntimeEnvironmentId: id } : { expectedSshConnectionId: id } - - await expect( - openWorkspaceBrowserTab({ ...request, ...expectedOwner('other') }) - ).rejects.toThrow('Unable to open URL.') - expect(createBrowserTab).not.toHaveBeenCalled() - expect(mocks.createRemote).not.toHaveBeenCalled() - } - ) - it.each([false, true])( 'does not fall back to a client browser when asserted runtime creation fails (client provider opt-in %s)', async (allowClientBrowserFallback) => { @@ -440,178 +93,6 @@ describe('openWorkspaceBrowserTab', () => { } ) - it('does not route an asserted floating-terminal source to either runtime or local browser', async () => { - const createBrowserTab = createBrowserTabMock() - mocks.state = { - activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, - activeWorkspaceExecutionHostId: toRuntimeExecutionHostId('hub-a'), - ...browserCapableRuntime('hub-a'), - createBrowserTab - } - - expect( - canOpenWorkspaceBrowserTabOnRuntime( - mocks.state as never, - FLOATING_TERMINAL_WORKTREE_ID, - 'hub-a' - ) - ).toBe(false) - - await expect( - openWorkspaceBrowserTab({ - workspaceId: FLOATING_TERMINAL_WORKTREE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(createBrowserTab).not.toHaveBeenCalled() - }) - - it('keeps runtime browser actions unavailable in a paired-web floating workspace', async () => { - mocks.pairedWeb = true - mocks.state = { - activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, - activeWorkspaceExecutionHostId: toRuntimeExecutionHostId('hub-a'), - ...browserCapableRuntime('hub-a'), - createBrowserTab: createBrowserTabMock() - } - - expect( - canOpenWorkspaceBrowserTabOnRuntime( - mocks.state as never, - FLOATING_TERMINAL_WORKTREE_ID, - 'hub-a' - ) - ).toBe(false) - await expect( - openWorkspaceBrowserTab({ - workspaceId: FLOATING_TERMINAL_WORKTREE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - // Two dev servers on one host must not share a tab title. - it('keeps the port in local-dev tab titles', async () => { - const createBrowserTab = createBrowserTabMock() - mocks.state = { - ...ownerState(LOCAL_EXECUTION_HOST_ID), - createBrowserTab - } - - for (const url of ['http://localhost:3000/', 'http://localhost:5173/app']) { - await openWorkspaceBrowserTab({ workspaceId: WORKSPACE_ID, url, intent: { kind: 'url' } }) - } - - expect(createBrowserTab.mock.calls.map((call) => call[2].title)).toEqual([ - 'localhost:3000', - 'localhost:5173/app' - ]) - }) - - it('uses the desktop provider when the runtime cannot stream browsers', async () => { - const createBrowserTab = createBrowserTabMock() - const sshHost = toSshExecutionHostId('ssh-target') - mocks.state = { - ...ownerState(sshHost, 'hub-a'), - createBrowserTab, - settings: { browserDefaultProfileIdByHostId: { local: 'local-profile' } } - } - mocks.createRemote.mockResolvedValue(false) - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'https://www.google.com/search?q=hooks', - intent: { kind: 'search', engine: 'google' } - }) - - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(createBrowserTab).toHaveBeenCalledWith( - WORKSPACE_ID, - 'https://www.google.com/search?q=hooks', - expect.objectContaining({ sessionProfileId: 'local-profile', title: 'Search Google' }) - ) - }) - - it.each([ - ['desktop hosting is on', true], - ['the setting asks for server rendering', false] - ])( - 'opens an asserted runtime link through desktop hosting without screencast support when %s', - async (_label, enabled) => { - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - settings: { browserClientHostedRemoteEnabled: enabled }, - runtimeStatusByEnvironmentId: new Map([ - ['hub-a', { status: { capabilities: [...BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES] } }] - ]), - createBrowserTab: vi.fn() - } - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'hub-a')).toBe( - true - ) - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ environmentId: 'hub-a', waitForRegistration: true }) - ) - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - } - ) - - it.each([ - ['an older peer lacks capabilities', undefined, true, false], - [ - 'a peer lacks a hosting capability', - BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES.slice(1), - true, - false - ], - [ - 'the viewing client is paired web', - [...BROWSER_CLIENT_HOSTING_RUNTIME_CAPABILITIES], - true, - true - ] - ])( - 'refuses an asserted runtime link when %s', - async (_label, capabilities, enabled, pairedWeb) => { - mocks.pairedWeb = pairedWeb - mocks.state = { - ...ownerState(toRuntimeExecutionHostId('hub-a')), - settings: { browserClientHostedRemoteEnabled: enabled }, - runtimeStatusByEnvironmentId: new Map([['hub-a', { status: { capabilities } }]]), - createBrowserTab: vi.fn() - } - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'hub-a')).toBe( - false - ) - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'hub-a' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.createRemote).not.toHaveBeenCalled() - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - } - ) - it('does not fall back locally when the runtime create outcome is unknown', async () => { const createBrowserTab = createBrowserTabMock() mocks.state = { @@ -675,114 +156,3 @@ describe('openWorkspaceBrowserTab', () => { expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() }) }) - -describe('links from a managed server reached over SSH', () => { - function sshDeployedRuntimeState( - orcadDeployment: Record, - settings: Record = {} - ): Record { - return { - ...ownerState(toRuntimeExecutionHostId('orcad-env')), - runtimeEnvironments: [{ id: 'orcad-env', orcadDeployment }], - // Headless orcad advertises no screencast backend. - runtimeStatusByEnvironmentId: new Map([ - ['orcad-env', { status: { capabilities: [] }, checkedAt: 1 }] - ]), - settings, - createBrowserTab: createBrowserTabMock() - } - } - const sshDeployment = { - kind: 'ssh', - sshTargetId: 'ssh-target', - sshTargetGeneration: 2, - localPort: 41000, - remotePort: 41001 - } - - it('opens the link in an SSH-routed desktop tab the runtime workspace still owns', async () => { - mocks.state = sshDeployedRuntimeState(sshDeployment) - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( - true - ) - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'orcad-env' - }) - - expect(mocks.state.createBrowserTab).toHaveBeenCalledWith( - WORKSPACE_ID, - 'http://localhost:3000/', - expect.objectContaining({ - browserRuntimeEnvironmentId: null, - executionHostId: toRuntimeExecutionHostId('orcad-env') - }) - ) - expect(mocks.createRemote).not.toHaveBeenCalled() - }) - - it('keeps a screencast-capable deployment on its runtime browser', async () => { - mocks.state = { - ...sshDeployedRuntimeState(sshDeployment), - ...browserCapableRuntime('orcad-env') - } - - await openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'orcad-env' - }) - - expect(mocks.createRemote).toHaveBeenCalledWith( - expect.objectContaining({ environmentId: 'orcad-env', waitForRegistration: true }) - ) - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - }) - - it.each([ - ['SSH browser routing is off', sshDeployment, { browserSshWorkspaceRoutingEnabled: false }], - [ - 'the user browses this host from this device', - sshDeployment, - { browserSshWorkspaceRoutingDisabledTargetIds: ['ssh-target'] } - ], - ['the server is a WSL deployment', { kind: 'wsl', distro: 'Ubuntu' }, {}] - ])('offers only the system browser when %s', async (_label, deployment, settings) => { - mocks.state = sshDeployedRuntimeState(deployment, settings) - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( - false - ) - await expect( - openWorkspaceBrowserTab({ - workspaceId: WORKSPACE_ID, - url: 'http://localhost:3000/', - intent: { kind: 'url' }, - expectedRuntimeEnvironmentId: 'orcad-env' - }) - ).rejects.toThrow('Unable to open URL.') - expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() - expect(mocks.createRemote).not.toHaveBeenCalled() - }) - - it('does not lend the SSH route to a link asserting another runtime', () => { - mocks.state = sshDeployedRuntimeState(sshDeployment) - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'other-env')).toBe( - false - ) - }) - - it('offers no in-app browser from the paired web client', () => { - mocks.pairedWeb = true - mocks.state = sshDeployedRuntimeState(sshDeployment) - - expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( - false - ) - }) -}) diff --git a/src/renderer/src/lib/workspace-directory.test.ts b/src/renderer/src/lib/workspace-directory.test.ts deleted file mode 100644 index be70449b2db..00000000000 --- a/src/renderer/src/lib/workspace-directory.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { folderWorkspaceKey } from '../../../shared/workspace-scope' -import { - detectedListingFixture, - worktreeFixture -} from '../components/native-chat/native-chat-workspace-test-fixtures' -import { makeFolderWorkspace } from '@/store/slices/worktrees-slice-test-fixtures' -import { resolveWorkspaceDirectory, type WorkspaceDirectoryState } from './workspace-directory' - -function catalog(overrides: Partial = {}): WorkspaceDirectoryState { - return { - detectedWorktreesByRepo: {}, - floatingWorkspacePath: null, - folderWorkspaces: [], - worktreesByRepo: { repo: [worktreeFixture('wt-1', '/visible', { hostId: 'local' })] }, - ...overrides - } -} - -describe('resolveWorkspaceDirectory', () => { - it('prefers a visible catalog row and resolves a detected-only row from the same snapshot', () => { - const state = catalog({ - detectedWorktreesByRepo: { - repo: detectedListingFixture([ - worktreeFixture('wt-1', '/detected-shadow', { hostId: 'local' }), - worktreeFixture('wt-detected', '/detected', { hostId: 'ssh:box-1' }) - ]) - } - }) - - expect(resolveWorkspaceDirectory(state, 'wt-1')).toBe('/visible') - expect(resolveWorkspaceDirectory(state, 'wt-detected', 'ssh:box-1')).toBe('/detected') - expect(resolveWorkspaceDirectory(state, 'wt-detected', 'local')).toBeNull() - }) - - it('applies the same host rules when the caller passes no detected rows', () => { - const folderKey = folderWorkspaceKey('folder-1') - const state: WorkspaceDirectoryState = { - floatingWorkspacePath: null, - folderWorkspaces: [ - makeFolderWorkspace({ folderPath: '/workspace/remote', connectionId: 'box-1' }) - ], - worktreesByRepo: { repo: [worktreeFixture('wt-1', '/visible')] } - } - - // Why: an unhosted row is a local row, and a folder on another host is not this one. - expect(resolveWorkspaceDirectory(state, 'wt-1', 'local')).toBe('/visible') - expect(resolveWorkspaceDirectory(state, folderKey, 'local')).toBeNull() - expect(resolveWorkspaceDirectory(state, folderKey, 'ssh:box-1')).toBe('/workspace/remote') - }) -}) diff --git a/src/renderer/src/lib/workspace-doc-address-input.test.ts b/src/renderer/src/lib/workspace-doc-address-input.test.ts index c36f037a294..ac252ba0664 100644 --- a/src/renderer/src/lib/workspace-doc-address-input.test.ts +++ b/src/renderer/src/lib/workspace-doc-address-input.test.ts @@ -59,167 +59,6 @@ beforeEach(() => { }) describe('resolveWorkspaceDocAddressTarget', () => { - it('resolves a file URL inside a sibling folder workspace on the same host', () => { - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'file:///srv/site/report.html') - ).toMatchObject({ - status: 'workspace-doc', - docLocation: { - worktreeId: 'folder:folder-1', - filePath: '/srv/site/report.html' - } - }) - }) - - it('does not cross SSH targets behind the same paired server', () => { - places.set(CURRENT, 'ssh:first') - places.set(OTHER, 'ssh:second') - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'file:///home/alice/wt2/report.html') - expect(plan.calls).toEqual([{ worktreeId: CURRENT, filePath: '/home/alice/wt2/report.html' }]) - }) - it('routes a pasted remote file URL through its workspace preview', () => { - expect( - resolveWorkspaceDocAddressTarget( - makeState(), - CURRENT, - ' file:///home/alice/wt1/docs/report%20one.html ' - ) - ).toMatchObject({ - status: 'workspace-doc', - docLocation: { - worktreeId: CURRENT, - filePath: '/home/alice/wt1/docs/report one.html' - } - }) - }) - - it('does not choose a matching root on another host', () => { - hosts.set(OTHER, 'server-2') - plan.result = { status: 'unsupported', message: 'outside workspace', reason: 'no-channel' } - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'file:///home/alice/wt2/report.html') - ).toEqual({ status: 'unsupported', message: 'outside workspace' }) - expect(plan.calls).toEqual([{ worktreeId: CURRENT, filePath: '/home/alice/wt2/report.html' }]) - }) - - it('skips ambiguous sibling hosts and uses the current host boundary', () => { - hosts.set(OTHER, null) - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'file:///home/alice/wt2/report.html') - expect(plan.calls).toEqual([{ worktreeId: CURRENT, filePath: '/home/alice/wt2/report.html' }]) - }) - - it('refuses a file URL when its current workspace host is ambiguous', () => { - hosts.set(CURRENT, null) - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'file:///home/alice/wt1/report.html') - ).toMatchObject({ status: 'unsupported' }) - expect(plan.calls).toEqual([]) - }) - - it('resolves an absolute path inside the current worktree to its document', () => { - const target = resolveWorkspaceDocAddressTarget( - makeState(), - CURRENT, - '/home/alice/wt1/docs/report.html' - ) - expect(target).toEqual({ - status: 'workspace-doc', - docLocation: { - kind: 'workspace-doc', - worktreeId: CURRENT, - filePath: '/home/alice/wt1/docs/report.html' - } - }) - }) - - it('falls back to another known worktree only when the current one does not contain the path', () => { - const target = resolveWorkspaceDocAddressTarget( - makeState(), - CURRENT, - '/home/alice/wt2/index.html' - ) - expect(target).toMatchObject({ - status: 'workspace-doc', - docLocation: { worktreeId: OTHER, filePath: '/home/alice/wt2/index.html' } - }) - }) - - // Prefer nested roots over outer containment so grants select the correct workspace/host owner. - it('attributes a file in a nested workspace to the nested root, not the outer one', () => { - const state = makeState() - const stateWithNested = { - ...state, - allWorktrees: () => [...state.allWorktrees(), { id: 'repo2::/srv', path: '/srv' }] - } as typeof state - - const target = resolveWorkspaceDocAddressTarget( - stateWithNested, - CURRENT, - '/srv/site/index.html' - ) - - expect(target).toMatchObject({ - status: 'workspace-doc', - docLocation: { worktreeId: 'folder:folder-1' } - }) - }) - - it('resolves a folder workspace root under its folder key', () => { - const target = resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/srv/site/index.html') - expect(target).toMatchObject({ - status: 'workspace-doc', - docLocation: { worktreeId: 'folder:folder-1', filePath: '/srv/site/index.html' } - }) - }) - - it('keeps a path outside every known workspace on the current host', () => { - expect(resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/etc/motd.html')).toEqual({ - status: 'workspace-doc', - docLocation: { kind: 'workspace-doc', worktreeId: CURRENT, filePath: '/etc/motd.html' } - }) - }) - - it('resolves a ./ relative path against the current worktree', () => { - const target = resolveWorkspaceDocAddressTarget(makeState(), CURRENT, './docs/report.html') - expect(target).toMatchObject({ - status: 'workspace-doc', - docLocation: { worktreeId: CURRENT, filePath: '/home/alice/wt1/docs/report.html' } - }) - }) - - // Containment is lexical, so dot segments are refused rather than resolved. - it('refuses dot segments in both the relative and the absolute form', () => { - expect(resolveWorkspaceDocAddressTarget(makeState(), CURRENT, './a/../../etc/x.html')).toEqual({ - status: 'not-a-workspace-doc' - }) - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/home/alice/wt1/../wt9/x.html') - ).toEqual({ status: 'not-a-workspace-doc' }) - }) - - // "docs/report.html" is indistinguishable from a hostname with a path. - it('leaves bare relative text and non-previewable extensions to the URL pipeline', () => { - expect(resolveWorkspaceDocAddressTarget(makeState(), CURRENT, 'docs/report.html')).toEqual({ - status: 'not-a-workspace-doc' - }) - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/home/alice/wt1/notes.md') - ).toEqual({ status: 'not-a-workspace-doc' }) - expect(plan.calls).toEqual([]) - }) - - it('keeps a local file on the file:// pipeline and surfaces an unsupported plan as its message', () => { - plan.result = { status: 'browser-tab', url: 'file:///x', title: 'x' } - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/home/alice/wt1/x.html') - ).toEqual({ status: 'not-a-workspace-doc' }) - - plan.result = { status: 'unsupported', message: 'no channel', reason: 'no-channel' } - expect( - resolveWorkspaceDocAddressTarget(makeState(), CURRENT, '/home/alice/wt1/x.html') - ).toEqual({ status: 'unsupported', message: 'no channel' }) - }) - it('resolves a current-workspace address without enumerating unrelated workspace roots', () => { const state = makeState() state.allWorktrees = vi.fn(() => { @@ -231,26 +70,6 @@ describe('resolveWorkspaceDocAddressTarget', () => { expect(state.allWorktrees).not.toHaveBeenCalled() }) - // The current worktree outranks every other root, including a more specific one nested inside it. - it('keeps the current worktree ahead of a workspace nested inside it', () => { - const state = makeState() - const nestedInsideCurrent = { - ...state, - allWorktrees: () => [ - ...state.allWorktrees(), - { id: 'repo3::/home/alice/wt1/vendor', path: '/home/alice/wt1/vendor' } - ] - } as typeof state - - expect( - resolveWorkspaceDocAddressTarget( - nestedInsideCurrent, - CURRENT, - '/home/alice/wt1/vendor/index.html' - ) - ).toMatchObject({ status: 'workspace-doc', docLocation: { worktreeId: CURRENT } }) - }) - it('short-circuits for a folder workspace that is the current workspace', () => { const state = makeState() const folderCurrent = { diff --git a/src/renderer/src/lib/workspace-editor-ssh-target.test.ts b/src/renderer/src/lib/workspace-editor-ssh-target.test.ts deleted file mode 100644 index b42b1cd60ca..00000000000 --- a/src/renderer/src/lib/workspace-editor-ssh-target.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getWorkspaceEditorSshHost } from './workspace-editor-ssh-target' - -const link = { sshTargetId: 'ssh-1', sshTargetGeneration: 1, localPort: 41000, remotePort: 6768 } -const WORKTREE_ID = 'repo-1::/home/ada/project' - -function stateWith( - environment: Record, - worktreeHostId: 'runtime:env-1' | 'ssh:box' = 'runtime:env-1' -): Parameters[0] { - return { - runtimeEnvironments: [{ id: 'env-1', ...environment }], - runtimeEnvironmentCatalogHydrated: true, - repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'runtime:env-1' }], - worktreesByRepo: { - 'repo-1': [ - { - id: WORKTREE_ID, - repoId: 'repo-1', - hostId: worktreeHostId, - runtimeOwnerEnvironmentId: 'env-1' - } - ] - } - } -} - -describe('getWorkspaceEditorSshHost', () => { - it('names the SSH host a managed server runs on for a workspace on that machine', () => { - expect( - getWorkspaceEditorSshHost(stateWith({ orcadDeployment: link }), { - workspaceId: WORKTREE_ID, - hostId: 'runtime:env-1' - }) - ).toEqual({ environmentId: 'env-1', sshTargetId: 'ssh-1' }) - }) - - it('names the SSH host of a paired server reached over SSH access', () => { - const sshAccess = { ...link, endpointId: 'tunnel', previousPreferredEndpointId: 'lan' } - expect( - getWorkspaceEditorSshHost(stateWith({ sshAccess }), { workspaceId: WORKTREE_ID }) - ).toEqual({ environmentId: 'env-1', sshTargetId: 'ssh-1' }) - }) - - it('refuses a workspace the server reaches through its own SSH host', () => { - expect( - getWorkspaceEditorSshHost(stateWith({ orcadDeployment: link }, 'ssh:box'), { - workspaceId: WORKTREE_ID, - hostId: 'ssh:box' - }) - ).toBeNull() - }) - - it('places a folder workspace by its own SSH connection, not only its server stamp', () => { - const folderState = ( - connectionId: string | null - ): Parameters[0] => ({ - runtimeEnvironments: [{ id: 'env-1', orcadDeployment: link }], - runtimeEnvironmentCatalogHydrated: true, - folderWorkspaces: [ - { id: 'fw-1', projectGroupId: 'g-1', connectionId, executionHostId: 'runtime:env-1' } - ], - projectGroups: [{ id: 'g-1', executionHostId: 'runtime:env-1' }] - }) - const ref = { workspaceId: 'folder:fw-1', hostId: 'runtime:env-1' as const } - expect(getWorkspaceEditorSshHost(folderState(null), ref)).toEqual({ - environmentId: 'env-1', - sshTargetId: 'ssh-1' - }) - expect(getWorkspaceEditorSshHost(folderState('nested'), ref)).toBeNull() - }) - - it.each([ - ['a network-paired server', {}], - ['an ephemeral VM', { orcadDeployment: { ...link, sshTargetId: 'runtime-ssh-vm-1' } }] - ])('refuses %s', (_label, environment) => { - expect( - getWorkspaceEditorSshHost(stateWith(environment), { workspaceId: WORKTREE_ID }) - ).toBeNull() - }) - - it('refuses a local or missing workspace', () => { - expect(getWorkspaceEditorSshHost({}, { workspaceId: 'local-repo::/tmp/x' })).toBeNull() - expect(getWorkspaceEditorSshHost(stateWith({ orcadDeployment: link }), null)).toBeNull() - }) -}) diff --git a/src/renderer/src/lib/workspace-emoji-picker-defaults.test.ts b/src/renderer/src/lib/workspace-emoji-picker-defaults.test.ts deleted file mode 100644 index 07ad35f3814..00000000000 --- a/src/renderer/src/lib/workspace-emoji-picker-defaults.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getWorkspaceEmojiPickerDefaults } from './workspace-emoji-picker-defaults' -import { searchWorkspaceEmojiShortcodes } from './workspace-emoji-shortcodes' - -describe('getWorkspaceEmojiPickerDefaults', () => { - it('matches the shortcode catalog exactly and lists distinct emoji', () => { - const defaults = getWorkspaceEmojiPickerDefaults() - expect(defaults).toHaveLength(48) - expect(new Set(defaults.map((entry) => entry.emoji)).size).toBe(48) - for (const entry of defaults) { - expect(searchWorkspaceEmojiShortcodes(entry.shortcode, 1)[0]).toEqual(entry) - } - }) -}) diff --git a/src/renderer/src/lib/workspace-emoji-picker-defaults.ts b/src/renderer/src/lib/workspace-emoji-picker-defaults.ts index 98c8011a495..c4e930bb7d6 100644 --- a/src/renderer/src/lib/workspace-emoji-picker-defaults.ts +++ b/src/renderer/src/lib/workspace-emoji-picker-defaults.ts @@ -3,7 +3,7 @@ import type { WorkspaceEmojiSuggestion } from './workspace-emoji-shortcodes' // Lives apart from the shortcode index so only the lazily loaded picker panel ships it. // Shown before the user searches: emoji that read well as workspace markers. // Only emoji-presentation code points: text-default ones (gear, warning, sun) render as monochrome glyphs. -// Literal pairs so opening the picker never builds the full shortcode catalog; a test keeps them in sync. +// Literal pairs so opening the picker never builds the full shortcode catalog. const WORKSPACE_EMOJI_PICKER_DEFAULTS: readonly WorkspaceEmojiSuggestion[] = [ { emoji: 'πŸš€', shortcode: 'rocket' }, { emoji: 'πŸ›', shortcode: 'bug' }, diff --git a/src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts b/src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts deleted file mode 100644 index 2e4dfac4a4a..00000000000 --- a/src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -describe('workspace emoji shortcode index laziness', () => { - beforeEach(() => { - vi.resetModules() - }) - - it('does not build the shared catalog when the renderer index is imported', async () => { - const shortcodeIndex = await import('./workspace-emoji-shortcodes') - const catalog = await import('../../../shared/emoji-shortcode-catalog') - - expect(catalog.isEmojiShortcodeCatalogBuiltForTest()).toBe(false) - - // Cursor/regex-only paths must stay off the catalog too. - expect(shortcodeIndex.getActiveWorkspaceEmojiShortcode('hi :tad', 7)).not.toBeNull() - expect(catalog.isEmojiShortcodeCatalogBuiltForTest()).toBe(false) - - expect(shortcodeIndex.searchWorkspaceEmojiShortcodes('tada')[0]?.emoji).toBe('πŸŽ‰') - expect(catalog.isEmojiShortcodeCatalogBuiltForTest()).toBe(true) - }) -}) diff --git a/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts b/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts deleted file mode 100644 index 4ab2772fc97..00000000000 --- a/src/renderer/src/lib/workspace-emoji-shortcodes.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - applyWorkspaceEmojiSuggestion, - getActiveWorkspaceEmojiShortcode, - insertWorkspaceEmojiAtSelection, - replaceCompletedWorkspaceEmojiShortcode -} from './workspace-emoji-shortcodes' - -describe('workspace emoji shortcodes', () => { - it('detects a shortcode at the caret without matching URLs or mid-word colons', () => { - expect(getActiveWorkspaceEmojiShortcode('Ship :win', 9)).toEqual({ - start: 5, - end: 9, - query: 'win' - }) - expect(getActiveWorkspaceEmojiShortcode('https://example.com', 19)).toBeNull() - expect(getActiveWorkspaceEmojiShortcode('fix:win', 7)).toBeNull() - }) - - it('replaces a completed shortcode and preserves text after the caret', () => { - expect(replaceCompletedWorkspaceEmojiShortcode('Ship :wink: today', 11)).toEqual({ - value: 'Ship πŸ˜‰ today', - cursor: 7 - }) - }) - - it('replaces a completed flag shortcode', () => { - expect(replaceCompletedWorkspaceEmojiShortcode(':flag_kr:', 9)).toEqual({ - value: 'πŸ‡°πŸ‡·', - cursor: 4 - }) - }) - - it('applies a selected suggestion to the active shortcode range', () => { - expect( - applyWorkspaceEmojiSuggestion( - 'Ship :win today', - { start: 5, end: 9, query: 'win' }, - { emoji: 'πŸ˜‰', shortcode: 'wink' } - ) - ).toEqual({ - value: 'Ship πŸ˜‰ today', - cursor: 8 - }) - }) -}) - -describe('insertWorkspaceEmojiAtSelection', () => { - it('pads the emoji so it never fuses with neighbouring text', () => { - expect(insertWorkspaceEmojiAtSelection('user-support', 0, 0, 'πŸš€')).toEqual({ - value: 'πŸš€ user-support', - cursor: 3 - }) - expect(insertWorkspaceEmojiAtSelection('user-support', 12, 12, 'πŸš€')).toEqual({ - value: 'user-support πŸš€ ', - cursor: 16 - }) - expect(insertWorkspaceEmojiAtSelection('ship it', 4, 4, 'πŸš€')).toEqual({ - value: 'ship πŸš€ it', - cursor: 7 - }) - }) - - it('replaces the selected range and clamps out-of-range selections', () => { - expect(insertWorkspaceEmojiAtSelection('ship bug fix', 5, 8, 'πŸ›')).toEqual({ - value: 'ship πŸ› fix', - cursor: 7 - }) - expect(insertWorkspaceEmojiAtSelection('', 4, 9, '✨')).toEqual({ value: '✨ ', cursor: 2 }) - }) -}) diff --git a/src/renderer/src/lib/workspace-file-browser-open-target.test.ts b/src/renderer/src/lib/workspace-file-browser-open-target.test.ts deleted file mode 100644 index 76b7d4f2b78..00000000000 --- a/src/renderer/src/lib/workspace-file-browser-open-target.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - getWorkspaceFileBrowserOpenTarget, - REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE, - TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE -} from './workspace-file-browser-open-target' -import { localAndSshTwinRows } from './workspace-twin-rows.test-fixture' - -const mocks = vi.hoisted(() => ({ - environmentId: null as string | null, - connectionId: null as string | null, - twin: false -})) - -vi.mock('@/lib/worktree-runtime-owner', () => ({ - runtimeEnvironmentIdOrLocalUnlessTwin: () => mocks.environmentId -})) - -vi.mock('@/store', () => ({ - useAppStore: { - getState: () => - mocks.twin - ? localAndSshTwinRows('repo-1', 'wt-1') - : { - repos: [{ id: 'repo-1', connectionId: mocks.connectionId }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] } - } - } -})) - -beforeEach(() => { - mocks.environmentId = null - mocks.connectionId = null - mocks.twin = false -}) - -describe('getWorkspaceFileBrowserOpenTarget', () => { - it('returns a reusable browser navigation target for local files', () => { - expect( - getWorkspaceFileBrowserOpenTarget({ - filePath: 'C:\\repo\\demo page.html', - worktreeId: 'wt-1' - }) - ).toEqual({ - status: 'ready', - url: 'file:///C:/repo/demo%20page.html', - title: 'demo page.html' - }) - }) - - it('still refuses remote files, which have no local file URL', () => { - mocks.connectionId = 'ssh-1' - - expect( - getWorkspaceFileBrowserOpenTarget({ - filePath: '/home/alice/report.html', - worktreeId: 'wt-1' - }) - ).toEqual({ - status: 'unsupported', - reason: 'remote-worktree', - message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE - }) - }) - - it('refuses a file:// URL for a project two hosts publish, naming why', () => { - mocks.twin = true - expect( - getWorkspaceFileBrowserOpenTarget({ filePath: '/srv/repo/report.html', worktreeId: 'wt-1' }) - ).toEqual({ - status: 'unsupported', - reason: 'remote-worktree', - message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE - }) - }) - - it('refuses paired files even when their connection ID is null', () => { - mocks.environmentId = 'managed-host' - expect( - getWorkspaceFileBrowserOpenTarget({ - filePath: '/srv/repo/report.html', - worktreeId: 'wt-1' - }) - ).toEqual({ - status: 'unsupported', - reason: 'remote-worktree', - message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE - }) - }) -}) diff --git a/src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts b/src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts deleted file mode 100644 index 3a62e10e56b..00000000000 --- a/src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { ExecutionHostId } from '../../../shared/execution-host' -import { readWorkspaceFileDragSource } from './workspace-file-drag' -import { writeWorkspaceFileDragSourceForWorkspace } from './workspace-file-drag-source' - -const mocks = vi.hoisted((): { host: ExecutionHostId | null; environmentId: string | null } => ({ - host: 'ssh:target-a', - environmentId: null -})) -vi.mock('@/store', () => ({ - useAppStore: { getState: () => ({ settings: { activeRuntimeEnvironmentId: 'foreign-env' } }) } -})) -vi.mock('./worktree-runtime-owner', () => ({ - getKnownExecutionHostIdForWorktree: () => mocks.host -})) -vi.mock('./resolve-owner', () => ({ - runtimeEnvironmentIdForWorkspaceOwner: () => ({ - kind: 'resolved', - environmentId: mocks.environmentId - }) -})) - -describe('workspace drag source server namespace', () => { - beforeEach(() => { - mocks.host = 'ssh:target-a' - mocks.environmentId = null - }) - - it.each([null, 'env-a'])('stamps SSH ownership from its workspace: %s', (environmentId) => { - mocks.environmentId = environmentId - const payload = new Map() - writeWorkspaceFileDragSourceForWorkspace( - { setData: (type, value) => payload.set(type, value) }, - 'folder:source' - ) - expect(readWorkspaceFileDragSource({ getData: (type) => payload.get(type) ?? '' })).toEqual({ - executionHostId: 'ssh:target-a', - workspaceId: 'folder:source', - ...(environmentId ? { runtimeEnvironmentId: environmentId } : {}) - }) - }) - - it('leaves an unknown workspace unstamped despite the focused server', () => { - mocks.host = null - const setData = vi.fn() - writeWorkspaceFileDragSourceForWorkspace({ setData }, 'folder:missing') - expect(setData).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/lib/workspace-file-drag.test.ts b/src/renderer/src/lib/workspace-file-drag.test.ts index 256e2fec1fe..b5f639962a7 100644 --- a/src/renderer/src/lib/workspace-file-drag.test.ts +++ b/src/renderer/src/lib/workspace-file-drag.test.ts @@ -5,9 +5,7 @@ import { WORKSPACE_FILE_PATHS_MIME, WORKSPACE_FILE_PATH_MIME, encodeWorkspaceFilePaths, - readWorkspaceFileDragPaths, - readWorkspaceFileDragSource, - writeWorkspaceFileDragSource + readWorkspaceFileDragPaths } from './workspace-file-drag' vi.mock('../../../shared/cross-platform-path', async (importOriginal) => { @@ -31,74 +29,6 @@ class FakeDataTransfer { } describe('workspace file drag payloads', () => { - it('rejects missing and malformed workspace ownership', () => { - const transfer = new FakeDataTransfer() - expect(readWorkspaceFileDragSource(transfer)).toBeNull() - transfer.setData('application/x-orca-workspace-file-source', '{"workspaceId":"worktree-1"}') - expect(readWorkspaceFileDragSource(transfer)).toBeNull() - }) - - it('round-trips the server namespace of a nested SSH target', () => { - const transfer = new FakeDataTransfer() - const source = { - executionHostId: 'ssh:remote-1', - workspaceId: 'folder:docs', - runtimeEnvironmentId: 'env-1' - } as const - writeWorkspaceFileDragSource(transfer, source) - expect(readWorkspaceFileDragSource(transfer)).toEqual(source) - }) - - it.each([null, 123, '', ' '])('rejects a malformed server namespace %s', (value) => { - const transfer = new FakeDataTransfer() - transfer.setData( - 'application/x-orca-workspace-file-source', - JSON.stringify({ - version: 1, - executionHostId: 'ssh:remote-1', - workspaceId: 'folder:docs', - runtimeEnvironmentId: value - }) - ) - expect(readWorkspaceFileDragSource(transfer)).toBeNull() - }) - - it('round-trips bounded multi-path payloads and removes nested duplicates', () => { - const transfer = new FakeDataTransfer() - transfer.setData( - WORKSPACE_FILE_PATHS_MIME, - encodeWorkspaceFilePaths(['/repo/src', '/repo/src/index.ts', '/repo/README.md']) - ) - - expect(readWorkspaceFileDragPaths(transfer)).toEqual({ - byteLength: 42, - pathCount: 2, - paths: ['/repo/src', '/repo/README.md'], - status: 'accepted' - }) - const selected = readWorkspaceFileDragPaths(transfer, { collapseDescendantsForMove: false }) - if (selected.status !== 'accepted') { - throw new Error('Expected selected resources to be accepted') - } - expect(selected.paths).toEqual(['/repo/src', '/repo/src/index.ts', '/repo/README.md']) - }) - - it('keeps sibling paths whose names only share a prefix', () => { - const transfer = new FakeDataTransfer() - transfer.setData( - WORKSPACE_FILE_PATHS_MIME, - encodeWorkspaceFilePaths(['/repo/src', '/repo/src-other', '/repo/src-other/index.ts']) - ) - - const result = readWorkspaceFileDragPaths(transfer) - - expect(result).toMatchObject({ - pathCount: 2, - paths: ['/repo/src', '/repo/src-other'], - status: 'accepted' - }) - }) - it('normalizes each accepted path once while pruning Windows-style nested duplicates', () => { const normalizePathForComparison = vi.mocked( crossPlatformPath.normalizeRuntimePathForComparison @@ -124,19 +54,6 @@ describe('workspace file drag payloads', () => { expect(normalizePathForComparison).toHaveBeenCalledTimes(paths.length) }) - it('rejects oversized raw multi-path data before JSON parsing', () => { - const transfer = new FakeDataTransfer() - const oversizedPrefix = 'πŸ˜€'.repeat(3) - transfer.setData(WORKSPACE_FILE_PATHS_MIME, `${oversizedPrefix}not-json`) - - expect(readWorkspaceFileDragPaths(transfer, { maxPathBytes: 5 })).toEqual({ - byteLength: 8, - pathCount: 0, - reason: 'paths-too-large', - status: 'rejected' - }) - }) - it('rejects decoded path lists by path count without falling back to a single path', () => { const transfer = new FakeDataTransfer() transfer.setData(WORKSPACE_FILE_PATHS_MIME, JSON.stringify(['/repo/a', '/repo/b'])) diff --git a/src/renderer/src/lib/workspace-ports/workspace-port-groups.test.ts b/src/renderer/src/lib/workspace-ports/workspace-port-groups.test.ts deleted file mode 100644 index e96a67b6fba..00000000000 --- a/src/renderer/src/lib/workspace-ports/workspace-port-groups.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - getExternalWorkspacePorts, - getWorkspacePortGroups, - getWorkspacePortsByWorktreeId -} from './workspace-port-groups' - -describe('workspace port group caches', () => { - it('returns stable empty references when no scan result exists', () => { - expect(getWorkspacePortsByWorktreeId(null).size).toBe(0) - expect(getWorkspacePortGroups(null)).toEqual([]) - expect(getExternalWorkspacePorts(null)).toEqual([]) - - expect(getWorkspacePortsByWorktreeId(null)).toBe(getWorkspacePortsByWorktreeId(undefined)) - expect(getWorkspacePortGroups(null)).toBe(getWorkspacePortGroups(undefined)) - expect(getExternalWorkspacePorts(null)).toBe(getExternalWorkspacePorts(undefined)) - }) -}) diff --git a/src/renderer/src/lib/workspace-ports/workspace-port-host-scoped-scan.test.ts b/src/renderer/src/lib/workspace-ports/workspace-port-host-scoped-scan.test.ts deleted file mode 100644 index fd42f11a1ef..00000000000 --- a/src/renderer/src/lib/workspace-ports/workspace-port-host-scoped-scan.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { callRuntimeRpc, supportsCapability } = vi.hoisted(() => ({ - callRuntimeRpc: vi.fn(), - supportsCapability: vi.fn() -})) -vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => ({ - ...(await importOriginal>()), - callRuntimeRpc, - runtimeEnvironmentSupportsCapability: supportsCapability -})) - -import { RuntimeRpcCallError } from '@/runtime/runtime-rpc-result' -import { - killWorkspacePortOnExecutionHost, - scanWorkspacePortsOnExecutionHost, - WORKSPACE_PORTS_SERVER_SSH_UPDATE_REASON, - type HostScopedPortHost -} from './workspace-port-scan-client' -import { WORKSPACE_PORTS_HOST_SCOPED_RUNTIME_CAPABILITY } from '../../../../shared/workspace-ports-runtime-capabilities' - -const host: HostScopedPortHost = { - route: { target: { kind: 'environment', environmentId: 'env-e' }, at: 'ssh:t' }, - executionHostId: 'ssh:t', - worktreeId: 'repo-1::/srv/w' -} - -const rowOnT = { - id: '0.0.0.0:3020:4242', - kind: 'external', - bindHost: '0.0.0.0', - connectHost: '127.0.0.1', - port: 3020, - pid: 4242, - protocol: 'unknown' -} - -beforeEach(() => { - callRuntimeRpc.mockReset() - supportsCapability.mockReset() - supportsCapability.mockResolvedValue(true) -}) - -describe('scanWorkspacePortsOnExecutionHost', () => { - it('asks the server to scan the workspace host and keeps the host it names', async () => { - callRuntimeRpc.mockResolvedValue({ - executionHostId: 'ssh:t', - platform: 'linux', - scannedAt: 1, - ports: [rowOnT] - }) - - const scan = await scanWorkspacePortsOnExecutionHost(host) - - expect(supportsCapability).toHaveBeenCalledWith( - 'env-e', - WORKSPACE_PORTS_HOST_SCOPED_RUNTIME_CAPABILITY - ) - expect(callRuntimeRpc).toHaveBeenCalledWith( - host.route.target, - 'workspacePorts.scanHost', - { worktree: 'id:repo-1::/srv/w' }, - expect.objectContaining({ timeoutMs: 15_000 }) - ) - expect(scan).toMatchObject({ executionHostId: 'ssh:t', ports: [rowOnT] }) - expect(scan.unavailableReason).toBeUndefined() - }) - - it('never asks an older server, whose scan would show the server instead of its SSH host', async () => { - supportsCapability.mockResolvedValue(false) - - const scan = await scanWorkspacePortsOnExecutionHost(host) - - expect(callRuntimeRpc).not.toHaveBeenCalled() - expect(scan).toMatchObject({ - executionHostId: 'ssh:t', - ports: [], - unavailableReason: WORKSPACE_PORTS_SERVER_SSH_UPDATE_REASON - }) - }) - - it('says "update server" when the server lacks the method despite advertising it', async () => { - callRuntimeRpc.mockRejectedValue( - new RuntimeRpcCallError({ - id: 'r', - ok: false, - error: { code: 'method_not_found', message: 'Unknown method' }, - _meta: { runtimeId: 'runtime-e' } - }) - ) - - await expect(scanWorkspacePortsOnExecutionHost(host)).resolves.toMatchObject({ - ports: [], - unavailableReason: WORKSPACE_PORTS_SERVER_SSH_UPDATE_REASON - }) - }) - - it("scans a recipe VM through this app's own runtime without a capability probe", async () => { - const recipeVm: HostScopedPortHost = { - route: { target: { kind: 'local' }, at: 'ssh:runtime-ssh-vm1' }, - executionHostId: 'ssh:runtime-ssh-vm1', - worktreeId: 'repo-1::/srv/w' - } - callRuntimeRpc.mockResolvedValue({ - executionHostId: 'ssh:runtime-ssh-vm1', - platform: 'unknown', - scannedAt: 1, - ports: [], - unavailableReason: 'The SSH host is not connected.' - }) - - const scan = await scanWorkspacePortsOnExecutionHost(recipeVm) - - expect(supportsCapability).not.toHaveBeenCalled() - expect(callRuntimeRpc).toHaveBeenCalledWith( - { kind: 'local' }, - 'workspacePorts.scanHost', - { worktree: 'id:repo-1::/srv/w' }, - expect.anything() - ) - // An unreachable VM stays "could not look", never an empty "no ports". - expect(scan.unavailableReason).toBe('The SSH host is not connected.') - }) - - it("drops rows the server scanned on a host other than the workspace's", async () => { - callRuntimeRpc.mockResolvedValue({ - executionHostId: 'local', - platform: 'linux', - scannedAt: 1, - ports: [rowOnT] - }) - - const scan = await scanWorkspacePortsOnExecutionHost(host) - - expect(scan.ports).toEqual([]) - expect(scan.unavailableReason).toMatch(/different host/) - }) -}) - -describe('killWorkspacePortOnExecutionHost', () => { - it('stops a row only through the server, naming the host the row was scanned on', async () => { - callRuntimeRpc.mockResolvedValue({ ok: true }) - - await expect( - killWorkspacePortOnExecutionHost(host, { scannedHostId: 'ssh:t', pid: 4242, port: 3020 }) - ).resolves.toEqual({ ok: true }) - - // Equal server/SSH pid+port values are unrelated processes; never fall back to repo-scoped server kills. - expect(callRuntimeRpc).toHaveBeenCalledOnce() - expect(callRuntimeRpc).toHaveBeenCalledWith( - host.route.target, - 'workspacePorts.killHost', - { worktree: 'id:repo-1::/srv/w', executionHostId: 'ssh:t', pid: 4242, port: 3020 }, - expect.objectContaining({ timeoutMs: 15_000 }) - ) - }) - - it('reports "update server" instead of falling back to the server-local kill', async () => { - callRuntimeRpc.mockRejectedValue( - new RuntimeRpcCallError({ - id: 'r', - ok: false, - error: { code: 'method_not_found', message: 'Unknown method' }, - _meta: { runtimeId: 'runtime-e' } - }) - ) - - await expect( - killWorkspacePortOnExecutionHost(host, { scannedHostId: 'ssh:t', pid: 4242, port: 3020 }) - ).resolves.toEqual({ ok: false, reason: WORKSPACE_PORTS_SERVER_SSH_UPDATE_REASON }) - expect(callRuntimeRpc).toHaveBeenCalledOnce() - }) -}) diff --git a/src/renderer/src/lib/workspace-ports/workspace-port-scan-debounce.test.ts b/src/renderer/src/lib/workspace-ports/workspace-port-scan-debounce.test.ts index dbaf50def7f..c7853693477 100644 --- a/src/renderer/src/lib/workspace-ports/workspace-port-scan-debounce.test.ts +++ b/src/renderer/src/lib/workspace-ports/workspace-port-scan-debounce.test.ts @@ -21,15 +21,6 @@ function good(portIds: string[]): WorkspacePortScanResult { } } -function unavailable(): WorkspacePortScanResult { - return { platform: 'unknown', scannedAt: 1, ports: [], unavailableReason: 'scan failed' } -} - -/** What the ports popover publishes when its own scan fails: reason + last-good ports. */ -function unavailableWithRetainedPorts(portIds: string[]): WorkspacePortScanResult { - return { ...good(portIds), unavailableReason: 'scan failed' } -} - const FAILURE_THRESHOLD = 2 function createHarness(): { @@ -58,46 +49,6 @@ function createHarness(): { } describe('reconcileTransientPortScanFailures', () => { - it('keeps the live indicator solid through a single transient failure', () => { - const { apply } = createHarness() - - const first = apply([{ key: 'host:all', result: good(['tcp:3000']) }]) - expect(first[0].result.ports).toHaveLength(1) - - const second = apply([{ key: 'host:all', result: unavailable() }]) - expect(second[0].result.ports).toHaveLength(1) - expect(second[0].result.unavailableReason).toBeUndefined() - }) - - it('drops ports only after failures reach the tolerance', () => { - const { apply } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - apply([{ key: 'h:all', result: unavailable() }]) - - const third = apply([{ key: 'h:all', result: unavailable() }]) - expect(third[0].result.ports).toHaveLength(0) - expect(third[0].result.unavailableReason).toBe('scan failed') - }) - - it('clears immediately when a reachable host reports zero ports', () => { - const { apply } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - - const next = apply([{ key: 'h:all', result: good([]) }]) - expect(next[0].result.ports).toHaveLength(0) - expect(next[0].result.unavailableReason).toBeUndefined() - }) - - it('resets the failure streak after a successful scan', () => { - const { apply } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - apply([{ key: 'h:all', result: unavailable() }]) - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - - const afterRecovery = apply([{ key: 'h:all', result: unavailable() }]) - expect(afterRecovery[0].result.ports).toHaveLength(1) - }) - it('prunes state for hosts that disappear', () => { const { apply, state } = createHarness() apply([{ key: 'gone:all', result: good(['tcp:3000']) }]) @@ -105,52 +56,4 @@ describe('reconcileTransientPortScanFailures', () => { expect(state.has('gone:all')).toBe(false) expect(state.has('other:all')).toBe(true) }) - - // Popover failures already publish last-good ports; do not count that as spent grace or the next poll drops them immediately. - it('still grants the grace period after a failure that retained its ports', () => { - const { apply, publish } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - const popoverResult = unavailableWithRetainedPorts(['tcp:3000']) - publish('h:all', popoverResult) - - const next = apply([{ key: 'h:all', result: unavailable() }]) - - expect(next[0].result).toBe(popoverResult) - expect(next[0].result.ports).toHaveLength(1) - }) - - it('still drops retained ports once failures reach the tolerance', () => { - const { apply, publish } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - publish('h:all', unavailableWithRetainedPorts(['tcp:3000'])) - apply([{ key: 'h:all', result: unavailable() }]) - - const next = apply([{ key: 'h:all', result: unavailable() }]) - - expect(next[0].result.ports).toHaveLength(0) - expect(next[0].result.unavailableReason).toBe('scan failed') - }) - - it('uses a newer manual result instead of resurrecting stale ports', () => { - const { apply, publish } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - publish('h:all', good([])) - - const next = apply([{ key: 'h:all', result: unavailable() }]) - - expect(next[0].result.ports).toHaveLength(0) - expect(next[0].result.unavailableReason).toBeUndefined() - }) - - it('starts a fresh failure streak after a newer manual result', () => { - const { apply, publish } = createHarness() - apply([{ key: 'h:all', result: good(['tcp:3000']) }]) - apply([{ key: 'h:all', result: unavailable() }]) - const manualResult = good([]) - publish('h:all', manualResult) - - const next = apply([{ key: 'h:all', result: unavailable() }]) - - expect(next[0].result).toBe(manualResult) - }) }) diff --git a/src/renderer/src/lib/workspace-ports/workspace-port-scan-publish.test.ts b/src/renderer/src/lib/workspace-ports/workspace-port-scan-publish.test.ts deleted file mode 100644 index d9030aad12c..00000000000 --- a/src/renderer/src/lib/workspace-ports/workspace-port-scan-publish.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -// @vitest-environment happy-dom - -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' - -vi.mock('@/lib/worktree-activation', () => ({ - activateAndRevealWorktree: vi.fn() -})) - -vi.mock('@/runtime/runtime-rpc-client', () => ({ - callRuntimeRpc: vi.fn(), - assertRuntimeEnvironmentCapability: vi.fn() -})) -vi.mock('@/runtime/runtime-rpc-result', () => ({ - RuntimeRpcCallError: class RuntimeRpcCallError extends Error { - code?: string - } -})) - -vi.mock('./workspace-port-scan-client', () => ({ - runWorkspacePortScanForTarget: vi.fn() -})) - -const { publishWorkspacePortScanForHost, WORKSPACE_PORT_ALL_HOSTS_SCAN_KEY } = - await import('./workspace-port-actions') -type WorkspacePortScanPublisher = Parameters[0] - -function scanWithPort(port: number, scannedAt: number): WorkspacePortScanResult { - return { - platform: 'linux', - scannedAt, - ports: [ - { - id: `tcp:${port}`, - bindHost: '0.0.0.0', - connectHost: '127.0.0.1', - port, - pid: 100 + port, - processName: 'node', - protocol: 'http', - kind: 'external' - } - ] - } -} - -/** Mirrors the store's replaceWorkspacePortScans semantics: one atomic update. */ -function makeStoreHarness(initial: Record = {}): { - scansByKey: Record - projections: { key: string; result: WorkspacePortScanResult }[] - publisher: Omit -} { - let scansByKey: Record = { ...initial } - const projections: { key: string; result: WorkspacePortScanResult }[] = [] - return { - get scansByKey() { - return scansByKey - }, - projections, - publisher: { - replaceWorkspacePortScans: ( - nextScansByKey: Record, - projection: { key: string; result: WorkspacePortScanResult } | null - ) => { - scansByKey = nextScansByKey - if (projection) { - projections.push(projection) - } - }, - getWorkspacePortScansByKey: () => scansByKey - } - } -} - -describe('publishWorkspacePortScanForHost', () => { - let localScan: WorkspacePortScanResult - let remoteScan: WorkspacePortScanResult - - beforeEach(() => { - localScan = scanWithPort(5173, 10) - remoteScan = scanWithPort(3000, 20) - }) - - it('keeps the other host in the projection when one host refreshes', () => { - const harness = makeStoreHarness({ 'local:all': localScan }) - - publishWorkspacePortScanForHost({ - ...harness.publisher, - scanKey: 'environment:env-1:all', - scan: remoteScan - }) - - const projection = harness.projections.at(-1) - expect(projection?.key).toBe(WORKSPACE_PORT_ALL_HOSTS_SCAN_KEY) - expect(projection?.result.ports.map((port) => port.port).sort()).toEqual([3000, 5173]) - expect(Object.keys(harness.scansByKey).sort()).toEqual(['environment:env-1:all', 'local:all']) - }) - - it('publishes map and projection in a single store update', () => { - const harness = makeStoreHarness({ 'local:all': localScan }) - const replaceSpy = vi.spyOn(harness.publisher, 'replaceWorkspacePortScans') - - publishWorkspacePortScanForHost({ - ...harness.publisher, - scanKey: 'environment:env-1:all', - scan: remoteScan - }) - - // Atomic replacement must notify once and keep map/projection coherent. - expect(replaceSpy).toHaveBeenCalledTimes(1) - const [nextScans, projection] = replaceSpy.mock.calls[0] - expect(Object.keys(nextScans).sort()).toEqual(['environment:env-1:all', 'local:all']) - expect(projection?.key).toBe(WORKSPACE_PORT_ALL_HOSTS_SCAN_KEY) - }) - - it('does not accumulate duplicate rows across repeated publishes', () => { - const harness = makeStoreHarness({ 'local:all': localScan }) - - publishWorkspacePortScanForHost({ - ...harness.publisher, - scanKey: 'environment:env-1:all', - scan: remoteScan - }) - publishWorkspacePortScanForHost({ - ...harness.publisher, - scanKey: 'environment:env-1:all', - scan: { ...remoteScan, scannedAt: 30 } - }) - - // Never store aggregate in per-host state or the next merge multiplies its own rows. - expect(harness.scansByKey[WORKSPACE_PORT_ALL_HOSTS_SCAN_KEY]).toBeUndefined() - expect( - harness.projections - .at(-1) - ?.result.ports.map((port) => port.port) - .sort() - ).toEqual([3000, 5173]) - }) -}) diff --git a/src/renderer/src/lib/workspace-session-editor-drafts.test.ts b/src/renderer/src/lib/workspace-session-editor-drafts.test.ts deleted file mode 100644 index 41d7669f1c8..00000000000 --- a/src/renderer/src/lib/workspace-session-editor-drafts.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionSnapshot } from './workspace-session' -import { buildWorkspaceSessionPayload } from './workspace-session' - -function createSnapshot( - overrides: Partial = {} -): WorkspaceSessionSnapshot { - return { - activeRepoId: 'repo-1', - activeWorkspaceKey: 'worktree:wt-1', - activeWorktreeId: 'wt-1', - activeTabId: 'tab-1', - tabsByWorktree: {}, - ptyIdsByTabId: {}, - terminalLayoutsByTabId: {}, - localOnlyScrollbackByTabId: {}, - activeTabIdByWorktree: {}, - openFiles: [], - editorDrafts: {}, - markdownFrontmatterVisible: {}, - activeFileIdByWorktree: {}, - activeTabTypeByWorktree: {}, - browserTabsByWorktree: {}, - browserPagesByWorkspace: {}, - remoteBrowserPageHandlesByPageId: {}, - activeBrowserTabIdByWorktree: {}, - browserUrlHistory: [], - workspaceDocHistory: [], - unifiedTabsByWorktree: {}, - groupsByWorktree: {}, - layoutByWorktree: {}, - activeGroupIdByWorktree: {}, - sshConnectionStates: new Map(), - repos: [], - worktreesByRepo: {}, - lastKnownRelayPtyIdByTabId: {}, - lastVisitedAtByWorktreeId: {}, - defaultTerminalTabsAppliedByWorktreeId: {}, - ...overrides - } -} - -describe('workspace session editor drafts', () => { - it('persists dirty editor drafts without saving clean file content', () => { - const payload = buildWorkspaceSessionPayload( - createSnapshot({ - openFiles: [ - { - id: '/tmp/dirty.md', - filePath: '/tmp/dirty.md', - relativePath: 'dirty.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: true - } as never, - { - id: '/tmp/clean.md', - filePath: '/tmp/clean.md', - relativePath: 'clean.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: false - } as never - ], - editorDrafts: { - '/tmp/dirty.md': '', - '/tmp/clean.md': 'clean draft should not persist' - } - }) - ) - - expect(payload.openFilesByWorktree?.['wt-1']).toEqual([ - expect.objectContaining({ - filePath: '/tmp/dirty.md', - dirtyDraftContent: '' - }), - expect.not.objectContaining({ - dirtyDraftContent: expect.any(String) - }) - ]) - }) - - it('persists the SSH target that owns an external host file', () => { - const payload = buildWorkspaceSessionPayload( - createSnapshot({ - openFiles: [ - { - id: '/tmp/ssh-preview.png', - filePath: '/tmp/ssh-preview.png', - relativePath: '/tmp/ssh-preview.png', - worktreeId: 'wt-1', - language: 'png', - mode: 'edit', - isDirty: false, - externalSshTargetId: 'ssh-1' - } as never - ] - }) - ) - - expect(payload.openFilesByWorktree?.['wt-1']?.[0]).toEqual( - expect.objectContaining({ externalSshTargetId: 'ssh-1' }) - ) - }) - - it('persists the disk baseline signature only alongside a dirty draft', () => { - const payload = buildWorkspaceSessionPayload( - createSnapshot({ - openFiles: [ - { - id: '/tmp/dirty.md', - filePath: '/tmp/dirty.md', - relativePath: 'dirty.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: true, - lastKnownDiskSignature: 'abc123' - } as never, - { - id: '/tmp/clean.md', - filePath: '/tmp/clean.md', - relativePath: 'clean.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: false, - lastKnownDiskSignature: 'def456' - } as never - ], - editorDrafts: { - '/tmp/dirty.md': 'unsaved edits' - } - }) - ) - - expect(payload.openFilesByWorktree?.['wt-1']).toEqual([ - expect.objectContaining({ - filePath: '/tmp/dirty.md', - dirtyDraftContent: 'unsaved edits', - lastKnownDiskSignature: 'abc123' - }), - // Clean tabs need no restore-conflict signature; persisting it bloats every write. - expect.not.objectContaining({ - lastKnownDiskSignature: expect.any(String) - }) - ]) - }) - - it('persists readOnly:true and never emits a dirty draft for read-only tabs', () => { - const payload = buildWorkspaceSessionPayload( - createSnapshot({ - openFiles: [ - { - id: '/home/user/.claude/log.jsonl', - filePath: '/home/user/.claude/log.jsonl', - relativePath: '/home/user/.claude/log.jsonl', - worktreeId: 'wt-1', - language: 'jsonl', - mode: 'edit', - // Why: even if isDirty is somehow set, a read-only tab must not - // persist a draft that a restore could write back to disk. - isDirty: true, - readOnly: true, - liveTail: true - } as never - ], - editorDrafts: { - '/home/user/.claude/log.jsonl': 'stray draft that must not persist' - } - }) - ) - - const persisted = payload.openFilesByWorktree?.['wt-1']?.[0] - expect(persisted).toEqual( - expect.objectContaining({ - filePath: '/home/user/.claude/log.jsonl', - readOnly: true, - liveTail: true - }) - ) - expect(persisted).toEqual( - expect.not.objectContaining({ dirtyDraftContent: expect.any(String) }) - ) - }) - - it('omits readOnly for ordinary writable tabs (writable is the default)', () => { - const payload = buildWorkspaceSessionPayload( - createSnapshot({ - openFiles: [ - { - id: '/tmp/clean.md', - filePath: '/tmp/clean.md', - relativePath: 'clean.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: false - } as never - ] - }) - ) - - const persisted = payload.openFilesByWorktree?.['wt-1']?.[0] - expect(persisted).toEqual(expect.objectContaining({ filePath: '/tmp/clean.md' })) - expect(persisted).not.toHaveProperty('readOnly') - }) -}) diff --git a/src/renderer/src/lib/workspace-session-folder-host-twin.test.ts b/src/renderer/src/lib/workspace-session-folder-host-twin.test.ts deleted file mode 100644 index dd551180098..00000000000 --- a/src/renderer/src/lib/workspace-session-folder-host-twin.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getFolderWorkspaceHostId } from '../../../shared/folder-workspace-execution-host' -import { FOLDER_HOST_CASES } from '../../../shared/folder-workspace-host-routing.test-fixture' -import { folderWorkspaceKey } from '../../../shared/workspace-scope' -import { adoptFromEndpoint } from '../store/adopt-from-endpoint' -import { buildHostIdByWorktreeId } from './workspace-session-host-persistence' - -// Main's router is checked against the same table in folder-workspace-mixed-host-routing.test.ts. -describe("the window's save partition for each folder kind local main lists", () => { - it.each(FOLDER_HOST_CASES)('$name', ({ folder, groups, expected }) => { - const local = { kind: 'local' } as const - const stampedGroups = groups.map((row) => - adoptFromEndpoint(local, { kind: 'projectGroup', row }) - ) - const stampedFolder = adoptFromEndpoint(local, { - kind: 'folderWorkspace', - row: folder, - // As the window's catalog resolver does, over the groups it already stamped. - resolveOwnHostId: (row) => getFolderWorkspaceHostId(row, stampedGroups) - }) - const hostIdOf = buildHostIdByWorktreeId({ - repos: [], - projectGroups: stampedGroups, - folderWorkspaces: [stampedFolder], - worktreesByRepo: {} - }) - expect(hostIdOf(folderWorkspaceKey(folder.id))).toBe(expected) - }) -}) diff --git a/src/renderer/src/lib/workspace-session-host-contention.test.ts b/src/renderer/src/lib/workspace-session-host-contention.test.ts index 47c8d092322..f702a9d8612 100644 --- a/src/renderer/src/lib/workspace-session-host-contention.test.ts +++ b/src/renderer/src/lib/workspace-session-host-contention.test.ts @@ -5,14 +5,10 @@ import type { TerminalTab } from '../../../shared/terminal-tab-types' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import type { ExecutionHostId } from '../../../shared/execution-host' import { folderWorkspaceKey } from '../../../shared/workspace-scope' -import { - indexWorktreeHostClaims, - mergeWorkspaceSessionsWithHostShadow -} from './workspace-session-host-contention' -import { pickPrimaryHostForClaims } from '../../../shared/workspace-session-contested-primary-host' +import './workspace-session-host-contention' +import '../../../shared/workspace-session-contested-primary-host' import { fetchWorkspaceSessionWithRuntimeHostOwners } from './workspace-session-host-hydration' import { - buildHostIdByWorktreeId, patchWorkspaceSessionByHost, persistWorkspaceSessionByHost, type HostPersistenceState @@ -58,86 +54,6 @@ function contestedState(overrides: Partial = {}): HostPers } } -describe('indexWorktreeHostClaims', () => { - it('records every host publishing the same workspace id', () => { - const claims = indexWorktreeHostClaims(contestedState().worktreesByRepo, new Map()) - - expect([...(claims.get(SHARED_ID) ?? [])].sort()).toEqual(['local', SSH_HOST]) - }) - - it('attributes an unqualified row through its repo when the repo names one host', () => { - const claims = indexWorktreeHostClaims( - { 'repo-a': [{ id: 'repo-a::/work/a', repoId: 'repo-a' }] }, - new Map([['repo-a', SSH_HOST]]) - ) - - expect([...(claims.get('repo-a::/work/a') ?? [])]).toEqual([SSH_HOST]) - }) - - it('leaves an unqualified row unattributed when its repo id is itself ambiguous', () => { - const claims = indexWorktreeHostClaims( - { 'repo-a': [{ id: 'repo-a::/work/a', repoId: 'repo-a' }] }, - new Map([['repo-a', null]]) - ) - - expect(claims.has('repo-a::/work/a')).toBe(false) - }) - - it('prefers local as primary, else the lowest host id', () => { - expect(pickPrimaryHostForClaims([SSH_HOST, 'local'])).toBe('local') - expect(pickPrimaryHostForClaims(['runtime:b', 'runtime:a'])).toBe('runtime:a') - }) -}) - -describe('buildHostIdByWorktreeId for a contested workspace id', () => { - it('routes a local/SSH collision to one deterministic primary', () => { - expect(buildHostIdByWorktreeId(contestedState())(SHARED_ID)).toBe('local') - }) - - it('gives two runtime claimants a runtime primary instead of folding them into local', () => { - const owner = buildHostIdByWorktreeId({ - repos: [], - worktreesByRepo: { - 'repo-shared': [ - { id: SHARED_ID, repoId: 'repo-shared', hostId: 'runtime:env-b' }, - { id: SHARED_ID, repoId: 'repo-shared', hostId: 'runtime:env-a' } - ] - } - }) - - expect(owner(SHARED_ID)).toBe('runtime:env-a') - }) -}) - -describe('mergeWorkspaceSessionsWithHostShadow', () => { - it('parks a co-claimant partition entry instead of letting it win the shared key', () => { - const merged = mergeWorkspaceSessionsWithHostShadow({ - local: sessionWithTabs({ [SHARED_ID]: [tab('local-tab')] }), - [SSH_HOST]: sessionWithTabs({ [SHARED_ID]: [tab('ssh-tab')] }) - }) - - expect(merged.session.tabsByWorktree[SHARED_ID]?.map((entry) => entry.id)).toEqual([ - 'local-tab' - ]) - expect( - (merged.shadow[SSH_HOST]?.tabsByWorktree?.[SHARED_ID] ?? []).map((entry) => entry.id) - ).toEqual(['ssh-tab']) - expect(merged.shadow.local).toBeUndefined() - }) - - it('leaves uncontested partitions untouched', () => { - const merged = mergeWorkspaceSessionsWithHostShadow({ - local: sessionWithTabs({ 'repo-a::/a': [tab('a', 'repo-a::/a')] }), - 'runtime:env-1': sessionWithTabs({ - 'repo-b::/b': [tab('b', 'repo-b::/b')] - }) - }) - - expect(Object.keys(merged.session.tabsByWorktree).sort()).toEqual(['repo-a::/a', 'repo-b::/b']) - expect(merged.shadow).toEqual({}) - }) -}) - type SessionWriteMock = Mock< (session: WorkspaceSessionState, hostId?: ExecutionHostId) => Promise > @@ -318,11 +234,6 @@ describe('read-time primary is the one the write path honours', () => { } } - it('keeps an SSH claimant out of the rotating runtime partition', () => { - // Claim sorting favors runtime over ssh, which must not move SSH rows out of their own partition. - expect(buildHostIdByWorktreeId(sshVersusRuntimeState())(SHARED_ID)).toBe(SSH_HOST) - }) - it('does not strand the runtime co-claimant when the SSH row is written', async () => { const set: SessionWriteMock = vi.fn(async () => {}) await persistWorkspaceSessionByHost( @@ -371,33 +282,6 @@ describe('read-time primary is the one the write path honours', () => { expect(localWrite?.tabsByWorktree[SHARED_ID]).toBeUndefined() }) - it('still migrates a workspace the catalog has re-attributed to another partition', () => { - const owner = buildHostIdByWorktreeId({ - repos: [], - worktreesByRepo: { - 'repo-shared': [{ id: SHARED_ID, repoId: 'repo-shared', hostId: RUNTIME_HOST }] - }, - contestedPrimaryHostBySessionKey: { [SHARED_ID]: 'local' } - }) - - expect(owner(SHARED_ID)).toBe(RUNTIME_HOST) - }) - - it('records the partition every restored key came from, contested or not', () => { - const merged = mergeWorkspaceSessionsWithHostShadow({ - local: sessionWithTabs({ [SHARED_ID]: [tab('local-tab')] }), - [RUNTIME_HOST]: sessionWithTabs({ - [SHARED_ID]: [tab('runtime-tab')], - [RUNTIME_ONLY_ID]: [tab('runtime-only-tab', RUNTIME_ONLY_ID)] - }) - }) - - expect(merged.primaryHostBySessionKey).toEqual({ - [SHARED_ID]: 'local', - [RUNTIME_ONLY_ID]: RUNTIME_HOST - }) - }) - it('does not name a runtime owner for a key the local partition kept', async () => { const read = await fetchWorkspaceSessionWithRuntimeHostOwners( { diff --git a/src/renderer/src/lib/workspace-session-host-persistence.test.ts b/src/renderer/src/lib/workspace-session-host-persistence.test.ts index 5e6d2535a10..546872fe8fa 100644 --- a/src/renderer/src/lib/workspace-session-host-persistence.test.ts +++ b/src/renderer/src/lib/workspace-session-host-persistence.test.ts @@ -2,18 +2,12 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../shared/constants' import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' -import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope' +import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { - buildHostIdByWorktreeId, - buildWorkspaceSessionHostSnapshots, patchWorkspaceSessionByHost, - persistWorkspaceSessionByHost, type HostPersistenceState } from './workspace-session-host-persistence' -import { - fetchWorkspaceSessionFromHosts, - fetchWorkspaceSessionWithRuntimeHostOwners -} from './workspace-session-host-hydration' +import { fetchWorkspaceSessionFromHosts } from './workspace-session-host-hydration' describe('fetchWorkspaceSessionFromHosts', () => { it('reads saved runtime host partitions before runtime repos are loaded', async () => { @@ -51,140 +45,6 @@ describe('fetchWorkspaceSessionFromHosts', () => { expect(session.tabsByWorktree[worktreeId]).toEqual(remoteSession.tabsByWorktree[worktreeId]) }) - it('returns runtime owners for worktrees loaded from runtime host partitions', async () => { - const worktreeId = 'remote-repo::/srv/remote-wt' - const get = vi.fn(async (hostId?: string): Promise => { - if (hostId === 'runtime:env-1') { - return { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - [worktreeId]: [ - { - id: 'remote-tab', - ptyId: null, - worktreeId, - title: 'Remote', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } - ] - } - } - } - return getDefaultWorkspaceSession() - }) - - const read = await fetchWorkspaceSessionWithRuntimeHostOwners({ get }, [], ['runtime:env-1']) - - expect(read.session.tabsByWorktree[worktreeId]).toHaveLength(1) - expect(read.runtimeHostIdByWorkspaceSessionKey).toEqual({ - [worktreeId]: 'runtime:env-1' - }) - }) - - it('normalizes canonical worktree session keys in runtime owner maps', async () => { - const worktreeId = 'remote-repo::/srv/remote-wt' - const workspaceKey = worktreeWorkspaceKey(worktreeId) - const get = vi.fn(async (hostId?: string): Promise => { - if (hostId === 'runtime:env-1') { - return { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - [workspaceKey]: [ - { - id: 'remote-tab', - ptyId: null, - worktreeId, - title: 'Remote', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } - ] - } - } - } - return getDefaultWorkspaceSession() - }) - - const read = await fetchWorkspaceSessionWithRuntimeHostOwners({ get }, [], ['runtime:env-1']) - - expect(read.runtimeHostIdByWorkspaceSessionKey).toEqual({ - [worktreeId]: 'runtime:env-1' - }) - }) - - it('returns runtime owners for folder workspace session keys', async () => { - const folderKey = folderWorkspaceKey('folder-1') - const get = vi.fn(async (hostId?: string): Promise => { - if (hostId === 'runtime:env-1') { - return { - ...getDefaultWorkspaceSession(), - activeWorkspaceKey: folderKey, - tabsByWorktree: { - [folderKey]: [ - { - id: 'remote-folder-tab', - ptyId: null, - worktreeId: folderKey, - title: 'Remote folder', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } - ] - } - } - } - return getDefaultWorkspaceSession() - }) - - const read = await fetchWorkspaceSessionWithRuntimeHostOwners({ get }, [], ['runtime:env-1']) - - expect(read.session.tabsByWorktree[folderKey]).toHaveLength(1) - expect(read.runtimeHostIdByWorkspaceSessionKey).toEqual({ - [folderKey]: 'runtime:env-1' - }) - }) - - it('returns runtime owners for sleeping-agent-only runtime worktrees', async () => { - const worktreeId = 'remote-repo::/srv/sleeping-wt' - const get = vi.fn(async (hostId?: string): Promise => { - if (hostId === 'runtime:env-1') { - return { - ...getDefaultWorkspaceSession(), - sleepingAgentSessionsByPaneKey: { - 'remote-tab:leaf-1': { - paneKey: 'remote-tab:leaf-1', - tabId: 'remote-tab', - worktreeId, - agent: 'codex', - providerSession: { key: 'session_id', id: 'codex-session-1' }, - prompt: 'finish the task', - state: 'working', - capturedAt: 1, - updatedAt: 1 - } - } - } - } - return getDefaultWorkspaceSession() - }) - - const read = await fetchWorkspaceSessionWithRuntimeHostOwners({ get }, [], ['runtime:env-1']) - - expect(read.session.sleepingAgentSessionsByPaneKey?.['remote-tab:leaf-1']?.worktreeId).toBe( - worktreeId - ) - expect(read.runtimeHostIdByWorkspaceSessionKey).toEqual({ - [worktreeId]: 'runtime:env-1' - }) - }) - it('routes restored runtime folder workspace patches back to the runtime host', async () => { const folderKey = folderWorkspaceKey('folder-1') const patch = vi.fn().mockResolvedValue(undefined) @@ -391,88 +251,6 @@ describe('fetchWorkspaceSessionFromHosts', () => { 'runtime:env-1' ) }) - - it('defaults duplicate repo ids to local when the worktree has no host metadata', () => { - const owner = buildHostIdByWorktreeId({ - repos: [ - { id: 'same-repo', connectionId: null, executionHostId: 'local' }, - { - id: 'same-repo', - connectionId: null, - executionHostId: 'runtime:env-1' - } - ], - worktreesByRepo: { - 'same-repo': [{ id: 'same-repo::/local-only', repoId: 'same-repo' }] - } - }) - - expect(owner('same-repo::/local-only')).toBe('local') - }) - - it('routes canonical worktree keys using their raw worktree owner', () => { - const worktreeId = 'remote-repo::/srv/remote-wt' - const owner = buildHostIdByWorktreeId({ - repos: [], - worktreesByRepo: { - 'remote-repo': [{ id: worktreeId, repoId: 'remote-repo', hostId: 'runtime:env-1' }] - } - }) - - expect(owner(worktreeWorkspaceKey(worktreeId))).toBe('runtime:env-1') - }) - - it('builds local-first host snapshots', () => { - const localWorktreeId = 'local-repo::C:\\src\\local' - const remoteWorktreeId = 'remote-repo::/srv/remote' - const makeTab = (id: string, worktreeId: string) => ({ - id, - ptyId: null, - worktreeId, - title: id, - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - }) - const payload: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - [localWorktreeId]: [makeTab('local-tab', localWorktreeId)], - [remoteWorktreeId]: [makeTab('remote-tab', remoteWorktreeId)] - } - } - const state = { - repos: [ - { id: 'local-repo', connectionId: null, executionHostId: 'local' }, - { - id: 'remote-repo', - connectionId: null, - executionHostId: 'runtime:env-1' - } - ], - worktreesByRepo: { - 'local-repo': [{ id: localWorktreeId, repoId: 'local-repo' }], - 'remote-repo': [ - { - id: remoteWorktreeId, - repoId: 'remote-repo', - hostId: 'runtime:env-1' - } - ] - } - } satisfies HostPersistenceState - - const snapshots = buildWorkspaceSessionHostSnapshots(payload, state) - - expect(snapshots.map((snapshot) => snapshot.hostId)).toEqual([undefined, 'runtime:env-1']) - expect(snapshots[0].state.tabsByWorktree).toEqual({ - [localWorktreeId]: [expect.objectContaining({ id: 'local-tab' })] - }) - expect(snapshots[1].state.tabsByWorktree).toEqual({ - [remoteWorktreeId]: [expect.objectContaining({ id: 'remote-tab' })] - }) - }) }) describe('patchWorkspaceSessionByHost write outcome', () => { @@ -600,105 +378,3 @@ describe('patchWorkspaceSessionByHost tab-keyed routing', () => { ) }) }) - -describe('buildHostIdByWorktreeId nested ownership', () => { - it('persists an SSH worktree in its paired HUB session partition', () => { - const worktreeId = 'nested-repo::/srv/remote-wt' - const owner = buildHostIdByWorktreeId({ - repos: [ - { - id: 'nested-repo', - connectionId: 'hub-private-ssh', - executionHostId: 'runtime:owner-hub' - } - ], - worktreesByRepo: { - 'nested-repo': [ - { - id: worktreeId, - repoId: 'nested-repo', - hostId: 'ssh:hub-private-ssh', - runtimeOwnerEnvironmentId: 'owner-hub' - } - ] - } - }) - - expect(owner(worktreeId)).toBe('runtime:owner-hub') - }) -}) - -describe('persistWorkspaceSessionByHost', () => { - it('awaits every host write before crossing the durable flush boundary', async () => { - const localWorktreeId = 'local-repo::/src/local' - const remoteWorktreeId = 'remote-repo::/srv/remote' - const payload: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - [localWorktreeId]: [ - { - id: 'local-tab', - ptyId: null, - worktreeId: localWorktreeId, - title: 'Local', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } - ], - [remoteWorktreeId]: [ - { - id: 'remote-tab', - ptyId: null, - worktreeId: remoteWorktreeId, - title: 'Remote', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } - ] - } - } - const set = vi.fn().mockResolvedValue(undefined) - const flush = vi.fn().mockResolvedValue(undefined) - - await persistWorkspaceSessionByHost({ get: vi.fn(), set, patch: vi.fn(), flush }, payload, { - repos: [ - { id: 'local-repo', connectionId: null, executionHostId: 'local' }, - { - id: 'remote-repo', - connectionId: null, - executionHostId: 'runtime:env-1' - } - ], - worktreesByRepo: { - 'local-repo': [{ id: localWorktreeId, repoId: 'local-repo' }], - 'remote-repo': [ - { - id: remoteWorktreeId, - repoId: 'remote-repo', - hostId: 'runtime:env-1' - } - ] - } - }) - - expect(set).toHaveBeenCalledTimes(2) - expect(set).toHaveBeenCalledWith( - expect.objectContaining({ - tabsByWorktree: { [localWorktreeId]: expect.any(Array) } - }), - undefined - ) - expect(set).toHaveBeenCalledWith( - expect.objectContaining({ - tabsByWorktree: { [remoteWorktreeId]: expect.any(Array) } - }), - 'runtime:env-1' - ) - expect(flush).toHaveBeenCalledTimes(1) - expect(set.mock.invocationCallOrder.at(-1)).toBeLessThan(flush.mock.invocationCallOrder[0]!) - }) -}) diff --git a/src/renderer/src/lib/workspace-session-host-split.test.ts b/src/renderer/src/lib/workspace-session-host-split.test.ts deleted file mode 100644 index d831430d33d..00000000000 --- a/src/renderer/src/lib/workspace-session-host-split.test.ts +++ /dev/null @@ -1,500 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { - splitWorkspaceSessionByHost, - mergeWorkspaceSessionsFromHosts, - type HostIdByWorktreeId -} from './workspace-session-host-split' -import { getDefaultWorkspaceSession } from '../../../shared/constants' -import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' -import { HOST_PARTITION_REDUNDANT_GLOBAL_FIELDS } from '../../../shared/workspace-session-host-field-ownership' -import type { BrowserPage } from '../../../shared/browser-workspace-types' -import type { Tab } from '../../../shared/tab-types' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' -import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' - -const RUNTIME_A: ExecutionHostId = 'runtime:env-a' -const RUNTIME_B: ExecutionHostId = 'runtime:env-b' - -function makeTab(id: string, worktreeId: string): TerminalTab { - return { - id, - ptyId: null, - worktreeId, - title: id, - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } -} - -function makeUnifiedTab(id: string, worktreeId: string): Tab { - return { - id, - entityId: id, - groupId: `group-${worktreeId}`, - worktreeId, - contentType: 'terminal', - label: id, - customLabel: null, - color: null, - sortOrder: 0, - createdAt: 1 - } -} - -function makeLayout(): TerminalLayoutSnapshot { - return { root: { type: 'leaf', leafId: 'leaf-1' }, activeLeafId: 'leaf-1', expandedLeafId: null } -} - -function makeBrowserPage(id: string, workspaceId: string, worktreeId: string): BrowserPage { - return { - id, - workspaceId, - worktreeId, - url: 'https://example.com', - title: 'Example', - loading: false, - faviconUrl: null, - canGoBack: false, - canGoForward: false, - loadError: null, - createdAt: 1 - } -} - -/** worktree id convention in these tests: `-wt-...`, except local ones. */ -function ownerByPrefix(): HostIdByWorktreeId { - return (worktreeId: string) => { - if (worktreeId.startsWith('a-')) { - return RUNTIME_A - } - if (worktreeId.startsWith('b-')) { - return RUNTIME_B - } - return LOCAL_EXECUTION_HOST_ID - } -} - -describe('splitWorkspaceSessionByHost', () => { - it('keeps global fields only on the local slice', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - activeRepoId: 'repo-1', - activeWorktreeId: 'local-wt', - activeTabId: 'tab-1', - browserUrlHistory: [ - { url: 'u', normalizedUrl: 'u', title: 't', lastVisitedAt: 1, visitCount: 1 } - ], - activeConnectionIdsAtShutdown: ['ssh-target'] - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[LOCAL_EXECUTION_HOST_ID]?.activeRepoId).toBe('repo-1') - expect(slices[LOCAL_EXECUTION_HOST_ID]?.activeConnectionIdsAtShutdown).toEqual(['ssh-target']) - // No runtime slice is created when nothing is worktree-owned by it. - expect(slices[RUNTIME_A]).toBeUndefined() - }) - - it('never replicates a local-owned global onto a non-local slice', () => { - // One global template copied local browserUrlHistory into every runtime, undoing #18161 on the next save (~66KB/host at 200 entries). - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - browserUrlHistory: [ - { url: 'u', normalizedUrl: 'u', title: 't', lastVisitedAt: 1, visitCount: 1 } - ], - workspaceDocHistory: [ - { - docLocation: { kind: 'workspace-doc', worktreeId: 'local-wt', filePath: 'a.md' }, - title: 'a.md', - lastVisitedAt: 2, - visitCount: 1 - } - ], - tabsByWorktree: { - 'local-wt': [makeTab('t-local', 'local-wt')], - 'a-wt': [makeTab('t-a', 'a-wt')], - 'b-wt': [makeTab('t-b', 'b-wt')] - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(Object.keys(slices).sort()).toEqual([LOCAL_EXECUTION_HOST_ID, RUNTIME_A, RUNTIME_B]) - for (const field of HOST_PARTITION_REDUNDANT_GLOBAL_FIELDS) { - expect(slices[LOCAL_EXECUTION_HOST_ID]?.[field]).toEqual(state[field]) - expect(Object.hasOwn(slices[RUNTIME_A] ?? {}, field)).toBe(false) - expect(Object.hasOwn(slices[RUNTIME_B] ?? {}, field)).toBe(false) - } - // Local already supplies globals, so merge never falls back to remote copies. - const merged = mergeWorkspaceSessionsFromHosts(slices) - for (const field of HOST_PARTITION_REDUNDANT_GLOBAL_FIELDS) { - expect(merged[field]).toEqual(state[field]) - } - }) - - it('routes worktree-keyed maps to their owner host', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { - 'local-wt': [makeTab('t-local', 'local-wt')], - 'a-wt': [makeTab('t-a', 'a-wt')], - 'b-wt': [makeTab('t-b', 'b-wt')] - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(Object.keys(slices[LOCAL_EXECUTION_HOST_ID]?.tabsByWorktree ?? {})).toEqual(['local-wt']) - expect(Object.keys(slices[RUNTIME_A]?.tabsByWorktree ?? {})).toEqual(['a-wt']) - expect(Object.keys(slices[RUNTIME_B]?.tabsByWorktree ?? {})).toEqual(['b-wt']) - }) - - it('routes host-qualified visit recency to the partition the key names', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - lastVisitedAtByWorktreeId: { - 'local-wt': 1, - 'a-wt': 2, - 'ssh:builder|ssh-wt': 3, - 'runtime:env-a|a-wt': 4 - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - // Host-qualified recency must share its workspace’s partition or adoption records visits without corresponding tabs (#12721). - expect(slices[LOCAL_EXECUTION_HOST_ID]?.lastVisitedAtByWorktreeId).toEqual({ 'local-wt': 1 }) - expect(slices[RUNTIME_A]?.lastVisitedAtByWorktreeId).toEqual({ - 'a-wt': 2, - 'runtime:env-a|a-wt': 4 - }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the literal is a well-formed ssh: host id; ExecutionHostId is a template-literal type a plain string cannot satisfy. - expect(slices['ssh:builder' as ExecutionHostId]?.lastVisitedAtByWorktreeId).toEqual({ - 'ssh:builder|ssh-wt': 3 - }) - }) - - it('routes tab-keyed maps via the owning tab worktree (legacy + unified)', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { 'a-wt': [makeTab('t-a', 'a-wt')] }, - unifiedTabs: { 'b-wt': [makeUnifiedTab('t-b', 'b-wt')] }, - terminalLayoutsByTabId: { 't-a': makeLayout(), 't-b': makeLayout() }, - remoteSessionIdsByTabId: { 't-a': 'sess-a', 't-b': 'sess-b' } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.terminalLayoutsByTabId).toHaveProperty('t-a') - expect(slices[RUNTIME_B]?.terminalLayoutsByTabId).toHaveProperty('t-b') - expect(slices[RUNTIME_A]?.remoteSessionIdsByTabId).toEqual({ 't-a': 'sess-a' }) - expect(slices[RUNTIME_B]?.remoteSessionIdsByTabId).toEqual({ 't-b': 'sess-b' }) - }) - - it('keeps orphan tab layouts (unknown worktree) in the local slice', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - terminalLayoutsByTabId: { orphan: makeLayout() } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[LOCAL_EXECUTION_HOST_ID]?.terminalLayoutsByTabId).toHaveProperty('orphan') - expect(slices[RUNTIME_A]).toBeUndefined() - }) - - it('routes tab-keyed rows through a caller-supplied tab index when the payload has no tab rows', () => { - // Layouts/PTY-only patches omit tabsByWorktree; use the live tab index for ownership. - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - terminalLayoutsByTabId: { 't-a': makeLayout() }, - remoteSessionIdsByTabId: { 't-a': 'sess-a' }, - terminalPtyIncarnationsByPaneKey: { 't-a:leaf-1': 'inc-3' } - } - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix(), { - worktreeIdByTabId: new Map([['t-a', 'a-wt-1']]) - }) - expect(slices[RUNTIME_A]?.terminalLayoutsByTabId).toHaveProperty('t-a') - expect(slices[RUNTIME_A]?.remoteSessionIdsByTabId).toEqual({ 't-a': 'sess-a' }) - expect(slices[RUNTIME_A]?.terminalPtyIncarnationsByPaneKey).toEqual({ 't-a:leaf-1': 'inc-3' }) - expect(slices[LOCAL_EXECUTION_HOST_ID]?.terminalLayoutsByTabId).toEqual({}) - expect(slices[LOCAL_EXECUTION_HOST_ID]?.remoteSessionIdsByTabId).toEqual({}) - }) - - it('keeps a tab the supplied index does not name in the local slice', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - terminalLayoutsByTabId: { orphan: makeLayout() } - } - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix(), { - worktreeIdByTabId: new Map([['t-a', 'a-wt-1']]) - }) - expect(slices[LOCAL_EXECUTION_HOST_ID]?.terminalLayoutsByTabId).toHaveProperty('orphan') - expect(slices[RUNTIME_A]).toBeUndefined() - }) - - it('routes browser pages via their record worktreeId', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - browserPagesByWorkspace: { - 'ws-a': [makeBrowserPage('p-a', 'ws-a', 'a-wt')], - 'ws-local': [makeBrowserPage('p-local', 'ws-local', 'local-wt')] - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.browserPagesByWorkspace).toHaveProperty('ws-a') - expect(slices[LOCAL_EXECUTION_HOST_ID]?.browserPagesByWorkspace).toHaveProperty('ws-local') - }) - - it('routes markdown frontmatter visibility via the open file worktree', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - openFilesByWorktree: { - 'a-wt': [ - { - filePath: '/a/file.md', - relativePath: 'file.md', - worktreeId: 'a-wt', - language: 'markdown' - } - ] - }, - markdownFrontmatterVisible: { '/a/file.md': true, '/unknown.md': true } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.markdownFrontmatterVisible).toEqual({ '/a/file.md': true }) - // Unknown file id has no owner β†’ stays local. - expect(slices[LOCAL_EXECUTION_HOST_ID]?.markdownFrontmatterVisible).toEqual({ - '/unknown.md': true - }) - }) - - it('partitions activeWorktreeIdsOnShutdown by owner', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - activeWorktreeIdsOnShutdown: ['a-wt', 'b-wt', 'local-wt'] - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.activeWorktreeIdsOnShutdown).toEqual(['a-wt']) - expect(slices[RUNTIME_B]?.activeWorktreeIdsOnShutdown).toEqual(['b-wt']) - expect(slices[LOCAL_EXECUTION_HOST_ID]?.activeWorktreeIdsOnShutdown).toEqual(['local-wt']) - }) - - it('routes sleeping agent records via their worktreeId', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - sleepingAgentSessionsByPaneKey: { - 'pane-a': { - paneKey: 'pane-a', - worktreeId: 'a-wt', - agent: 'claude', - providerSession: { key: 'session_id', id: 'x' }, - prompt: 'p', - state: 'done', - capturedAt: 1, - updatedAt: 2 - } - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.sleepingAgentSessionsByPaneKey).toHaveProperty('pane-a') - }) - - it('routes terminal incarnation authority with its owning surface', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { 'a-wt': [makeTab('tab-a', 'a-wt')] }, - terminalPtyIncarnationsByPaneKey: { 'tab-a:leaf-a': 'inc-a' }, - terminalSurfaceTombstonesByPaneKey: { - 'tab-b:leaf-b': { - worktreeId: 'b-wt', - parentTabId: 'tab-b', - leafId: 'leaf-b', - ptyId: 'pty-b', - incarnationId: 'inc-b', - retiredAt: 1 - } - } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[RUNTIME_A]?.terminalPtyIncarnationsByPaneKey).toEqual({ - 'tab-a:leaf-a': 'inc-a' - }) - expect(slices[RUNTIME_B]?.terminalSurfaceTombstonesByPaneKey).toHaveProperty('tab-b:leaf-b') - }) - - it('does not send host-private topology authority through renderer partitions', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { 'a-wt': [makeTab('tab-a', 'a-wt')] }, - terminalTopologyRevisionByRepoId: { 'a-repo': 7 } - } - - const slices = splitWorkspaceSessionByHost(state, ownerByPrefix()) - - expect(slices[LOCAL_EXECUTION_HOST_ID]?.terminalTopologyRevisionByRepoId).toBeUndefined() - expect(slices[RUNTIME_A]?.terminalTopologyRevisionByRepoId).toBeUndefined() - }) -}) - -describe('mergeWorkspaceSessionsFromHosts', () => { - it('takes global fields from the local slice', () => { - const merged = mergeWorkspaceSessionsFromHosts({ - [LOCAL_EXECUTION_HOST_ID]: { - ...getDefaultWorkspaceSession(), - activeRepoId: 'local-repo', - activeWorktreeId: 'local-wt' - }, - [RUNTIME_A]: { - ...getDefaultWorkspaceSession(), - // A non-local slice's global fields must lose to local. - activeRepoId: 'runtime-repo', - tabsByWorktree: { 'a-wt': [makeTab('t-a', 'a-wt')] } - } - }) - - expect(merged.activeRepoId).toBe('local-repo') - expect(merged.tabsByWorktree).toHaveProperty('a-wt') - }) - - it('falls back to a non-local slice for globals when local is absent', () => { - const merged = mergeWorkspaceSessionsFromHosts({ - [RUNTIME_A]: { - ...getDefaultWorkspaceSession(), - activeRepoId: 'runtime-repo' - } - }) - expect(merged.activeRepoId).toBe('runtime-repo') - }) - - it('tolerates missing and empty slices', () => { - const merged = mergeWorkspaceSessionsFromHosts({}) - expect(merged.tabsByWorktree).toBeUndefined() - expect(() => mergeWorkspaceSessionsFromHosts({ [RUNTIME_A]: undefined })).not.toThrow() - }) - - it('does not merge host-private topology authority into unified renderer state', () => { - const merged = mergeWorkspaceSessionsFromHosts({ - [LOCAL_EXECUTION_HOST_ID]: { - ...getDefaultWorkspaceSession(), - terminalTopologyRevisionByRepoId: { duplicate: 3 } - }, - [RUNTIME_A]: { - ...getDefaultWorkspaceSession(), - terminalTopologyRevisionByRepoId: { duplicate: 9 } - } - }) - - expect(merged.terminalTopologyRevisionByRepoId).toBeUndefined() - }) -}) - -describe('split β†’ merge round trip', () => { - function roundTrip(state: WorkspaceSessionState): WorkspaceSessionState { - return mergeWorkspaceSessionsFromHosts(splitWorkspaceSessionByHost(state, ownerByPrefix())) - } - - it('preserves a representative multi-host state', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - activeRepoId: 'repo-1', - activeWorktreeId: 'a-wt', - activeTabId: 't-a', - tabsByWorktree: { - 'local-wt': [makeTab('t-local', 'local-wt')], - 'a-wt': [makeTab('t-a', 'a-wt')], - 'b-wt': [makeTab('t-b', 'b-wt')] - }, - unifiedTabs: { 'b-wt': [makeUnifiedTab('t-b', 'b-wt')] }, - terminalLayoutsByTabId: { 't-local': makeLayout(), 't-a': makeLayout(), 't-b': makeLayout() }, - remoteSessionIdsByTabId: { 't-a': 'sess-a' }, - activeTabIdByWorktree: { 'local-wt': 't-local', 'a-wt': 't-a' }, - activeWorktreeIdsOnShutdown: ['a-wt', 'b-wt'], - lastVisitedAtByWorktreeId: { 'a-wt': 10, 'local-wt': 5 }, - defaultTerminalTabsAppliedByWorktreeId: { 'a-wt': true }, - browserTabsByWorktree: {}, - browserPagesByWorkspace: {}, - browserUrlHistory: [ - { url: 'u', normalizedUrl: 'u', title: 't', lastVisitedAt: 1, visitCount: 1 } - ] - } - - expect(roundTrip(state)).toEqual(state) - }) - - it('preserves the default (empty) session', () => { - const state = getDefaultWorkspaceSession() - expect(roundTrip(state)).toEqual(state) - }) - - it('keeps orphan-owned entries in local and preserves them', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - terminalLayoutsByTabId: { orphan: makeLayout() }, - remoteSessionIdsByTabId: { orphan: 'sess' } - } - const result = roundTrip(state) - expect(result.terminalLayoutsByTabId).toEqual(state.terminalLayoutsByTabId) - expect(result.remoteSessionIdsByTabId).toEqual(state.remoteSessionIdsByTabId) - }) - - it('handles a host with only runtime-owned worktrees (empty local maps)', () => { - const state: WorkspaceSessionState = { - ...getDefaultWorkspaceSession(), - tabsByWorktree: { 'a-wt': [makeTab('t-a', 'a-wt')] }, - terminalLayoutsByTabId: { 't-a': makeLayout() } - } - expect(roundTrip(state)).toEqual(state) - }) -}) - -/** Pin both local-precedence rules used by main’s nonlocal-global pruning or it could discard values renderer merge would read. */ -describe('mergeWorkspaceSessionsFromHosts global-field precedence', () => { - const localEntry = { - url: 'local', - normalizedUrl: 'local', - title: 'l', - lastVisitedAt: 2, - visitCount: 1 - } - const hostEntry = { - url: 'host', - normalizedUrl: 'host', - title: 'h', - lastVisitedAt: 1, - visitCount: 1 - } - - it("takes a global field from 'local' whenever local has one, ignoring every other slice", () => { - const merged = mergeWorkspaceSessionsFromHosts({ - [LOCAL_EXECUTION_HOST_ID]: { - ...getDefaultWorkspaceSession(), - browserUrlHistory: [localEntry] - }, - [RUNTIME_A]: { ...getDefaultWorkspaceSession(), browserUrlHistory: [hostEntry] } - }) - expect(merged.browserUrlHistory).toEqual([localEntry]) - }) - - it('falls back to another slice only when local does not have the field', () => { - const local = getDefaultWorkspaceSession() - delete local.browserUrlHistory - const merged = mergeWorkspaceSessionsFromHosts({ - [LOCAL_EXECUTION_HOST_ID]: local, - [RUNTIME_A]: { ...getDefaultWorkspaceSession(), browserUrlHistory: [hostEntry] } - }) - expect(merged.browserUrlHistory).toEqual([hostEntry]) - }) -}) diff --git a/src/renderer/src/lib/workspace-session-hydration-keys.test.ts b/src/renderer/src/lib/workspace-session-hydration-keys.test.ts deleted file mode 100644 index 54248e45a38..00000000000 --- a/src/renderer/src/lib/workspace-session-hydration-keys.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' -import { - collectFolderWorkspaceKeysFromSession, - collectWorktreeHydrationRepoIdsFromSession -} from './workspace-session-hydration-keys' - -describe('collectFolderWorkspaceKeysFromSession', () => { - it('keeps folder workspace selection markers valid without scheduling a Git scan', () => { - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: {}, - activeTabTypeByWorktree: { 'folder:folder-1': 'terminal' } - } as unknown as WorkspaceSessionState - - expect(collectFolderWorkspaceKeysFromSession(session)).toEqual(['folder:folder-1']) - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual([]) - }) -}) - -describe('collectWorktreeHydrationRepoIdsFromSession', () => { - it('includes persisted terminal tabs and ignores folder workspaces', () => { - const session = { - activeWorktreeIdsOnShutdown: [ - 'repo-a::/worktree-a', - 'repo-b::/worktree-b', - 'folder:folder-1' - ], - tabsByWorktree: { - 'repo-a::/worktree-a': [{ ptyId: 'pty-a' }], - 'repo-b::/worktree-b': [{ ptyId: null }], - 'folder:folder-1': [{ ptyId: 'pty-folder' }] - } - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-a', 'repo-b']) - }) - - it('recognizes split-pane and remote persisted sessions', () => { - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - terminalLayoutsByTabId: { - 'tab-a': { ptyIdsByLeafId: { 'pane:1': 'pty-a' } } - }, - tabsByWorktree: { - 'repo-a::/worktree-a': [{ id: 'tab-a', ptyId: null }], - 'repo-b::/worktree-b': [{ id: 'tab-b', ptyId: null }] - }, - remoteSessionIdsByTabId: { 'tab-b': 'remote-session' } - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-a', 'repo-b']) - }) - - it('matches canonical session keys against raw shutdown worktree IDs', () => { - const rawWorktreeId = 'repo-a::/worktree-a' - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - activeWorktreeIdsOnShutdown: [rawWorktreeId], - tabsByWorktree: { - [`worktree:${rawWorktreeId}`]: [{ ptyId: 'pty-a' }] - } - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-a']) - }) - - it('excludes runtime-owned session worktrees for raw and canonical owner keys', () => { - const rawWorktreeId = 'repo-a::/remote/worktree' - const canonicalWorktreeKey = `worktree:${rawWorktreeId}` - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: { - [canonicalWorktreeKey]: [{ ptyId: 'pty-a' }] - } - } as unknown as WorkspaceSessionState - - expect( - collectWorktreeHydrationRepoIdsFromSession(session, { - [rawWorktreeId]: 'runtime:env-1' - }) - ).toEqual([]) - expect( - collectWorktreeHydrationRepoIdsFromSession(session, { - [canonicalWorktreeKey]: 'runtime:env-1' - }) - ).toEqual([]) - }) - - it('keeps SSH-owned session worktrees eligible for local recovery routing', () => { - const rawWorktreeId = 'repo-a::/ssh/worktree' - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: { - [rawWorktreeId]: [{ ptyId: 'ssh:ssh-target@@pty-a' }] - } - } as unknown as WorkspaceSessionState - - expect( - collectWorktreeHydrationRepoIdsFromSession(session, { - [rawWorktreeId]: 'ssh:ssh-target' - }) - ).toEqual(['repo-a']) - }) - - it('returns repository IDs in deterministic order', () => { - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: { - 'repo-b::/worktree-b': [{ ptyId: 'pty-b' }], - 'repo-a::/worktree-a': [{ ptyId: 'pty-a' }] - } - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-a', 'repo-b']) - }) - - it('excludes repositories referenced only by unbounded history maps (visit recency, default tabs)', () => { - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: { 'repo-chrome::/wt': [{ ptyId: 'pty-a' }] }, - lastVisitedAtByWorktreeId: { - 'repo-chrome::/wt': 10, - 'repo-history-only::/visited': 20 - }, - defaultTerminalTabsAppliedByWorktreeId: { - 'repo-default-only::/applied': true - } - } as unknown as WorkspaceSessionState - - // Why: history-only repos hydrate their maps unfiltered and would bloat the selective fetch. - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-chrome']) - }) - - it('does not scan repositories retained only by empty chrome maps or selection markers', () => { - const emptyTabsByWorktree = Object.fromEntries( - Array.from({ length: 326 }, (_, index) => [`repo-empty-${index}::/wt`, []]) - ) - const staleActiveTabTypes = Object.fromEntries( - Array.from({ length: 326 }, (_, index) => [`repo-empty-${index}::/wt`, 'terminal']) - ) - const session = { - activeRepoId: null, - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: { - ...emptyTabsByWorktree, - 'repo-live::/wt': [{ ptyId: 'pty-a' }] - }, - openFilesByWorktree: { 'repo-empty-editor::/wt': [] }, - browserTabsByWorktree: { 'repo-empty-browser::/wt': [] }, - activeFileIdByWorktree: { 'repo-empty-active-file::/wt': '/stale/file.ts' }, - activeBrowserTabIdByWorktree: { 'repo-empty-active-browser::/wt': 'stale-browser' }, - activeTabIdByWorktree: { 'repo-empty-active-terminal::/wt': 'stale-terminal' }, - activeGroupIdByWorktree: { 'repo-empty-active-group::/wt': 'stale-group' }, - activeTabTypeByWorktree: staleActiveTabTypes - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-live']) - }) - - it('includes a repository referenced only by activeRepoId (no active worktree, no tabs)', () => { - const session = { - activeRepoId: 'repo-active-only', - activeWorktreeId: null, - activeTabId: null, - tabsByWorktree: {} - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual(['repo-active-only']) - }) - - it('includes repositories referenced only by active, editor, browser, or sleeping-agent state', () => { - const session = { - activeRepoId: null, - activeWorktreeId: 'repo-active::/active', - activeTabId: null, - tabsByWorktree: {}, - terminalLayoutsByTabId: {}, - openFilesByWorktree: { 'repo-editor::/editor': [{ filePath: '/editor/file.ts' }] }, - browserTabsByWorktree: { 'repo-browser::/browser': [{ id: 'browser-a' }] }, - sleepingAgentSessionsByPaneKey: { - 'tab:leaf': { worktreeId: 'repo-agent::/agent' } - } - } as unknown as WorkspaceSessionState - - expect(collectWorktreeHydrationRepoIdsFromSession(session)).toEqual([ - 'repo-active', - 'repo-agent', - 'repo-browser', - 'repo-editor' - ]) - }) -}) diff --git a/src/shared/emoji-shortcode-catalog.ts b/src/shared/emoji-shortcode-catalog.ts index 51907f26b7b..d29f6849bbe 100644 --- a/src/shared/emoji-shortcode-catalog.ts +++ b/src/shared/emoji-shortcode-catalog.ts @@ -57,11 +57,6 @@ export function getStandardEmojiShortcodeEntries(): readonly StandardEmojiShortc return loadCatalog().entries } -/** Test-only probe for the lazy-boundary guard; never branch on this in product code. */ -export function isEmojiShortcodeCatalogBuiltForTest(): boolean { - return catalog !== null -} - /** Prefer spelled-out names over symbolic +1/-1, cryptic no/v and flag_xx namespaces for branch/directory labels. */ function primaryShortcode(shortcodes: readonly string[]): string { const named = shortcodes.filter((candidate) => /^[a-z]/i.test(candidate))