mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(terminal): a CLI or phone close through main keeps a pinned tab, as the window-era close did * feat(terminal): main applies phone closes, renames, pane titles, sleeps and agent-launch tabs itself, with or without a window (layout-owner build) * fix(terminal): a tab close through main also ends the tab's dormant SSH relay session; agent-launch tabs main writes keep the window's focus rule, prompt and outcome (layout-owner build) * feat(terminal): main starts a new tab's first terminal under the window's ids, and the pane attaches to it (layout-owner build) * test(terminal): tests that counted the window's create calls or publications see main's start of a new tab * fix(terminal): an account-switch restart binds only into a pane main's layout holds, with no window tree (layout-owner build, L197) * test(terminal): the new-tab start tests hold no type assertions; the start's ledger row is L252 * fix(terminal): a new tab's pane attaches to any terminal main binds it to, and a failed start keeps the tab's startup * test(terminal): a Claude account-switch restart's tests draw the pane main holds * fix(terminal): a launch pane main takes back still tells the window why, which drops its hold and prompt; a rename through main answers the name it stored (layout-owner build) * fix(terminal): a new tab another owner's terminal won attaches to it, and a failed start's startup never runs twice * fix(terminal): main's pane close reads its stop result as a type only, so no runtime module loads before its base class * fix(terminal): a phone's close through main drops the closed rows from what phones list at once, not at the window's next publish (layout-owner build) * chore(terminal): mark the phone-close row drop interim (ledger L260) * test(worktree-create): a new local tab's startup counts as run when main starts it (layout-owner build) * test(worktree-create): record main's starts without a type assertion * fix(terminal): a tab closed before main declines its start gets no startup back (layout-owner build) * fix(terminal): the window's new tab starts in main when its own window asks over IPC, which labels itself a client (layout-owner build) * fix(terminal): a refused close of a pinned tab leaves its running agent launch alone (layout-owner build)
396 lines
15 KiB
TypeScript
396 lines
15 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { applyWindowLayoutCommand, type WindowLayoutCommand } from './runtime-layout-apply'
|
||
import type { RuntimeStore } from './runtime-store-contract'
|
||
import type { ExecutionHostId } from '../../shared/execution-host'
|
||
import type { LayoutApplyOutcome } from '../../shared/workspace-layout/workspace-layout-command-types'
|
||
|
||
const mocks = vi.hoisted(() => ({
|
||
markAgentLaunchesClosedByUser: vi.fn(),
|
||
frozen: new Set<string>()
|
||
}))
|
||
vi.mock('../agent-launch/agent-launch-pane-attachment', () => ({
|
||
markAgentLaunchesClosedByUser: mocks.markAgentLaunchesClosedByUser
|
||
}))
|
||
vi.mock('../persistence/moving-wsl-projects/wsl-move-scope-guard', () => ({
|
||
isWslMoveWorkspaceSessionFrozen: (workspaceId: string) => mocks.frozen.has(workspaceId)
|
||
}))
|
||
|
||
const CLOSE = { type: 'closePane', workspace: 'wt-1', tabId: 'tab-1', leafId: 'leaf-1' } as const
|
||
|
||
function storeWith() {
|
||
// A close takes every tab it names, as main's store does for an unpinned tab.
|
||
const applyLayoutCommand = vi.fn(
|
||
async (command: WindowLayoutCommand): Promise<LayoutApplyOutcome> => ({
|
||
ok: true,
|
||
result: command.type === 'closeTabs' ? { closed: [...command.tabIds], refused: [] } : {}
|
||
})
|
||
)
|
||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the apply path reads only applyLayoutCommand.
|
||
const store = { applyLayoutCommand } as unknown as RuntimeStore
|
||
return { store, applyLayoutCommand }
|
||
}
|
||
|
||
describe('applyWindowLayoutCommand', () => {
|
||
beforeEach(() => {
|
||
mocks.markAgentLaunchesClosedByUser.mockReset()
|
||
mocks.frozen.clear()
|
||
})
|
||
|
||
it('closes a pane as the user’s close, in every local or SSH holder', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local', 'runtime:env-1', 'ssh:target-1'], CLOSE)
|
||
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith({ ...CLOSE, userPaneClose: true }, [
|
||
'local',
|
||
'ssh:target-1'
|
||
])
|
||
expect(mocks.markAgentLaunchesClosedByUser).toHaveBeenCalledWith('wt-1', {
|
||
kind: 'pane',
|
||
tabId: 'tab-1',
|
||
leafId: 'leaf-1'
|
||
})
|
||
})
|
||
|
||
it('applies other commands to the routed partition only', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const rename = { ...CLOSE, type: 'renamePane', title: 'Pane' } as const
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local', 'ssh:target-1'], rename)
|
||
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(rename, ['local'])
|
||
expect(mocks.markAgentLaunchesClosedByUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('records a sleep and a wake in every partition holding the tab, and in no paired server’s', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const paneKey = 'tab-1:11111111-1111-4111-8111-111111111111'
|
||
const wake: WindowLayoutCommand = {
|
||
type: 'wake',
|
||
workspace: 'wt-1',
|
||
tabId: 'tab-1',
|
||
paneKeys: [paneKey]
|
||
}
|
||
const sleep: WindowLayoutCommand = { ...wake, type: 'sleep', records: [] }
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local', 'ssh:target-1', 'runtime:env-1'], sleep)
|
||
await applyWindowLayoutCommand(store, () => ['local', 'ssh:target-1'], wake)
|
||
|
||
expect(applyLayoutCommand.mock.calls).toEqual([
|
||
[sleep, ['local', 'ssh:target-1']],
|
||
[wake, ['local', 'ssh:target-1']]
|
||
])
|
||
})
|
||
|
||
it('routes a group tree command by its workspace, with no tab to look up', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const hostIdsFor = vi.fn(() => ['ssh:target-1' as const])
|
||
const ratios = {
|
||
type: 'setGroupRatios',
|
||
workspace: 'wt-1',
|
||
groupLayout: { type: 'leaf', groupId: 'g1' }
|
||
} as const
|
||
|
||
await applyWindowLayoutCommand(store, hostIdsFor, ratios)
|
||
|
||
expect(hostIdsFor).toHaveBeenCalledWith('wt-1', null)
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(ratios, ['ssh:target-1'])
|
||
})
|
||
|
||
it('renames editor files in the workspace’s routed partition, an SSH one too', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const hostIdsFor = vi.fn(() => ['ssh:target-1' as const])
|
||
const rename: WindowLayoutCommand = {
|
||
type: 'renameEditorFiles',
|
||
workspace: 'wt-1',
|
||
renames: [{ fileId: '/a.ts', newFileId: '/b.ts' }]
|
||
}
|
||
|
||
await applyWindowLayoutCommand(store, hostIdsFor, rename)
|
||
|
||
expect(hostIdsFor).toHaveBeenCalledWith('wt-1', null)
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(rename, ['ssh:target-1'])
|
||
})
|
||
|
||
it('records the window’s tab close in every holder, forced, stopping no terminal', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const close: WindowLayoutCommand = {
|
||
type: 'closeTabs',
|
||
workspace: 'wt-1',
|
||
tabIds: ['tab-1'],
|
||
recordClose: 'user'
|
||
}
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local', 'runtime:env-1', 'ssh:target-1'], close)
|
||
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith({ ...close, force: true }, [
|
||
'local',
|
||
'ssh:target-1'
|
||
])
|
||
expect(mocks.markAgentLaunchesClosedByUser).toHaveBeenCalledWith('wt-1', {
|
||
kind: 'tab',
|
||
tabId: 'tab-1'
|
||
})
|
||
})
|
||
|
||
it('marks a user close’s launches by the terminal’s id, not its tab-bar entry’s', async () => {
|
||
const { store } = storeWith()
|
||
const session = { unifiedTabs: { 'wt-1': [{ id: 'entry-1', entityId: 'term-1' }] } }
|
||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the close reads only unifiedTabs.
|
||
const withSession = { ...store, getWorkspaceSession: () => session } as unknown as RuntimeStore
|
||
const close: WindowLayoutCommand = {
|
||
type: 'closeTabs',
|
||
workspace: 'wt-1',
|
||
tabIds: ['entry-1'],
|
||
recordClose: 'user'
|
||
}
|
||
|
||
await applyWindowLayoutCommand(withSession, () => ['local'], close)
|
||
|
||
expect(mocks.markAgentLaunchesClosedByUser).toHaveBeenCalledWith('wt-1', {
|
||
kind: 'tab',
|
||
tabId: 'term-1'
|
||
})
|
||
})
|
||
|
||
it('leaves the launches of a tab the close refused alone', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
applyLayoutCommand.mockResolvedValueOnce({
|
||
ok: true,
|
||
result: { closed: [], refused: [{ tabId: 'tab-1', code: 'tab_pinned' }] }
|
||
})
|
||
const close: WindowLayoutCommand = {
|
||
type: 'closeTabs',
|
||
workspace: 'wt-1',
|
||
tabIds: ['tab-1'],
|
||
refusePinned: true,
|
||
recordClose: 'user'
|
||
}
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local'], close)
|
||
|
||
expect(mocks.markAgentLaunchesClosedByUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('leaves a cleanup close’s launches alone, and a workspace main lacks to today’s close', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
applyLayoutCommand.mockResolvedValueOnce({ ok: true, result: { alreadyClosed: true } })
|
||
const close: WindowLayoutCommand = {
|
||
type: 'closeTabs',
|
||
workspace: 'wt-1',
|
||
tabIds: ['tab-1'],
|
||
recordClose: 'cleanup'
|
||
}
|
||
|
||
await expect(applyWindowLayoutCommand(store, () => ['local'], close)).resolves.toEqual({
|
||
ok: false,
|
||
code: 'workspace_not_found'
|
||
})
|
||
expect(mocks.markAgentLaunchesClosedByUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('moves a dragged-out pane in every holder, and its pane-keyed owners follow', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
applyLayoutCommand.mockResolvedValueOnce({
|
||
ok: true,
|
||
result: { tabId: 'tab-new', ptyId: 'pty-1' }
|
||
})
|
||
const session = { unifiedTabs: { 'wt-1': [{ id: 'entry-1', entityId: 'term-1' }] } }
|
||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the move reads only unifiedTabs.
|
||
const withSession = { ...store, getWorkspaceSession: () => session } as unknown as RuntimeStore
|
||
const onPaneMoved = vi.fn()
|
||
const move: WindowLayoutCommand = {
|
||
type: 'movePaneToNewTab',
|
||
workspace: 'wt-1',
|
||
tabId: 'entry-1',
|
||
leafId: 'leaf-1',
|
||
groupId: 'g1',
|
||
// The window cannot name the new tab.
|
||
windowDetach: { ptyId: 'pty-1', tabId: 'tab-window', startupCwd: '/wt/app' }
|
||
}
|
||
|
||
await applyWindowLayoutCommand(
|
||
withSession,
|
||
() => ['ssh:t', 'runtime:env-1', 'local'],
|
||
move,
|
||
onPaneMoved
|
||
)
|
||
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(
|
||
{ ...move, windowDetach: { ptyId: 'pty-1', startupCwd: '/wt/app' } },
|
||
['ssh:t', 'local']
|
||
)
|
||
expect(onPaneMoved).toHaveBeenCalledWith({
|
||
worktreeId: 'wt-1',
|
||
sourceTabId: 'term-1',
|
||
targetTabId: 'tab-new',
|
||
leafId: 'leaf-1',
|
||
ptyId: 'pty-1'
|
||
})
|
||
applyLayoutCommand.mockResolvedValueOnce({ ok: false, code: 'pty_mismatch' })
|
||
await applyWindowLayoutCommand(withSession, () => ['local'], move, onPaneMoved)
|
||
expect(onPaneMoved).toHaveBeenCalledTimes(1)
|
||
// A committed move still answers ok when re-keying throws.
|
||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||
applyLayoutCommand.mockResolvedValueOnce({ ok: true, result: { tabId: 'tab-2', ptyId: null } })
|
||
onPaneMoved.mockImplementationOnce(() => {
|
||
throw new Error('rekey failed')
|
||
})
|
||
await expect(
|
||
applyWindowLayoutCommand(withSession, () => ['local'], move, onPaneMoved)
|
||
).resolves.toMatchObject({ ok: true })
|
||
})
|
||
|
||
it('refuses a tab a paired server routes, touching nothing', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
|
||
await expect(
|
||
applyWindowLayoutCommand(store, () => ['runtime:env-1', 'local'], CLOSE)
|
||
).resolves.toEqual({ ok: false, code: 'host_not_owned' })
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
expect(mocks.markAgentLaunchesClosedByUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('moves editor tabs within one partition or across two, and refuses a paired server', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const moveTabToWorkspaceAcrossPartitions = vi.fn(async (): Promise<LayoutApplyOutcome> => ({
|
||
ok: true,
|
||
result: {}
|
||
}))
|
||
store.moveTabToWorkspaceAcrossPartitions = moveTabToWorkspaceAcrossPartitions
|
||
const move: WindowLayoutCommand = {
|
||
type: 'moveTabToWorkspace',
|
||
workspace: 'wt-1',
|
||
toWorkspace: 'wt-2',
|
||
tabs: [{ tabId: 'tab-1', newTabId: 'tab-1', newEntityId: 'file-2' }],
|
||
groupId: 'g2'
|
||
}
|
||
const hostIdsFor = vi.fn((_worktreeId: string, _tabId: string | null) => [
|
||
'ssh:target-1' as const
|
||
])
|
||
|
||
await applyWindowLayoutCommand(store, hostIdsFor, move)
|
||
expect(hostIdsFor.mock.calls).toEqual([
|
||
['wt-1', null],
|
||
['wt-2', null]
|
||
])
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(move, ['ssh:target-1'])
|
||
|
||
applyLayoutCommand.mockClear()
|
||
await applyWindowLayoutCommand(
|
||
store,
|
||
(worktreeId) => [worktreeId === 'wt-1' ? 'local' : 'ssh:target-1'],
|
||
move
|
||
)
|
||
expect(moveTabToWorkspaceAcrossPartitions).toHaveBeenCalledWith(move, 'local', 'ssh:target-1')
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
|
||
const partitions = {
|
||
'a paired server': [['local'], ['runtime:env-1'], 'host_not_owned'],
|
||
'an unknown target': [['local'], [], 'host_not_owned']
|
||
} as const
|
||
for (const [from, to, code] of Object.values(partitions)) {
|
||
await expect(
|
||
applyWindowLayoutCommand(
|
||
store,
|
||
(worktreeId) => [...(worktreeId === 'wt-1' ? from : to)],
|
||
move
|
||
)
|
||
).resolves.toEqual({ ok: false, code })
|
||
}
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('refuses any command on a workspace a WSL move has paused, writing nothing', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
mocks.frozen.add('wt-1')
|
||
const commands: WindowLayoutCommand[] = [
|
||
CLOSE,
|
||
{ ...CLOSE, type: 'renamePane', title: 'Pane' },
|
||
{ type: 'closeTabs', workspace: 'wt-1', tabIds: ['tab-1'], recordClose: 'user' },
|
||
{ type: 'setGroupRatios', workspace: 'wt-1', groupLayout: { type: 'leaf', groupId: 'g-1' } }
|
||
]
|
||
|
||
for (const command of commands) {
|
||
await expect(applyWindowLayoutCommand(store, () => ['local'], command)).resolves.toEqual({
|
||
ok: false,
|
||
code: 'wsl_move_in_progress'
|
||
})
|
||
}
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
expect(mocks.markAgentLaunchesClosedByUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('refuses a tab move into a paused workspace, which would drop the moved tab', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
mocks.frozen.add('wt-2')
|
||
const move: WindowLayoutCommand = {
|
||
type: 'moveTabToWorkspace',
|
||
workspace: 'wt-1',
|
||
toWorkspace: 'wt-2',
|
||
tabs: [{ tabId: 'tab-1', newTabId: 'tab-2', newEntityId: 'tab-2' }],
|
||
groupId: 'g-2'
|
||
}
|
||
|
||
await expect(applyWindowLayoutCommand(store, () => ['local'], move)).resolves.toEqual({
|
||
ok: false,
|
||
code: 'wsl_move_in_progress'
|
||
})
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('applies as before on a workspace no move has paused', async () => {
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
mocks.frozen.add('wt-other')
|
||
|
||
await expect(applyWindowLayoutCommand(store, () => ['local'], CLOSE)).resolves.toEqual({
|
||
ok: true,
|
||
result: {}
|
||
})
|
||
expect(applyLayoutCommand).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
it("writes one disk move's renames across workspaces, a command per partition, or refuses", async () => {
|
||
const renames = [{ fileId: '/a.ts', newFileId: '/b.ts' }]
|
||
const rename: WindowLayoutCommand = {
|
||
type: 'renameEditorFiles',
|
||
workspace: 'wt-1',
|
||
renames,
|
||
alsoIn: [{ workspace: 'wt-2', renames }]
|
||
}
|
||
const { store, applyLayoutCommand } = storeWith()
|
||
const applyEditorFileRenamesAcrossPartitions = vi.fn(async (): Promise<LayoutApplyOutcome> => ({
|
||
ok: true,
|
||
result: {}
|
||
}))
|
||
Object.assign(store, { applyEditorFileRenamesAcrossPartitions })
|
||
|
||
await applyWindowLayoutCommand(store, () => ['local'], rename)
|
||
expect(applyLayoutCommand).toHaveBeenCalledWith(rename, ['local'])
|
||
|
||
applyLayoutCommand.mockClear()
|
||
const hosts =
|
||
(other: ExecutionHostId) =>
|
||
(workspace: string): ExecutionHostId[] => [workspace === 'wt-2' ? other : 'local']
|
||
// Each partition's renames in its own command, all in one durable write.
|
||
expect(await applyWindowLayoutCommand(store, hosts('ssh:target-1'), rename)).toEqual({
|
||
ok: true,
|
||
result: {}
|
||
})
|
||
expect(applyEditorFileRenamesAcrossPartitions).toHaveBeenCalledWith([
|
||
{ hostId: 'local', command: { type: 'renameEditorFiles', workspace: 'wt-1', renames } },
|
||
{ hostId: 'ssh:target-1', command: { type: 'renameEditorFiles', workspace: 'wt-2', renames } }
|
||
])
|
||
expect(await applyWindowLayoutCommand(store, hosts('runtime:env-1'), rename)).toEqual({
|
||
ok: false,
|
||
code: 'host_not_owned'
|
||
})
|
||
mocks.frozen.add('wt-2')
|
||
expect(await applyWindowLayoutCommand(store, () => ['local'], rename)).toEqual({
|
||
ok: false,
|
||
code: 'wsl_move_in_progress'
|
||
})
|
||
expect(applyLayoutCommand).not.toHaveBeenCalled()
|
||
})
|
||
})
|