mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
test: remove 75 low-value renderer store tests (#37658)
This commit is contained in:
@@ -11478,7 +11478,6 @@
|
||||
"src/renderer/src/store/slices/worktrees/teardown/remove-worktree-shared-records.test.ts": 2086,
|
||||
"src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.test.ts": 2481,
|
||||
"src/renderer/src/store/slices/worktrees/teardown/worktree-delete-state.test.ts": 1160,
|
||||
"src/renderer/src/store/slices/wsl-hosts.test.ts": 1908,
|
||||
"src/renderer/src/store/slices/wsl-moved-project-chat-hydration.test.ts": 1658,
|
||||
"src/renderer/src/store/slices/wsl-moved-project-chat-purge.test.ts": 1222,
|
||||
"src/renderer/src/store/store-identity-churn-probe.test.ts": 67,
|
||||
@@ -11486,17 +11485,11 @@
|
||||
"src/renderer/src/store/terminals/restored-relay-session-identity.test.ts": 2196,
|
||||
"src/renderer/src/store/terminals/restored-terminal-rebind-restarts.test.ts": 1886,
|
||||
"src/renderer/src/store/terminals/structured-chat-tab-rename.test.ts": 3154,
|
||||
"src/renderer/src/store/terminals/terminal-chat-pane-layout-owner.test.ts": 1753,
|
||||
"src/renderer/src/store/terminals/terminal-exited-pane-liveness.test.ts": 1721,
|
||||
"src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1763,
|
||||
"src/renderer/src/store/terminals/terminal-pane-expansion-write-bailout.test.tsx": 1927,
|
||||
"src/renderer/src/store/terminals/terminal-shutdown-guards-identity.test.ts": 47,
|
||||
"src/renderer/src/store/terminals/terminal-sleep-layout-owner.test.ts": 58,
|
||||
"src/renderer/src/store/terminals/terminal-startup-queue-restart.test.ts": 1895,
|
||||
"src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1945,
|
||||
"src/renderer/src/store/terminals/terminal-tab-create-through-layout-owner.test.ts": 1863,
|
||||
"src/renderer/src/store/terminals/terminal-tab-mirrored-id.test.ts": 1662,
|
||||
"src/renderer/src/store/terminals/terminal-workspace-routing.scale.test.ts": 104,
|
||||
"src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 302,
|
||||
"src/renderer/src/store/use-runtime-status-view.test.tsx": 1979,
|
||||
"src/renderer/src/store/workspace-layout-cache.test.ts": 63,
|
||||
@@ -11504,7 +11497,6 @@
|
||||
"src/renderer/src/store/workspace-record-selectors.test.ts": 93,
|
||||
"src/renderer/src/store/worktree-diff-comments-selector.test.ts": 346,
|
||||
"src/renderer/src/store/worktree-host-collision-index.test.ts": 1875,
|
||||
"src/renderer/src/store/worktree-visibility-defaults-by-host.test.ts": 45,
|
||||
"src/renderer/src/web/preload-api/web-agent-session-attachment-upload.test.ts": 1588,
|
||||
"src/renderer/src/web/preload-api/web-ai-vault-search-compat.test.ts": 306,
|
||||
"src/renderer/src/web/preload-api/web-chat-permission-normalization.test.ts": 293,
|
||||
|
||||
-35
@@ -106,18 +106,6 @@ describe('confirmed discards apply only to the confirmed removal', () => {
|
||||
expect(shownContents()).toEqual(['typed after confirming'])
|
||||
})
|
||||
|
||||
it('reaches the teardown through removeWorktree', async () => {
|
||||
vi.stubGlobal('window', { api: createStoreCascadesMockApi() })
|
||||
const store = dirtyStore([GONE])
|
||||
const confirmed = confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: 'local' }])
|
||||
await store.getState().removeWorktree({ id: GONE, executionHostId: 'local' }, false, {
|
||||
confirmedDiscards: confirmed
|
||||
})
|
||||
expect(store.getState().openFiles).toEqual([])
|
||||
await settle()
|
||||
expect(show).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(purgePaths)('%s still offers text a dialog confirmed', async (_, purge, host) => {
|
||||
const store = dirtyStore([GONE], host)
|
||||
confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: host ?? 'local' }])
|
||||
@@ -156,19 +144,6 @@ describe('delayed text is committed before a purge', () => {
|
||||
panes.forEach((unregister) => unregister())
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves a surviving workspace’s CSV cell editing', () => {
|
||||
const survivor = `${TEST_REPO.id}::/survivor`
|
||||
const store = dirtyStore([GONE, survivor])
|
||||
const cell = vi.fn()
|
||||
const unregister = registerPendingEditorFlush(`${survivor}/file.ts`, cell, () => true)
|
||||
try {
|
||||
store.getState().purgeWorktreeTerminalState([GONE])
|
||||
expect(cell).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
unregister()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('the closed-files notice', () => {
|
||||
@@ -237,14 +212,4 @@ describe('the closed-files notice', () => {
|
||||
unregister()
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to a sharing tab’s draft when the dirty tab has none', async () => {
|
||||
const store = dirtyStore([GONE])
|
||||
const tab = (id: string) =>
|
||||
makeOpenFile({ id, filePath: '/w/shared.ts', worktreeId: GONE, isDirty: true })
|
||||
store.setState({ openFiles: [tab('first'), tab('second')], editorDrafts: { second: 'kept' } })
|
||||
store.getState().purgeWorktreeTerminalState([GONE])
|
||||
await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1))
|
||||
expect(shownContents()).toEqual(['kept'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import type { WslHostStatus } from '../../../../shared/wsl-host-types'
|
||||
import { createTestStore } from './store-test-helpers'
|
||||
|
||||
it('publishes a deferral-only change for an already-ready WSL host', () => {
|
||||
const store = createTestStore()
|
||||
const ready: WslHostStatus = {
|
||||
environmentId: 'wsl-ubuntu',
|
||||
distro: 'Ubuntu',
|
||||
state: 'ready',
|
||||
detail: null
|
||||
}
|
||||
store.getState().applyWslHostStatus(ready)
|
||||
const deferred: WslHostStatus = {
|
||||
...ready,
|
||||
deferredUpdate: {
|
||||
candidateVersion: 'next',
|
||||
code: 'live-terminals',
|
||||
reason: 'Terminals are still running.',
|
||||
forceable: true
|
||||
}
|
||||
}
|
||||
store.getState().applyWslHostStatus(deferred)
|
||||
expect(store.getState().wslHostStatuses[ready.environmentId]).toEqual(deferred)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { create, type StoreApi } from 'zustand'
|
||||
import { withReactCommitCascadeWriteProbe } from './react-commit-cascade-write-probe'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { create } from 'zustand'
|
||||
import './react-commit-cascade-write-probe'
|
||||
import {
|
||||
armStoreIdentityChurnProbe,
|
||||
disarmStoreIdentityChurnProbe,
|
||||
@@ -41,53 +41,6 @@ describe('store identity churn probe', () => {
|
||||
disarmStoreIdentityChurnProbe()
|
||||
})
|
||||
|
||||
it('flags a refresh that rebuilds an array with unchanged contents', () => {
|
||||
const store = createProbeStore()
|
||||
armStoreIdentityChurnProbe()
|
||||
|
||||
store.getState().refresh([{ id: 'a', label: 'A' }])
|
||||
|
||||
expect(churnFor('rows')).toBe(1)
|
||||
expect(readStoreIdentityChurnReport()[0]).toMatchObject({
|
||||
field: 'rows',
|
||||
churnedWrites: 1,
|
||||
replacedWrites: 1,
|
||||
sites: []
|
||||
})
|
||||
})
|
||||
|
||||
it('flags a keyed update that rewrites an entry with the same value', () => {
|
||||
const store = createProbeStore()
|
||||
armStoreIdentityChurnProbe()
|
||||
|
||||
store.getState().touch('a', 'idle')
|
||||
|
||||
expect(churnFor('entries')).toBe(1)
|
||||
})
|
||||
|
||||
it('does not flag writes that change the value', () => {
|
||||
const store = createProbeStore()
|
||||
armStoreIdentityChurnProbe()
|
||||
|
||||
store.getState().refresh([{ id: 'a', label: 'B' }])
|
||||
store.getState().touch('a', 'running')
|
||||
store.getState().bump()
|
||||
|
||||
expect(readStoreIdentityChurnReport()).toEqual([])
|
||||
})
|
||||
|
||||
it('names the write site when capture is requested', () => {
|
||||
const store = createProbeStore()
|
||||
armStoreIdentityChurnProbe({ captureSites: true })
|
||||
|
||||
store.getState().refresh([{ id: 'a', label: 'A' }])
|
||||
|
||||
const [row] = readStoreIdentityChurnReport()
|
||||
expect(row.sites).toHaveLength(1)
|
||||
expect(row.sites[0]).toMatchObject({ churnedWrites: 1 })
|
||||
expect(row.sites[0].site).toContain('store-identity-churn-probe.test')
|
||||
})
|
||||
|
||||
it('leaves a functional updater to zustand: called once, with the live state', () => {
|
||||
const store = createProbeStore()
|
||||
const seen: unknown[] = []
|
||||
@@ -107,81 +60,4 @@ describe('store identity churn probe', () => {
|
||||
expect(store.getState().counter).toBe(1)
|
||||
expect(churnFor('rows')).toBe(1)
|
||||
})
|
||||
|
||||
it('passes disarmed writes straight through without reading state', () => {
|
||||
const innerSet = vi.fn()
|
||||
const innerGet = vi.fn(() => ({ counter: 0 }))
|
||||
const api = { setState: innerSet, getState: innerGet } as unknown as StoreApi<{
|
||||
counter: number
|
||||
}>
|
||||
const creator = withStoreIdentityChurnProbe<{ counter: number }>(() => ({ counter: 0 }))
|
||||
creator(innerSet, innerGet, api)
|
||||
const updater = (state: { counter: number }) => ({ counter: state.counter + 1 })
|
||||
|
||||
api.setState(updater, true)
|
||||
|
||||
// The disarmed path forwards the exact arguments and never calls get().
|
||||
expect(innerSet).toHaveBeenCalledTimes(1)
|
||||
expect(innerSet.mock.calls[0]).toEqual([updater, true])
|
||||
expect(innerGet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('composes with the cascade probe without dropping or doubling a write', () => {
|
||||
// Mirrors store/index.ts: churn probe outermost, cascade probe inside it.
|
||||
const store = create<ProbeState>()(
|
||||
withStoreIdentityChurnProbe(
|
||||
withReactCommitCascadeWriteProbe((set) => ({
|
||||
rows: [{ id: 'a', label: 'A' }],
|
||||
entries: { a: { status: 'idle' } },
|
||||
counter: 0,
|
||||
refresh: (rows) => set({ rows }),
|
||||
touch: (id, status) =>
|
||||
set((state) => ({ entries: { ...state.entries, [id]: { status } } })),
|
||||
bump: () => set((state) => ({ counter: state.counter + 1 }))
|
||||
}))
|
||||
)
|
||||
)
|
||||
let updaterCalls = 0
|
||||
armStoreIdentityChurnProbe({ captureSites: true })
|
||||
|
||||
store.getState().bump()
|
||||
store.setState((state) => {
|
||||
updaterCalls += 1
|
||||
return { counter: state.counter + 10 }
|
||||
})
|
||||
store.getState().refresh([{ id: 'a', label: 'A' }])
|
||||
store.setState({ ...store.getState(), counter: 100 }, true)
|
||||
|
||||
expect(updaterCalls).toBe(1)
|
||||
expect(store.getState().counter).toBe(100)
|
||||
expect(store.getState().rows).toEqual([{ id: 'a', label: 'A' }])
|
||||
// The named site is this test, not the sibling probe's wrapper frame.
|
||||
const [row] = readStoreIdentityChurnReport()
|
||||
expect(row).toMatchObject({ field: 'rows', churnedWrites: 1 })
|
||||
expect(row.sites[0].site).toContain('store-identity-churn-probe.test')
|
||||
})
|
||||
|
||||
it('still sees churn on a replace write', () => {
|
||||
const store = createProbeStore()
|
||||
const rows = store.getState().rows
|
||||
armStoreIdentityChurnProbe()
|
||||
|
||||
store.setState({ ...store.getState(), rows: [{ ...rows[0] }] }, true)
|
||||
|
||||
expect(churnFor('rows')).toBe(1)
|
||||
})
|
||||
|
||||
it('treats distinct class instances as changed rather than equal', () => {
|
||||
const store = create<{ value: unknown; put: (value: unknown) => void }>()(
|
||||
withStoreIdentityChurnProbe((set) => ({
|
||||
value: new Map([['a', 1]]),
|
||||
put: (value) => set({ value })
|
||||
}))
|
||||
)
|
||||
armStoreIdentityChurnProbe()
|
||||
|
||||
store.getState().put(new Map([['a', 1]]))
|
||||
|
||||
expect(readStoreIdentityChurnReport()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
|
||||
import { buildWorkspaceSessionPayload } from '@/lib/workspace-session'
|
||||
import { getOrphanTerminalIds } from '../slices/terminal-orphan-helpers'
|
||||
import '../slices/terminal-orphan-helpers'
|
||||
import { createTestStore, makeTab, makeWorktree } from '../slices/store-test-helpers'
|
||||
|
||||
const layoutOwner = vi.hoisted(() => ({ isMain: false }))
|
||||
@@ -94,57 +94,5 @@ describe.each([false, true])(
|
||||
expect(payload.activeWorktreeIdsOnShutdown).toContain(WORKTREE_ID)
|
||||
expect(payload.activeConnectionIdsAtShutdown).toEqual([TARGET_ID])
|
||||
})
|
||||
|
||||
it('rebinds the restored tab to its live relay PTY and keeps publishing that id', async () => {
|
||||
const store = seedStore()
|
||||
store.getState().hydrateWorkspaceSession(persistedSession())
|
||||
|
||||
await store.getState().reconnectPersistedTerminals()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].ptyId).toBe(RELAY_PTY_ID)
|
||||
expect(store.getState().ptyIdsByTabId[TAB_ID]).toEqual([RELAY_PTY_ID])
|
||||
expect(buildWorkspaceSessionPayload(store.getState()).remoteSessionIdsByTabId).toEqual({
|
||||
[TAB_ID]: RELAY_PTY_ID
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a deferred relay session id while the host is disconnected', async () => {
|
||||
const store = seedStore('disconnected')
|
||||
store.getState().hydrateWorkspaceSession(persistedSession())
|
||||
await store.getState().reconnectPersistedTerminals()
|
||||
|
||||
expect(store.getState().deferredSshSessionIdsByTabId[TAB_ID]).toBe(RELAY_PTY_ID)
|
||||
// Loss of contact is not evidence the remote PTY exited, so the handle survives into the next write.
|
||||
expect(buildWorkspaceSessionPayload(store.getState()).remoteSessionIdsByTabId).toEqual({
|
||||
[TAB_ID]: RELAY_PTY_ID
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves a tab whose relay handle is unverifiable alone instead of retiring it', async () => {
|
||||
const store = seedStore('disconnected')
|
||||
store.getState().hydrateWorkspaceSession(persistedSession())
|
||||
await store.getState().reconnectPersistedTerminals()
|
||||
|
||||
// No live PTY, no row binding, host unreachable — `unverifiable`, never `exited`.
|
||||
expect([...getOrphanTerminalIds(store.getState(), WORKTREE_ID)]).toEqual([])
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID].map((tab) => tab.id)).toEqual([TAB_ID])
|
||||
})
|
||||
|
||||
it('does not invent a session id for a tab that never had one', () => {
|
||||
const store = seedStore()
|
||||
const session = persistedSession()
|
||||
session.tabsByWorktree[WORKTREE_ID] = [
|
||||
makeTab({ id: TAB_ID, worktreeId: WORKTREE_ID, ptyId: null })
|
||||
]
|
||||
delete session.remoteSessionIdsByTabId
|
||||
delete session.activeConnectionIdsAtShutdown
|
||||
session.activeWorktreeIdsOnShutdown = []
|
||||
store.getState().hydrateWorkspaceSession(session)
|
||||
|
||||
const payload = buildWorkspaceSessionPayload(store.getState())
|
||||
|
||||
expect(payload.remoteSessionIdsByTabId).toBeUndefined()
|
||||
expect(payload.activeWorktreeIdsOnShutdown).toEqual([])
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -59,24 +59,6 @@ function structuredTab(): Tab {
|
||||
}
|
||||
}
|
||||
|
||||
const TERMINAL_TAB_ID = 'terminal-1'
|
||||
const TERMINAL_UNIFIED_ID = 'unified-terminal-1'
|
||||
|
||||
function terminalTab(): Tab {
|
||||
return {
|
||||
id: TERMINAL_UNIFIED_ID,
|
||||
entityId: TERMINAL_TAB_ID,
|
||||
groupId: 'group-1',
|
||||
worktreeId: WORKTREE,
|
||||
contentType: 'terminal',
|
||||
label: 'Terminal',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 1,
|
||||
createdAt: 2
|
||||
}
|
||||
}
|
||||
|
||||
function storeWithStructuredTab(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
@@ -112,43 +94,6 @@ function expectChange(windowShows: () => void, mainGets: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
describe('renaming a terminal tab still resolves', () => {
|
||||
it('routes a terminal rename through its entityId, not the unified id', async () => {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
repos: [{ id: 'local-repo', path: '/tmp/app', name: 'app' }] as never,
|
||||
worktreesByRepo: {
|
||||
'local-repo': [makeWorktree({ id: WORKTREE, repoId: 'local-repo', path: '/tmp/app' })]
|
||||
},
|
||||
unifiedTabsByWorktree: { [WORKTREE]: [terminalTab(), structuredTab()] }
|
||||
})
|
||||
|
||||
// Keyed by the TERMINAL's entityId — the structured tab must not absorb it.
|
||||
store.getState().setTabCustomTitle(TERMINAL_TAB_ID, 'Build logs')
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const tabs = store.getState().unifiedTabsByWorktree[WORKTREE] ?? []
|
||||
expectChange(
|
||||
() => expect(tabs.find((t) => t.id === TERMINAL_UNIFIED_ID)?.customLabel).toBe('Build logs'),
|
||||
{ type: 'renameTab', tabId: TERMINAL_UNIFIED_ID, title: 'Build logs' }
|
||||
)
|
||||
expect(tabs.find((t) => t.id === STRUCTURED_TAB_ID)?.customLabel).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recoloring a structured chat tab', () => {
|
||||
it('writes the color onto the agent-session tab', async () => {
|
||||
const store = storeWithStructuredTab()
|
||||
store.getState().setTabColor(STRUCTURED_TAB_ID, 'red')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expectChange(() => expect(colorOf(store)).toBe('red'), {
|
||||
type: 'setTabProps',
|
||||
tabId: STRUCTURED_TAB_ID,
|
||||
color: 'red'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('renaming a structured chat tab', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(toast.error).mockClear()
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as AgentStatusModule from '@/lib/agent-status'
|
||||
import { createTestStore, makeWorktree, seedStore } from '../slices/store-test-helpers'
|
||||
import { createStoreCascadesMockApi } from '../slices/store-cascades-test-harness'
|
||||
import { _resetLayoutOwnerPaneIntentsForTests } from '@/runtime/layout-owner-pane-intents'
|
||||
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { LayoutApplyParams } from '../../../../shared/rpc-contract/workspace-layout-params'
|
||||
import { applyLayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-commands'
|
||||
import { layoutCommandsOf } from '@/runtime/layout-apply-test-double'
|
||||
import type {
|
||||
LayoutTerminalTab,
|
||||
WorkspaceLayoutModel
|
||||
} from '../../../../shared/workspace-layout/workspace-layout-model'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
layoutOwnerIsMain: true,
|
||||
callRuntimeRpc: vi.fn(async (_target: unknown, _method: string, _params: unknown) => ({
|
||||
ok: true,
|
||||
result: {}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('../../../../shared/layout-owner-switch', () => ({
|
||||
get ORCA_LAYOUT_OWNER_IS_MAIN() {
|
||||
return mocks.layoutOwnerIsMain
|
||||
}
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc }))
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() }
|
||||
}))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof AgentStatusModule>()
|
||||
return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) }
|
||||
})
|
||||
|
||||
createStoreCascadesMockApi()
|
||||
|
||||
const WT = 'repo1::/path/wt1'
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
let store: ReturnType<typeof createTestStore>
|
||||
/** Main's copy; it changes only through what the window sends. */
|
||||
let main: WorkspaceLayoutModel
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.layoutOwnerIsMain = true
|
||||
mocks.callRuntimeRpc.mockClear()
|
||||
_resetLayoutOwnerPaneIntentsForTests()
|
||||
store = createTestStore()
|
||||
seedStore(store, {
|
||||
worktreesByRepo: { repo1: [makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' })] },
|
||||
activeWorktreeId: WT
|
||||
})
|
||||
main = {
|
||||
hostId: LOCAL_EXECUTION_HOST_ID,
|
||||
workspaces: {},
|
||||
records: {},
|
||||
legacy: { terminalRowOwners: {} }
|
||||
}
|
||||
})
|
||||
|
||||
/** Applies what the window sent to main's copy, as `layout.apply` does; any refusal fails. */
|
||||
function applySent() {
|
||||
const commands = mocks.callRuntimeRpc.mock.calls.flatMap(([, , params]) =>
|
||||
layoutCommandsOf(LayoutApplyParams.parse(params).command)
|
||||
)
|
||||
mocks.callRuntimeRpc.mockClear()
|
||||
let id = 0
|
||||
for (const command of commands) {
|
||||
const applied = applyLayoutCommand(main, command, {
|
||||
mintId: () => `main-${++id}`,
|
||||
mintLeafId: () => `leaf-${++id}`,
|
||||
now: () => 1
|
||||
})
|
||||
if (!applied.ok) {
|
||||
throw new Error(`${command.type} refused: ${applied.code}`)
|
||||
}
|
||||
main = applied.model
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
function mainTab(tabId: string): LayoutTerminalTab {
|
||||
const tab = main.workspaces[WT]!.tabs.find((entry) => entry.id === tabId)
|
||||
if (tab?.kind !== 'terminal') {
|
||||
throw new Error('main holds no such terminal tab')
|
||||
}
|
||||
return tab
|
||||
}
|
||||
|
||||
/** A one-pane tab main holds, as the window made it. */
|
||||
function heldTab(): string {
|
||||
const tab = store.getState().createTab(WT, undefined, undefined, { initialLeafId: LEAF })
|
||||
applySent()
|
||||
return tab.id
|
||||
}
|
||||
|
||||
function setChatLeaf(tabId: string, chatLeafId: string | null): void {
|
||||
const { chatLeafId: _old, ...layout } = store.getState().terminalLayoutsByTabId[tabId]!
|
||||
store.getState().setTabLayout(tabId, chatLeafId ? { ...layout, chatLeafId } : layout)
|
||||
}
|
||||
|
||||
describe('the chat pane in a layout-owner build', () => {
|
||||
it('records each chat pane the window picks in main, so its save changes none', () => {
|
||||
const tabId = heldTab()
|
||||
|
||||
setChatLeaf(tabId, LEAF)
|
||||
expect(applySent()).toEqual([{ type: 'setChatPane', workspace: WT, tabId, leafId: LEAF }])
|
||||
expect(mainTab(tabId).panes.chatLeafId).toBe(LEAF)
|
||||
|
||||
setChatLeaf(tabId, null)
|
||||
applySent()
|
||||
expect(mainTab(tabId).panes.chatLeafId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -3,42 +3,11 @@ import {
|
||||
flushTerminalInputActivity,
|
||||
resetTerminalInputActivityCoalescingForTests
|
||||
} from '@/lib/terminal-input-activity-coalescing'
|
||||
import { createTestStore, makeLayout } from '../slices/store-test-helpers'
|
||||
import { createTestStore } from '../slices/store-test-helpers'
|
||||
|
||||
afterEach(resetTerminalInputActivityCoalescingForTests)
|
||||
|
||||
describe('terminal no-op subscriber budget', () => {
|
||||
it('publishes real mutations once and keeps repeated actions silent', () => {
|
||||
const store = createTestStore()
|
||||
const draft = { tabId: 'tab', agent: 'codex', text: 'draft', createdAt: 1 } as const
|
||||
store.getState().seedNativeChatLaunchPrompt(draft)
|
||||
store.getState().seedNativeChatLaunchDraft(draft)
|
||||
store.getState().setTabLayout('tab', makeLayout())
|
||||
const listener = vi.fn()
|
||||
store.subscribe(listener)
|
||||
|
||||
const actions = [
|
||||
() => store.getState().markDefaultTerminalTabsApplied('folder-workspace'),
|
||||
() => store.getState().markUnverifiedPtyLoss('tab'),
|
||||
() => store.getState().markPtySourceDisowned('pty'),
|
||||
() => store.getState().markNativeChatLaunchPromptFailed('tab'),
|
||||
() => store.getState().markNativeChatLaunchDraftAdopted('tab'),
|
||||
() => store.getState().resolveNativeChatLaunchDraft('tab', draft),
|
||||
() => store.getState().replaceTerminalLayoutPanePtyId('tab', 'leaf', 'pty'),
|
||||
() => store.getState().clearNativeChatLaunchPrompt('tab'),
|
||||
() => store.getState().clearNativeChatLaunchDraft('tab')
|
||||
]
|
||||
for (const action of actions) {
|
||||
listener.mockClear()
|
||||
action()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
const before = store.getState()
|
||||
action()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState()).toBe(before)
|
||||
}
|
||||
})
|
||||
|
||||
it('retains draft generations when stale resolutions arrive without notifying', () => {
|
||||
const store = createTestStore()
|
||||
const draft = { tabId: 'tab', agent: 'codex', text: 'new draft', createdAt: 2 } as const
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Profiler } from 'react'
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createTestStore } from '../slices/store-test-helpers'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
type TestStore = ReturnType<typeof createTestStore>
|
||||
|
||||
const TAB_ID = 'tab-1'
|
||||
const NO_OP_WRITES = 25
|
||||
|
||||
function recordPublishedMapKeys(store: TestStore): string[] {
|
||||
const published: string[] = []
|
||||
store.subscribe((next, previous) => {
|
||||
if (next.expandedPaneByTabId !== previous.expandedPaneByTabId) {
|
||||
published.push('expandedPaneByTabId')
|
||||
}
|
||||
if (next.canExpandPaneByTabId !== previous.canExpandPaneByTabId) {
|
||||
published.push('canExpandPaneByTabId')
|
||||
}
|
||||
})
|
||||
return published
|
||||
}
|
||||
|
||||
// Mirrors use-terminal-workspace-store-bindings.ts:17, which subscribes to the raw map.
|
||||
function ExpandedPaneSubscriber({ store }: { store: TestStore }): React.JSX.Element {
|
||||
const expandedPaneByTabId = store((s) => s.expandedPaneByTabId)
|
||||
return <span>{String(expandedPaneByTabId[TAB_ID] === true)}</span>
|
||||
}
|
||||
|
||||
function CanExpandPaneSubscriber({ store }: { store: TestStore }): React.JSX.Element {
|
||||
const canExpandPaneByTabId = store((s) => s.canExpandPaneByTabId)
|
||||
return <span>{String(canExpandPaneByTabId[TAB_ID] === true)}</span>
|
||||
}
|
||||
|
||||
function renderCommitCounter(subscriber: React.JSX.Element): () => number {
|
||||
let commits = 0
|
||||
render(
|
||||
<Profiler
|
||||
id="terminal-pane-expansion"
|
||||
onRender={() => {
|
||||
commits += 1
|
||||
}}
|
||||
>
|
||||
{subscriber}
|
||||
</Profiler>
|
||||
)
|
||||
const mountCommits = commits
|
||||
return () => commits - mountCommits
|
||||
}
|
||||
|
||||
describe('setTabPaneExpanded', () => {
|
||||
it('publishes the first write for an unseen tab and a real toggle', () => {
|
||||
const store = createTestStore()
|
||||
const published = recordPublishedMapKeys(store)
|
||||
|
||||
store.getState().setTabPaneExpanded(TAB_ID, false)
|
||||
expect(published).toEqual(['expandedPaneByTabId'])
|
||||
expect(store.getState().expandedPaneByTabId[TAB_ID]).toBe(false)
|
||||
|
||||
store.getState().setTabPaneExpanded(TAB_ID, true)
|
||||
expect(published).toEqual(['expandedPaneByTabId', 'expandedPaneByTabId'])
|
||||
expect(store.getState().expandedPaneByTabId[TAB_ID]).toBe(true)
|
||||
})
|
||||
|
||||
it('bails out when the value is unchanged', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().setTabPaneExpanded(TAB_ID, false)
|
||||
const before = store.getState().expandedPaneByTabId
|
||||
// Root identity too: returning `{}` keeps the map but allocates a new root, so zustand still walks every listener.
|
||||
const rootBefore = store.getState()
|
||||
const published = recordPublishedMapKeys(store)
|
||||
|
||||
for (let i = 0; i < NO_OP_WRITES; i += 1) {
|
||||
store.getState().setTabPaneExpanded(TAB_ID, false)
|
||||
}
|
||||
|
||||
expect(published).toEqual([])
|
||||
expect(store.getState().expandedPaneByTabId).toBe(before)
|
||||
expect(store.getState()).toBe(rootBefore)
|
||||
})
|
||||
|
||||
it('costs no React commit in a map subscriber when the value is unchanged', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().setTabPaneExpanded(TAB_ID, false)
|
||||
const commitsSinceMount = renderCommitCounter(<ExpandedPaneSubscriber store={store} />)
|
||||
|
||||
for (let i = 0; i < NO_OP_WRITES; i += 1) {
|
||||
act(() => {
|
||||
store.getState().setTabPaneExpanded(TAB_ID, false)
|
||||
})
|
||||
}
|
||||
expect(commitsSinceMount()).toBe(0)
|
||||
|
||||
act(() => {
|
||||
store.getState().setTabPaneExpanded(TAB_ID, true)
|
||||
})
|
||||
expect(commitsSinceMount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setTabCanExpandPane', () => {
|
||||
it('publishes the first write for an unseen tab and a real toggle', () => {
|
||||
const store = createTestStore()
|
||||
const published = recordPublishedMapKeys(store)
|
||||
|
||||
store.getState().setTabCanExpandPane(TAB_ID, false)
|
||||
expect(published).toEqual(['canExpandPaneByTabId'])
|
||||
expect(store.getState().canExpandPaneByTabId[TAB_ID]).toBe(false)
|
||||
|
||||
store.getState().setTabCanExpandPane(TAB_ID, true)
|
||||
expect(published).toEqual(['canExpandPaneByTabId', 'canExpandPaneByTabId'])
|
||||
expect(store.getState().canExpandPaneByTabId[TAB_ID]).toBe(true)
|
||||
})
|
||||
|
||||
it('bails out when the value is unchanged', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().setTabCanExpandPane(TAB_ID, false)
|
||||
const before = store.getState().canExpandPaneByTabId
|
||||
const rootBefore = store.getState()
|
||||
const published = recordPublishedMapKeys(store)
|
||||
|
||||
for (let i = 0; i < NO_OP_WRITES; i += 1) {
|
||||
store.getState().setTabCanExpandPane(TAB_ID, false)
|
||||
}
|
||||
|
||||
expect(published).toEqual([])
|
||||
expect(store.getState().canExpandPaneByTabId).toBe(before)
|
||||
expect(store.getState()).toBe(rootBefore)
|
||||
})
|
||||
|
||||
it('costs no React commit in a map subscriber when the value is unchanged', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().setTabCanExpandPane(TAB_ID, false)
|
||||
const commitsSinceMount = renderCommitCounter(<CanExpandPaneSubscriber store={store} />)
|
||||
|
||||
for (let i = 0; i < NO_OP_WRITES; i += 1) {
|
||||
act(() => {
|
||||
store.getState().setTabCanExpandPane(TAB_ID, false)
|
||||
})
|
||||
}
|
||||
expect(commitsSinceMount()).toBe(0)
|
||||
|
||||
act(() => {
|
||||
store.getState().setTabCanExpandPane(TAB_ID, true)
|
||||
})
|
||||
expect(commitsSinceMount()).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import { createTerminalShutdownGuardController } from './terminal-shutdown-guards'
|
||||
|
||||
vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({
|
||||
restorePtyDataHandlersAfterFailedShutdown: vi.fn(),
|
||||
unregisterPtyDataHandlers: vi.fn(() => [])
|
||||
}))
|
||||
vi.mock('@/components/terminal-pane/terminal-parked-watcher-registry', () => ({
|
||||
disposeParkedTerminalWatchersForPtyIds: vi.fn()
|
||||
}))
|
||||
vi.mock('@/components/terminal-pane/pty-shutdown-exit-deferral', () => ({
|
||||
clearCommittedPtyShutdownSettlements: vi.fn(),
|
||||
hasCommittedPtyShutdownSettlement: vi.fn(() => false),
|
||||
markCommittedPtyShutdowns: vi.fn(),
|
||||
noteCommittedPtyShutdownSettlements: vi.fn(),
|
||||
settleDeferredPtyShutdownExits: vi.fn()
|
||||
}))
|
||||
|
||||
function harness(initial: Partial<AppState>, exitGuardPtyIds: readonly string[]) {
|
||||
let current = initial as AppState
|
||||
const set = vi.fn((update: unknown) => {
|
||||
const patch =
|
||||
typeof update === 'function' ? (update as (s: AppState) => object)(current) : update
|
||||
current = { ...current, ...(patch as object) }
|
||||
})
|
||||
const guards = createTerminalShutdownGuardController({
|
||||
exitGuardPtyIds,
|
||||
get: (() => current) as never,
|
||||
keepIdentifiers: false,
|
||||
rendererShutdownPtyIds: exitGuardPtyIds,
|
||||
runtimeOwnsRemotePtys: false,
|
||||
set: set as never,
|
||||
tabs: []
|
||||
})
|
||||
return { guards, set, state: () => current }
|
||||
}
|
||||
|
||||
describe('markShutdownPending identity', () => {
|
||||
it('does not write the store when there is nothing to guard', () => {
|
||||
const { guards, set } = harness({ suppressedPtyExitIds: {}, pendingPtyShutdownIds: {} }, [])
|
||||
|
||||
guards.markShutdownPending()
|
||||
|
||||
expect(set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still counts a pending owner when every id is already suppressed', () => {
|
||||
const suppressedPtyExitIds: Record<string, true> = { 'pty-1': true }
|
||||
const { guards, state } = harness(
|
||||
{ suppressedPtyExitIds, pendingPtyShutdownIds: { 'pty-1': 1 } },
|
||||
['pty-1']
|
||||
)
|
||||
|
||||
guards.markShutdownPending()
|
||||
|
||||
expect(state().suppressedPtyExitIds).toBe(suppressedPtyExitIds)
|
||||
expect(state().pendingPtyShutdownIds).toEqual({ 'pty-1': 2 })
|
||||
})
|
||||
|
||||
it('suppresses the ids that were not yet suppressed', () => {
|
||||
const { guards, state } = harness(
|
||||
{ suppressedPtyExitIds: { 'pty-1': true }, pendingPtyShutdownIds: {} },
|
||||
['pty-1', 'pty-2']
|
||||
)
|
||||
|
||||
guards.markShutdownPending()
|
||||
|
||||
expect(state().suppressedPtyExitIds).toEqual({ 'pty-1': true, 'pty-2': true })
|
||||
expect(state().pendingPtyShutdownIds).toEqual({ 'pty-1': 1, 'pty-2': 1 })
|
||||
})
|
||||
})
|
||||
@@ -1,114 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session/lifecycle/agent-session-resume'
|
||||
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
import type { recordLayoutApply } from '@/runtime/layout-apply-send'
|
||||
import {
|
||||
recordPaneSleepThroughLayoutOwner,
|
||||
recordPaneWakeThroughLayoutOwner,
|
||||
recordSleepingRecordChangesThroughLayoutOwner
|
||||
} from './terminal-sleep-layout-owner'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ send: vi.fn<typeof recordLayoutApply>() }))
|
||||
|
||||
vi.mock('@/runtime/layout-apply-send', () => ({ recordLayoutApply: mocks.send }))
|
||||
|
||||
const WT = 'repo1::/path/wt1'
|
||||
const STATE = {}
|
||||
const A1 = 'tab-a:11111111-1111-4111-8111-111111111111'
|
||||
const A2 = 'tab-a:22222222-2222-4222-8222-222222222222'
|
||||
const B1 = 'tab-b:33333333-3333-4333-8333-333333333333'
|
||||
|
||||
function record(paneKey: string): SleepingAgentSessionRecord {
|
||||
return {
|
||||
paneKey,
|
||||
worktreeId: WT,
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: paneKey },
|
||||
prompt: '',
|
||||
state: 'done',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
/** A window holding these terminal tabs in WT. */
|
||||
function holding(tabIds: string[]): { tabsByWorktree: Record<string, TerminalTab[]> } {
|
||||
return {
|
||||
tabsByWorktree: {
|
||||
[WT]: tabIds.map((id) => ({
|
||||
id,
|
||||
ptyId: null,
|
||||
worktreeId: WT,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('sleep and wake through the layout owner', () => {
|
||||
beforeEach(() => {
|
||||
mocks.send.mockReset()
|
||||
})
|
||||
|
||||
it('sends one sleep per tab naming only the panes it stopped', () => {
|
||||
const records = {
|
||||
[A1]: record(A1),
|
||||
[A2]: record(A2),
|
||||
[B1]: record(B1),
|
||||
// A legacy pane key names no pane main holds.
|
||||
'tab-a:1': record('tab-a:1')
|
||||
}
|
||||
|
||||
recordPaneSleepThroughLayoutOwner(STATE, WT, records)
|
||||
|
||||
expect(mocks.send.mock.calls.map((call) => call[2])).toEqual([
|
||||
{ type: 'sleep', tabId: 'tab-a', paneKeys: [A1, A2], records: [record(A1), record(A2)] },
|
||||
{ type: 'sleep', tabId: 'tab-b', paneKeys: [B1], records: [record(B1)] }
|
||||
])
|
||||
})
|
||||
|
||||
it('sends nothing when nothing slept, as a worktree removal', () => {
|
||||
recordPaneSleepThroughLayoutOwner(STATE, WT, {})
|
||||
|
||||
expect(mocks.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('records the wake of the one pane the window resumed, and has main start nothing', () => {
|
||||
recordPaneWakeThroughLayoutOwner(STATE, WT, A2)
|
||||
|
||||
expect(mocks.send).toHaveBeenCalledWith(STATE, WT, {
|
||||
type: 'wake',
|
||||
tabId: 'tab-a',
|
||||
paneKeys: [A2],
|
||||
recordOnly: true
|
||||
})
|
||||
})
|
||||
|
||||
it('records a live capture and a dropped record without stopping or starting anything', () => {
|
||||
const live = { ...record(A1), state: 'working' as const, origin: 'live' as const }
|
||||
const kept = record(B1)
|
||||
|
||||
recordSleepingRecordChangesThroughLayoutOwner(
|
||||
holding(['tab-a', 'tab-b']),
|
||||
{ [A2]: record(A2), [B1]: kept },
|
||||
{ [A1]: live, [B1]: kept }
|
||||
)
|
||||
|
||||
expect(mocks.send.mock.calls.map((call) => call[2])).toEqual([
|
||||
{ type: 'sleep', tabId: 'tab-a', paneKeys: [A1], records: [live], recordOnly: true },
|
||||
{ type: 'wake', tabId: 'tab-a', paneKeys: [A2], recordOnly: true }
|
||||
])
|
||||
})
|
||||
|
||||
it('sends nothing for a tab the window closed: main dropped its records with the tab', () => {
|
||||
recordSleepingRecordChangesThroughLayoutOwner(holding(['tab-b']), { [A2]: record(A2) }, {})
|
||||
// A periodic capture from a stale agent status can name the closed tab again.
|
||||
recordSleepingRecordChangesThroughLayoutOwner(holding(['tab-b']), {}, { [A1]: record(A1) })
|
||||
|
||||
expect(mocks.send).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -89,19 +89,6 @@ vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
mockApi.worktrees.updateMeta.mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('commits the close in main even when the SSH shutdown throws', async () => {
|
||||
mockApi.pty.kill.mockRejectedValue(new Error('relay shutdown failed'))
|
||||
const store = storeWithSshTab()
|
||||
|
||||
store.getState().closeTab('ssh-tab')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(mockApi.pty.kill).toHaveBeenCalledWith(SSH_PTY)
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([
|
||||
{ worktreeId: SSH_WORKTREE, tabId: 'ssh-tab', reason: 'user' }
|
||||
])
|
||||
})
|
||||
|
||||
it('removes the tab and kills it without waiting for main to write the close', async () => {
|
||||
closeTerminalSurface.mockReturnValue(new Promise<void>(() => {}))
|
||||
layoutApplyTestTransport.answerWith('pending')
|
||||
@@ -114,27 +101,6 @@ vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
await Promise.resolve()
|
||||
expect(mockApi.pty.kill).toHaveBeenCalledWith(SSH_PTY)
|
||||
})
|
||||
|
||||
it.each([['user'], ['cleanup']] as const)('sends the intent for a %s close', (reason) => {
|
||||
storeWithSshTab().getState().closeTab('ssh-tab', { reason })
|
||||
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([
|
||||
{ worktreeId: SSH_WORKTREE, tabId: 'ssh-tab', reason }
|
||||
])
|
||||
})
|
||||
|
||||
// Why: exits still retire through main's own exit handling until main decides exits itself.
|
||||
it('sends nothing for a pty-exit close', () => {
|
||||
storeWithSshTab().getState().closeTab('ssh-tab', { reason: 'pty-exit' })
|
||||
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([])
|
||||
})
|
||||
|
||||
it('sends nothing when a paired host owns the close', () => {
|
||||
storeWithSshTab().getState().closeTab('ssh-tab', { remoteCloseOwnedByHost: true })
|
||||
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,30 +110,6 @@ vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
|
||||
const WORKTREE = 'repo::/tmp/app'
|
||||
|
||||
/** Maps a closing tab has no entry in; closing must not give them a new reference. */
|
||||
const UNTOUCHED_FIELDS = [
|
||||
'terminalLayoutsByTabId',
|
||||
'ptyIdsByTabId',
|
||||
'runtimePaneTitlesByTabId',
|
||||
'lastKnownRelayPtyIdByTabId',
|
||||
'deferredSshSessionIdsByTabId',
|
||||
'pendingReconnectPtyIdByTabId',
|
||||
'pendingStartupByTabId',
|
||||
'automaticAgentResumeClaimsByTabId',
|
||||
'nativeChatLaunchPromptByTabId',
|
||||
'nativeChatLaunchDraftByTabId',
|
||||
'pendingInitialCwdByTabId',
|
||||
'pendingIssueCommandSplitByTabId',
|
||||
'expandedPaneByTabId',
|
||||
'canExpandPaneByTabId',
|
||||
'cacheTimerByKey',
|
||||
'lastTerminalInputAtByPaneKey',
|
||||
'unreadTerminalTabs',
|
||||
'unreadTerminalPanes',
|
||||
'unreadAgentCompletionPanes',
|
||||
'tabBarOrderByWorktree'
|
||||
] as const
|
||||
|
||||
function storeWithTwoTabs(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
@@ -195,23 +137,6 @@ vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
mockApi.worktrees.updateMeta.mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('keeps the reference of every per-tab map the closing tab had no entry in', () => {
|
||||
const store = storeWithTwoTabs()
|
||||
const before = store.getState()
|
||||
const snapshot = Object.fromEntries(
|
||||
UNTOUCHED_FIELDS.map((field) => [field, before[field]])
|
||||
) as Record<string, unknown>
|
||||
|
||||
store.getState().closeTab('tab-a')
|
||||
|
||||
const after = store.getState()
|
||||
// The tab really closed — otherwise the identity assertions below are vacuous.
|
||||
expect(after.tabsByWorktree[WORKTREE].map((tab) => tab.id)).toEqual(['tab-b'])
|
||||
for (const field of UNTOUCHED_FIELDS) {
|
||||
expect(after[field], field).toBe(snapshot[field])
|
||||
}
|
||||
})
|
||||
|
||||
it('still drops the closing tab from a map that did hold it', () => {
|
||||
const store = storeWithTwoTabs()
|
||||
store.setState({
|
||||
@@ -233,88 +158,3 @@ vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const mockApi = createStoreCascadesMockApi()
|
||||
const mockWindow = globalThis.window
|
||||
|
||||
const REMOTE_WORKTREE = 'remote-repo::/srv/app'
|
||||
const LOCAL_WORKTREE = 'local-repo::/tmp/app'
|
||||
|
||||
function storeWithBothWorktrees(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
repos: [
|
||||
{ id: 'remote-repo', path: '/srv/app', name: 'app', connectionId: 'ssh-1' },
|
||||
{ id: 'local-repo', path: '/tmp/app', name: 'app' }
|
||||
] as never,
|
||||
worktreesByRepo: {
|
||||
'remote-repo': [
|
||||
makeWorktree({ id: REMOTE_WORKTREE, repoId: 'remote-repo', path: '/srv/app' })
|
||||
],
|
||||
'local-repo': [makeWorktree({ id: LOCAL_WORKTREE, repoId: 'local-repo', path: '/tmp/app' })]
|
||||
},
|
||||
tabsByWorktree: {
|
||||
[REMOTE_WORKTREE]: [makeTab({ id: 'remote-tab', worktreeId: REMOTE_WORKTREE })],
|
||||
[LOCAL_WORKTREE]: [makeTab({ id: 'local-tab', worktreeId: LOCAL_WORKTREE })]
|
||||
}
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
const closeTerminalSurface = vi.fn()
|
||||
Object.assign(mockApi, { session: { closeTerminalSurface } })
|
||||
|
||||
describe('closeTab close-record mirror', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.window = mockWindow
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
layoutApplyTestTransport.reset()
|
||||
closeTerminalSurface.mockResolvedValue(undefined)
|
||||
mockApi.worktrees.updateMeta.mockResolvedValue({})
|
||||
})
|
||||
|
||||
// Cover every workspace kind: close mirror suppresses re-seeding before main’s saved copy hydrates.
|
||||
it.each([
|
||||
['remote-tab', REMOTE_WORKTREE],
|
||||
['local-tab', LOCAL_WORKTREE]
|
||||
])('mirrors the record main is told to write for %s', (tabId, worktreeId) => {
|
||||
const store = storeWithBothWorktrees()
|
||||
|
||||
store.getState().closeTab(tabId)
|
||||
|
||||
expect(store.getState().closedTerminalTabTombstonesByTabId[tabId]).toEqual({
|
||||
closedAt: expect.any(Number),
|
||||
worktreeId,
|
||||
reason: 'user'
|
||||
})
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([{ worktreeId, tabId, reason: 'user' }])
|
||||
})
|
||||
|
||||
it('mirrors a cleanup close with its reason', () => {
|
||||
const store = storeWithBothWorktrees()
|
||||
|
||||
store.getState().closeTab('remote-tab', { reason: 'cleanup' })
|
||||
|
||||
expect(store.getState().closedTerminalTabTombstonesByTabId['remote-tab']?.reason).toBe(
|
||||
'cleanup'
|
||||
)
|
||||
})
|
||||
|
||||
// Main alone closes for a process exit; a paired host owns its own records.
|
||||
it.each([
|
||||
['a pty-exit close', { reason: 'pty-exit' as const }],
|
||||
['a host-owned close', { remoteCloseOwnedByHost: true }]
|
||||
])('mirrors nothing for %s', (_label, opts) => {
|
||||
const store = storeWithBothWorktrees()
|
||||
|
||||
store.getState().closeTab('remote-tab', opts)
|
||||
|
||||
expect(store.getState().closedTerminalTabTombstonesByTabId).toEqual({})
|
||||
expect(sentCloses(closeTerminalSurface)).toEqual([])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import type { LayoutApplyReply } from '../../../../shared/workspace-layout/workspace-layout-command-types'
|
||||
import {
|
||||
_resetLayoutFramePositionForTests,
|
||||
beginLayoutFramePositions,
|
||||
endLayoutFramePositions,
|
||||
noteLayoutFramePosition
|
||||
} from '@/runtime/layout-frame-position'
|
||||
import {
|
||||
_resetLayoutOwnerPaneIntentsForTests,
|
||||
takeTabRowFields
|
||||
} from '@/runtime/layout-owner-pane-intents'
|
||||
import { createTestStore, makeTab } from '../slices/store-test-helpers'
|
||||
import {
|
||||
createInitialTerminalThroughLayoutOwner,
|
||||
recordCreatedTerminalTabInLayoutOwner
|
||||
} from './terminal-tab-create-through-layout-owner'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const held: {
|
||||
pairedServer: string | null
|
||||
twin: boolean
|
||||
reply: ((value: LayoutApplyReply) => void) | null
|
||||
fail: ((error: Error) => void) | null
|
||||
} = { pairedServer: null, twin: false, reply: null, fail: null }
|
||||
return Object.assign(held, { callRuntimeRpc: vi.fn() })
|
||||
})
|
||||
|
||||
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc }))
|
||||
vi.mock('@/lib/resolve-owner', () => ({
|
||||
runtimeEnvironmentIdUnlessTwin: () => mocks.pairedServer,
|
||||
runtimeEnvironmentIdForWorkspaceOwner: () =>
|
||||
mocks.twin ? { kind: 'ambiguous' } : { kind: 'resolved', environmentId: null }
|
||||
}))
|
||||
|
||||
const WORKTREE = 'repo1::/path/wt'
|
||||
|
||||
function stateWith(tabIds: string[] = [], activeWorktreeId = WORKTREE): AppState {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
activeWorktreeId,
|
||||
tabsByWorktree: { [WORKTREE]: tabIds.map((id) => makeTab({ id, worktreeId: WORKTREE })) }
|
||||
})
|
||||
return store.getState()
|
||||
}
|
||||
|
||||
function start(state: () => AppState = () => stateWith()) {
|
||||
const create = vi.fn()
|
||||
const routed = createInitialTerminalThroughLayoutOwner(state, WORKTREE, create)
|
||||
const command = mocks.callRuntimeRpc.mock.calls[0]?.[2]?.command
|
||||
return { create, routed, command }
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
beforeEach(() => {
|
||||
_resetLayoutOwnerPaneIntentsForTests()
|
||||
endLayoutFramePositions()
|
||||
mocks.pairedServer = null
|
||||
mocks.twin = false
|
||||
mocks.callRuntimeRpc.mockReset()
|
||||
mocks.callRuntimeRpc.mockImplementation(
|
||||
() =>
|
||||
new Promise<LayoutApplyReply>((resolve, reject) => {
|
||||
mocks.reply = resolve
|
||||
mocks.fail = reject
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('createInitialTerminalThroughLayoutOwner', () => {
|
||||
it('asks main for the first tab and leaves drawing it, with the window’s row fields, to main’s frame', async () => {
|
||||
const { create, routed, command } = start()
|
||||
|
||||
expect(routed).toBe(true)
|
||||
expect(command).toMatchObject({
|
||||
type: 'createTerminalTab',
|
||||
workspace: WORKTREE,
|
||||
initial: 'activation'
|
||||
})
|
||||
noteLayoutFramePosition(1)
|
||||
mocks.reply?.({ ok: true, result: { tabId: command.tabId, leafId: command.leafId } })
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
// Kept for the frame, however late an owed write lets it in.
|
||||
expect(takeTabRowFields(command.tabId)).toEqual({ pendingActivationSpawn: true })
|
||||
})
|
||||
|
||||
it('draws nothing itself when no frame is coming: the reopened stream’s snapshot brings it', async () => {
|
||||
const { create, command } = start()
|
||||
|
||||
mocks.reply?.({ ok: true, result: { tabId: command.tabId, leafId: command.leafId } })
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
expect(takeTabRowFields(command.tabId)).toEqual({ pendingActivationSpawn: true })
|
||||
})
|
||||
|
||||
it('draws nothing itself when the stream ends before main’s frame', async () => {
|
||||
_resetLayoutFramePositionForTests()
|
||||
beginLayoutFramePositions()
|
||||
const { create, command } = start()
|
||||
|
||||
mocks.reply?.({
|
||||
ok: true,
|
||||
result: { tabId: command.tabId, leafId: command.leafId },
|
||||
frameSeq: 1
|
||||
})
|
||||
await settle()
|
||||
endLayoutFramePositions()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
expect(takeTabRowFields(command.tabId)).toEqual({ pendingActivationSpawn: true })
|
||||
})
|
||||
|
||||
it('draws nothing itself when a frame showed the tab before the stream ended', async () => {
|
||||
let state = stateWith()
|
||||
const { create, command } = start(() => state)
|
||||
|
||||
state = stateWith([command.tabId])
|
||||
mocks.reply?.({ ok: true, result: { tabId: command.tabId, leafId: command.leafId } })
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('draws nothing when main’s copy already has a tab there', async () => {
|
||||
const { create, command } = start()
|
||||
|
||||
mocks.reply?.({ ok: true, result: {} })
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
expect(takeTabRowFields(command.tabId)).toBeNull()
|
||||
})
|
||||
|
||||
it('opens the terminal as today when main holds no partition for the workspace, or fails', async () => {
|
||||
const unowned = start()
|
||||
mocks.reply?.({ ok: false, code: 'host_not_owned' })
|
||||
await settle()
|
||||
expect(unowned.create).toHaveBeenCalledWith()
|
||||
|
||||
mocks.callRuntimeRpc.mockClear()
|
||||
const failed = start()
|
||||
mocks.fail?.(new Error('layout_store_unavailable'))
|
||||
await settle()
|
||||
expect(failed.create).toHaveBeenCalledWith()
|
||||
expect(takeTabRowFields(failed.command.tabId)).toBeNull()
|
||||
})
|
||||
|
||||
it('opens nothing as today once the user has left the workspace', async () => {
|
||||
let state = stateWith()
|
||||
const { create } = start(() => state)
|
||||
|
||||
state = stateWith([], 'repo1::/path/other')
|
||||
mocks.reply?.({ ok: false, code: 'host_not_owned' })
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens nothing as today once the workspace got a tab meanwhile', async () => {
|
||||
let state = stateWith()
|
||||
const { create } = start(() => state)
|
||||
|
||||
state = stateWith(['tab-opened-meanwhile'])
|
||||
mocks.fail?.(new Error('layout_store_unavailable'))
|
||||
await settle()
|
||||
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves a paired server’s workspace to the caller', () => {
|
||||
mocks.pairedServer = 'env-1'
|
||||
|
||||
const { routed } = start()
|
||||
|
||||
expect(routed).toBe(false)
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recordCreatedTerminalTabInLayoutOwner', () => {
|
||||
const command = {
|
||||
type: 'createTerminalTab' as const,
|
||||
workspace: WORKTREE,
|
||||
tabId: 'tab-drawn',
|
||||
leafId: 'leaf-drawn'
|
||||
}
|
||||
|
||||
it.each([
|
||||
[false, 1],
|
||||
[true, 0]
|
||||
])(
|
||||
'on host_not_owned (twin: %s), closes the drawn tab unless a paired server may hold it',
|
||||
async (twin, closes) => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
mocks.twin = twin
|
||||
const state = stateWith(['tab-drawn'])
|
||||
const closeTab = vi.fn()
|
||||
recordCreatedTerminalTabInLayoutOwner({ ...state, closeTab }, command)
|
||||
mocks.reply?.({ ok: false, code: 'host_not_owned' })
|
||||
await settle()
|
||||
expect(closeTab).toHaveBeenCalledTimes(closes)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -26,15 +26,4 @@ describe('createTab with a mirrored host tab id', () => {
|
||||
store.getState().createTab(WT, undefined, undefined, { id: MIRRORED, mirroredHostTab: true })
|
||||
).toThrow(/already exists/)
|
||||
})
|
||||
it('still ignores a mirrored id from a caller that did not opt in', () => {
|
||||
const tab = seeded().getState().createTab(WT, undefined, undefined, { id: MIRRORED })
|
||||
expect(tab.id).not.toBe(MIRRORED)
|
||||
})
|
||||
it('ignores an opted-in id that is not a mirror of a valid host tab id', () => {
|
||||
const tab = seeded().getState().createTab(WT, undefined, undefined, {
|
||||
id: 'plain-tab',
|
||||
mirroredHostTab: true
|
||||
})
|
||||
expect(tab.id).not.toBe('plain-tab')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import {
|
||||
getRemoteConnectionIdForWorktree,
|
||||
worktreeUsesRemoteConnection,
|
||||
worktreeUsesWslPath
|
||||
} from './terminal-workspace-routing'
|
||||
import { rightSidebarShowsPullRequestData } from '@/lib/right-sidebar-visibility'
|
||||
|
||||
const REPO_COUNT = 10
|
||||
const WORKTREE_COUNT = 400
|
||||
|
||||
/** Counts every `id` read so a rescan shows up as a multiple of the row count. */
|
||||
function buildCountingState(): {
|
||||
state: AppState
|
||||
reads: () => number
|
||||
worktreeId: string
|
||||
} {
|
||||
let idReads = 0
|
||||
const repos = Array.from({ length: REPO_COUNT }, (_, index) => ({
|
||||
id: `repo-${index}`,
|
||||
name: `repo-${index}`,
|
||||
path: `/repos/repo-${index}`,
|
||||
connectionId: null
|
||||
}))
|
||||
const worktreesByRepo: Record<string, unknown[]> = {}
|
||||
const perRepo = WORKTREE_COUNT / REPO_COUNT
|
||||
for (let repoIndex = 0; repoIndex < REPO_COUNT; repoIndex++) {
|
||||
worktreesByRepo[`repo-${repoIndex}`] = Array.from({ length: perRepo }, (_, index) => {
|
||||
const id = `repo-${repoIndex}::/repos/repo-${repoIndex}/wt-${index}`
|
||||
return {
|
||||
get id() {
|
||||
idReads++
|
||||
return id
|
||||
},
|
||||
repoId: `repo-${repoIndex}`,
|
||||
path: `/repos/repo-${repoIndex}/wt-${index}`,
|
||||
branch: `branch-${index}`,
|
||||
hostId: 'local'
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
state: {
|
||||
repos,
|
||||
worktreesByRepo,
|
||||
folderWorkspaces: [],
|
||||
projectGroups: [],
|
||||
activeView: 'worktrees',
|
||||
activeWorktreeId: 'repo-9::/repos/repo-9/wt-39',
|
||||
rightSidebarOpen: true,
|
||||
rightSidebarTab: 'checks'
|
||||
} as unknown as AppState,
|
||||
reads: () => idReads,
|
||||
worktreeId: 'repo-9::/repos/repo-9/wt-39'
|
||||
}
|
||||
}
|
||||
|
||||
describe('terminal workspace routing scales with tab count, not workspace count', () => {
|
||||
it('answers repeated owner lookups without rescanning every worktree', () => {
|
||||
const { state, reads, worktreeId } = buildCountingState()
|
||||
const CALLS = 200
|
||||
|
||||
for (let call = 0; call < CALLS; call++) {
|
||||
worktreeUsesRemoteConnection(state, worktreeId)
|
||||
getRemoteConnectionIdForWorktree(state, worktreeId)
|
||||
worktreeUsesWslPath(state, worktreeId)
|
||||
rightSidebarShowsPullRequestData(state)
|
||||
}
|
||||
|
||||
// One index build plus O(1) hits; rescanning costs CALLS × WORKTREE_COUNT ID reads.
|
||||
expect(reads()).toBeLessThanOrEqual(WORKTREE_COUNT * 2)
|
||||
expect(reads()).toBeLessThan(CALLS * WORKTREE_COUNT)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
|
||||
import { DEFAULT_REPO_BADGE_COLOR } from '../../../../shared/constants'
|
||||
import { buildRuntimeSessionPlaceholders } from './workspace-terminal-placeholders'
|
||||
import { addHydratedSshWorktreePlaceholders } from './workspace-terminal-ssh-placeholders'
|
||||
@@ -16,131 +16,10 @@ const repo: Repo = {
|
||||
executionHostId: 'local'
|
||||
}
|
||||
|
||||
const worktree: Worktree = {
|
||||
id: 'repo-1::/repos/one',
|
||||
repoId: 'repo-1',
|
||||
hostId: 'local',
|
||||
displayName: 'main',
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
linkedGitLabIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
path: '/repos/one',
|
||||
head: '',
|
||||
branch: '',
|
||||
isBare: false,
|
||||
isMainWorktree: true
|
||||
}
|
||||
|
||||
describe('buildRuntimeSessionPlaceholders', () => {
|
||||
it('returns the original repos array when no placeholder repo is needed', () => {
|
||||
const repos = [repo]
|
||||
const worktreesByRepo = { 'repo-1': [worktree] }
|
||||
|
||||
const result = buildRuntimeSessionPlaceholders({
|
||||
repos,
|
||||
runtimeHostIdByWorkspaceSessionKey: {},
|
||||
worktreesByRepo
|
||||
})
|
||||
|
||||
// Hydration writes these straight to the store; a fresh array would rerender
|
||||
// every component selecting the whole repo list for no data change.
|
||||
expect(result.repos).toBe(repos)
|
||||
expect(result.worktreesByRepo).toBe(worktreesByRepo)
|
||||
})
|
||||
|
||||
it('keeps the original repos array when the session only references known repos', () => {
|
||||
const repos = [repo]
|
||||
|
||||
const result = buildRuntimeSessionPlaceholders({
|
||||
repos,
|
||||
runtimeHostIdByWorkspaceSessionKey: { 'repo-1::/repos/one': 'runtime:host-1' },
|
||||
worktreesByRepo: { 'repo-1': [worktree] }
|
||||
})
|
||||
|
||||
expect(result.repos).toBe(repos)
|
||||
})
|
||||
|
||||
it('still appends a placeholder repo for an unknown runtime workspace', () => {
|
||||
const repos = [repo]
|
||||
|
||||
const result = buildRuntimeSessionPlaceholders({
|
||||
repos,
|
||||
runtimeHostIdByWorkspaceSessionKey: { 'repo-2::/repos/two': 'runtime:host-1' },
|
||||
worktreesByRepo: { 'repo-1': [worktree] }
|
||||
})
|
||||
|
||||
expect(result.repos).not.toBe(repos)
|
||||
expect(result.repos.map((entry) => entry.id)).toEqual(['repo-1', 'repo-2'])
|
||||
expect(repos).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addHydratedSshWorktreePlaceholders', () => {
|
||||
it('returns the original map when no SSH placeholder is needed', () => {
|
||||
const worktreesByRepo = { 'repo-1': [worktree] }
|
||||
|
||||
const result = addHydratedSshWorktreePlaceholders([repo], worktreesByRepo, {
|
||||
'repo-1::/repos/one': []
|
||||
})
|
||||
|
||||
expect(result).toBe(worktreesByRepo)
|
||||
})
|
||||
|
||||
it('returns the original map when the SSH worktree is already present', () => {
|
||||
const sshRepo: Repo = { ...repo, id: 'ssh-repo', connectionId: 'ssh-1' }
|
||||
const sshWorktree: Worktree = { ...worktree, id: 'ssh-repo::/repos/ssh', repoId: 'ssh-repo' }
|
||||
const worktreesByRepo = { 'ssh-repo': [sshWorktree] }
|
||||
|
||||
const result = addHydratedSshWorktreePlaceholders([sshRepo], worktreesByRepo, {
|
||||
'ssh-repo::/repos/ssh': []
|
||||
})
|
||||
|
||||
expect(result).toBe(worktreesByRepo)
|
||||
})
|
||||
|
||||
it('still adds a placeholder for an SSH worktree with no row, without mutating the caller', () => {
|
||||
const sshRepo: Repo = { ...repo, id: 'ssh-repo', connectionId: 'ssh-1' }
|
||||
const worktreesByRepo = { 'ssh-repo': [] as Worktree[] }
|
||||
|
||||
const result = addHydratedSshWorktreePlaceholders([sshRepo], worktreesByRepo, {
|
||||
'ssh-repo::/repos/ssh': []
|
||||
})
|
||||
|
||||
expect(result).not.toBe(worktreesByRepo)
|
||||
expect(result['ssh-repo'].map((entry) => entry.id)).toEqual(['ssh-repo::/repos/ssh'])
|
||||
expect(worktreesByRepo['ssh-repo']).toHaveLength(0)
|
||||
})
|
||||
|
||||
const placeholderFor = (repos: Repo[]) =>
|
||||
addHydratedSshWorktreePlaceholders(repos, {}, { 'ssh-repo::/repos/ssh': [] })['ssh-repo'][0]
|
||||
|
||||
it('stamps a direct SSH placeholder with its target, as a listing would', () => {
|
||||
const sshRepo: Repo = { ...repo, id: 'ssh-repo', connectionId: 'box', executionHostId: null }
|
||||
expect(placeholderFor([sshRepo])).toMatchObject({ hostId: 'ssh:box' })
|
||||
expect(placeholderFor([sshRepo]).runtimeOwnerEnvironmentId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stamps a server-nested SSH placeholder with both the target and the server', () => {
|
||||
const hubRepo: Repo = {
|
||||
...repo,
|
||||
id: 'ssh-repo',
|
||||
connectionId: 'box',
|
||||
executionHostId: 'runtime:hub'
|
||||
}
|
||||
expect(placeholderFor([hubRepo])).toMatchObject({
|
||||
hostId: 'ssh:box',
|
||||
runtimeOwnerEnvironmentId: 'hub'
|
||||
})
|
||||
})
|
||||
|
||||
it('routes a restored, selected server-nested SSH tab through its server, not a direct dial', () => {
|
||||
const hubRepo: Repo = {
|
||||
...repo,
|
||||
@@ -165,14 +44,6 @@ describe('addHydratedSshWorktreePlaceholders', () => {
|
||||
owner: { endpoint: { kind: 'environment', environmentId: 'hub' }, at: 'ssh:box' }
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves the placeholder unstamped when the repo id lives on several hosts', () => {
|
||||
const local: Repo = { ...repo, id: 'ssh-repo', connectionId: null, executionHostId: 'local' }
|
||||
const hub: Repo = { ...local, connectionId: 'box', executionHostId: 'runtime:hub' }
|
||||
const placeholder = placeholderFor([local, hub])
|
||||
expect(placeholder.hostId).toBeUndefined()
|
||||
expect(placeholder.runtimeOwnerEnvironmentId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildRuntimeSessionPlaceholders for a server repo that nests SSH', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
|
||||
import { applyLayoutFrame, EMPTY_WORKSPACE_LAYOUT_CACHE } from './workspace-layout-cache'
|
||||
import { applyLayoutFrame } from './workspace-layout-cache'
|
||||
|
||||
function layout(worktreeId: string, tabIds: string[] = []): PublishedWorkspaceLayout {
|
||||
return {
|
||||
@@ -13,31 +13,6 @@ function layout(worktreeId: string, tabIds: string[] = []): PublishedWorkspaceLa
|
||||
}
|
||||
|
||||
describe('applyLayoutFrame', () => {
|
||||
it('a workspace frame replaces one workspace and leaves the rest untouched', () => {
|
||||
const b = layout('b')
|
||||
const cache = { a: layout('a'), b }
|
||||
const next = applyLayoutFrame(cache, {
|
||||
type: 'workspace',
|
||||
key: 'a',
|
||||
layout: layout('a', ['t1'])
|
||||
})
|
||||
expect(next).toEqual({ a: layout('a', ['t1']), b })
|
||||
expect(next.b).toBe(b)
|
||||
expect(cache).toEqual({ a: layout('a'), b })
|
||||
expect(
|
||||
applyLayoutFrame(EMPTY_WORKSPACE_LAYOUT_CACHE, { type: 'workspace', key: 'c', layout: b })
|
||||
).toEqual({ c: b })
|
||||
})
|
||||
|
||||
it('a removed frame drops one workspace; an absent one keeps the cache', () => {
|
||||
const b = layout('b')
|
||||
const cache = { a: layout('a'), b }
|
||||
const next = applyLayoutFrame(cache, { type: 'removed', key: 'a' })
|
||||
expect(next).toEqual({ b })
|
||||
expect(next.b).toBe(b)
|
||||
expect(applyLayoutFrame(next, { type: 'removed', key: 'a' })).toBe(next)
|
||||
})
|
||||
|
||||
it('a snapshot replaces the whole cache: absent workspaces go, nothing is merged', () => {
|
||||
const b = layout('b')
|
||||
const cache = { a: layout('a', ['t1', 't2']), b, gone: layout('gone') }
|
||||
@@ -53,22 +28,4 @@ describe('applyLayoutFrame', () => {
|
||||
// An equal entry keeps its object, so its readers do not re-render.
|
||||
expect(next.b).toBe(b)
|
||||
})
|
||||
|
||||
it('a snapshot equal by value keeps the cache object', () => {
|
||||
const cache = { a: layout('a', ['t1']), b: layout('b') }
|
||||
const equal = applyLayoutFrame(cache, {
|
||||
type: 'snapshot',
|
||||
workspaces: [
|
||||
{ key: 'a', layout: layout('a', ['t1']) },
|
||||
{ key: 'b', layout: layout('b') }
|
||||
]
|
||||
})
|
||||
expect(equal).toBe(cache)
|
||||
const subset = applyLayoutFrame(cache, {
|
||||
type: 'snapshot',
|
||||
workspaces: [{ key: 'a', layout: layout('a', ['t1']) }]
|
||||
})
|
||||
expect(subset).toEqual({ a: cache.a })
|
||||
expect(subset.a).toBe(cache.a)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,14 +82,6 @@ function withoutTab(frame: PublishedWorkspaceLayout, tabId: string): PublishedWo
|
||||
}
|
||||
|
||||
describe('laying frame slices over the store', () => {
|
||||
it('drops a terminal tab main closed, with its pane layout', async () => {
|
||||
const { state, session, frame } = await restored()
|
||||
const patch = patchFor(state, session, withoutTab(frame, 'tab-shell'))
|
||||
expect(patch.tabsByWorktree?.[GIT_KEY]?.map((tab) => tab.id)).not.toContain('tab-shell')
|
||||
expect(patch.terminalLayoutsByTabId).not.toHaveProperty('tab-shell')
|
||||
expect(patch.unifiedTabsByWorktree?.[GIT_KEY]?.map((tab) => tab.id)).not.toContain('tab-shell')
|
||||
})
|
||||
|
||||
it('keeps the pane layout of a tab the window’s own pane command holds, until main drops the tab', async () => {
|
||||
const { state, session, frame } = await restored()
|
||||
const newRatios: PublishedWorkspaceLayout = {
|
||||
@@ -109,33 +101,6 @@ describe('laying frame slices over the store', () => {
|
||||
).not.toHaveProperty('tab-shell')
|
||||
})
|
||||
|
||||
it('keeps an open file’s editor state and place, and drops a file main closed', async () => {
|
||||
const { state, session, frame } = await restored()
|
||||
const file = state.openFiles.find((entry) => entry.worktreeId === GIT_KEY)!
|
||||
const edited = { ...state, openFiles: [{ ...file, isDirty: true }] }
|
||||
expect(patchFor(edited, session, frame).openFiles).toBeUndefined()
|
||||
const closed = patchFor(edited, session, {
|
||||
...withoutTab(frame, 'editor:src/app.ts'),
|
||||
editorFiles: []
|
||||
})
|
||||
expect(closed.openFiles).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps a browser tab’s page state, and drops the pages of a tab main closed', async () => {
|
||||
const { state, session, frame } = await restored()
|
||||
const browser = state.browserTabsByWorktree[GIT_KEY]![0]!
|
||||
const browsing = {
|
||||
...state,
|
||||
browserTabsByWorktree: { [GIT_KEY]: [{ ...browser, url: 'https://example.com/next' }] }
|
||||
}
|
||||
expect(patchFor(browsing, session, frame).browserTabsByWorktree).toBeUndefined()
|
||||
const closed = patchFor(browsing, session, {
|
||||
...withoutTab(frame, 'browser-1'),
|
||||
browserTabs: []
|
||||
})
|
||||
expect(closed.browserPagesByWorkspace).not.toHaveProperty('browser-1')
|
||||
})
|
||||
|
||||
it('keeps a client-hosted browser tab this desktop created, with its page and handle, at its place (L274)', async () => {
|
||||
const { state, session, frame } = await restored()
|
||||
const page = state.browserPagesByWorkspace['browser-1']![0]!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity'
|
||||
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import '../../../shared/workspace-scope'
|
||||
import type { AppState } from './types'
|
||||
import { makeFolderWorkspace, makeWorktree } from './slices/worktrees-slice-test-fixtures'
|
||||
import { selectWorkspaceByHostTarget } from './workspace-record-selectors'
|
||||
@@ -51,25 +51,4 @@ describe('selectWorkspaceByHostTarget', () => {
|
||||
)
|
||||
).toBe(hubRow)
|
||||
})
|
||||
|
||||
it('matches a folder workspace on its host only', () => {
|
||||
const state = makeState()
|
||||
const key = folderWorkspaceKey('folder-1')
|
||||
expect(
|
||||
selectWorkspaceByHostTarget(state, hostWorktreeTarget({ id: key, hostId: 'local' }))?.path
|
||||
).toBe(folder.folderPath)
|
||||
expect(
|
||||
selectWorkspaceByHostTarget(state, hostWorktreeTarget({ id: key, hostId: 'ssh:build-box' }))
|
||||
).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectWorkspaceByHostTarget bucket keys', () => {
|
||||
it('finds a row filed under a bucket that is not its repo id', () => {
|
||||
const row = makeWorktree({ id: 'wt-9', repoId: 'repo-9', hostId: 'ssh:remote' })
|
||||
const state = makeState({ worktreesByRepo: { remote: [row] } })
|
||||
expect(
|
||||
selectWorkspaceByHostTarget(state, hostWorktreeTarget({ id: 'wt-9', hostId: 'ssh:remote' }))
|
||||
).toBe(row)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { DiffComment } from '../../../shared/diff-comment-types'
|
||||
|
||||
import type { AppState } from './types'
|
||||
import { makeWorktree } from './slices/worktrees-slice-test-fixtures'
|
||||
import {
|
||||
selectWorktreeDiffComments,
|
||||
selectWorktreeDiffCommentsOrEmpty
|
||||
} from './worktree-diff-comments-selector'
|
||||
import './worktree-diff-comments-selector'
|
||||
import { createDiffCommentViewSelector, sourceParts } from './diff-comment-owner'
|
||||
|
||||
const TARGET = 'repo-9::/w/target'
|
||||
@@ -94,23 +91,3 @@ describe('note owner view selectors', () => {
|
||||
expect(large.catalogWriteReads).toBeLessThanOrEqual(8 * 1000 + 9 * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectWorktreeDiffComments', () => {
|
||||
it('reads a replacement worktree snapshot and handles an absent id', () => {
|
||||
const firstComments = [makeComment('comment-1')]
|
||||
const nextComments = [makeComment('comment-2')]
|
||||
const first = {
|
||||
'repo-9': [makeWorktree({ id: TARGET, repoId: 'repo-9', diffComments: firstComments })]
|
||||
}
|
||||
const next = {
|
||||
'repo-9': [makeWorktree({ id: TARGET, repoId: 'repo-9', diffComments: nextComments })]
|
||||
}
|
||||
|
||||
expect(selectWorktreeDiffComments({ worktreesByRepo: first }, TARGET)).toBe(firstComments)
|
||||
expect(selectWorktreeDiffComments({ worktreesByRepo: next }, TARGET)).toBe(nextComments)
|
||||
expect(selectWorktreeDiffComments({ worktreesByRepo: next }, null)).toBeUndefined()
|
||||
expect(selectWorktreeDiffCommentsOrEmpty({ worktreesByRepo: next }, null)).toBe(
|
||||
selectWorktreeDiffCommentsOrEmpty({ worktreesByRepo: next }, undefined)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,14 +50,6 @@ describe('id-keyed worktree projections keep distinct hosts distinct', () => {
|
||||
// Last wins: fetchWorktrees replaces, so the later entry is the current one.
|
||||
expect(all[0]?.head).toBe('fresh')
|
||||
})
|
||||
|
||||
it('treats an unqualified row as its own bucket rather than inventing a host', () => {
|
||||
const unqualified: Worktree = { ...baseWorktree, displayName: 'unqualified orca' }
|
||||
|
||||
const all = getIndexedAllWorktrees(byRepo(unqualified, sshRow))
|
||||
|
||||
expect(all).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorktreeOnHostFromState refuses a lookup that still matches two copies', () => {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getRepoOwnerWorktreeVisibilityDefaults } from './worktree-visibility-defaults-by-host'
|
||||
|
||||
describe('getRepoOwnerWorktreeVisibilityDefaults', () => {
|
||||
it('resolves defaults from each repository owner', () => {
|
||||
const settings = { worktreeVisibilityDefaults: { external: 'hide' as const } }
|
||||
const defaultsByHost = {
|
||||
local: { external: 'hide' as const },
|
||||
'runtime:show-host': { external: 'show' as const },
|
||||
'runtime:legacy-host': null
|
||||
}
|
||||
|
||||
expect(
|
||||
getRepoOwnerWorktreeVisibilityDefaults(
|
||||
{ executionHostId: 'runtime:show-host' },
|
||||
settings,
|
||||
defaultsByHost
|
||||
)
|
||||
).toEqual({ external: 'show' })
|
||||
expect(
|
||||
getRepoOwnerWorktreeVisibilityDefaults({ connectionId: 'ssh-box' }, settings, defaultsByHost)
|
||||
).toEqual({ external: 'hide' })
|
||||
expect(
|
||||
getRepoOwnerWorktreeVisibilityDefaults(
|
||||
{ executionHostId: 'runtime:legacy-host' },
|
||||
settings,
|
||||
defaultsByHost
|
||||
)
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user