mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(terminal): stop worktree park title feedback loop
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types'
|
||||
|
||||
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const harness = vi.hoisted(() => ({
|
||||
worktreeId: 'repo::/cold-park-pre-gate',
|
||||
coverageCalls: 0,
|
||||
syncCalls: 0,
|
||||
slotRenders: 0,
|
||||
renderedParkedSets: [] as string[][]
|
||||
}))
|
||||
|
||||
vi.mock('../../store', async () => {
|
||||
const { create } = await import('zustand')
|
||||
const useAppStore = create(() => ({
|
||||
activeGroupIdByWorktree: {} as Record<string, string | undefined>,
|
||||
groupsByWorktree: {} as Record<string, TabGroup[]>,
|
||||
pendingStartupByTabId: {} as Record<string, unknown>,
|
||||
runtimeStatusByEnvironmentId: new Map<string, unknown>(),
|
||||
settings: {} as Record<string, unknown>,
|
||||
sleepingAgentSessionsByPaneKey: {} as Record<string, unknown>,
|
||||
tabsByWorktree: {} as Record<string, TerminalTab[]>,
|
||||
terminalLayoutsByTabId: {} as Record<string, unknown>,
|
||||
unifiedTabsByWorktree: {} as Record<string, Tab[]>,
|
||||
consumeSuppressedPtyExit: () => false,
|
||||
focusGroup: () => {},
|
||||
reconcileWorktreeTabModel: () => ({ renderableTabCount: 2 }),
|
||||
setActiveWorktree: () => {}
|
||||
}))
|
||||
return { useAppStore }
|
||||
})
|
||||
|
||||
vi.mock('../native-chat/use-native-chat-toggle-shortcut', () => ({
|
||||
useNativeChatToggleShortcut: () => {}
|
||||
}))
|
||||
|
||||
vi.mock('./TerminalOverlaySlot', () => ({
|
||||
TerminalOverlaySlot: () => {
|
||||
harness.slotRenders += 1
|
||||
return null
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('./terminal-parked-tab-watchers', async () => {
|
||||
const { useAppStore } = await import('../../store')
|
||||
return {
|
||||
canWatcherCoverParkedTerminalTab: () => {
|
||||
harness.coverageCalls += 1
|
||||
return harness.coverageCalls % 2 === 1
|
||||
},
|
||||
disposeParkedTerminalWatchersForWorktree: () => {},
|
||||
syncParkedTerminalTabWatchers: (args: {
|
||||
worktreeId: string
|
||||
parkedTabIds: ReadonlySet<string>
|
||||
}) => {
|
||||
harness.syncCalls += 1
|
||||
harness.renderedParkedSets.push([...args.parkedTabIds].sort())
|
||||
if (args.parkedTabIds.size === 0) {
|
||||
return
|
||||
}
|
||||
;(
|
||||
useAppStore as unknown as {
|
||||
setState: (update: (state: ParkingStoreState) => Partial<ParkingStoreState>) => void
|
||||
}
|
||||
).setState((state) => ({
|
||||
tabsByWorktree: {
|
||||
...state.tabsByWorktree,
|
||||
[args.worktreeId]: state.tabsByWorktree[args.worktreeId].map((tab) => ({
|
||||
...tab,
|
||||
title: `${tab.id}-${harness.syncCalls}`
|
||||
}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./terminal-parking-e2e-overrides', () => ({
|
||||
getTerminalParkingPolicyOverrides: () => ({ coldParkDelayMs: 0, hotRetainMs: 0 })
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/crash-breadcrumb-recorder', () => ({
|
||||
recordRendererCrashBreadcrumb: vi.fn()
|
||||
}))
|
||||
|
||||
import { useAppStore } from '../../store'
|
||||
import TerminalPaneOverlayLayer from './TerminalPaneOverlayLayer'
|
||||
|
||||
const TAB_IDS = ['tab-a', 'tab-b'] as const
|
||||
const GROUP_ID = 'group-a'
|
||||
|
||||
type ParkingStoreState = {
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
}
|
||||
|
||||
const parkingStore = useAppStore as unknown as {
|
||||
getState: () => ParkingStoreState
|
||||
setState: (partial: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
function terminalTab(id: string): TerminalTab {
|
||||
return {
|
||||
id,
|
||||
worktreeId: harness.worktreeId,
|
||||
ptyId: `${harness.worktreeId}@@session-${id}`,
|
||||
title: id,
|
||||
generation: 0
|
||||
} as TerminalTab
|
||||
}
|
||||
|
||||
function unifiedTerminalTab(id: string): Tab {
|
||||
return {
|
||||
id: `unified-${id}`,
|
||||
entityId: id,
|
||||
worktreeId: harness.worktreeId,
|
||||
groupId: GROUP_ID,
|
||||
contentType: 'terminal',
|
||||
label: id,
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
function renderProductionLayer(root: Root, coldParkTerminalPanes = true): unknown {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
let thrown: unknown = null
|
||||
try {
|
||||
act(() => {
|
||||
root.render(
|
||||
<TerminalPaneOverlayLayer
|
||||
worktreeId={harness.worktreeId}
|
||||
worktreePath="cold-park-pre-gate"
|
||||
isWorktreeActive={false}
|
||||
coldParkTerminalPanes={coldParkTerminalPanes}
|
||||
/>
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
consoleError.mockRestore()
|
||||
return thrown
|
||||
}
|
||||
|
||||
describe('TerminalPaneOverlayLayer cold-park pre-gate loop', () => {
|
||||
let container: HTMLDivElement
|
||||
let root: Root | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
harness.coverageCalls = 0
|
||||
harness.syncCalls = 0
|
||||
harness.slotRenders = 0
|
||||
harness.renderedParkedSets.length = 0
|
||||
const tabs = TAB_IDS.map(terminalTab)
|
||||
const unifiedTabs = TAB_IDS.map(unifiedTerminalTab)
|
||||
parkingStore.setState({
|
||||
activeGroupIdByWorktree: { [harness.worktreeId]: GROUP_ID },
|
||||
groupsByWorktree: {
|
||||
[harness.worktreeId]: [
|
||||
{
|
||||
id: GROUP_ID,
|
||||
worktreeId: harness.worktreeId,
|
||||
activeTabId: unifiedTabs[0].id,
|
||||
tabOrder: unifiedTabs.map((tab) => tab.id),
|
||||
recentTabIds: [unifiedTabs[0].id]
|
||||
}
|
||||
]
|
||||
},
|
||||
tabsByWorktree: { [harness.worktreeId]: tabs },
|
||||
unifiedTabsByWorktree: { [harness.worktreeId]: unifiedTabs }
|
||||
})
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
act(() => root?.unmount())
|
||||
} catch {
|
||||
// A failed initial commit leaves no mounted tree to clean up.
|
||||
}
|
||||
root = undefined
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('settles title publications without suppressing PTY rechecks', () => {
|
||||
root = createRoot(container)
|
||||
const thrown = renderProductionLayer(root)
|
||||
|
||||
expect(thrown).toBeNull()
|
||||
expect(harness.coverageCalls).toBe(1)
|
||||
expect(harness.syncCalls).toBeLessThan(10)
|
||||
expect(harness.renderedParkedSets).not.toHaveLength(0)
|
||||
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
|
||||
new Set([TAB_IDS.join(',')])
|
||||
)
|
||||
expect(harness.slotRenders).toBe(0)
|
||||
|
||||
const settledSyncCalls = harness.syncCalls
|
||||
const state = parkingStore.getState()
|
||||
act(() => {
|
||||
parkingStore.setState({
|
||||
tabsByWorktree: {
|
||||
...state.tabsByWorktree,
|
||||
[harness.worktreeId]: state.tabsByWorktree[harness.worktreeId].map((tab) =>
|
||||
tab.id === TAB_IDS[1] ? { ...tab, ptyId: `${tab.ptyId}-replacement` } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(harness.coverageCalls).toBe(2)
|
||||
expect(harness.syncCalls).toBeGreaterThan(settledSyncCalls)
|
||||
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
|
||||
new Set([TAB_IDS.join(',')])
|
||||
)
|
||||
expect(harness.slotRenders).toBe(0)
|
||||
|
||||
expect(renderProductionLayer(root, false)).toBeNull()
|
||||
expect(harness.slotRenders).toBeGreaterThan(0)
|
||||
|
||||
expect(renderProductionLayer(root)).toBeNull()
|
||||
expect(harness.renderedParkedSets.at(-1)).toEqual([...TAB_IDS])
|
||||
})
|
||||
})
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
disposeParkedTerminalWatchersForWorktree,
|
||||
syncParkedTerminalTabWatchers
|
||||
} from './terminal-parked-tab-watchers'
|
||||
import { useWorktreeParkTerminalTabs } from './use-worktree-park-terminal-tabs'
|
||||
|
||||
type TerminalOverlayTabAssignment = {
|
||||
groupId: string
|
||||
@@ -77,7 +78,7 @@ export function useTerminalTabColdParking(args: {
|
||||
}): ReadonlySet<string> {
|
||||
const {
|
||||
worktreeId,
|
||||
terminalTabs,
|
||||
terminalTabs: publishedTerminalTabs,
|
||||
assignments,
|
||||
isWorktreeActive,
|
||||
coldParkTerminalPanes,
|
||||
@@ -86,6 +87,7 @@ export function useTerminalTabColdParking(args: {
|
||||
activityTerminalPortals,
|
||||
activationDeferredMountTabIds
|
||||
} = args
|
||||
const terminalTabs = useWorktreeParkTerminalTabs(publishedTerminalTabs, coldParkTerminalPanes)
|
||||
const pendingStartupByTabId = useAppStore((state) => state.pendingStartupByTabId)
|
||||
const terminalParkingEnabled = useAppStore(
|
||||
(state) => state.settings?.terminalHiddenViewParking !== false
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useRef } from 'react'
|
||||
import type { TerminalTab } from '../../../../shared/types'
|
||||
|
||||
function haveSameTerminalParkingInputs(
|
||||
left: readonly TerminalTab[],
|
||||
right: readonly TerminalTab[]
|
||||
): boolean {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every(
|
||||
(tab, index) =>
|
||||
tab.id === right[index].id &&
|
||||
tab.ptyId === right[index].ptyId &&
|
||||
tab.pendingActivationSpawn === right[index].pendingActivationSpawn
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function useWorktreeParkTerminalTabs(
|
||||
terminalTabs: readonly TerminalTab[],
|
||||
coldParkTerminalPanes: boolean
|
||||
): readonly TerminalTab[] {
|
||||
const stableTabsRef = useRef(terminalTabs)
|
||||
// Why: a dominant worktree park hides pre-gate churn from final-verdict damping.
|
||||
if (
|
||||
!coldParkTerminalPanes ||
|
||||
!haveSameTerminalParkingInputs(stableTabsRef.current, terminalTabs)
|
||||
) {
|
||||
stableTabsRef.current = terminalTabs
|
||||
}
|
||||
return stableTabsRef.current
|
||||
}
|
||||
Reference in New Issue
Block a user