mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* fix(session-tabs): a windowless layout-owner server lists and pushes main's tab groups and order With the layout-owner switch on and no window (orca serve, orcad), session.tabs rows came from the snapshot hydrated from the saved session; main's layout was merged in only when a window published. A routed reorder or Move to Split reached main's layout but not session.tabs.list or the phone. Main now merges its layout into the rows it holds on each layout change and before each list, with the same merge a window publication gets. * refactor(session-tabs): keep the windowless merge with main's publisher, under the file's line cap * WIP * fix(session-tabs): every stored tab row follows main's layout, with or without a window With the layout-owner switch on, the one writer of the rows phones read now merges main's layout into every write for a workspace main owns, and main re-merges the held rows after each layout commit on any host. A CLI rename, create or close reaches phones without a window publish, and a row main's layout dropped cannot come back from an older snapshot. A write that main's layout wholly overrides stores nothing, so list calls on settled rows bump no version and push nothing. A pane's own title now counts as a manual name. This commit also untracks a local vitest config and a scratch test that the previous work-in-progress commit picked up by mistake. * test(session-tabs): a rebuild from the saved session and a CLI rename keep main's groups and names on phones * fix(session-tabs): the row writer only reorders and regroups; layout commits add main's new rows With the layout-owner switch on, the one writer of the rows phones read now takes only main's tab order and groups. It never adds or drops a row and never replaces a terminal id, so a freshly started terminal keeps its id and rows the exit filters removed stay removed. After each layout commit, on any host, the held rows take main's new rows, names and order, keep any row main's layout lacks (closes are the close path's job), and pass any added terminal through the same exit filters the window path uses. A main binding that runs nowhere no longer replaces a running terminal's id. Both read main's last projection, so neither adds a session read per write or per workspace. A CLI close still leaves its row until the dropped-leaf hook lands; its test is marked as an expected failure until then. * test(session-tabs): the row writer's order and a commit merge's keep rules, unit by unit * fix(session-tabs): a kept row keeps its group, and an unchanged commit pushes nothing A layout commit's merge placed a row it kept (a tab main closed, until the dropped-leaf hook removes it) by its row id, but groups list a terminal by its tab, so the row fell into the active group. Both the writer's reorder and the merge now place rows through one shared group-placement step keyed by the tab. A commit now pushes only when the writer stored something. The writer skips a write only when it keeps the stored occupant, publication and retirement proofs. A terminal row a commit rebinds also passes the exit filters, a main binding the host saw exit no longer replaces a running terminal, and a binding main made before its terminal connected reaches the rows when it connects. * test(session-tabs): a kept row's place and the no-push commit are separate tests * fix(session-tabs): a rebuild with a new publication stamp is still skipped; simpler commit merge The writer's skip compared publication stamps, but a rebuild from the saved session mints a new stamp from the clock each time, so every rebuild minted a version and pushed. It now stores only for a different workspace instance or new retirement proofs; an absent one keeps what is stored. The commit merge again runs the exit filters only for added terminal rows (they admit any row the held rows list, so a rebind changed nothing), and a terminal connecting now schedules the merge itself rather than a separate check, since an unchanged merge stores and pushes nothing. * refactor(session-tabs): a commit merge pushes after every store again; name the window-path exit test No reachable case was found where a commit merge's rows differ from the held rows while the writer keeps the stored ones, so the extra "only when stored" check had nothing to guard and goes. The exit-rule test now says it covers the window's publication path too. * fix(session-tabs): a window reload's new publication stamp is stored, not skipped The writer skipped a write that main's order wholly undid without comparing publication stamps, so a window reload that republished the same rows under a new stamp kept the old one. The reconciler then saw its accepted stamp missing and asked the window to republish on every sync, and a host-built stamp could stay on window rows. The skip now treats stamps as unchanged only when both are host-built, so repeated rebuilds from the saved session still mint nothing. The windowed test of a row main's layout lacks also replays the window's older snapshot.
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
// A windowless server's held session.tabs rows across main's layout commits: what a commit keeps,
|
|
// what it pushes, and which writes the row writer may skip.
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-types'
|
|
import { HEADLESS_LEAF_ID, TEST_WORKTREE_ID } from '../orca-runtime-test-fixtures.spec'
|
|
import {
|
|
SURFACE_TAB_ID,
|
|
surfaceRuntime
|
|
} from '../terminal/surface/terminal-surface-replies.test-fixture'
|
|
import {
|
|
MOVED_STRIP,
|
|
THIRD_TAB_ID,
|
|
moveAndSplitTabs,
|
|
threeTabSurfaceRuntime
|
|
} from './session-tabs-layout-moves.test-fixture'
|
|
|
|
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
|
|
|
type Runtime = ReturnType<typeof surfaceRuntime>['runtime']
|
|
|
|
const held = (runtime: Runtime) => runtime.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)
|
|
const heldStrip = (runtime: Runtime): string[][] =>
|
|
(held(runtime)?.tabGroups ?? []).map((group) => group.tabOrder)
|
|
|
|
/** The rows with whole tabs in reverse order, each tab's panes as they were. */
|
|
function tabsInReverseTabOrder<Row extends { type: string; id: string; parentTabId?: string }>(
|
|
rows: readonly Row[]
|
|
): Row[] {
|
|
const tabOf = (row: Row): string => row.parentTabId ?? row.id
|
|
return [...new Set(rows.map(tabOf))]
|
|
.toReversed()
|
|
.flatMap((tab) => rows.filter((row) => tabOf(row) === tab))
|
|
}
|
|
|
|
async function windowlessRows(runtime: Runtime): Promise<void> {
|
|
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
|
|
await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
|
|
await vi.runAllTimersAsync()
|
|
}
|
|
|
|
describe('held session.tabs rows across layout commits (switch on)', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
})
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
/** Three tabs moved and split, then the CLI closes the first: kept until L255's hook drops it. */
|
|
async function closedFirstTab(): Promise<ReturnType<typeof threeTabSurfaceRuntime>> {
|
|
const harness = threeTabSurfaceRuntime()
|
|
await windowlessRows(harness.runtime)
|
|
await moveAndSplitTabs(harness.runtime)
|
|
await vi.runAllTimersAsync()
|
|
expect(heldStrip(harness.runtime)).toEqual(MOVED_STRIP)
|
|
await harness.runtime.applyLayoutCommand({
|
|
type: 'closeTabs',
|
|
workspace: TEST_WORKTREE_ID,
|
|
tabIds: [THIRD_TAB_ID],
|
|
force: true
|
|
})
|
|
await vi.runAllTimersAsync()
|
|
return harness
|
|
}
|
|
|
|
it('a tab main closed keeps its group and place, not the active group', async () => {
|
|
const { runtime } = await closedFirstTab()
|
|
|
|
expect(heldStrip(runtime)).toEqual(MOVED_STRIP)
|
|
})
|
|
|
|
it('after it, a write that changes no row pushes nothing', async () => {
|
|
const { runtime, store, setSession } = await closedFirstTab()
|
|
let pushes = 0
|
|
runtime.onMobileSessionTabsChanged(() => {
|
|
pushes += 1
|
|
})
|
|
|
|
setSession(store.getSession())
|
|
await vi.runAllTimersAsync()
|
|
|
|
expect(pushes).toBe(0)
|
|
})
|
|
|
|
it("another workspace's commits push nothing for a workspace with a kept row", async () => {
|
|
const { runtime } = await closedFirstTab()
|
|
let pushes = 0
|
|
runtime.onMobileSessionTabsChanged((snapshot) => {
|
|
if (snapshot.worktree === TEST_WORKTREE_ID) {
|
|
pushes += 1
|
|
}
|
|
})
|
|
|
|
for (let commit = 0; commit < 3; commit += 1) {
|
|
await runtime.applyLayoutCommand({
|
|
type: 'createTerminalTab',
|
|
workspace: 'repo-1::/tmp/worktree-b',
|
|
tabId: `b-tab-${commit}`
|
|
})
|
|
await vi.runAllTimersAsync()
|
|
}
|
|
|
|
expect(pushes).toBe(0)
|
|
})
|
|
|
|
it("stores a write that names a new occupant or proof, though main's order undoes its order", async () => {
|
|
const { runtime } = threeTabSurfaceRuntime()
|
|
await windowlessRows(runtime)
|
|
const before = held(runtime)
|
|
if (!before) {
|
|
throw new Error('the list built rows')
|
|
}
|
|
const proof = {
|
|
parentTabId: 'gone',
|
|
leafId: 'gone-leaf',
|
|
ptyId: 'pty-gone',
|
|
terminal: 'term-gone'
|
|
}
|
|
|
|
runtime.storeMobileSessionSnapshot(TEST_WORKTREE_ID, {
|
|
...before,
|
|
worktreeInstanceId: 'successor',
|
|
retiredTerminalSurfaces: [proof],
|
|
tabs: tabsInReverseTabOrder(before.tabs)
|
|
})
|
|
|
|
expect(held(runtime)).toMatchObject({
|
|
worktreeInstanceId: 'successor',
|
|
retiredTerminalSurfaces: [proof]
|
|
})
|
|
// Main's order still holds.
|
|
expect(held(runtime)?.tabs).toEqual(before.tabs)
|
|
})
|
|
|
|
it('a pane main rebound takes the new terminal once it connects, with no commit', async () => {
|
|
const { runtime, bindPane } = surfaceRuntime()
|
|
await windowlessRows(runtime)
|
|
const rowId = `${SURFACE_TAB_ID}::${HEADLESS_LEAF_ID}`
|
|
const ptyOf = () => {
|
|
const row = held(runtime)?.tabs.find((tab) => tab.id === rowId)
|
|
return row?.type === 'terminal' ? row.ptyId : undefined
|
|
}
|
|
|
|
await bindPane({
|
|
worktreeId: TEST_WORKTREE_ID,
|
|
tabId: SURFACE_TAB_ID,
|
|
leafId: HEADLESS_LEAF_ID,
|
|
ptyId: 'pty-new'
|
|
})
|
|
await vi.runAllTimersAsync()
|
|
// Not running yet: the running terminal stands.
|
|
expect(ptyOf()).toBe('pty-a')
|
|
|
|
runtime.registerPty('pty-new', TEST_WORKTREE_ID, null, {
|
|
tabId: SURFACE_TAB_ID,
|
|
leafId: HEADLESS_LEAF_ID
|
|
})
|
|
await vi.runAllTimersAsync()
|
|
|
|
expect(ptyOf()).toBe('pty-new')
|
|
})
|
|
})
|