mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
test: shared switch-on fixtures so 71 layout tests pass with main owning the layout (#31076)
* test: shared switch-on fixtures so 71 layout-owner test files run with the switch on - layoutApplyTestTransport answers layout.apply (accept, apply to main's model with its frame, pending, refuse) - main's loaded layout as snapshot frames, shared from the frame-sync test - tab-layout maps for the editor store harnesses; missing store methods in the main harnesses - workspaceRepos/registerWorkspaceRepos so main's load keeps seeded layouts - ORCA_LAYOUT_OWNER=main runs vitest with the switch on - the 4 rollback-build tests stay switch-off, labelled * test: keep switch-off assertions exact, settle only after layout commands, reset the transport per test * test: parse the transport's command without Reflect.get; name the staged-session type * test: pin the test transport in the layout.apply census; route the autosave fixture as unit support --------- Co-authored-by: E2E Test <e2e@test.local>
This commit is contained in:
@@ -23,7 +23,9 @@ const unitHarnesses = [
|
||||
]
|
||||
const supportBridges = new Set([
|
||||
'src/renderer/src/runtime/web-runtime-browser-creation-placement-test-rig.ts',
|
||||
'src/renderer/src/components/editor/editor-model-lifetime-fixture.ts'
|
||||
'src/renderer/src/components/editor/editor-model-lifetime-fixture.ts',
|
||||
// Autosave suites' store rig; it takes the editor harness's tab-layout maps.
|
||||
'src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts'
|
||||
])
|
||||
|
||||
function productConsumers(file, graph, unitFiles) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { defaultExclude, defineConfig } from 'vitest/config'
|
||||
import { UNIT_INCLUDE, UNIT_EXCLUDE } from './scripts/ci-unit-files.mjs'
|
||||
@@ -16,7 +17,9 @@ const transforms = {
|
||||
define: {
|
||||
ORCA_FEATURE_WALL_ENABLED: 'true',
|
||||
// Why: tests only interpolate it; they must not depend on the checkout's HEAD.
|
||||
ORCA_BUILD_COMMIT: JSON.stringify('0'.repeat(40))
|
||||
ORCA_BUILD_COMMIT: JSON.stringify('0'.repeat(40)),
|
||||
// A dark switch set to 'main' in the env (`ORCA_LAYOUT_OWNER=main pnpm vitest`) is on in tests.
|
||||
...createRequire(import.meta.url)('./dark-build-switches.cjs').turnedOnDarkBuildDefines()
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
|
||||
@@ -24,6 +24,9 @@ vi.mock('electron', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
|
||||
import {
|
||||
registerRendererShutdownCheckpointHandler,
|
||||
SHUTDOWN_CHECKPOINT_FLUSH_DEADLINE_MS
|
||||
@@ -31,6 +34,11 @@ import {
|
||||
|
||||
const AWAIT_CHANNEL = 'app:await-before-unload-checkpoint'
|
||||
|
||||
/** What is staged for a window's save: the save itself, or main's merge of it when main owns the
|
||||
* layout (its content is covered by renderer-shutdown-checkpoint-sleep-records.test.ts). */
|
||||
const windowSave = (state: Partial<WorkspaceSessionState>): unknown =>
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN ? expect.any(Object) : state
|
||||
|
||||
describe('registerRendererShutdownCheckpointHandler', () => {
|
||||
beforeEach(() => {
|
||||
syncHandlers.clear()
|
||||
@@ -41,6 +49,8 @@ describe('registerRendererShutdownCheckpointHandler', () => {
|
||||
it('stages every shutdown mutation before queueing persistence', () => {
|
||||
const callOrder: string[] = []
|
||||
const store = {
|
||||
// Read by the switch-on staging, which keeps main's own layout.
|
||||
getWorkspaceSession: vi.fn(() => getDefaultWorkspaceSession()),
|
||||
stageWorkspaceSessionBeforeUnload: vi.fn((_state, hostId?: string) => {
|
||||
callOrder.push(`session:${hostId ?? 'local'}`)
|
||||
}),
|
||||
@@ -64,12 +74,12 @@ describe('registerRendererShutdownCheckpointHandler', () => {
|
||||
|
||||
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
localSession,
|
||||
windowSave(localSession),
|
||||
undefined
|
||||
)
|
||||
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
remoteSession,
|
||||
windowSave(remoteSession),
|
||||
'runtime:host-1'
|
||||
)
|
||||
expect(store.updateUI).toHaveBeenCalledWith({ activeView: 'settings' })
|
||||
@@ -84,6 +94,7 @@ describe('registerRendererShutdownCheckpointHandler', () => {
|
||||
|
||||
it('never stages the source partition of a fenced host', () => {
|
||||
const store = {
|
||||
getWorkspaceSession: vi.fn(() => getDefaultWorkspaceSession()),
|
||||
stageWorkspaceSessionBeforeUnload: vi.fn(),
|
||||
getSshTarget: vi.fn(() => ({ orcadFence: { environmentId: 'env-1' } })),
|
||||
updateUI: vi.fn(),
|
||||
@@ -98,7 +109,7 @@ describe('registerRendererShutdownCheckpointHandler', () => {
|
||||
})
|
||||
|
||||
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenCalledTimes(1)
|
||||
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenCalledWith({}, undefined)
|
||||
expect(store.stageWorkspaceSessionBeforeUnload).toHaveBeenCalledWith(windowSave({}), undefined)
|
||||
expect(event.returnValue).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock('electron', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import { registerSessionHandlers } from './session'
|
||||
|
||||
describe('registerSessionHandlers', () => {
|
||||
@@ -33,6 +34,8 @@ describe('registerSessionHandlers', () => {
|
||||
getSshTarget: vi.fn((id: string) =>
|
||||
id === 'fenced' ? { orcadFence: { environmentId: 'env-1' } } : {}
|
||||
),
|
||||
// Read by the switch-on save, which keeps main's own layout.
|
||||
getWorkspaceSession: vi.fn(() => getDefaultWorkspaceSession()),
|
||||
setWorkspaceSession: vi.fn(),
|
||||
patchWorkspaceSession: vi.fn(),
|
||||
flushPendingOrThrowAsync: vi.fn(() => Promise.resolve())
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
hostWorktreeTarget,
|
||||
type HostWorktreeTarget
|
||||
} from '../../shared/worktree/host-qualified-identity'
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
|
||||
const path = '/remote/repository'
|
||||
const rows = [{ path, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true }]
|
||||
@@ -68,7 +69,11 @@ function fixture(fields: Pick<Repo, 'connectionId' | 'executionHostId'>) {
|
||||
getAllWorktreeLineage: () => ({}),
|
||||
getAllWorkspaceLineage: () => ({}),
|
||||
removeWorktreeLineage: vi.fn(),
|
||||
captureNativeLocalWorktreeMetadataScanExpectation: () => undefined
|
||||
captureNativeLocalWorktreeMetadataScanExpectation: () => undefined,
|
||||
// Read by main's switch-on layout purge after a listing.
|
||||
getWorkspaceSessionHostIds: () => ['local'],
|
||||
getWorkspaceSession: () => getDefaultWorkspaceSession(),
|
||||
removeWorkspaceSessionStateForWorktree: vi.fn()
|
||||
}
|
||||
const context = {
|
||||
store,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { resetSshProviderAuthorities } from '../ssh/ssh-provider-authority'
|
||||
import { createWorktreeRuntimeStub, type WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
|
||||
import { handlers, mainWindow, store } from './worktrees-test-ipc-surface'
|
||||
import { configureMetadataPruningStoreMocks } from './worktrees-test-metadata-pruning-store'
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import {
|
||||
ORIGINAL_PLATFORM,
|
||||
setPlatform,
|
||||
@@ -143,6 +144,8 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub {
|
||||
store.removeWorktreeMeta,
|
||||
store.pruneSessionlessMissingLocalWorktreeMetadataForRepo,
|
||||
store.removeWorkspaceSessionStateForWorktree,
|
||||
store.getWorkspaceSessionHostIds,
|
||||
store.getWorkspaceSession,
|
||||
store.getAllWorktreeLineage,
|
||||
store.removeWorktreeLineage,
|
||||
store.getAllWorkspaceLineage,
|
||||
@@ -220,6 +223,8 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub {
|
||||
updatedAt: 0
|
||||
}
|
||||
])
|
||||
store.getWorkspaceSessionHostIds.mockReturnValue(['local'])
|
||||
store.getWorkspaceSession.mockReturnValue(getDefaultWorkspaceSession())
|
||||
store.getAllWorktreeLineage.mockReturnValue({})
|
||||
store.getAllWorkspaceLineage.mockReturnValue({})
|
||||
store.getFolderWorkspaces.mockReturnValue([])
|
||||
|
||||
@@ -37,6 +37,9 @@ export type TestStore = {
|
||||
removeWorktreeMeta: TargetStoreMock
|
||||
pruneSessionlessMissingLocalWorktreeMetadataForRepo: StoreMock
|
||||
removeWorkspaceSessionStateForWorktree: KeyedStoreMock
|
||||
/** Read by main's switch-on layout purge after a listing. */
|
||||
getWorkspaceSessionHostIds: StoreMock
|
||||
getWorkspaceSession: StoreMock
|
||||
getAllWorktreeLineage: StoreMock
|
||||
removeWorktreeLineage: TargetStoreMock
|
||||
getAllWorkspaceLineage: StoreMock
|
||||
@@ -72,6 +75,8 @@ export const store: TestStore = {
|
||||
removeWorktreeMeta: vi.fn(),
|
||||
pruneSessionlessMissingLocalWorktreeMetadataForRepo: vi.fn(),
|
||||
removeWorkspaceSessionStateForWorktree: vi.fn(),
|
||||
getWorkspaceSessionHostIds: vi.fn(),
|
||||
getWorkspaceSession: vi.fn(),
|
||||
getAllWorktreeLineage: vi.fn(),
|
||||
removeWorktreeLineage: vi.fn(),
|
||||
getAllWorkspaceLineage: vi.fn(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
closeTestStores,
|
||||
testState,
|
||||
createStore,
|
||||
writeDataFile,
|
||||
readDataFile
|
||||
readDataFile,
|
||||
testState,
|
||||
workspaceRepos,
|
||||
writeDataFile
|
||||
} from './persistence-test-harness'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { writeFileSync, rmSync, mkdtempSync, mkdirSync } from 'node:fs'
|
||||
@@ -77,7 +78,7 @@ describe('Store', () => {
|
||||
mkdirSync(agentHooksDir, { recursive: true })
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -189,7 +190,7 @@ describe('Store', () => {
|
||||
mkdirSync(agentHooksDir, { recursive: true })
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -300,7 +301,7 @@ describe('Store', () => {
|
||||
mkdirSync(agentHooksDir, { recursive: true })
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -396,7 +397,7 @@ describe('Store', () => {
|
||||
const ownerPaneKey = makePaneKey('tab-target', TEST_LEAF_2)
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -432,7 +433,7 @@ describe('Store', () => {
|
||||
it('persists fallback aliases when a legacy split layout has no PTY leaf bindings', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -522,7 +523,7 @@ describe('Store', () => {
|
||||
const leafCount = 130_000
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -578,7 +579,7 @@ describe('Store', () => {
|
||||
const stablePaneKey = makePaneKey('tab1', TEST_LEAF_1)
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
closeTestStores,
|
||||
testState,
|
||||
createStore,
|
||||
writeDataFile,
|
||||
makeTerminalTab
|
||||
makeTerminalTab,
|
||||
registerWorkspaceRepos,
|
||||
testState,
|
||||
workspaceRepos,
|
||||
writeDataFile
|
||||
} from './persistence-test-harness'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { rmSync, mkdtempSync } from 'node:fs'
|
||||
@@ -68,7 +70,7 @@ describe('Store', () => {
|
||||
it('remaps legacy SSH lease leaf ids by PTY when the layout is already normalized', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -320,6 +322,7 @@ describe('Store', () => {
|
||||
|
||||
it('reconciles only the incarnation of an unchanged durable PTY binding', async () => {
|
||||
const store = await createStore()
|
||||
registerWorkspaceRepos(store, 'wt1')
|
||||
const paneKey = `tab1:${TEST_LEAF_1}`
|
||||
store.setWorkspaceSession({
|
||||
...getDefaultWorkspaceSession(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
closeTestStores,
|
||||
testState,
|
||||
createStore,
|
||||
makeTerminalTab
|
||||
makeTerminalTab,
|
||||
registerWorkspaceRepos,
|
||||
testState
|
||||
} from './persistence-test-harness'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
@@ -290,6 +291,7 @@ describe('Store', () => {
|
||||
'preserves a reconciled incarnation across a renderer snapshot ($label)',
|
||||
async ({ hostId }) => {
|
||||
const store = await createStore()
|
||||
registerWorkspaceRepos(store, 'wt1')
|
||||
const paneKey = `tab1:${TEST_LEAF_1}`
|
||||
store.setWorkspaceSession(
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Project, ProjectHostSetup } from '../shared/project-types'
|
||||
import type { Repo } from '../shared/repo-types'
|
||||
import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types'
|
||||
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from '../shared/worktree/id'
|
||||
import type { PersistedState } from '../shared/persisted-state-types'
|
||||
import { getDefaultPersistedState } from '../shared/constants'
|
||||
import type { SshRemotePtyLease } from '../shared/ssh-types'
|
||||
@@ -165,6 +166,23 @@ export const makeRepo = (overrides: Partial<Repo> = {}): Repo => ({
|
||||
...overrides
|
||||
})
|
||||
|
||||
/**
|
||||
* The repos of the given workspaces, as a real profile registers them: main's load drops a stored
|
||||
* layout whose repo is not registered.
|
||||
*/
|
||||
export function workspaceRepos(...worktreeIds: string[]): Repo[] {
|
||||
return [...new Set(worktreeIds.map(getRepoIdFromWorktreeId))].map((id) => makeRepo({ id }))
|
||||
}
|
||||
|
||||
/** Registers the repos of the given workspaces in a running store; see `workspaceRepos`. */
|
||||
export function registerWorkspaceRepos(store: Store, ...worktreeIds: string[]): void {
|
||||
for (const repo of workspaceRepos(...worktreeIds)) {
|
||||
if (!store.getRepo(repo.id)) {
|
||||
store.addRepo(repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const makeProject = (overrides: Partial<Project> = {}): Project => ({
|
||||
id: 'project-1',
|
||||
displayName: 'Project',
|
||||
|
||||
@@ -9,13 +9,14 @@ import { MAX_BROWSER_HISTORY_ENTRIES } from '../shared/workspace-session-browser
|
||||
import {
|
||||
closeTestStores,
|
||||
createSqliteTestStore,
|
||||
readPersistedStateJson,
|
||||
testState,
|
||||
createStore,
|
||||
dataFile,
|
||||
writeDataFile,
|
||||
makeRepo,
|
||||
makeTerminalTab
|
||||
makeTerminalTab,
|
||||
readPersistedStateJson,
|
||||
testState,
|
||||
workspaceRepos,
|
||||
writeDataFile
|
||||
} from './persistence-test-harness'
|
||||
import {
|
||||
TEST_LEAF_1,
|
||||
@@ -457,7 +458,7 @@ describe('Store', () => {
|
||||
it('remaps legacy SSH lease leaf ids when loading legacy workspace layouts', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
@@ -519,7 +520,7 @@ describe('Store', () => {
|
||||
mkdirSync(agentHooksDir, { recursive: true })
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
repos: workspaceRepos('wt1'),
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
closeTestStores,
|
||||
testState,
|
||||
createStore,
|
||||
makeTerminalTab
|
||||
makeTerminalTab,
|
||||
registerWorkspaceRepos,
|
||||
testState
|
||||
} from './persistence-test-harness'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { rmSync, mkdtempSync } from 'node:fs'
|
||||
@@ -68,6 +69,7 @@ describe('Store', () => {
|
||||
})
|
||||
it('adds a missing split leaf to the durable root when a new pane spawns before layout debounce', async () => {
|
||||
const store = await createStore()
|
||||
registerWorkspaceRepos(store, 'wt1')
|
||||
store.setWorkspaceSession({
|
||||
activeRepoId: 'r1',
|
||||
activeWorktreeId: 'wt1',
|
||||
|
||||
@@ -8,6 +8,9 @@ import { fixture } from './profile-state-delayed-authority-fixture'
|
||||
import type * as AgreementModule from '../terminal-topology/terminal-pane-placement-agreement'
|
||||
import type { PersistPtyBindingArgs } from './pty-binding-persistence'
|
||||
|
||||
// rollback build (switch-off) — delete with the switch-off code post-flip
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
|
||||
|
||||
const agreementCheck = vi.hoisted(() => ({ throws: false }))
|
||||
vi.mock('../terminal-topology/terminal-pane-placement-agreement', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof AgreementModule>()
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import { WindowSession, withoutLeaf } from './terminal-topology-window-session-fixture'
|
||||
import { checkWorkspaceLayoutRules } from '../../../shared/workspace-layout/workspace-layout-rules'
|
||||
|
||||
// rollback build (switch-off) — delete with the switch-off code post-flip
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
|
||||
|
||||
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
|
||||
vi.mock('../../ssh/config/ssh-config-parser', () => ({
|
||||
loadUserSshConfig: () => ({ hosts: [] }),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { withChatTabChanges, type ChatTabChange } from './agent-session-tab-layout'
|
||||
|
||||
// rollback build (switch-off) — delete with the switch-off code post-flip
|
||||
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
|
||||
|
||||
const WT = 'repo-1::/tmp/worktree-a'
|
||||
|
||||
const EMPTY: WorkspaceSessionState = {
|
||||
|
||||
@@ -181,8 +181,13 @@ describe('a saved browser tab on a host with no window', () => {
|
||||
page: 'page-new'
|
||||
})
|
||||
|
||||
const saved = getSession().browserTabsByWorktree?.[TEST_WORKTREE_ID] ?? []
|
||||
expect(saved.map((tab) => [tab.id, tab.url])).toEqual([['page-new', 'https://example.com/new']])
|
||||
// A switch-on build saves the page once main's layout write lands.
|
||||
await vi.waitFor(() => {
|
||||
const saved = getSession().browserTabsByWorktree?.[TEST_WORKTREE_ID] ?? []
|
||||
expect(saved.map((tab) => [tab.id, tab.url])).toEqual([
|
||||
['page-new', 'https://example.com/new']
|
||||
])
|
||||
})
|
||||
// Which tab shows is a view's choice; opening one records none.
|
||||
expect(getSession().activeBrowserTabIdByWorktree?.[TEST_WORKTREE_ID]).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { TerminalLayoutSnapshot } from '../../shared/terminal-tab-types'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { makePaneKey } from '../../shared/stable-pane-id'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
|
||||
|
||||
const REPO_ID = 'repo-1'
|
||||
const WORKTREE_ID = `${REPO_ID}::/workspace`
|
||||
@@ -222,6 +223,9 @@ function createHarness(
|
||||
}
|
||||
}
|
||||
},
|
||||
/** The split pane's terminal starting, as its window reports it. */
|
||||
registerSplitPty: (leafId: string) =>
|
||||
runtime.registerPty(SPLIT_PTY_ID, WORKTREE_ID, connectionId, { tabId: TAB_ID, leafId }),
|
||||
resolveReveal: () => resolveReveal?.({ tabId: TAB_ID }),
|
||||
resolveSpawn: () => resolveSpawn?.({ id: SPLIT_PTY_ID, incarnationId: SPLIT_INCARNATION_ID })
|
||||
}
|
||||
@@ -232,6 +236,8 @@ describe('remote runtime terminal split authority', () => {
|
||||
const harness = createHarness(true, { rendererMounted: true, graphOnlySource: true })
|
||||
|
||||
const split = harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' })
|
||||
// A switch-on build tells the window once main has admitted the new pane.
|
||||
await vi.waitFor(() => expect(harness.rendererSplitTerminal).toHaveBeenCalled())
|
||||
|
||||
const newLeafId = harness.rendererSplitTerminal.mock.calls[0]?.[2]?.newLeafId
|
||||
expect(newLeafId).toEqual(expect.any(String))
|
||||
@@ -246,6 +252,10 @@ describe('remote runtime terminal split authority', () => {
|
||||
telemetrySource: undefined,
|
||||
newLeafId
|
||||
})
|
||||
// A switch-on build binds the new pane only once its terminal registered, before the graph.
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
harness.registerSplitPty(newLeafId)
|
||||
}
|
||||
harness.runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
} from '../../shared/agent-prompt-injection'
|
||||
import { getDefaultWorkspaceSession } from '../../shared/constants'
|
||||
import { makePaneKey } from '../../shared/stable-pane-id'
|
||||
import type { LayoutCommand } from '../../shared/workspace-layout/workspace-layout-command-types'
|
||||
import { applyCommandToSession } from '../persistence/terminal-topology/terminal-pane-admission'
|
||||
import { resolveHostId } from '../persistence/loading-store/session-host-partitions'
|
||||
import { toComparableRelaySshPtyId } from '../../shared/ssh-pty-id'
|
||||
import { spawnedPtyEnv } from './spawned-pty-env-fixtures'
|
||||
import {
|
||||
@@ -589,6 +592,16 @@ function makeRuntimeStoreWithWorkspaceSession(
|
||||
getWorkspaceSession: (hostId?: string) =>
|
||||
hostId === undefined || hostId === ownerHostId ? session : getDefaultWorkspaceSession(),
|
||||
setWorkspaceSession: vi.fn(setSession),
|
||||
// Main's `layout.apply` on this partition, as the Store applies it (switch-on writes).
|
||||
applyLayoutCommand: vi.fn(async (command: LayoutCommand, hostIds?: readonly string[]) => {
|
||||
const change = applyCommandToSession(
|
||||
resolveHostId(hostIds?.[0] ?? ownerHostId),
|
||||
session,
|
||||
command
|
||||
)
|
||||
session = change.session ?? session
|
||||
return change.value
|
||||
}),
|
||||
// Headless close is a durable transaction; keep the in-memory fixture's
|
||||
// persistence contract equivalent to the production store.
|
||||
flushOrThrow: vi.fn(),
|
||||
|
||||
@@ -21,6 +21,7 @@ import { RpcDispatcher } from '../dispatcher'
|
||||
import { STRUCTURED_AGENT_SESSION_METHODS } from './structured-agent-session'
|
||||
import { createStructuredAgentSessionLogger } from '../../../native-chat/agent-session-wire/structured-agent-session-logger'
|
||||
import { recordingStructuredAgentSessionLogger } from '../../../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
|
||||
import { resetStructuredAgentSessionClosesBeforeCreateForTests } from './structured-agent-session-close-before-create'
|
||||
|
||||
export const SESSION = 'session-alpha'
|
||||
export const FINGERPRINT = 'f'.repeat(64)
|
||||
@@ -210,9 +211,9 @@ export function hostStub(): StructuredAgentSessionHost {
|
||||
subscribeTurnCompletions: vi.fn(() => () => undefined),
|
||||
unsubscribe: vi.fn()
|
||||
})
|
||||
const store = { getOperationRow: vi.fn(() => null), getRecord: vi.fn(() => null) }
|
||||
// Not a call: the logger the host hands a runtime caller that reports for it.
|
||||
const logger = recordingStructuredAgentSessionLogger().logger
|
||||
Reflect.set(hostCalls, 'deps', { logger, store: { getOperationRow: vi.fn(() => null) } })
|
||||
Reflect.set(hostCalls, 'deps', { logger: recordingStructuredAgentSessionLogger().logger, store })
|
||||
return hostCalls as unknown as StructuredAgentSessionHost
|
||||
}
|
||||
|
||||
@@ -322,6 +323,8 @@ export const STRUCTURED_MOBILE_CLIENT = {
|
||||
|
||||
/** Every suite wants the same lifecycle: a fresh stub per test, no host left installed. */
|
||||
export function installStructuredHostStub(): void {
|
||||
// A switch-on close remembers the session, refusing its later create; each test starts clean.
|
||||
resetStructuredAgentSessionClosesBeforeCreateForTests()
|
||||
setStructuredAgentSessionHost(hostStub())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStore, type StoreApi } from 'zustand/vanilla'
|
||||
import { createEditorSlice } from '@/store/slices/editor'
|
||||
import type { AppState } from '@/store'
|
||||
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
import { emptyTabLayoutState } from '@/store/slices/editor-slice-test-harness'
|
||||
|
||||
export type FakeEditorDisk = {
|
||||
files: Map<string, string>
|
||||
@@ -71,8 +72,9 @@ export function stubEditorWindow(): ReturnType<typeof vi.fn> {
|
||||
}
|
||||
|
||||
export function createEditorStore(): StoreApi<AppState> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any, typescript/consistent-type-assertions -- SAFETY: a partial store; tests read only the slices it builds.
|
||||
return createStore<any>()((...args: any[]) => ({
|
||||
...emptyTabLayoutState(),
|
||||
activeWorktreeId: 'wt-1',
|
||||
repos: [],
|
||||
worktreesByRepo: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/runtime/runtime-compatibility-test-fixture'
|
||||
import { clearRuntimeCompatibilityCacheForTests } from '@/runtime/runtime-rpc-client'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { emptyTabLayoutState } from '../../store/slices/editor-slice-test-harness'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getConnectionIdForFile: vi.fn()
|
||||
@@ -42,8 +43,9 @@ type WindowStub = {
|
||||
}
|
||||
|
||||
function createEditorStore(): StoreApi<AppState> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any, typescript/consistent-type-assertions -- SAFETY: a partial store; tests read only the slices it builds.
|
||||
return createStore<any>()((...args: any[]) => ({
|
||||
...emptyTabLayoutState(),
|
||||
activeWorktreeId: 'wt-1',
|
||||
settings: {
|
||||
editorAutoSave: true,
|
||||
|
||||
@@ -6,12 +6,14 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { ORCA_EDITOR_SAVE_DIRTY_FILES_EVENT } from '../../../../shared/editor-save-events'
|
||||
import { attachEditorAutosaveController } from './editor-autosave-controller'
|
||||
import { __clearSelfWriteRegistryForTests } from './editor-self-write-registry'
|
||||
import { emptyTabLayoutState } from '../../store/slices/editor-slice-test-harness'
|
||||
|
||||
vi.mock('@/lib/connection-context', () => ({ getConnectionIdForFile: () => null }))
|
||||
|
||||
function createEditorStore(): StoreApi<AppState> {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions, typescript/no-explicit-any -- SAFETY: the editor save path reads only the slice and fields built here.
|
||||
return createStore<any>()((...args: any[]) => ({
|
||||
...emptyTabLayoutState(),
|
||||
settings: { editorAutoSave: false },
|
||||
repos: [],
|
||||
worktreesByRepo: {
|
||||
@@ -71,8 +73,10 @@ describe('saving a restored tab', () => {
|
||||
store
|
||||
.getState()
|
||||
.openFile({ filePath, relativePath, worktreeId, language: 'markdown', mode: 'edit' })
|
||||
store.getState().setEditorDraft(filePath, 'edited')
|
||||
store.getState().markFileDirty(filePath, true)
|
||||
// The file's own id: its path, or an owned id when main owns the layout.
|
||||
const fileId = store.getState().openFiles.find((file) => file.filePath === filePath)!.id
|
||||
store.getState().setEditorDraft(fileId, 'edited')
|
||||
store.getState().markFileDirty(fileId, true)
|
||||
const detach = attachEditorAutosaveController(store)
|
||||
try {
|
||||
await saveDirtyFiles()
|
||||
|
||||
+16
-4
@@ -9,6 +9,14 @@ import type { Tab, TabGroup } from '../../../../shared/tab-types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { useTabDragSplit, type TabDragItemData } from '../tab-group/useTabDragSplit'
|
||||
import { isFloatingTerminalDragTarget } from './floating-terminal-titlebar-drag-target'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('../browser-pane/host-guest/webview-registry', () => ({
|
||||
acquireWebviewsDragPassthrough: vi.fn(() => vi.fn())
|
||||
@@ -145,23 +153,27 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('floating workspace tab reorder', () => {
|
||||
it('moves a tab after the hovered tab when released on its right half', () => {
|
||||
it('moves a tab after the hovered tab when released on its right half', async () => {
|
||||
dropTab('tab-a', 'tab-c', 290)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(floatingTabOrder()).toEqual(['tab-b', 'tab-c', 'tab-a'])
|
||||
})
|
||||
|
||||
it('moves a tab before the hovered tab when released on its left half', () => {
|
||||
it('moves a tab before the hovered tab when released on its left half', async () => {
|
||||
dropTab('tab-c', 'tab-a', 210)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(floatingTabOrder()).toEqual(['tab-c', 'tab-a', 'tab-b'])
|
||||
})
|
||||
|
||||
it('leaves the order alone when a tab is dropped on itself', () => {
|
||||
it('leaves the order alone when a tab is dropped on itself', async () => {
|
||||
dropTab('tab-b', 'tab-b', 290)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(floatingTabOrder()).toEqual(['tab-a', 'tab-b', 'tab-c'])
|
||||
})
|
||||
|
||||
it('republishes sortOrder so the panel renders the committed order', () => {
|
||||
it('republishes sortOrder so the panel renders the committed order', async () => {
|
||||
dropTab('tab-a', 'tab-c', 290)
|
||||
await layoutApplyTestTransport.settle()
|
||||
const tabs = useAppStore.getState().unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
|
||||
expect(tabs.map((tab) => [tab.id, tab.sortOrder])).toEqual([
|
||||
['tab-a', 2],
|
||||
|
||||
@@ -5,6 +5,14 @@ import {
|
||||
stubShallowSelector,
|
||||
stubTooltip
|
||||
} from './tab-bar-windows-shell-launch-render-stubs'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const appStoreSnapshot: {
|
||||
activeTabId: string | null
|
||||
@@ -452,6 +460,7 @@ describe('TabBar context menu wiring', () => {
|
||||
expect(sortable[0].props.isPinned).toBe(true)
|
||||
|
||||
;(sortable[0].props.onTogglePin as () => void)()
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(unpinTabMock).toHaveBeenCalledWith('unified-term-1')
|
||||
expect(pinTabMock).not.toHaveBeenCalled()
|
||||
|
||||
@@ -13,13 +13,19 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
const setTabGroupSplitRatioMock = vi.fn()
|
||||
const recordFeatureInteractionMock = vi.fn()
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
recordFeatureInteraction: recordFeatureInteractionMock,
|
||||
setTabGroupSplitRatio: setTabGroupSplitRatioMock
|
||||
})
|
||||
}))
|
||||
vi.mock('../../store', () => {
|
||||
const state = (): Record<string, unknown> => ({
|
||||
recordFeatureInteraction: recordFeatureInteractionMock,
|
||||
setTabGroupSplitRatio: setTabGroupSplitRatioMock,
|
||||
layoutByWorktree: {}
|
||||
})
|
||||
// The switch-on ratio commit reads the store directly.
|
||||
const useAppStore = Object.assign(
|
||||
(selector: (state: Record<string, unknown>) => unknown) => selector(state()),
|
||||
{ getState: state }
|
||||
)
|
||||
return { useAppStore }
|
||||
})
|
||||
|
||||
vi.mock('./TabGroupPanel', () => ({
|
||||
default: ({ groupId }: { groupId: string }) => <div data-testid={`panel-${groupId}`} />
|
||||
|
||||
@@ -29,6 +29,13 @@ import {
|
||||
type WindowLaunchCase
|
||||
} from '../../../../shared/launch-parity-window-request.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const holder = vi.hoisted((): LaunchStoreHolder => ({ store: null }))
|
||||
vi.mock('@/store', async () =>
|
||||
(await import('@/lib/launch-parity-renderer.test-fixture')).launchStoreModuleMock(holder)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { sendTerminalInputThroughPane } from './pty-connection-test-dom'
|
||||
import { flushAsyncTicks } from './pty-connection-test-async'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
import {
|
||||
LEAF_1,
|
||||
LEAF_2,
|
||||
@@ -259,6 +260,8 @@ export function registerPtyConnectionPtyExitTeardownTests(): void {
|
||||
expect(onPtyExit).toBeTypeOf('function')
|
||||
|
||||
onPtyExit?.('pty-pane-2')
|
||||
// A switch-on build closes it on main's answer.
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(deps.clearExitedPanePtyLayoutBinding).toHaveBeenCalledWith(2, 'pty-pane-2')
|
||||
expect(deps.onPtyExitRef.current).not.toHaveBeenCalled()
|
||||
@@ -760,6 +763,8 @@ export function registerPtyConnectionPtyExitTeardownTests(): void {
|
||||
|
||||
capturedDataCallback.current?.('shell prompt')
|
||||
onPtyExit?.('pty-pane-2')
|
||||
// A switch-on build closes it on main's answer.
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(manager.closePane).toHaveBeenCalledWith(2)
|
||||
})
|
||||
@@ -809,6 +814,8 @@ export function registerPtyConnectionPtyExitTeardownTests(): void {
|
||||
|
||||
terminalInputHandler?.('exit\r')
|
||||
onPtyExit?.('pty-pane-2')
|
||||
// A switch-on build closes it on main's answer.
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(manager.closePane).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe } from 'vitest'
|
||||
import { describe, vi } from 'vitest'
|
||||
import { registerPtyConnectionAgentSessionResumeTests } from './pty-connection-agent-session-resume.spec'
|
||||
import { registerPtyConnectionColdRestoreAgentResumeTests } from './pty-connection-cold-restore-agent-resume.spec'
|
||||
import { registerPtyConnectionColdRestoreRepaintTests } from './pty-connection-cold-restore-repaint.spec'
|
||||
@@ -9,6 +9,13 @@ import { registerPtyConnectionRestartReplacementTests } from './pty-connection-r
|
||||
import { registerPtyConnectionSessionLivenessTests } from './pty-connection-session-liveness.spec'
|
||||
import { registerPtyConnectionSleepingResumeBannerTests } from './pty-connection-sleeping-resume-banner.spec'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
describe('pty-connection-agent-session-resume', registerPtyConnectionAgentSessionResumeTests)
|
||||
describe(
|
||||
'pty-connection-cold-restore-agent-resume',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bindPtyConnectionTestMocks } from './pty-connection-mocks.spec'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushAsyncTicks } from './pty-connection-test-async'
|
||||
import { sendTerminalInputThroughPane } from './pty-connection-test-dom'
|
||||
@@ -348,6 +349,8 @@ export function registerPtyConnectionSessionLivenessTests(): void {
|
||||
// Restart rebinds the pane to a new live PTY; its later real exit must not be ignored by a stale guard.
|
||||
transport.getPtyId.mockReturnValue('pty-pane-2-restarted')
|
||||
onPtyExit?.('pty-pane-2-restarted')
|
||||
// A switch-on build closes it on main's answer.
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(manager.closePane).toHaveBeenCalledTimes(1)
|
||||
expect(manager.closePane).toHaveBeenCalledWith(2)
|
||||
|
||||
@@ -238,6 +238,7 @@ export type MockPaneManager = {
|
||||
closePane: Mock
|
||||
getActivePane: Mock<() => { id: number; leafId?: string } | null>
|
||||
getNumericIdForLeaf: Mock<(leafId: string) => number | null>
|
||||
getLeafId: Mock<(paneId: number) => string | null>
|
||||
setActivePane: Mock<(paneId: number) => void>
|
||||
}
|
||||
|
||||
@@ -265,6 +266,9 @@ export function createManager(
|
||||
getNumericIdForLeaf: vi.fn((leafId: string) => {
|
||||
return panes.find((candidate) => candidate.leafId === leafId)?.id ?? null
|
||||
}),
|
||||
getLeafId: vi.fn((paneId: number) => {
|
||||
return panes.find((candidate) => candidate.id === paneId)?.leafId ?? null
|
||||
}),
|
||||
setActivePane: vi.fn((paneId: number) => {
|
||||
activePaneId = paneId
|
||||
})
|
||||
|
||||
+8
-1
@@ -1,4 +1,4 @@
|
||||
import { describe } from 'vitest'
|
||||
import { describe, vi } from 'vitest'
|
||||
import { registerPtyConnectionVisibilityResumeSizeTests } from './pty-connection-visibility-resume-size.spec'
|
||||
import { registerPtyConnectionHiddenCodexQueriesTests } from './pty-connection-hidden-codex-queries.spec'
|
||||
import { registerPtyConnectionHiddenDeliveryGateTests } from './pty-connection-hidden-delivery-gate.spec'
|
||||
@@ -6,6 +6,13 @@ import { registerPtyConnectionParkRevealModelSnapshotTests } from './pty-connect
|
||||
import { registerPtyConnectionReplayPayloadHandlingTests } from './pty-connection-replay-payload-handling.spec'
|
||||
import { registerPtyConnectionPtyExitTeardownTests } from './pty-connection-pty-exit-teardown.spec'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
describe('pty-connection-visibility-resume-size', registerPtyConnectionVisibilityResumeSizeTests)
|
||||
describe('pty-connection-hidden-codex-queries', registerPtyConnectionHiddenCodexQueriesTests)
|
||||
describe('pty-connection-hidden-delivery-gate', registerPtyConnectionHiddenDeliveryGateTests)
|
||||
|
||||
@@ -4,6 +4,13 @@ import type { AgentLaunchPaneVerdict } from '../../../shared/agent-launch-pane-v
|
||||
import { createTabsSliceMockApi } from '../store/slices/tabs-slice-test-harness'
|
||||
import { createTestStore } from '../store/slices/store-test-helpers'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof AgentStatusModule>()),
|
||||
|
||||
@@ -19,6 +19,13 @@ import {
|
||||
launchWorkspaceId
|
||||
} from '../../../shared/launch-parity-window-request.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const holder = vi.hoisted((): LaunchStoreHolder => ({ store: null }))
|
||||
vi.mock('@/store', async () =>
|
||||
(await import('@/lib/launch-parity-renderer.test-fixture')).launchStoreModuleMock(holder)
|
||||
|
||||
@@ -5,6 +5,13 @@ import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../shar
|
||||
import { createTestStore, makeWorktree, seedStore } from '../store/slices/store-test-helpers'
|
||||
import { createStoreCascadesMockApi } from '../store/slices/store-cascades-test-harness'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const storeBox = vi.hoisted(() => {
|
||||
const box: { store: unknown } = { store: null }
|
||||
return box
|
||||
|
||||
@@ -105,6 +105,8 @@ const store = {
|
||||
allWorktrees: vi.fn(() => store.worktreesByRepo['repo-1']),
|
||||
tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
|
||||
unifiedTabsByWorktree: emptyUnifiedTabsByWorktree,
|
||||
groupsByWorktree: {},
|
||||
layoutByWorktree: {},
|
||||
activeGroupIdByWorktree: {},
|
||||
openFiles: [] as { id: string; worktreeId: string }[],
|
||||
browserTabsByWorktree: {} as Record<string, { id: string }[]>,
|
||||
|
||||
@@ -8,6 +8,13 @@ import { getDefaultSettings } from '../../../shared/constants'
|
||||
import { WIN_PATH, WSL_PATH } from '../../../shared/launch-parity-window-cases.test-fixture'
|
||||
import { launchWorkspaceId } from '../../../shared/launch-parity-window-request.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const holder = vi.hoisted((): LaunchStoreHolder => ({ store: null }))
|
||||
vi.mock('@/store', async () =>
|
||||
(await import('@/lib/launch-parity-renderer.test-fixture')).launchStoreModuleMock(holder)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import type { TerminalTab } from '../../../shared/terminal-tab-types'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const PI_TRANSCRIPT_PATH = join(tmpdir(), 'pi-session-1.jsonl')
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import { useAppStore } from '@/store'
|
||||
import { makeWorktree, TEST_REPO } from '../store/slices/worktrees-slice-test-fixtures'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222'
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { getProviderSessionClaimKey } from './sleeping-agent-pane-ownership'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222'
|
||||
|
||||
@@ -5,6 +5,13 @@ import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
import { getProviderSessionClaimKey } from './sleeping-agent-pane-ownership'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -5,6 +5,13 @@ import { parseWorkspaceSession } from '../../../shared/workspace-session-schema'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222'
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialState = useAppStore.getState()
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const HOST_TAB_ID = 'web-terminal-host-tab'
|
||||
|
||||
@@ -2,6 +2,13 @@ import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -4,12 +4,20 @@ import type { AppState } from '@/store'
|
||||
import { useAppStore } from '@/store'
|
||||
import { registerTerminalPresentationIpcBridge } from '@/hooks/ipc-events/terminal-presentation-ipc-bridge'
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import type { RuntimeTerminalSummary } from '../../../shared/runtime-types'
|
||||
import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types'
|
||||
import { activateAndRevealWorktree } from './worktree-activation'
|
||||
import { gateWorktreeAgentActivation } from './worktree-agent-activation-gate'
|
||||
import { makeCreatedAgentWorktree as makeWorktree } from './worktree-activation-created-agent.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
/**
|
||||
* Conditional old-host reconstruction, not proof of STA-9417: a host-created worktree with a
|
||||
* split of the (unmounted) primary tab, then the first activation of that worktree. When main's
|
||||
@@ -257,30 +265,33 @@ describe('old-host split of an unmounted tab, then first activation', () => {
|
||||
await runScenario(false)
|
||||
})
|
||||
|
||||
// The window alone, as above; the odd runs mint the duplicate.
|
||||
it.fails('stays duplicate-free across 8 create-then-activate cycles, with or without the reveal', async () => {
|
||||
let duplicates = 0
|
||||
for (let run = 0; run < 8; run++) {
|
||||
const worktree = makeWorktree()
|
||||
const primaryPtyId = `${worktree.id}@@primary-${run}`
|
||||
const setupPtyId = `${worktree.id}@@setup-${run}`
|
||||
useAppStore.setState(initialState, true)
|
||||
useAppStore.setState(baseState())
|
||||
const host = stubHost(primaryPtyId, setupPtyId)
|
||||
registerTerminalPresentationIpcBridge([])
|
||||
seedRevealedPrimaryTab(worktree.id, primaryPtyId)
|
||||
// Odd runs lose the reveal, as the natural repro did.
|
||||
if (run % 2 === 0) {
|
||||
host.reveal()(setupSplitReveal(`reveal-${run}`, worktree.id, setupPtyId))
|
||||
// The window alone, as above; the odd runs mint the duplicate. Main owning the create fixes it.
|
||||
;(ORCA_LAYOUT_OWNER_IS_MAIN ? it : it.fails)(
|
||||
'stays duplicate-free across 8 create-then-activate cycles, with or without the reveal',
|
||||
async () => {
|
||||
let duplicates = 0
|
||||
for (let run = 0; run < 8; run++) {
|
||||
const worktree = makeWorktree()
|
||||
const primaryPtyId = `${worktree.id}@@primary-${run}`
|
||||
const setupPtyId = `${worktree.id}@@setup-${run}`
|
||||
useAppStore.setState(initialState, true)
|
||||
useAppStore.setState(baseState())
|
||||
const host = stubHost(primaryPtyId, setupPtyId)
|
||||
registerTerminalPresentationIpcBridge([])
|
||||
seedRevealedPrimaryTab(worktree.id, primaryPtyId)
|
||||
// Odd runs lose the reveal, as the natural repro did.
|
||||
if (run % 2 === 0) {
|
||||
host.reveal()(setupSplitReveal(`reveal-${run}`, worktree.id, setupPtyId))
|
||||
}
|
||||
await activateFirstTime(worktree.id)
|
||||
duplicates += [...ownersByPty(worktree.id).values()].filter(
|
||||
(owners) => owners.length > 1
|
||||
).length
|
||||
duplicates += (useAppStore.getState().tabsByWorktree[worktree.id] ?? []).length - 1
|
||||
}
|
||||
await activateFirstTime(worktree.id)
|
||||
duplicates += [...ownersByPty(worktree.id).values()].filter(
|
||||
(owners) => owners.length > 1
|
||||
).length
|
||||
duplicates += (useAppStore.getState().tabsByWorktree[worktree.id] ?? []).length - 1
|
||||
expect(duplicates).toBe(0)
|
||||
}
|
||||
expect(duplicates).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
it('rejects a split into a tab it lacks without switching to that workspace', () => {
|
||||
const worktree = makeWorktree()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-session-contracts'
|
||||
import type { Tab, TabGroup } from '../../../shared/tab-types'
|
||||
import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../shared/tab-types'
|
||||
import type { AgentSessionStatusSummary } from '../../../shared/agent-session-wire'
|
||||
import type { StructuredAgentSessionLaunchIntent } from '@/lib/launch-structured-agent-session'
|
||||
import type * as LaunchAdmissionModule from './structured-agent-session-launch-admission'
|
||||
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => ({
|
||||
type StoreState = {
|
||||
unifiedTabsByWorktree: Record<string, Tab[]>
|
||||
groupsByWorktree: Record<string, TabGroup[]>
|
||||
layoutByWorktree: Record<string, TabGroupLayoutNode>
|
||||
activeGroupIdByWorktree: Record<string, string>
|
||||
nativeChatLaunchDraftByTabId: Record<string, { text: string; adopted?: boolean }>
|
||||
}
|
||||
@@ -35,6 +36,14 @@ function emptyStoreState(): StoreState {
|
||||
return {
|
||||
unifiedTabsByWorktree: {},
|
||||
groupsByWorktree: { 'wt-new-chat': [group('group-left'), group('group-right')] },
|
||||
layoutByWorktree: {
|
||||
'wt-new-chat': {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', groupId: 'group-left' },
|
||||
second: { type: 'leaf', groupId: 'group-right' }
|
||||
}
|
||||
},
|
||||
activeGroupIdByWorktree: { 'wt-new-chat': 'group-left' },
|
||||
nativeChatLaunchDraftByTabId: {}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ import type { AppState } from '@/store/types'
|
||||
import { wakeSleepingAgentsForWorktreeInBackground } from './wake-sleeping-agents-in-background'
|
||||
import { waitForWorktreeAgentActivationGateForTests } from './worktree-agent-activation-gate'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const WT = 'repo-1::/repo'
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
@@ -12,6 +12,13 @@ import {
|
||||
} from '@/lib/workspace-terminal-host-authority'
|
||||
import { makeFolderWorkspace } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof AgentStatusModule>()
|
||||
|
||||
@@ -3,6 +3,13 @@ import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { useAppStore } from '@/store'
|
||||
import { activateAndRevealWorkspace, activateAndRevealWorktree } from './worktree-activation'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
} from '@/lib/worktree-activation-created-agent.test-fixture'
|
||||
import { stubRuntimeEnvironmentBridge } from '../runtime/runtime-environment-bridge-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
function makeWebRuntimeWorktree() {
|
||||
|
||||
@@ -20,6 +20,13 @@ import {
|
||||
registerWorktreeActivationReset
|
||||
} from './worktree-activation.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const defaultChat = vi.hoisted(() => ({
|
||||
open: vi.fn(),
|
||||
awaitsDetection: vi.fn(),
|
||||
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
} from '@/lib/worktree-activation-created-agent.test-fixture'
|
||||
import { waitForWorktreeAgentActivationGateForTests } from './worktree-agent-activation-gate'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
createdTabStartups
|
||||
} from './worktree-activation.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
|
||||
registerWorktreeActivationReset()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import path from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '@/store'
|
||||
@@ -11,6 +12,23 @@ import * as sleepingResume from './resume-sleeping-agent-session'
|
||||
import { activateAndRevealWorktree } from './worktree-activation'
|
||||
import { waitForWorktreeAgentActivationGateForTests } from './worktree-agent-activation-gate'
|
||||
import { makeCreatedAgentWorktree as makeWorktree } from './worktree-activation-created-agent.test-fixture'
|
||||
import { emptySession } from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
import { loadMainLayoutModel } from '@/runtime/main-loaded-layout-frames.test-fixture'
|
||||
import { openWorkspaceLayoutFrameSync } from '@/runtime/workspace-layout-frame-sync'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('@/runtime/runtime-layout-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeLayoutClientWithTestFrames(await importOriginal())
|
||||
)
|
||||
|
||||
const initialState = useAppStore.getState()
|
||||
|
||||
@@ -163,6 +181,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
useAppStore.setState(initialState, true)
|
||||
layoutApplyTestTransport.reset()
|
||||
})
|
||||
|
||||
describe('worktree agent activation seam', () => {
|
||||
@@ -212,15 +231,29 @@ describe('worktree agent activation seam', () => {
|
||||
it('adopts a live terminal without spawning a fallback', async () => {
|
||||
const worktree = makeWorktree()
|
||||
const livePtyId = `${worktree.id}@@live-codex`
|
||||
useAppStore.setState(baseState())
|
||||
// Restored with no tabs: frames draw this workspace.
|
||||
useAppStore.setState({ ...baseState(), unifiedTabsByWorktree: { [worktree.id]: [] } })
|
||||
stubInventory({ livePtyId })
|
||||
// A switch-on build shows the adopted terminal from main's frame.
|
||||
layoutApplyTestTransport.answerWith('apply')
|
||||
layoutApplyTestTransport.useMainModel(loadMainLayoutModel('local', emptySession()))
|
||||
const frames = openWorkspaceLayoutFrameSync(useAppStore)
|
||||
frames.start()
|
||||
try {
|
||||
expect(activateAndRevealWorktree(worktree.id)).toEqual({ primaryTabId: null })
|
||||
await waitForWorktreeAgentActivationGateForTests(worktree.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(activateAndRevealWorktree(worktree.id)).toEqual({ primaryTabId: null })
|
||||
await waitForWorktreeAgentActivationGateForTests(worktree.id)
|
||||
|
||||
const tabs = useAppStore.getState().tabsByWorktree[worktree.id] ?? []
|
||||
expect(tabs).toHaveLength(1)
|
||||
expect(tabs[0]?.ptyId).toBe(livePtyId)
|
||||
const tabs = useAppStore.getState().tabsByWorktree[worktree.id] ?? []
|
||||
expect(tabs).toHaveLength(1)
|
||||
// Switch-on, the pane binds to the terminal main's frame names; the row keeps no ptyId.
|
||||
const shownPtyId = ORCA_LAYOUT_OWNER_IS_MAIN
|
||||
? frames.terminalTab(worktree.id, tabs[0]!.id)?.terminal?.panes[0]?.ptyId
|
||||
: tabs[0]?.ptyId
|
||||
expect(shownPtyId).toBe(livePtyId)
|
||||
} finally {
|
||||
frames.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('spawns a fallback when the workspace has no agent', async () => {
|
||||
|
||||
@@ -25,6 +25,13 @@ import {
|
||||
seedEmptyActivatableWorktree
|
||||
} from './worktree-activation-created-agent.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initialAppStoreState = useAppStore.getState()
|
||||
const worktree = makeCreatedAgentWorktree()
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ vi.mock('@/runtime/structured-agent-session-client', () => ({
|
||||
import { useAppStore } from '@/store'
|
||||
import { launchStructuredWorktreeSession } from './worktree-creation-structured-session'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const initial = useAppStore.getState()
|
||||
|
||||
function worktree(name: string, host: 'paired' | 'local' = 'paired'): Worktree {
|
||||
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
resetHostSessionMirrorHydrationForTests
|
||||
} from '@/runtime/host-session-mirror-hydration'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
// Pins the activation contract behind the run6-review-pr-11959 incident shape:
|
||||
// a persisted (husk) tab whose pane cannot resume in place gets ONE appended
|
||||
// replacement tab per provider session — the husk is retained for scrollback —
|
||||
|
||||
@@ -7,6 +7,13 @@ import { makeCreatedAgentWorktree } from '@/lib/worktree-activation-created-agen
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { waitForWorktreeAgentActivationGateForTests } from './worktree-agent-activation-gate'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
// Red repro for the aug20 "windows 2" incident (restart-reattach/resume-relaunch):
|
||||
// a runtime-owned (paired remote) worktree's web-mirror tab holds a sleeping
|
||||
// record for an agent whose host PTY is STILL ALIVE, but at activation time the
|
||||
|
||||
@@ -149,6 +149,8 @@ describe('layout.apply senders', () => {
|
||||
expect(layoutApplySenders(path.resolve(__dirname, '..'))).toEqual({
|
||||
'runtime/generated-tab-titles-send.ts': 1,
|
||||
'runtime/layout-apply-send.ts': 1,
|
||||
// Tests' stand-in for main: it answers layout.apply, never sends it.
|
||||
'runtime/layout-apply-test-transport.test-fixture.ts': 1,
|
||||
'store/terminals/terminal-tab-create-through-layout-owner.ts': 2
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
// A stand-in for main's `layout.apply` and `layout.subscribe` in switch-on renderer tests. It answers
|
||||
// each command the window sends (accept, apply to main's model, leave pending, or refuse) and, when
|
||||
// applying, emits main's frame at the reply's stream position, as main does.
|
||||
//
|
||||
// Install it in a test file, before any import that reaches the RPC client:
|
||||
// vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
// (await import('@/runtime/layout-apply-test-transport.test-fixture'))
|
||||
// .runtimeRpcClientWithLayoutApply(await importOriginal()))
|
||||
// and, for frames, `@/runtime/runtime-layout-client` with `runtimeLayoutClientWithTestFrames`.
|
||||
// Imports stay clear of the RPC client at runtime: a mock factory must not reach what it replaces.
|
||||
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { applyLayoutCommand } from '../../../shared/workspace-layout/workspace-layout-commands'
|
||||
import type {
|
||||
LayoutApplyReply,
|
||||
LayoutCommand,
|
||||
LayoutCommandResult
|
||||
} from '../../../shared/workspace-layout/workspace-layout-command-types'
|
||||
import type { WorkspaceLayoutModel } from '../../../shared/workspace-layout/workspace-layout-model'
|
||||
import { publishWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
|
||||
import type { WorkspaceLayoutChangeFrame } from '../../../shared/workspace-layout/workspace-layout-stream-frames'
|
||||
import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
|
||||
import { layoutSnapshotFrame, publishedLayoutsOf } from './main-loaded-layout-frames.test-fixture'
|
||||
import type * as LayoutClient from './runtime-layout-client'
|
||||
import type * as RpcClient from './runtime-rpc-client'
|
||||
|
||||
type Refusal = Extract<LayoutApplyReply, { ok: false }>['code']
|
||||
|
||||
/**
|
||||
* `accept`: ok, with a result naming the ids the command did; no frame (main's copy is not
|
||||
* modelled). `apply`: main's model (`useMainModel`) applies it; the reply carries its real result
|
||||
* and the position of the frame emitted with it. `pending`: no answer until `answerPending`.
|
||||
*/
|
||||
export type LayoutApplyTestAnswer =
|
||||
| 'accept'
|
||||
| 'apply'
|
||||
| 'pending'
|
||||
| { refuse: Refusal }
|
||||
| ((command: LayoutCommand) => LayoutApplyReply | 'pending')
|
||||
|
||||
type FrameSink = {
|
||||
onFrame: (frame: WorkspaceLayoutChangeFrame) => void
|
||||
onPosition?: (seq: number) => void
|
||||
}
|
||||
|
||||
type PendingApply = { command: LayoutCommand; resolve: (reply: LayoutApplyReply) => void }
|
||||
|
||||
type TransportState = {
|
||||
answer: LayoutApplyTestAnswer
|
||||
sent: LayoutCommand[]
|
||||
pending: PendingApply[]
|
||||
model: WorkspaceLayoutModel | null
|
||||
seq: number
|
||||
sinks: Set<FrameSink>
|
||||
inFlight: Promise<LayoutApplyReply>[]
|
||||
}
|
||||
|
||||
const fresh = (): TransportState => ({
|
||||
answer: 'accept',
|
||||
sent: [],
|
||||
pending: [],
|
||||
model: null,
|
||||
seq: 0,
|
||||
sinks: new Set(),
|
||||
inFlight: []
|
||||
})
|
||||
|
||||
let state = fresh()
|
||||
|
||||
/** What main's result names for a command it accepted, read off the command. */
|
||||
function acceptedResult(command: LayoutCommand): LayoutCommandResult {
|
||||
const result: LayoutCommandResult = {}
|
||||
if ('tabId' in command && typeof command.tabId === 'string') {
|
||||
result.tabId = command.tabId
|
||||
}
|
||||
if ('leafId' in command && typeof command.leafId === 'string') {
|
||||
result.leafId = command.leafId
|
||||
}
|
||||
if ('groupId' in command && typeof command.groupId === 'string') {
|
||||
result.groupId = command.groupId
|
||||
}
|
||||
if ('pageId' in command && typeof command.pageId === 'string') {
|
||||
result.pageId = command.pageId
|
||||
}
|
||||
if (command.type === 'closeTabs') {
|
||||
return { ...result, closed: [...command.tabIds], refused: [] }
|
||||
}
|
||||
if (command.type === 'sleep') {
|
||||
return { ...result, slept: [...(command.paneKeys ?? [])] }
|
||||
}
|
||||
if (command.type === 'wake') {
|
||||
return { ...result, woken: [...(command.paneKeys ?? [])] }
|
||||
}
|
||||
if (command.type === 'splitPane' && command.newLeafId) {
|
||||
return { ...result, leafId: command.newLeafId }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Shape check only: the window's own sender built it.
|
||||
function isLayoutCommand(value: unknown): value is LayoutCommand {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'type' in value &&
|
||||
typeof value.type === 'string' &&
|
||||
'workspace' in value &&
|
||||
typeof value.workspace === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
/** The command of a `layout.apply` request's params. */
|
||||
function commandOf(params: unknown): unknown {
|
||||
return typeof params === 'object' && params !== null && 'command' in params
|
||||
? params.command
|
||||
: undefined
|
||||
}
|
||||
|
||||
function emit(frame: WorkspaceLayoutChangeFrame): void {
|
||||
for (const sink of state.sinks) {
|
||||
sink.onFrame(frame)
|
||||
if (frame.seq !== undefined) {
|
||||
sink.onPosition?.(frame.seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Main's apply without its routing: no host partitions, `host_not_owned`, forced closes, or a
|
||||
* frame position on refusals. */
|
||||
function applyToMainModel(command: LayoutCommand): LayoutApplyReply {
|
||||
const model = state.model
|
||||
if (!model) {
|
||||
throw new Error('layoutApplyTestTransport: answer "apply" needs useMainModel first')
|
||||
}
|
||||
const applied = applyLayoutCommand(model, command, {
|
||||
mintId: createBrowserUuid,
|
||||
mintLeafId: createBrowserUuid,
|
||||
now: () => Date.now()
|
||||
})
|
||||
if (!applied.ok) {
|
||||
return applied
|
||||
}
|
||||
state.model = applied.model
|
||||
state.seq += 1
|
||||
const seq = state.seq
|
||||
const key = command.workspace
|
||||
const workspace = applied.model.workspaces[key]
|
||||
// Main publishes before it replies.
|
||||
emit(
|
||||
workspace
|
||||
? { type: 'workspace', key, layout: publishWorkspaceLayout(workspace, model.hostId), seq }
|
||||
: { type: 'removed', key, seq }
|
||||
)
|
||||
return { ok: true, result: applied.result, frameSeq: seq }
|
||||
}
|
||||
|
||||
/** The current answer to one command; `pending` when it waits for `answerPending`. */
|
||||
function replyTo(command: LayoutCommand): LayoutApplyReply | 'pending' {
|
||||
const answer = state.answer
|
||||
if (typeof answer === 'function') {
|
||||
return answer(command)
|
||||
}
|
||||
if (answer === 'accept') {
|
||||
return { ok: true, result: acceptedResult(command) }
|
||||
}
|
||||
if (answer === 'apply') {
|
||||
return applyToMainModel(command)
|
||||
}
|
||||
return answer === 'pending' ? 'pending' : { ok: false, code: answer.refuse }
|
||||
}
|
||||
|
||||
function answerCommand(command: LayoutCommand): Promise<LayoutApplyReply> {
|
||||
state.sent.push(command)
|
||||
const reply = replyTo(command)
|
||||
if (reply === 'pending') {
|
||||
return new Promise((resolve) => state.pending.push({ command, resolve }))
|
||||
}
|
||||
const answered = Promise.resolve(reply)
|
||||
state.inFlight.push(answered)
|
||||
return answered
|
||||
}
|
||||
|
||||
// layout-apply-send.ts's chain (reply, frame wait, handler) runs a few microtasks after the answer.
|
||||
const REPLY_CHAIN_TURNS = 5
|
||||
|
||||
export const layoutApplyTestTransport = {
|
||||
/** How later commands are answered; `accept` after each `reset`. */
|
||||
answerWith(answer: LayoutApplyTestAnswer): void {
|
||||
state.answer = answer
|
||||
},
|
||||
/** Main's model, which `apply` edits and frames publish. */
|
||||
useMainModel(model: WorkspaceLayoutModel): void {
|
||||
state.model = model
|
||||
},
|
||||
get mainModel(): WorkspaceLayoutModel | null {
|
||||
return state.model
|
||||
},
|
||||
/** Every command sent, in order, with its workspace. */
|
||||
get sent(): readonly LayoutCommand[] {
|
||||
return state.sent
|
||||
},
|
||||
sentOfType<T extends LayoutCommand['type']>(type: T): Extract<LayoutCommand, { type: T }>[] {
|
||||
return state.sent.filter(
|
||||
(command): command is Extract<LayoutCommand, { type: T }> => command.type === type
|
||||
)
|
||||
},
|
||||
get pendingCount(): number {
|
||||
return state.pending.length
|
||||
},
|
||||
/** Answers every pending command: with `reply`, else as the current answer (`accept` when that
|
||||
* is still `pending`). */
|
||||
answerPending(reply?: LayoutApplyReply): void {
|
||||
const pending = state.pending
|
||||
state.pending = []
|
||||
for (const { command, resolve } of pending) {
|
||||
const next = replyTo(command)
|
||||
const answer =
|
||||
reply ?? (next === 'pending' ? { ok: true, result: acceptedResult(command) } : next)
|
||||
state.inFlight.push(Promise.resolve(answer))
|
||||
resolve(answer)
|
||||
}
|
||||
},
|
||||
/** Resolves once every answered command's handler ran; real or fake timers alike. */
|
||||
async settle(): Promise<void> {
|
||||
while (state.inFlight.length > 0) {
|
||||
const answered = state.inFlight
|
||||
state.inFlight = []
|
||||
await Promise.all(answered)
|
||||
for (let turn = 0; turn < REPLY_CHAIN_TURNS; turn += 1) {
|
||||
await Promise.resolve()
|
||||
}
|
||||
}
|
||||
},
|
||||
/** Sends a frame main made on its own (a CLI create, a load repair). */
|
||||
emitFrame(frame: WorkspaceLayoutChangeFrame): void {
|
||||
emit(frame)
|
||||
},
|
||||
/** Answers one `layout.apply` request's params; the RPC mock below routes here. */
|
||||
call(params: unknown): Promise<LayoutApplyReply> {
|
||||
const command = commandOf(params)
|
||||
if (!isLayoutCommand(command)) {
|
||||
return Promise.reject(new Error('layout.apply without a command'))
|
||||
}
|
||||
return answerCommand(command)
|
||||
},
|
||||
reset(): void {
|
||||
state = fresh()
|
||||
}
|
||||
}
|
||||
|
||||
type RpcClientModule = typeof RpcClient
|
||||
|
||||
/** The RPC client module with local `layout.apply` answered by the transport. */
|
||||
export function runtimeRpcClientWithLayoutApply(actual: RpcClientModule): RpcClientModule {
|
||||
function callRuntimeRpc<TResult>(
|
||||
target: RuntimeClientTarget,
|
||||
method: string,
|
||||
params?: unknown,
|
||||
options?: Parameters<RpcClientModule['callRuntimeRpc']>[3]
|
||||
): Promise<TResult> {
|
||||
if (target.kind === 'local' && method === 'layout.apply') {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: callers of layout.apply type TResult as LayoutApplyReply.
|
||||
return layoutApplyTestTransport.call(params) as Promise<TResult>
|
||||
}
|
||||
return actual.callRuntimeRpc<TResult>(target, method, params, options)
|
||||
}
|
||||
return { ...actual, callRuntimeRpc }
|
||||
}
|
||||
|
||||
type LayoutClientModule = typeof LayoutClient
|
||||
|
||||
/** The layout client module with the local stream fed by the transport: main's model's snapshot
|
||||
* first, then the frames `apply` and `emitFrame` send. */
|
||||
export function runtimeLayoutClientWithTestFrames(actual: LayoutClientModule): LayoutClientModule {
|
||||
const subscribeRuntimeLayout: LayoutClientModule['subscribeRuntimeLayout'] = (
|
||||
target,
|
||||
params,
|
||||
onFrame,
|
||||
onStatus,
|
||||
onPosition
|
||||
) => {
|
||||
if (target.kind !== 'local') {
|
||||
return actual.subscribeRuntimeLayout(target, params, onFrame, onStatus, onPosition)
|
||||
}
|
||||
const sink: FrameSink = { onFrame, onPosition }
|
||||
state.sinks.add(sink)
|
||||
onStatus({ state: 'connecting' })
|
||||
queueMicrotask(() => {
|
||||
if (!state.sinks.has(sink)) {
|
||||
return
|
||||
}
|
||||
onStatus({ state: 'live' })
|
||||
onFrame(layoutSnapshotFrame(state.model ? publishedLayoutsOf(state.model) : {}, state.seq))
|
||||
onPosition?.(state.seq)
|
||||
})
|
||||
return {
|
||||
close: () => {
|
||||
state.sinks.delete(sink)
|
||||
onStatus({ state: 'closed' })
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...actual, subscribeRuntimeLayout }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Main's loaded layout as the frames its stream sends: what main publishes for a stored partition,
|
||||
// so a switch-on test feeds the window the layout main holds instead of the window's own restore.
|
||||
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
import { loadWorkspaceLayoutWithoutReport } from '../../../shared/workspace-layout/workspace-layout-load'
|
||||
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
|
||||
import type { WorkspaceLayoutModel } from '../../../shared/workspace-layout/workspace-layout-model'
|
||||
import {
|
||||
publishWorkspaceLayout,
|
||||
type PublishedWorkspaceLayout
|
||||
} from '../../../shared/workspace-layout/workspace-layout-published'
|
||||
import type { WorkspaceLayoutChangeFrame } from '../../../shared/workspace-layout/workspace-layout-stream-frames'
|
||||
|
||||
/** The model main loads from one stored partition, with the ids it mints at load. */
|
||||
export function loadMainLayoutModel(
|
||||
hostId: ExecutionHostId,
|
||||
session: WorkspaceSessionState
|
||||
): WorkspaceLayoutModel {
|
||||
return loadWorkspaceLayoutWithoutReport(hostId, session, nameBasedLoadContext()).layout
|
||||
}
|
||||
|
||||
/** What main publishes for each workspace of a model, by workspace key. */
|
||||
export function publishedLayoutsOf(
|
||||
model: WorkspaceLayoutModel
|
||||
): Record<string, PublishedWorkspaceLayout> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(model.workspaces).map(([key, workspace]) => [
|
||||
key,
|
||||
publishWorkspaceLayout(workspace, model.hostId)
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
/** What main publishes for one stored partition, by workspace key. */
|
||||
export function mainLoadedLayouts(
|
||||
hostId: ExecutionHostId,
|
||||
session: WorkspaceSessionState
|
||||
): Record<string, PublishedWorkspaceLayout> {
|
||||
return publishedLayoutsOf(loadMainLayoutModel(hostId, session))
|
||||
}
|
||||
|
||||
/** The snapshot frame a stream opens with. */
|
||||
export function layoutSnapshotFrame(
|
||||
layouts: Record<string, PublishedWorkspaceLayout>,
|
||||
seq?: number
|
||||
): WorkspaceLayoutChangeFrame {
|
||||
return {
|
||||
type: 'snapshot',
|
||||
workspaces: Object.entries(layouts).map(([key, layout]) => ({ key, layout })),
|
||||
...(seq === undefined ? {} : { seq })
|
||||
}
|
||||
}
|
||||
@@ -224,12 +224,18 @@ function seedPairedClientStore(): TestStore {
|
||||
},
|
||||
worktreesByRepo: {
|
||||
repo1: [
|
||||
makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' }),
|
||||
// The paired server owns this workspace, so its titles stay the window's to apply.
|
||||
makeWorktree({
|
||||
id: WT,
|
||||
repoId: 'repo1',
|
||||
path: '/path/wt1',
|
||||
runtimeOwnerEnvironmentId: ENV
|
||||
}),
|
||||
makeWorktree({ id: LOCAL_WT, repoId: 'repo1', path: '/path/local-wt' })
|
||||
]
|
||||
},
|
||||
activeWorktreeId: WT
|
||||
} as Partial<AppState>)
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,10 @@ import type * as AgentStatusModule from '@/lib/agent-status'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture'
|
||||
import { loadWorkspaceLayoutWithoutReport } from '../../../shared/workspace-layout/workspace-layout-load'
|
||||
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
|
||||
import {
|
||||
localDesktopSession,
|
||||
WORKSPACE_LAYOUT_PROFILES
|
||||
} from '../../../shared/workspace-layout/workspace-layout-profile.test-fixture'
|
||||
import {
|
||||
publishWorkspaceLayout,
|
||||
type PublishedWorkspaceLayout
|
||||
} from '../../../shared/workspace-layout/workspace-layout-published'
|
||||
import type { WorkspaceLayoutChangeFrame } from '../../../shared/workspace-layout/workspace-layout-stream-frames'
|
||||
import {
|
||||
emptySession,
|
||||
@@ -30,6 +24,7 @@ import {
|
||||
} from './layout-frame-position'
|
||||
import { holdTabPaneTree, requestTabPaneTreeRedraw } from './layout-owner-pane-intents'
|
||||
import { openWorkspaceLayoutFrameSync } from './workspace-layout-frame-sync'
|
||||
import { layoutSnapshotFrame, mainLoadedLayouts } from './main-loaded-layout-frames.test-fixture'
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
@@ -85,35 +80,14 @@ const SLICES: (keyof LayoutFrameSlices)[] = [
|
||||
'activeTabTypeByWorktree'
|
||||
]
|
||||
|
||||
/** What main publishes for one stored partition. */
|
||||
function published(
|
||||
hostId: ExecutionHostId,
|
||||
session: WorkspaceSessionState
|
||||
): Record<string, PublishedWorkspaceLayout> {
|
||||
const { layout } = loadWorkspaceLayoutWithoutReport(hostId, session, nameBasedLoadContext())
|
||||
return Object.fromEntries(
|
||||
Object.entries(layout.workspaces).map(([key, workspace]) => [
|
||||
key,
|
||||
publishWorkspaceLayout(workspace, hostId)
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
function snapshot(layouts: Record<string, PublishedWorkspaceLayout>): WorkspaceLayoutChangeFrame {
|
||||
return {
|
||||
type: 'snapshot',
|
||||
workspaces: Object.entries(layouts).map(([key, layout]) => ({ key, layout }))
|
||||
}
|
||||
}
|
||||
|
||||
const flushMicrotasks = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
async function bootWithFrames(hostId: ExecutionHostId, session: WorkspaceSessionState) {
|
||||
const store = createTestStore()
|
||||
await bootWindowFromPartitions(store, [{ hostId, session }])
|
||||
const sync = openWorkspaceLayoutFrameSync(store)
|
||||
const layouts = published(hostId, session)
|
||||
stream.onFrame!(snapshot(layouts))
|
||||
const layouts = mainLoadedLayouts(hostId, session)
|
||||
stream.onFrame!(layoutSnapshotFrame(layouts))
|
||||
return { store, sync, layouts }
|
||||
}
|
||||
|
||||
@@ -145,7 +119,7 @@ describe('the window drawn from layout frames', () => {
|
||||
await bootWindowFromPartitions(store, [{ hostId: profile.hostId, session: profile.build() }])
|
||||
const before = store.getState()
|
||||
const sync = openWorkspaceLayoutFrameSync(store)
|
||||
stream.onFrame!(snapshot(published(profile.hostId, profile.build())))
|
||||
stream.onFrame!(layoutSnapshotFrame(mainLoadedLayouts(profile.hostId, profile.build())))
|
||||
sync.start()
|
||||
const after = store.getState()
|
||||
expect(SLICES.filter((slice) => after[slice] !== before[slice])).toEqual([])
|
||||
@@ -333,7 +307,7 @@ describe('the window drawn from layout frames', () => {
|
||||
expect(layoutFramesFlowing()).toBe(true)
|
||||
const renamed = structuredClone(layouts)
|
||||
renamed[GIT_KEY]!.tabs.find((tab) => tab.id === 'tab-unbound')!.customTitle = 'from main'
|
||||
stream.onFrame!(snapshot(renamed))
|
||||
stream.onFrame!(layoutSnapshotFrame(renamed))
|
||||
stream.onStatus!({ state: 'live' })
|
||||
await flushMicrotasks()
|
||||
|
||||
@@ -412,8 +386,8 @@ describe('the window drawn from layout frames', () => {
|
||||
const store = createTestStore()
|
||||
await bootWindowFromPartitions(store, [{ hostId: 'local', session: emptySession() }])
|
||||
const sync = openWorkspaceLayoutFrameSync(store)
|
||||
const layouts = published('local', localDesktopSession())
|
||||
stream.onFrame!(snapshot(layouts))
|
||||
const layouts = mainLoadedLayouts('local', localDesktopSession())
|
||||
stream.onFrame!(layoutSnapshotFrame(layouts))
|
||||
sync.start()
|
||||
await flushMicrotasks()
|
||||
expect(store.getState().tabsByWorktree[GIT_KEY]).toBeUndefined()
|
||||
@@ -434,9 +408,9 @@ describe('the window drawn from layout frames', () => {
|
||||
await bootWindowFromPartitions(store, [{ hostId: 'local', session: localDesktopSession() }])
|
||||
const sync = openWorkspaceLayoutFrameSync(store)
|
||||
sync.start()
|
||||
const layouts = published('local', localDesktopSession())
|
||||
const layouts = mainLoadedLayouts('local', localDesktopSession())
|
||||
layouts[GIT_KEY]!.tabs.find((tab) => tab.id === 'tab-shell')!.customTitle = 'from main'
|
||||
stream.onFrame!(snapshot(layouts))
|
||||
stream.onFrame!(layoutSnapshotFrame(layouts))
|
||||
await flushMicrotasks()
|
||||
expect(
|
||||
store.getState().tabsByWorktree[GIT_KEY]!.find((tab) => tab.id === 'tab-shell')!.customTitle
|
||||
|
||||
@@ -22,8 +22,16 @@ import {
|
||||
import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness'
|
||||
import { createTestStore } from '../store/slices/store-test-helpers'
|
||||
import type { AppState } from '../store/types'
|
||||
import { layoutApplyTestTransport } from './layout-apply-test-transport.test-fixture'
|
||||
import { openWorkspaceLayoutFrameSync } from './workspace-layout-frame-sync'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof AgentStatusModule>()
|
||||
@@ -228,6 +236,7 @@ describe('tab properties through main', () => {
|
||||
// Today: the window acts on its own copy.
|
||||
const today = await boot(stored)
|
||||
entry.windowAction(today.getState())
|
||||
await layoutApplyTestTransport.settle()
|
||||
const todayTabs = shownTabs(today.getState())
|
||||
expect(todayTabs).not.toEqual(shownTabs((await boot(stored)).getState()))
|
||||
const expected = { ...todayTabs, ...entry.mainShows }
|
||||
@@ -245,6 +254,7 @@ describe('tab properties through main', () => {
|
||||
|
||||
// Main's answer then runs the window's own action, which leaves what today's action shows.
|
||||
entry.windowAction(drawn.getState())
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(shownTabs(drawn.getState())).toEqual(expected)
|
||||
sync.close()
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { resolveTerminalTabTitle } from '../../../../shared/tab-title-resolution'
|
||||
import { createTestStore, makeWorktree, seedStore } from './store-test-helpers'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const WORKTREE_ID = 'repo1::/path/wt1'
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
@@ -26,7 +34,7 @@ describe('generated agent tab titles', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('stays disabled by default when agent prompts arrive', () => {
|
||||
it('stays disabled by default when agent prompts arrive', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, false)
|
||||
@@ -36,12 +44,13 @@ describe('generated agent tab titles', () => {
|
||||
prompt: 'Refactor the auth middleware',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBeUndefined()
|
||||
expect(store.getState().unifiedTabsByWorktree[WORKTREE_ID][0].generatedLabel).toBeUndefined()
|
||||
})
|
||||
|
||||
it('generates one stable title from the first known agent prompt when enabled', () => {
|
||||
it('generates one stable title from the first known agent prompt when enabled', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -51,11 +60,13 @@ describe('generated agent tab titles', () => {
|
||||
prompt: 'Can you please refactor the auth middleware to use JWT tokens?',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setAgentStatus(makePaneKey(tabId, LEAF_ID), {
|
||||
state: 'working',
|
||||
prompt: 'Replace this with a later task name',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Refactor the auth middleware to use JWT'
|
||||
@@ -65,7 +76,7 @@ describe('generated agent tab titles', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('replaces a raw dispatch preamble title when orchestration display metadata arrives', () => {
|
||||
it('replaces a raw dispatch preamble title when orchestration display metadata arrives', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -83,6 +94,7 @@ orca orchestration send --to term_parent
|
||||
Implement the detailed worker instructions that should not be the short label`,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Implement the detailed worker'
|
||||
@@ -96,6 +108,7 @@ Implement the detailed worker instructions that should not be the short label`,
|
||||
displayName: 'Better worker label'
|
||||
}
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Better worker label'
|
||||
@@ -105,7 +118,7 @@ Implement the detailed worker instructions that should not be the short label`,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not replace with sticky orchestration when a new non-dispatch prompt arrives', () => {
|
||||
it('does not replace with sticky orchestration when a new non-dispatch prompt arrives', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -124,6 +137,7 @@ Implement the detailed worker instructions that should not be the short label`
|
||||
prompt: dispatchPrompt,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setRuntimeAgentOrchestrationByPaneKey({
|
||||
[paneKey]: {
|
||||
taskId: 'task-1',
|
||||
@@ -132,6 +146,7 @@ Implement the detailed worker instructions that should not be the short label`
|
||||
displayName: 'Better worker label'
|
||||
}
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Better worker label'
|
||||
)
|
||||
@@ -143,13 +158,14 @@ Implement the detailed worker instructions that should not be the short label`
|
||||
prompt: 'Refactor the auth middleware to use JWT tokens for session recovery',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Better worker label'
|
||||
)
|
||||
})
|
||||
|
||||
it('generates from a new non-dispatch prompt when sticky orchestration remains but no title exists', () => {
|
||||
it('generates from a new non-dispatch prompt when sticky orchestration remains but no title exists', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -165,6 +181,7 @@ Your task ID is: task-1
|
||||
Old dispatch task that already finished`,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setRuntimeAgentOrchestrationByPaneKey({
|
||||
[paneKey]: {
|
||||
taskId: 'task-1',
|
||||
@@ -173,6 +190,7 @@ Old dispatch task that already finished`,
|
||||
displayName: 'Stale worker label'
|
||||
}
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
// Clear any title set during the dispatch turn so the next non-dispatch
|
||||
// prompt is a pure first-write with sticky orchestration still present.
|
||||
const tabs = store.getState().tabsByWorktree[WORKTREE_ID]
|
||||
@@ -196,6 +214,7 @@ Old dispatch task that already finished`,
|
||||
prompt: 'Can you please refactor the auth middleware to use JWT tokens?',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Refactor the auth middleware to use JWT'
|
||||
@@ -205,7 +224,7 @@ Old dispatch task that already finished`,
|
||||
)
|
||||
})
|
||||
|
||||
it('does not re-pin sticky task A labels onto a later dispatch task B preamble', () => {
|
||||
it('does not re-pin sticky task A labels onto a later dispatch task B preamble', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -220,6 +239,7 @@ Your task ID is: task-a
|
||||
Implement task A worker instructions that should not stick`,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setRuntimeAgentOrchestrationByPaneKey({
|
||||
[paneKey]: {
|
||||
taskId: 'task-a',
|
||||
@@ -228,6 +248,7 @@ Implement task A worker instructions that should not stick`,
|
||||
displayName: 'Worker A label'
|
||||
}
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe('Worker A label')
|
||||
|
||||
store.getState().setAgentStatus(paneKey, {
|
||||
@@ -239,13 +260,14 @@ Your task ID is: task-b
|
||||
Implement task B worker instructions for the next dispatch`,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Implement task B worker instructions'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not force-replace titles when sticky orchestration updates after a non-dispatch prompt', () => {
|
||||
it('does not force-replace titles when sticky orchestration updates after a non-dispatch prompt', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -256,6 +278,7 @@ Implement task B worker instructions for the next dispatch`,
|
||||
prompt: 'Can you please refactor the auth middleware to use JWT tokens?',
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Refactor the auth middleware to use JWT'
|
||||
)
|
||||
@@ -268,13 +291,14 @@ Implement task B worker instructions for the next dispatch`,
|
||||
displayName: 'Stale orchestration label'
|
||||
}
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().tabsByWorktree[WORKTREE_ID][0].generatedTitle).toBe(
|
||||
'Refactor the auth middleware to use JWT'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not trim the full paste-sized prompt before generating an optional title', () => {
|
||||
it('does not trim the full paste-sized prompt before generating an optional title', async () => {
|
||||
vi.useFakeTimers()
|
||||
const trimSpy = vi.spyOn(String.prototype, 'trim')
|
||||
const store = createTestStore()
|
||||
@@ -286,6 +310,7 @@ Implement task B worker instructions for the next dispatch`,
|
||||
prompt,
|
||||
agentType: 'codex'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const tab = store.getState().tabsByWorktree[WORKTREE_ID][0]
|
||||
expect(tab.generatedTitle).toBe('Fix the flaky status tests large pasted')
|
||||
@@ -296,7 +321,7 @@ Implement task B worker instructions for the next dispatch`,
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps manual rename precedence over generated and live titles', () => {
|
||||
it('keeps manual rename precedence over generated and live titles', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
const tabId = seedWorktree(store, true)
|
||||
@@ -306,15 +331,18 @@ Implement task B worker instructions for the next dispatch`,
|
||||
prompt: 'Fix the flaky status tests',
|
||||
agentType: 'claude'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().updateTabTitle(tabId, 'Claude working')
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setTabCustomTitle(tabId, 'Status tests')
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const tab = store.getState().tabsByWorktree[WORKTREE_ID][0]
|
||||
expect(resolveTerminalTabTitle(tab, true)).toBe('Status tests')
|
||||
expect(tab.generatedTitle).toBe('Fix the flaky status tests')
|
||||
})
|
||||
|
||||
it('does not generate a title for quick command labeled tabs', () => {
|
||||
it('does not generate a title for quick command labeled tabs', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
@@ -335,6 +363,7 @@ Implement task B worker instructions for the next dispatch`,
|
||||
prompt: 'Fix the flaky status tests',
|
||||
agentType: 'claude'
|
||||
})
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const tab = store.getState().tabsByWorktree[WORKTREE_ID][0]
|
||||
expect(tab.generatedTitle).toBeUndefined()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type * as AgentStatusModule from '@/lib/agent-status'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
import { createTestStore, makeWorktree } from './store-test-helpers'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ callRuntimeRpc: vi.fn() }))
|
||||
@@ -18,6 +19,11 @@ vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) }
|
||||
})
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
mocks.callRuntimeRpc.mockImplementation((_target: unknown, method: string, params: unknown) =>
|
||||
method === 'layout.apply' ? layoutApplyTestTransport.call(params) : undefined
|
||||
)
|
||||
|
||||
const WORKTREE_ID = 'repo1::/path/wt1'
|
||||
const DOC_LOCATION = {
|
||||
kind: 'workspace-doc' as const,
|
||||
|
||||
@@ -8,6 +8,13 @@ import {
|
||||
MANAGED_BROWSER_UNAVAILABLE_MESSAGE
|
||||
} from '@/lib/client-creation-action-policy'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const createWebRuntimeSessionBrowserTabMock = vi.hoisted(() => vi.fn())
|
||||
const createWebRuntimeSessionTerminalMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createEditorStore, createEditorTabsStore } from './editor-slice-test-harness'
|
||||
import type { AppState } from '../types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
|
||||
const { toastErrorMock } = vi.hoisted(() => ({
|
||||
toastErrorMock: vi.fn()
|
||||
@@ -644,14 +645,17 @@ describe('createEditorSlice openDiff', () => {
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']).toEqual([
|
||||
expect.objectContaining({
|
||||
entityId: '/repo/a.ts',
|
||||
isPinned: true,
|
||||
isPreview: false
|
||||
isPinned: true
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entityId: 'wt-1::diff::unstaged::b.ts',
|
||||
isPreview: true
|
||||
})
|
||||
])
|
||||
// Not a preview: a switch-on build omits the flag rather than writing false.
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']?.[0]?.isPreview).toBe(
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN ? undefined : false
|
||||
)
|
||||
})
|
||||
|
||||
it('makes a preview file permanent without pinning the tab', () => {
|
||||
@@ -678,10 +682,13 @@ describe('createEditorSlice openDiff', () => {
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']).toEqual([
|
||||
expect.objectContaining({
|
||||
entityId: '/repo/a.ts',
|
||||
isPinned: undefined,
|
||||
isPreview: false
|
||||
isPinned: undefined
|
||||
})
|
||||
])
|
||||
// Not a preview: a switch-on build omits the flag rather than writing false.
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']?.[0]?.isPreview).toBe(
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN ? undefined : false
|
||||
)
|
||||
})
|
||||
|
||||
it('does not replace a dirty file that was opened as a preview', () => {
|
||||
@@ -714,13 +721,16 @@ describe('createEditorSlice openDiff', () => {
|
||||
])
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']).toEqual([
|
||||
expect.objectContaining({
|
||||
entityId: '/repo/a.ts',
|
||||
isPreview: false
|
||||
entityId: '/repo/a.ts'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
entityId: 'wt-1::diff::unstaged::b.ts',
|
||||
isPreview: true
|
||||
})
|
||||
])
|
||||
// Not a preview: a switch-on build omits the flag rather than writing false.
|
||||
expect(store.getState().unifiedTabsByWorktree['wt-1']?.[0]?.isPreview).toBe(
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN ? undefined : false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,12 +4,21 @@ import { createEditorSlice } from './editor'
|
||||
import { createTabsSlice } from './tabs'
|
||||
import type { AppState } from '../types'
|
||||
|
||||
/** The tab-bar maps switch-on editor paths read; a store without the tabs slice needs them too. */
|
||||
export function emptyTabLayoutState(): Pick<
|
||||
AppState,
|
||||
'unifiedTabsByWorktree' | 'groupsByWorktree' | 'layoutByWorktree'
|
||||
> {
|
||||
return { unifiedTabsByWorktree: {}, groupsByWorktree: {}, layoutByWorktree: {} }
|
||||
}
|
||||
|
||||
export function createEditorStore(): StoreApi<AppState> {
|
||||
// Only the editor slice + activeWorktreeId are needed for these tests.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any, typescript/consistent-type-assertions -- SAFETY: a partial store; tests read only the slices it builds.
|
||||
return createStore<any>()((...args: any[]) => ({
|
||||
activeWorktreeId: 'wt-1',
|
||||
tabsByWorktree: {},
|
||||
...emptyTabLayoutState(),
|
||||
browserTabsByWorktree: {},
|
||||
activeBrowserTabId: null,
|
||||
activeBrowserTabIdByWorktree: {},
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { StoreApi } from 'zustand/vanilla'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createEditorStore, createEditorTabsStore } from './editor-slice-test-harness'
|
||||
import {
|
||||
createEditorStore,
|
||||
createEditorTabsStore,
|
||||
ownedEditorFileId
|
||||
} from './editor-slice-test-harness'
|
||||
import type { AppState } from '../types'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
|
||||
const { toastErrorMock } = vi.hoisted(() => ({
|
||||
toastErrorMock: vi.fn()
|
||||
@@ -20,6 +25,17 @@ vi.mock('@/runtime/close-mirrored-editor-tab', () => ({
|
||||
notifyHostOfMirroredEditorCloseMock(...args)
|
||||
}))
|
||||
|
||||
function fileIdOf(store: { getState: () => AppState }, filePath: string): string | undefined {
|
||||
return store.getState().openFiles.find((file) => file.filePath === filePath)?.id
|
||||
}
|
||||
|
||||
/** A floating file's id: its path, or an owned id when main owns the layout. */
|
||||
function expectedFileId(filePath: string): unknown {
|
||||
return ORCA_LAYOUT_OWNER_IS_MAIN
|
||||
? ownedEditorFileId(filePath, FLOATING_TERMINAL_WORKTREE_ID, null)
|
||||
: filePath
|
||||
}
|
||||
|
||||
describe('createEditorSlice floating editor activation', () => {
|
||||
it('creates a visible floating editor tab when the floating workspace is empty', () => {
|
||||
const store = createEditorTabsStore()
|
||||
@@ -36,10 +52,12 @@ describe('createEditorSlice floating editor activation', () => {
|
||||
{ suppressActiveRuntimeFallback: true }
|
||||
)
|
||||
|
||||
const fileId = fileIdOf(store, '/tmp/orca/notes.md')
|
||||
expect(fileId).toEqual(expectedFileId('/tmp/orca/notes.md'))
|
||||
const tab = store.getState().unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.[0]
|
||||
expect(tab).toMatchObject({
|
||||
contentType: 'editor',
|
||||
entityId: '/tmp/orca/notes.md',
|
||||
entityId: fileId,
|
||||
label: 'notes.md',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID
|
||||
})
|
||||
@@ -47,9 +65,7 @@ describe('createEditorSlice floating editor activation', () => {
|
||||
activeTabId: tab?.id,
|
||||
tabOrder: [tab?.id]
|
||||
})
|
||||
expect(store.getState().activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(
|
||||
'/tmp/orca/notes.md'
|
||||
)
|
||||
expect(store.getState().activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(fileId)
|
||||
})
|
||||
|
||||
it('opens floating markdown tabs without changing the main active editor surface', () => {
|
||||
@@ -72,8 +88,8 @@ describe('createEditorSlice floating editor activation', () => {
|
||||
|
||||
expect(store.getState().activeFileId).toBe('/repo/main.md')
|
||||
expect(store.getState().activeTabType).toBe('editor')
|
||||
expect(store.getState().activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(
|
||||
'/tmp/orca/untitled.md'
|
||||
expect(store.getState().activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toEqual(
|
||||
expectedFileId('/tmp/orca/untitled.md')
|
||||
)
|
||||
expect(store.getState().activeTabTypeByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe('editor')
|
||||
})
|
||||
|
||||
@@ -5,10 +5,12 @@ import type { AppState } from '../types'
|
||||
import type { BrowserTab } from '../../../../shared/browser-workspace-types'
|
||||
import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types'
|
||||
import { makeTab } from './store-session-test-harness'
|
||||
import { emptyTabLayoutState } from './editor-slice-test-harness'
|
||||
|
||||
function createEditorStore(overrides?: Partial<AppState>): StoreApi<AppState> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any, typescript/consistent-type-assertions -- SAFETY: a partial store; tests read only the slices it builds.
|
||||
return createStore<any>()((...args: any[]) => ({
|
||||
...emptyTabLayoutState(),
|
||||
activeWorktreeId: 'wt-1',
|
||||
tabsByWorktree: {},
|
||||
browserTabsByWorktree: {},
|
||||
|
||||
@@ -47,6 +47,15 @@ import {
|
||||
remapClosedTerminalTabSnapshotCwds,
|
||||
restoreRecentlyClosedTabPosition
|
||||
} from './recently-closed-tabs'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
const WT = 'repo1::/path/wt1'
|
||||
|
||||
@@ -66,15 +75,18 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('terminal recently-closed capture', () => {
|
||||
it('captures a snapshot and kind entry on user close', () => {
|
||||
it('captures a snapshot and kind entry on user close', async () => {
|
||||
const store = makeSeededStore()
|
||||
const tab = store
|
||||
.getState()
|
||||
.createTab(WT, undefined, undefined, { startupCwd: '/path/wt1/packages/app' })
|
||||
store.getState().setTabCustomTitle(tab.id, 'build shell')
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setTabColor(tab.id, '#ff0000')
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -97,23 +109,25 @@ describe('terminal recently-closed capture', () => {
|
||||
expect(store.getState().recentlyClosedTabKindsByWorktree[WT]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips capture for a confirmed parked PTY exit', () => {
|
||||
it('skips capture for a confirmed parked PTY exit', async () => {
|
||||
const store = makeSeededStore()
|
||||
const tab = store.getState().createTab(WT)
|
||||
|
||||
store.getState().closeTab(tab.id, { captureRecentlyClosed: false })
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]).toBeUndefined()
|
||||
expect(store.getState().recentlyClosedTabKindsByWorktree[WT]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('caps the terminal stack at 10 snapshots, newest first', () => {
|
||||
it('caps the terminal stack at 10 snapshots, newest first', async () => {
|
||||
const store = makeSeededStore()
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const tab = store
|
||||
.getState()
|
||||
.createTab(WT, undefined, undefined, { startupCwd: `/path/wt1/dir-${i}` })
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
}
|
||||
|
||||
const stack = store.getState().recentlyClosedTerminalTabsByWorktree[WT]
|
||||
@@ -153,7 +167,7 @@ describe('terminal snapshot cwd remapping', () => {
|
||||
})
|
||||
|
||||
describe('reopenClosedTerminalTab', () => {
|
||||
it('restores the tab bar and group position of a closed middle tab', () => {
|
||||
it('restores the tab bar and group position of a closed middle tab', async () => {
|
||||
const store = makeSeededStore()
|
||||
const first = store.getState().createTab(WT)
|
||||
const middle = store.getState().createTab(WT)
|
||||
@@ -161,7 +175,9 @@ describe('reopenClosedTerminalTab', () => {
|
||||
store.getState().setTabBarOrder(WT, [first.id, middle.id, last.id])
|
||||
|
||||
store.getState().closeTab(middle.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const restored = store
|
||||
.getState()
|
||||
@@ -177,17 +193,21 @@ describe('reopenClosedTerminalTab', () => {
|
||||
).toEqual([first.id, restored!.id, last.id])
|
||||
})
|
||||
|
||||
it('recreates a fresh terminal with the snapshot cwd, shell, title, and color', () => {
|
||||
it('recreates a fresh terminal with the snapshot cwd, shell, title, and color', async () => {
|
||||
const store = makeSeededStore()
|
||||
const tab = store
|
||||
.getState()
|
||||
.createTab(WT, undefined, 'zsh', { startupCwd: '/path/wt1/packages/app' })
|
||||
store.getState().setTabCustomTitle(tab.id, 'build shell')
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().setTabColor(tab.id, '#ff0000')
|
||||
await layoutApplyTestTransport.settle()
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WT]).toHaveLength(0)
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
const restored = store.getState().tabsByWorktree[WT]
|
||||
expect(restored).toHaveLength(1)
|
||||
@@ -203,17 +223,20 @@ describe('reopenClosedTerminalTab', () => {
|
||||
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns false when the stack is empty (no double-restore)', () => {
|
||||
it('returns false when the stack is empty (no double-restore)', async () => {
|
||||
const store = makeSeededStore()
|
||||
const tab = store.getState().createTab(WT)
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(false)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WT]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('skips local reopen on a remote-runtime worktree (host owns the terminal)', () => {
|
||||
it('skips local reopen on a remote-runtime worktree (host owns the terminal)', async () => {
|
||||
// Why: a `runtime:` host id makes the worktree host-owned; a local createTab
|
||||
// would leave an unbacked phantom tab, so reopen must bail and let the
|
||||
// cross-type dispatcher fall through to browser/editor.
|
||||
@@ -232,24 +255,27 @@ describe('reopenClosedTerminalTab', () => {
|
||||
})
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(false)
|
||||
await layoutApplyTestTransport.settle()
|
||||
// No local tab spawned, and the snapshot is left intact (not consumed).
|
||||
expect(store.getState().tabsByWorktree[WT] ?? []).toHaveLength(0)
|
||||
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('reopens a legacy local worktree while another runtime is merely focused', () => {
|
||||
it('reopens a legacy local worktree while another runtime is merely focused', async () => {
|
||||
const store = makeSeededStore()
|
||||
store.setState({
|
||||
settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'env-1' }
|
||||
})
|
||||
const tab = store.getState().createTab(WT)
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WT]).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('reopens SSH terminals through the renderer-owned create path', () => {
|
||||
it('reopens SSH terminals through the renderer-owned create path', async () => {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
worktreesByRepo: {
|
||||
@@ -263,8 +289,10 @@ describe('reopenClosedTerminalTab', () => {
|
||||
.getState()
|
||||
.createTab(WT, undefined, undefined, { startupCwd: '/remote/wt1/packages/app' })
|
||||
store.getState().closeTab(tab.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().tabsByWorktree[WT]?.[0]?.startupCwd).toBe('/remote/wt1/packages/app')
|
||||
})
|
||||
})
|
||||
@@ -325,12 +353,17 @@ describe('restoreRecentlyClosedTabPosition', () => {
|
||||
{ groupId: 'captured-group', groupIndex: 0 }
|
||||
)
|
||||
|
||||
expect(reordered).toHaveBeenCalledWith('captured-group', ['captured-tab', 'captured-sibling'], {
|
||||
recordInteraction: false
|
||||
})
|
||||
// A switch-on build also marks the reorder as one main already made.
|
||||
expect(reordered).toHaveBeenCalledWith(
|
||||
'captured-group',
|
||||
['captured-tab', 'captured-sibling'],
|
||||
ORCA_LAYOUT_OWNER_IS_MAIN
|
||||
? { recordInteraction: false, layoutOwnerHandled: true }
|
||||
: { recordInteraction: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('does not reuse a stale flat index after group drag reordering', () => {
|
||||
it('does not reuse a stale flat index after group drag reordering', async () => {
|
||||
const store = makeSeededStore()
|
||||
const first = store.getState().createTab(WT)
|
||||
const middle = store.getState().createTab(WT)
|
||||
@@ -343,12 +376,14 @@ describe('restoreRecentlyClosedTabPosition', () => {
|
||||
store.getState().reorderUnifiedTabs(groupId, [first.id, last.id, middle.id])
|
||||
|
||||
store.getState().closeTab(middle.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(store.getState().recentlyClosedTerminalTabsByWorktree[WT]?.[0]?.position).toEqual({
|
||||
groupId,
|
||||
groupIndex: 2
|
||||
})
|
||||
|
||||
expect(store.getState().reopenClosedTerminalTab(WT)).toBe(true)
|
||||
await layoutApplyTestTransport.settle()
|
||||
const restored = store
|
||||
.getState()
|
||||
.tabsByWorktree[WT]?.find((tab) => tab.id !== first.id && tab.id !== last.id)
|
||||
@@ -362,7 +397,7 @@ describe('restoreRecentlyClosedTabPosition', () => {
|
||||
})
|
||||
|
||||
describe('reopenClosedTab cross-type MRU', () => {
|
||||
it('reopens closed tabs of mixed kinds in most-recent-first order', () => {
|
||||
it('reopens closed tabs of mixed kinds in most-recent-first order', async () => {
|
||||
const store = makeSeededStore()
|
||||
|
||||
// Close order: editor → terminal → browser.
|
||||
@@ -372,6 +407,7 @@ describe('reopenClosedTab cross-type MRU', () => {
|
||||
.getState()
|
||||
.createTab(WT, undefined, undefined, { startupCwd: '/path/wt1/api' })
|
||||
store.getState().closeTab(terminal.id)
|
||||
await layoutApplyTestTransport.settle()
|
||||
const browser = store.getState().createBrowserTab(WT, 'https://example.com', { title: 'Ex' })
|
||||
store.getState().closeBrowserTab(browser.id)
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import { createTestStore, makeUnifiedTab, makeTabGroup } from './store-test-helpers'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
// Mock sonner (imported transitively by repos.ts via the full store).
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
@@ -51,25 +59,30 @@ describe('tab view mode', () => {
|
||||
seedTwoLeafTabs(store)
|
||||
})
|
||||
|
||||
it('toggles a tab to chat and back to terminal', () => {
|
||||
it('toggles a tab to chat and back to terminal', async () => {
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBeUndefined()
|
||||
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('chat')
|
||||
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('terminal')
|
||||
})
|
||||
|
||||
it('setTabViewMode sets an explicit mode', () => {
|
||||
it('setTabViewMode sets an explicit mode', async () => {
|
||||
store.getState().setTabViewMode('left-tab', 'chat')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('chat')
|
||||
store.getState().setTabViewMode('left-tab', 'terminal')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('terminal')
|
||||
})
|
||||
|
||||
it('mutates only the targeted tab, leaving a split sibling unchanged', () => {
|
||||
it('mutates only the targeted tab, leaving a split sibling unchanged', async () => {
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('chat')
|
||||
// The sibling leaf's tab keeps its (default) view mode.
|
||||
@@ -105,16 +118,19 @@ describe('tab view mode', () => {
|
||||
|
||||
let rowTabId = ''
|
||||
|
||||
it('toggleTabViewMode patches the row in the same write', () => {
|
||||
it('toggleTabViewMode patches the row in the same write', async () => {
|
||||
store.getState().toggleTabViewMode(rowTabId)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(terminalRow(rowTabId)?.viewMode).toBe('chat')
|
||||
|
||||
store.getState().toggleTabViewMode(rowTabId)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(terminalRow(rowTabId)?.viewMode).toBe('terminal')
|
||||
})
|
||||
|
||||
it('setTabViewMode patches the row in the same write', () => {
|
||||
it('setTabViewMode patches the row in the same write', async () => {
|
||||
store.getState().setTabViewMode(rowTabId, 'chat')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(terminalRow(rowTabId)?.viewMode).toBe('chat')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import { createTestStore, makeWorktree, seedStore } from '../slices/store-test-helpers'
|
||||
import { layoutApplyTestTransport } from '@/runtime/layout-apply-test-transport.test-fixture'
|
||||
|
||||
// Main's answer to the window's layout commands, as a switch-on build sends them.
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) =>
|
||||
(
|
||||
await import('@/runtime/layout-apply-test-transport.test-fixture')
|
||||
).runtimeRpcClientWithLayoutApply(await importOriginal())
|
||||
)
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() }
|
||||
@@ -68,7 +76,7 @@ function colorOf(store: ReturnType<typeof createTestStore>): string | null | und
|
||||
}
|
||||
|
||||
describe('renaming a terminal tab still resolves', () => {
|
||||
it('routes a terminal rename through its entityId, not the unified id', () => {
|
||||
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,
|
||||
@@ -80,6 +88,7 @@ describe('renaming a terminal tab still resolves', () => {
|
||||
|
||||
// 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] ?? []
|
||||
expect(tabs.find((t) => t.id === TERMINAL_UNIFIED_ID)?.customLabel).toBe('Build logs')
|
||||
@@ -88,21 +97,24 @@ describe('renaming a terminal tab still resolves', () => {
|
||||
})
|
||||
|
||||
describe('recoloring a structured chat tab', () => {
|
||||
it('writes the color onto the agent-session tab', () => {
|
||||
it('writes the color onto the agent-session tab', async () => {
|
||||
const store = storeWithStructuredTab()
|
||||
store.getState().setTabColor(STRUCTURED_TAB_ID, 'red')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(colorOf(store)).toBe('red')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renaming a structured chat tab', () => {
|
||||
it('clears the custom label when the rename is emptied', () => {
|
||||
it('clears the custom label when the rename is emptied', async () => {
|
||||
const store = storeWithStructuredTab()
|
||||
store.getState().setTabCustomTitle(STRUCTURED_TAB_ID, 'Flaky retry test')
|
||||
await layoutApplyTestTransport.settle()
|
||||
// Guard: without the intermediate assertion this case passes on a rename
|
||||
// that never wrote anything, since the label starts out null too.
|
||||
expect(labelOf(store)).toBe('Flaky retry test')
|
||||
store.getState().setTabCustomTitle(STRUCTURED_TAB_ID, null)
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(labelOf(store)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Switch-off builds (the switch reads off in tests): a workspace's first group keeps a minted id.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { build, testContext, WS } from './workspace-layout-command.test-fixture'
|
||||
|
||||
// rollback build (switch-off) — delete with the switch-off code post-flip
|
||||
vi.mock('../layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: false }))
|
||||
|
||||
describe('a workspace first group with the layout-owner switch off', () => {
|
||||
it('is a minted id, whatever group the sender names', () => {
|
||||
const plain = build(testContext(), [{ type: 'createTerminalTab', workspace: WS }])
|
||||
|
||||
Reference in New Issue
Block a user