mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
test: remove low-value terminal coordination and startup cases (#40512)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5878,7 +5878,6 @@
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/output-restore/pty-connection-post-dispose-restore-termination.test.ts": 1215,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/agent-identity/pty-connection-process-exit-row-retire.test.ts": 10626,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-owner-hydration-backend-retention.test.ts": 4181,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/startup/pty-connection-queued-startup-consume.test.ts": 4181,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/reattach/pty-connection-reattach-binding.test.ts": 5240,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/input/pty-connection-remote-new-terminal-typeahead.test.ts": 1864,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/reattach/pty-connection-remote-snapshot-image-title.test.ts": 1754,
|
||||
@@ -5895,7 +5894,6 @@
|
||||
"src/shared/terminal/protocol/escape/terminal-synchronized-output-socket-splits.test.ts": 92,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/output/foreground-renderer-risk-scan-tail.test.ts": 111,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/spawn/hidden-agent-resume-deferral.test.ts": 55,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold-wiring.test.ts": 2484,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/startup/main-pane-resume-banner.test.ts": 59,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/spawn/main-pane-start-wait.test.ts": 76,
|
||||
"src/renderer/src/components/terminal-pane/pty-connection/serialization/pane-serializer-register.test.ts": 123,
|
||||
|
||||
-130
@@ -221,42 +221,6 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void {
|
||||
}
|
||||
})
|
||||
|
||||
it('holds the post-flood repaint while the user reads scrollback and runs it on return to the bottom', async () => {
|
||||
const { pane, dataCallback, getMainBufferSnapshot, resolveFirstSnapshot } =
|
||||
await startInFlightRestore()
|
||||
const { markTerminalFollowOutput, markTerminalPinnedViewport } =
|
||||
await import('@/lib/pane-manager/terminal-scroll-intent')
|
||||
pane.terminal.buffer.active.viewportY = 42
|
||||
pane.terminal.buffer.active.baseY = 100
|
||||
markTerminalPinnedViewport(pane.terminal)
|
||||
|
||||
dataCallback('f'.repeat(300 * 1024), { seq: 300 * 1024 + 64, rawLength: 300 * 1024 })
|
||||
dataCallback('g'.repeat(300 * 1024), { seq: 600 * 1024 + 64, rawLength: 300 * 1024 })
|
||||
|
||||
try {
|
||||
vi.useFakeTimers()
|
||||
resolveFirstSnapshot({ data: 'flood snapshot\r\n', cols: 100, rows: 30, seq: 64 })
|
||||
await flushAsyncTicks(20)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Quiet flood, but the user is still scrolled back: the clear-and-replay repaint must not move their viewport.
|
||||
vi.advanceTimersByTime(2_100)
|
||||
await flushAsyncTicks(20)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
vi.advanceTimersByTime(60_000)
|
||||
await flushAsyncTicks(20)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Returning to the bottom is the event that releases it — the heal is deferred, never dropped.
|
||||
pane.terminal.buffer.active.viewportY = pane.terminal.buffer.active.baseY
|
||||
markTerminalFollowOutput(pane.terminal)
|
||||
await flushAsyncTicks(20)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('sends salvaged queries immediately from an overflowing restore queue', async () => {
|
||||
const { pane, transport, dataCallback } = await startInFlightRestore()
|
||||
|
||||
@@ -280,38 +244,6 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void {
|
||||
expect(written).not.toContain('aaaa')
|
||||
expect(written).not.toContain('bbbb')
|
||||
})
|
||||
|
||||
it('keeps the hidden-pane drop sentinel arming a reveal restore (gate unchanged)', async () => {
|
||||
enableMainAuthority()
|
||||
const isVisibleRef = { current: false }
|
||||
const deps = createDeps({ isVisibleRef })
|
||||
const { pane, dataCallback } = await connectHiddenPane(deps)
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
transportOptions.onPtySpawn?.('pty-id')
|
||||
const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot)
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'hidden reveal snapshot\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: 64
|
||||
})
|
||||
|
||||
// Hidden pane: the sentinel latches restore-needed but must not fetch.
|
||||
dataCallback('', { droppedOutput: true })
|
||||
await flushAsyncTicks(8)
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
|
||||
// Reveal: the latched restore fetches exactly one snapshot.
|
||||
isVisibleRef.current = true
|
||||
const { requestTerminalBacklogRecovery } =
|
||||
await import('@/lib/pane-manager/pane-terminal-output-scheduler')
|
||||
requestTerminalBacklogRecovery(pane.terminal as never)
|
||||
await flushAsyncTicks(20)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
expect(writtenFloodData(pane)).toContain('hidden reveal snapshot')
|
||||
})
|
||||
})
|
||||
|
||||
describe('post-restore backlog reconciliation', () => {
|
||||
@@ -357,20 +289,6 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void {
|
||||
return pane.terminal.write.mock.calls.map((call) => String(call[0])).join('')
|
||||
}
|
||||
|
||||
it('drops backlog chunks the restored snapshot already covers', async () => {
|
||||
const { pane, dataCallback } = await restoreVisiblePaneToBaseline()
|
||||
|
||||
// Whole chunk at or before the baseline seq: duplicate, never written.
|
||||
dataCallback('OLD-DUPLICATE', { seq: 60, rawLength: 13 })
|
||||
await flushAsyncTicks(8)
|
||||
expect(writtenData(pane)).not.toContain('OLD-DUPLICATE')
|
||||
|
||||
// Contiguous post-baseline chunk flows through normally.
|
||||
dataCallback('NEW', { seq: 67, rawLength: 3 })
|
||||
await flushAsyncTicks(8)
|
||||
expect(writtenData(pane)).toContain('NEW')
|
||||
})
|
||||
|
||||
it('records a 2031 subscribe on a chunk the restored snapshot drops as duplicate', async () => {
|
||||
// Scan before duplicate reconciliation: replay omits mode tracking, leaving this delivery as the only subscription evidence.
|
||||
enableMainAuthority()
|
||||
@@ -389,54 +307,6 @@ export function registerPtyConnectionHiddenBacklogReconciliationTests(): void {
|
||||
expect(transport.sendInput).not.toHaveBeenCalledWith(expect.stringMatching(/\?997/))
|
||||
})
|
||||
|
||||
it('slices a partial overlap when raw and clean lengths match', async () => {
|
||||
const { pane, dataCallback } = await restoreVisiblePaneToBaseline()
|
||||
|
||||
// start seq 61 < baseline 64 < end seq 67 — only the last 3 chars are new.
|
||||
dataCallback('ABCDEF', { seq: 67, rawLength: 6 })
|
||||
await flushAsyncTicks(8)
|
||||
|
||||
const written = writtenData(pane)
|
||||
expect(written).toContain('DEF')
|
||||
expect(written).not.toContain('ABC')
|
||||
})
|
||||
|
||||
it('forces a fresh snapshot for an overlap whose offsets cannot be mapped', async () => {
|
||||
const { pane, dataCallback, getMainBufferSnapshot } = await restoreVisiblePaneToBaseline()
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'second snapshot\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: 80
|
||||
})
|
||||
|
||||
// rawLength (6) !== data.length (4): OSC stripping makes the slice offset unmappable — restore from a fresh snapshot instead.
|
||||
dataCallback('ABCD', { seq: 67, rawLength: 6 })
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(writtenData(pane)).not.toContain('ABCD')
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(2)
|
||||
expect(writtenData(pane)).toContain('second snapshot')
|
||||
})
|
||||
|
||||
it('detects a seq gap after restore and forces another restore', async () => {
|
||||
const { pane, dataCallback, getMainBufferSnapshot } = await restoreVisiblePaneToBaseline()
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'gap-heal snapshot\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: 120
|
||||
})
|
||||
|
||||
// Why: a chunk starting past the continuity point means main trimmed bytes after the overflow marker — only the model snapshot can heal the gap.
|
||||
dataCallback('AFTER-GAP', { seq: 96, rawLength: 9 })
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(writtenData(pane)).not.toContain('AFTER-GAP')
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(2)
|
||||
expect(writtenData(pane)).toContain('gap-heal snapshot')
|
||||
})
|
||||
|
||||
it('writes genuinely-new live output whose seq sits below an empty-backlog baseline', async () => {
|
||||
// E2E twin: a live chunk's seq sits below main's cumulative baseline, but with an empty pending queue main can never re-deliver — so it must write, not drop.
|
||||
enableMainAuthority()
|
||||
|
||||
-208
@@ -1,6 +1,5 @@
|
||||
import { bindPtyConnectionTestMocks } from '../test-fixtures/pty-connection-mocks.spec'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RESET_AFTER_BYTE_GAP } from '../../../../../../shared/terminal/protocol/input-modes/terminal-mode-reset-profiles'
|
||||
import { flushAsyncTicks, createDeferred } from '../test-fixtures/pty-connection-test-async'
|
||||
import {
|
||||
createMockTransport,
|
||||
@@ -111,28 +110,6 @@ export function registerPtyConnectionHiddenDeliveryGateTests(): void {
|
||||
return { transport, pane, dataCallback: capturedDataCallback.current!, binding }
|
||||
}
|
||||
|
||||
// Only the restore-needed marker proves a byte gap; reset emulator state here with the cross-chunk parser (STA-4042).
|
||||
it('closes the emulator state gap when a drop is announced', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { pane, dataCallback } = await connectHiddenPane(deps)
|
||||
dataCallback('hidden output\r\n', { seq: 16, rawLength: 16 })
|
||||
pane.terminal.write.mockClear()
|
||||
|
||||
const { _dispatchPtyModelRestoreNeededForTest } =
|
||||
await import('../../pty-transport/delivery/pty-model-restore-channel')
|
||||
_dispatchPtyModelRestoreNeededForTest({
|
||||
id: 'pty-id',
|
||||
reason: 'hidden-drop',
|
||||
markerSeq: 64
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const gapReset = written.find((data) => data === RESET_AFTER_BYTE_GAP)
|
||||
expect(gapReset).toBeDefined()
|
||||
})
|
||||
|
||||
it('marks the PTY hidden on hidden output and clears it before requesting restore on reveal', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
@@ -313,90 +290,6 @@ export function registerPtyConnectionHiddenDeliveryGateTests(): void {
|
||||
expect(setHiddenRendererPty).toHaveBeenLastCalledWith('pty-id', true)
|
||||
})
|
||||
|
||||
it('marks hidden codex panes immediately — no startup renderer-query window remains', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const { transport, dataCallback } = await connectHiddenPane(deps)
|
||||
const setHiddenRendererPty = getSetHiddenRendererPtyMock()
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
transportOptions.onPtySpawn?.('pty-id')
|
||||
const factsHandler =
|
||||
await import('../../pty-transport/output/terminal-side-effect-facts-handler')
|
||||
|
||||
// Hidden Codex startups use the ordinary delivery gate; main owns startup query replies.
|
||||
dataCallback('startup probe output\r\n')
|
||||
expect(setHiddenRendererPty).toHaveBeenCalledWith('pty-id', true)
|
||||
|
||||
factsHandler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-id',
|
||||
seq: 8,
|
||||
facts: [{ kind: '2031-subscribe' }]
|
||||
})
|
||||
expect(transport.sendInput).not.toHaveBeenCalledWith(expect.stringMatching(/\?997/))
|
||||
})
|
||||
|
||||
it('never answers a 2031-subscribe fact, hidden or visible', async () => {
|
||||
// Regression pin (#9993): a subscribe arms future notifications, it is not a query.
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { transport } = await connectHiddenPane(deps)
|
||||
// Simulate spawn completion so the pane registers its fact consumer (mock transport never calls onPtySpawn).
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
transportOptions.onPtySpawn?.('pty-id')
|
||||
const factsHandler =
|
||||
await import('../../pty-transport/output/terminal-side-effect-facts-handler')
|
||||
|
||||
factsHandler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-id',
|
||||
seq: 12,
|
||||
facts: [{ kind: '2031-subscribe' }]
|
||||
})
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
factsHandler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-id',
|
||||
seq: 24,
|
||||
facts: [{ kind: '2031-subscribe' }]
|
||||
})
|
||||
|
||||
expect(transport.sendInput).not.toHaveBeenCalledWith(expect.stringMatching(/\?997/))
|
||||
expect(transport.sendInputImmediate).not.toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\?997/)
|
||||
)
|
||||
})
|
||||
|
||||
it('registers the fact-observed 2031 subscription for later theme flips', async () => {
|
||||
enableMainAuthority()
|
||||
const recordPaneMode2031Subscription = vi.fn()
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
recordPaneMode2031Subscription
|
||||
})
|
||||
const { transport } = await connectHiddenPane(deps)
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
transportOptions.onPtySpawn?.('pty-id')
|
||||
const factsHandler =
|
||||
await import('../../pty-transport/output/terminal-side-effect-facts-handler')
|
||||
|
||||
factsHandler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-id',
|
||||
seq: 12,
|
||||
facts: [{ kind: '2031-subscribe' }]
|
||||
})
|
||||
|
||||
// Why: without the registry write, maybePushMode2031Flip won't push CSI 997 after a theme change, so the TUI keeps a stale theme.
|
||||
expect(recordPaneMode2031Subscription).toHaveBeenCalledWith(1, 'dark')
|
||||
expect(transport.sendInput).not.toHaveBeenCalledWith(expect.stringMatching(/\?997/))
|
||||
})
|
||||
|
||||
it('retires the fact-registered subscription when the TUI withdraws it', async () => {
|
||||
// Gated panes never receive withdrawal bytes; only this fact can retire a subscription before theme flips send replies to the replacement shell.
|
||||
enableMainAuthority()
|
||||
@@ -435,107 +328,6 @@ export function registerPtyConnectionHiddenDeliveryGateTests(): void {
|
||||
expect(paneLastThemeModeRef.current.get(1)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves the chunk scanner silent on a gate-managed PTY', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { transport, dataCallback } = await connectHiddenPane(deps)
|
||||
const before = transport.sendInput.mock.calls.length
|
||||
|
||||
dataCallback('\x1b[?2031h')
|
||||
|
||||
const replies = transport.sendInput.mock.calls
|
||||
.slice(before)
|
||||
.flat()
|
||||
.filter((arg) => String(arg).includes('997'))
|
||||
expect(replies).toEqual([])
|
||||
})
|
||||
|
||||
it('declares hidden-at-spawn on connect for hidden panes', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { transport } = await connectHiddenPane(deps)
|
||||
// Why: mark the PTY hidden before its first byte, closing the spawn-time DA1-loss window where neither side replied.
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ initiallyHidden: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps visible spawns undeclared (visible spawn unchanged)', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps()
|
||||
const { transport } = await connectHiddenPane(deps)
|
||||
expect(transport.connect.mock.calls[0]![0]).not.toHaveProperty('initiallyHidden')
|
||||
})
|
||||
|
||||
it('declares hidden-at-spawn for hidden codex panes too', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const { transport } = await connectHiddenPane(deps)
|
||||
// Codex startup is main-owned from byte zero; no temporary renderer reply window remains (pty.test.ts DA1-from-model).
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ initiallyHidden: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('does not gate or fact-reply when the hidden-delivery kill switch is off', async () => {
|
||||
enableMainAuthority()
|
||||
mockStoreState.settings = {
|
||||
...mockStoreState.settings,
|
||||
terminalHiddenDeliveryGate: false
|
||||
} as StoreState['settings']
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { transport, dataCallback } = await connectHiddenPane(deps)
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
transportOptions.onPtySpawn?.('pty-id')
|
||||
const setHiddenRendererPty = getSetHiddenRendererPtyMock()
|
||||
|
||||
dataCallback('hidden output\r\n')
|
||||
expect(setHiddenRendererPty).not.toHaveBeenCalled()
|
||||
|
||||
// Why: gate off keeps the byte-scan responder authoritative — the fact must not produce a second reply.
|
||||
const factsHandler =
|
||||
await import('../../pty-transport/output/terminal-side-effect-facts-handler')
|
||||
factsHandler._dispatchTerminalSideEffectBatchForTest({
|
||||
ptyId: 'pty-id',
|
||||
seq: 12,
|
||||
facts: [{ kind: '2031-subscribe' }]
|
||||
})
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears a marked-hidden PTY on dispose so a remount is never gated', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const { dataCallback, binding } = await connectHiddenPane(deps)
|
||||
const setHiddenRendererPty = getSetHiddenRendererPtyMock()
|
||||
|
||||
dataCallback('hidden output\r\n')
|
||||
expect(setHiddenRendererPty).toHaveBeenLastCalledWith('pty-id', true)
|
||||
|
||||
binding.dispose()
|
||||
expect(setHiddenRendererPty).toHaveBeenLastCalledWith('pty-id', false)
|
||||
})
|
||||
|
||||
it('never treats a live chunk that strips to empty as a restore marker', async () => {
|
||||
// Why: an all-OSC-9999 chunk arrives as '' after stripping; only pty:modelRestoreNeeded may trigger a restore, not the empty byte path.
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: true } })
|
||||
const { dataCallback } = await connectHiddenPane(deps)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
|
||||
dataCallback('', { seq: 32, rawLength: 24 })
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('gates marker re-arms during an in-flight foreground restore and repaints once after', async () => {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: true } })
|
||||
|
||||
-55
@@ -356,61 +356,6 @@ export function registerPtyConnectionHiddenOutputRestoreTests(): void {
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('writes ordinary hidden output live instead of proactively restoring a snapshot', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks4 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const hidden = 'small-hidden-output\r\n'
|
||||
const live = 'visible-after-hidden\r\n'
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: `snapshot-with-${hidden}`,
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: hidden.length + live.length
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks4)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(hidden, expect.any(Function))
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).not.toHaveBeenCalled()
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(hidden)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(live, expect.any(Function))
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('pauses capable paired output while hidden and restores exactly on reveal', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks5 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
|
||||
-56
@@ -53,62 +53,6 @@ export function registerPtyConnectionHiddenSnapshotLiveOverlapTests(): void {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('writes foreground chunks that are newer than the restored main snapshot', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const hidden = 'x'.repeat(2 * 1024 * 1024 + 1)
|
||||
const live = 'new-live-output\r\n'
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'snapshot-before-live\r\n',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: hidden.length
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
'snapshot-before-live\r\n',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(live, expect.any(Function))
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('writes only the live chunk suffix not covered by the restored main snapshot', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
|
||||
-123
@@ -2,7 +2,6 @@ import { bindPtyConnectionTestMocks } from '../test-fixtures/pty-connection-mock
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushAsyncTicks, createDeferred } from '../test-fixtures/pty-connection-test-async'
|
||||
import {
|
||||
LEAF_1,
|
||||
captureCallbackTerminalWrites,
|
||||
createMockTransport,
|
||||
createPane,
|
||||
@@ -170,63 +169,6 @@ export function registerPtyConnectionHiddenSnapshotResizeSignalsTests(): void {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('does not signal SIGWINCH after hidden-backlog snapshot replay when dimensions are unchanged', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const signalPty = window.api.pty.signal as unknown as ReturnType<typeof vi.fn>
|
||||
const hidden = 'x'.repeat(2 * 1024 * 1024 + 1)
|
||||
const live = 'visible-after\r\n'
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'snapshot-state\r\n',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: hidden.length + live.length
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks2)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
transport.resize.mockClear()
|
||||
signalPty.mockClear()
|
||||
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('snapshot-state\r\n', expect.any(Function))
|
||||
expect(transport.resize).not.toHaveBeenCalledWith(120, 40)
|
||||
expect(signalPty).not.toHaveBeenCalledWith('pty-id', 'SIGWINCH')
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('writes a background-origin alternate-screen repaint that arrives after the reveal restore', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks3 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
@@ -400,70 +342,5 @@ export function registerPtyConnectionHiddenSnapshotResizeSignalsTests(): void {
|
||||
expect(transport.resize).not.toHaveBeenCalledWith(132, 40)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('restores a dropped daemon alt frame from the model when the fit lands back on the capture grid', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks5 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('tab-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) =>
|
||||
sessionId
|
||||
? {
|
||||
id: sessionId,
|
||||
snapshot: 'PREFIX-SCROLLBACK' + 'ALT-FRAME-BODY',
|
||||
snapshotPrefixAnsi: 'PREFIX-SCROLLBACK',
|
||||
snapshotFrameAnsi: 'ALT-FRAME-BODY',
|
||||
snapshotFrameRestoreAnsi: 'RESTORE-LIVE-STATE',
|
||||
snapshotCols: 200,
|
||||
snapshotRows: 50,
|
||||
isAlternateScreen: true
|
||||
}
|
||||
: null
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'tab-pty' }] }
|
||||
}
|
||||
const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot)
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: '\x1b[?1049hMODEL-FRAME',
|
||||
scrollbackAnsi: '',
|
||||
frameRestoreAnsi: 'MODEL-RESTORE',
|
||||
cols: 200,
|
||||
rows: 50,
|
||||
seq: 10,
|
||||
alternateScreen: true
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
pane.terminal.cols = 200
|
||||
pane.terminal.rows = 50
|
||||
const writtenText = (): string => pane.terminal.write.mock.calls.map((c) => c[0]).join('')
|
||||
// The replay measures a transient narrower grid; the fit then lands back on the capture grid.
|
||||
pane.fitAddon.proposeDimensions = vi.fn(() =>
|
||||
writtenText().includes('RESTORE-LIVE-STATE')
|
||||
? { cols: 200, rows: 50 }
|
||||
: { cols: 120, rows: 40 }
|
||||
)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' }
|
||||
})
|
||||
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks5)
|
||||
)
|
||||
await flushAsyncTicks(40)
|
||||
|
||||
expect(writtenText()).not.toContain('ALT-FRAME-BODY')
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledWith('tab-pty', expect.anything())
|
||||
await vi.waitFor(() => expect(writtenText()).toContain('MODEL-FRAME'))
|
||||
disposable.dispose()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+1
-332
@@ -1,22 +1,17 @@
|
||||
import { bindPtyConnectionTestMocks } from '../test-fixtures/pty-connection-mocks.spec'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
POST_REPLAY_LIVE_AGENT_REATTACH_RESET,
|
||||
POST_REPLAY_REATTACH_RESET,
|
||||
RESET_TERMINAL_CURSOR_STYLE
|
||||
} from '../../../../../../shared/terminal/protocol/input-modes/terminal-mode-reset-profiles'
|
||||
import { replayEpilogue } from '../test-fixtures/pty-connection-test-replay-epilogue'
|
||||
import { flushAsyncTicks } from '../test-fixtures/pty-connection-test-async'
|
||||
import {
|
||||
NORMAL_BUFFER_PROLOGUE,
|
||||
ANSI_POSITIONED_CURSOR_AGENT_REATTACH_SCREEN
|
||||
} from '../test-fixtures/pty-connection-test-constants'
|
||||
import { ANSI_POSITIONED_CURSOR_AGENT_REATTACH_SCREEN } from '../test-fixtures/pty-connection-test-constants'
|
||||
import {
|
||||
withMockedDocumentActiveElement,
|
||||
configureTerminalFocusMode
|
||||
} from '../test-fixtures/pty-connection-test-dom'
|
||||
import {
|
||||
terminalWriteText,
|
||||
createMockTransport,
|
||||
createPane,
|
||||
captureCallbackTerminalWrites,
|
||||
@@ -83,68 +78,6 @@ export function registerPtyConnectionReplayPayloadHandlingTests(): void {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('preserves a scrolled-up viewport after hidden-backlog snapshot replay', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const hidden = 'x'.repeat(2 * 1024 * 1024 + 1)
|
||||
const live = 'visible-after\r\n'
|
||||
getMainBufferSnapshot.mockResolvedValue({
|
||||
data: 'snapshot-state\r\n',
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
seq: hidden.length + live.length
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
pane.terminal.buffer.active.viewportY = 42
|
||||
pane.terminal.buffer.active.baseY = 100
|
||||
pane.terminal.write.mockImplementation((data: string | Uint8Array, callback?: () => void) => {
|
||||
data = terminalWriteText(data)
|
||||
if (data.includes('snapshot-state')) {
|
||||
pane.terminal.buffer.active.viewportY = 0
|
||||
}
|
||||
callback?.()
|
||||
})
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('snapshot-state\r\n', expect.any(Function))
|
||||
expect(pane.terminal.scrollToLine).toHaveBeenCalledWith(42)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('cancels a delayed snapshot scroll restore when the pane binding is disposed', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
@@ -276,189 +209,6 @@ export function registerPtyConnectionReplayPayloadHandlingTests(): void {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('rebuilds WebGL after remote buffered replay arrives on an already-open pane', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks4 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
enableActiveRuntimeEnvironment()
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return { id: 'remote:env-1@@terminal-1', replay: '' }
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const refresh = vi.fn()
|
||||
const terminal = pane.terminal as typeof pane.terminal & {
|
||||
_core?: { refresh: typeof refresh }
|
||||
}
|
||||
terminal._core = { refresh }
|
||||
terminal.write = vi.fn((_data: string | Uint8Array, callback?: () => void) => {
|
||||
callback?.()
|
||||
})
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks4)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.('remote prompt\r\n$ ')
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('remote prompt\r\n$ ', expect.any(Function))
|
||||
expect(refresh).toHaveBeenCalledWith(0, 39, true)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('preserves live agent modes when queued replay data carries the Cursor Agent screen', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
enableActiveRuntimeEnvironment()
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return { id: 'remote:env-1@@terminal-1', replay: '' }
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
setReattachPaneTitle('renamed shell')
|
||||
|
||||
const pane = createPane(1)
|
||||
const textarea = {} as HTMLTextAreaElement
|
||||
configureTerminalFocusMode(pane, textarea)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = await withMockedDocumentActiveElement(textarea, async () => {
|
||||
const agentTitleMocks5 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
|
||||
const connection = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks5)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.(ANSI_POSITIONED_CURSOR_AGENT_REATTACH_SCREEN)
|
||||
await flushAsyncTicks(12)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
replayEpilogue(POST_REPLAY_LIVE_AGENT_REATTACH_RESET),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(transport.sendInput).toHaveBeenCalledWith('\x1b[I', 'query-reply')
|
||||
return connection
|
||||
})
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
// Styled headers need CSI stripping; raw-byte lastIndexOf would silently miss this case.
|
||||
it('still detects the Cursor Agent screen when CSI styling splits the header and the marker', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
enableActiveRuntimeEnvironment()
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return { id: 'remote:env-1@@terminal-1', replay: '' }
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
setReattachPaneTitle('renamed shell')
|
||||
|
||||
const pane = createPane(1)
|
||||
const textarea = {} as HTMLTextAreaElement
|
||||
configureTerminalFocusMode(pane, textarea)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = await withMockedDocumentActiveElement(textarea, async () => {
|
||||
const agentTitleMocks6 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
|
||||
const connection = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks6)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.(
|
||||
'\x1b[4;3HCursor \x1b[1mAgent\x1b[0m\x1b[9;3H→\x1b[0m Plan, search, build anything'
|
||||
)
|
||||
await flushAsyncTicks(12)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
replayEpilogue(POST_REPLAY_LIVE_AGENT_REATTACH_RESET),
|
||||
expect.any(Function)
|
||||
)
|
||||
return connection
|
||||
})
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
// Bound the scan to current-screen bytes; headers buried in old scrollback must not classify the live pane.
|
||||
it('ignores a Cursor Agent screen buried beyond the payload scan tail limit', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
enableActiveRuntimeEnvironment()
|
||||
const transport = createMockTransport('remote:env-1@@terminal-1')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return { id: 'remote:env-1@@terminal-1', replay: '' }
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
setReattachPaneTitle('renamed shell')
|
||||
|
||||
const pane = createPane(1)
|
||||
const textarea = {} as HTMLTextAreaElement
|
||||
configureTerminalFocusMode(pane, textarea)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = await withMockedDocumentActiveElement(textarea, async () => {
|
||||
const agentTitleMocks7 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
|
||||
const connection = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks7)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.(
|
||||
`${ANSI_POSITIONED_CURSOR_AGENT_REATTACH_SCREEN}${'shell scrollback\r\n'.repeat(20_000)}`
|
||||
)
|
||||
await flushAsyncTicks(12)
|
||||
|
||||
expect(transport.sendInput.mock.calls.map(([data]) => data)).not.toContain('\x1b[I')
|
||||
return connection
|
||||
})
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('downgrades a scrollback-only Cursor Agent signal when the parsed viewport shows a shell', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
enableActiveRuntimeEnvironment()
|
||||
@@ -515,87 +265,6 @@ export function registerPtyConnectionReplayPayloadHandlingTests(): void {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('does not clear restored scrollback when eager metadata replay opts out', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks9 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
pane.terminal.write = vi.fn((_data: string | Uint8Array, callback?: () => void) => {
|
||||
callback?.()
|
||||
})
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks9)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.('\x1b]0;Restored title\x07', { clearBeforeReplay: false })
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
NORMAL_BUFFER_PROLOGUE,
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
'\x1b]0;Restored title\x07',
|
||||
expect.any(Function)
|
||||
)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('does not write a clear or reset for empty eager metadata replay', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks10 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedReplayCallback: {
|
||||
current: ((data: string, meta?: { clearBeforeReplay?: boolean }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedReplayCallback.current = callbacks.onReplayData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
pane.terminal.write = vi.fn((_data: string | Uint8Array, callback?: () => void) => {
|
||||
callback?.()
|
||||
})
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks10)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedReplayCallback.current?.('', { clearBeforeReplay: false })
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalled()
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('coalesces remote replay payloads that overlap before parsing starts', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks11 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
|
||||
-369
@@ -155,190 +155,6 @@ export function registerPtyConnectionStalledHiddenRestoreTests(): void {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
// A dropped span can swallow bold reset; abandonment must ground the pen when no snapshot will restore it (STA-4042).
|
||||
it('clears the SGR pen before draining abandoned foreground chunks', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
const snapshot = createDeferred<{ data: string; cols: number; rows: number; seq: number }>()
|
||||
getMainBufferSnapshot.mockReturnValue(snapshot.promise)
|
||||
// The dropped span is where `ESC[22m` would have been; the pen is left bold.
|
||||
const hidden = '\x1b[1mbold-run-opened-while-hidden\r\n'
|
||||
const live = 'live-after-reveal\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks2)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
// Let the foreground deadline expire so the restore is abandoned.
|
||||
vi.advanceTimersByTime(750)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const resetIndex = written.indexOf(ABANDONED_RESTORE_GAP)
|
||||
const liveIndex = written.findIndex((data) => data.includes('live-after-reveal'))
|
||||
expect(resetIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(resetIndex).toBeLessThan(liveIndex)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('grounds byte-gap state before a remote restore re-arms', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks3 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const remotePtyId = 'remote:env-1@@terminal-rearm'
|
||||
const transport = createMockTransport(remotePtyId)
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return remotePtyId
|
||||
}
|
||||
)
|
||||
const snapshot = createDeferred<{ data: string; cols: number; rows: number; seq: number }>()
|
||||
transport.serializeBuffer = vi.fn().mockReturnValue(snapshot.promise)
|
||||
transportFactoryQueue.push(transport)
|
||||
const hidden = 'x'.repeat(2 * 1024 * 1024 + 1)
|
||||
const live = 'remote-live-after-rearm\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
createManager(1) as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks3)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
vi.advanceTimersByTime(750)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const resetIndex = written.indexOf(ABANDONED_RESTORE_GAP)
|
||||
const liveIndex = written.indexOf(live)
|
||||
expect(resetIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveIndex).toBeGreaterThan(resetIndex)
|
||||
// Remote re-arm already grounds the gap; abandonment must not do it again.
|
||||
expect(written.filter((data) => data === ABANDONED_RESTORE_GAP)).toHaveLength(1)
|
||||
expect(written.join('')).not.toContain('main recovery was unavailable')
|
||||
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('falls back after repeated null hidden restore retries and drains blocked foreground', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks4 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockResolvedValue(null)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const live = 'visible-after-null-retries\r\n'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks4)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(live, expect.any(Function))
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
}
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const warningIndex = written.findIndex((data) =>
|
||||
data.includes('main recovery was unavailable')
|
||||
)
|
||||
const liveIndex = written.indexOf(live)
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(4)
|
||||
expect(warningIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveIndex).toBeGreaterThan(warningIndex)
|
||||
// Ground the abandoned gap once before draining blocked foreground bytes.
|
||||
expect(written.filter((data) => data === ABANDONED_RESTORE_GAP)).toHaveLength(1)
|
||||
expect(written.indexOf(ABANDONED_RESTORE_GAP)).toBeLessThan(liveIndex)
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('drops pending foreground overflow when a stalled hidden restore falls back', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks5 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
@@ -398,191 +214,6 @@ export function registerPtyConnectionStalledHiddenRestoreTests(): void {
|
||||
disposable.dispose()
|
||||
})
|
||||
|
||||
it('coalesces tiny pending foreground chunks when stalled hidden restore falls back', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks6 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockReturnValue(
|
||||
createDeferred<{ data: string; cols: number; rows: number; seq: number }>().promise
|
||||
)
|
||||
const hidden = 'hidden-codex-output\r\n'
|
||||
const chunkCount = 2_000
|
||||
const liveChunk = 'x'
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
startup: { command: 'codex' }
|
||||
})
|
||||
const disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks6)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
for (let index = 0; index < chunkCount; index += 1) {
|
||||
capturedDataCallback.current?.(liveChunk, {
|
||||
seq: hidden.length + index + 1,
|
||||
rawLength: liveChunk.length
|
||||
})
|
||||
}
|
||||
await flushAsyncTicks(4)
|
||||
|
||||
vi.advanceTimersByTime(750)
|
||||
vi.advanceTimersByTime(0)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
const written = pane.terminal.write.mock.calls.map(([data]) => data as string)
|
||||
const warningIndex = written.findIndex((data) =>
|
||||
data.includes('main recovery was unavailable')
|
||||
)
|
||||
const combinedLive = liveChunk.repeat(chunkCount)
|
||||
const liveWrites = written.filter((data) => data === combinedLive)
|
||||
expect(warningIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(liveWrites).toHaveLength(1)
|
||||
expect(written.indexOf(combinedLive)).toBeGreaterThan(warningIndex)
|
||||
|
||||
disposable.dispose()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps foreground output when hidden-backlog snapshot recovery is unavailable', async () => {
|
||||
const pendingTimeouts: {
|
||||
canceled: boolean
|
||||
delay: number
|
||||
fn: () => void
|
||||
id: number
|
||||
}[] = []
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const originalClearTimeout = globalThis.clearTimeout
|
||||
let nextTimeoutId = 1
|
||||
globalThis.setTimeout = vi.fn((fn: () => void, delay?: number) => {
|
||||
const timeout = {
|
||||
canceled: false,
|
||||
delay: typeof delay === 'number' ? delay : 0,
|
||||
fn,
|
||||
id: nextTimeoutId++
|
||||
}
|
||||
pendingTimeouts.push(timeout)
|
||||
return timeout.id as unknown as ReturnType<typeof setTimeout>
|
||||
}) as unknown as typeof setTimeout
|
||||
globalThis.clearTimeout = vi.fn((id: ReturnType<typeof setTimeout>) => {
|
||||
const numericId = id as unknown as number
|
||||
const timeout = pendingTimeouts.find((candidate) => candidate.id === numericId)
|
||||
if (timeout) {
|
||||
timeout.canceled = true
|
||||
}
|
||||
}) as unknown as typeof clearTimeout
|
||||
|
||||
const runNextTimeoutWithDelay = async (delay: number): Promise<void> => {
|
||||
const index = pendingTimeouts.findIndex(
|
||||
(timeout) => !timeout.canceled && timeout.delay === delay
|
||||
)
|
||||
expect(index).toBeGreaterThanOrEqual(0)
|
||||
const [timeout] = pendingTimeouts.splice(index, 1)
|
||||
timeout.fn()
|
||||
await flushAsyncTicks(20)
|
||||
}
|
||||
const drainTimeoutsWithDelay = async (delay: number): Promise<void> => {
|
||||
while (pendingTimeouts.some((timeout) => !timeout.canceled && timeout.delay === delay)) {
|
||||
await runNextTimeoutWithDelay(delay)
|
||||
}
|
||||
}
|
||||
|
||||
let disposable: { dispose: () => void } | null = null
|
||||
try {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks7 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('pty-id')
|
||||
const capturedDataCallback: {
|
||||
current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null
|
||||
} = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>
|
||||
getMainBufferSnapshot.mockResolvedValue(null)
|
||||
|
||||
const hidden = 'x'.repeat(2 * 1024 * 1024 + 1)
|
||||
const live = 'foreground-after-unavailable\r\n'
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false }
|
||||
})
|
||||
disposable = connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks7)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.(hidden, { seq: hidden.length, rawLength: hidden.length })
|
||||
;(deps.isVisibleRef as { current: boolean }).current = true
|
||||
capturedDataCallback.current?.(live, {
|
||||
seq: hidden.length + live.length,
|
||||
rawLength: live.length
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining(live),
|
||||
expect.any(Function)
|
||||
)
|
||||
|
||||
await runNextTimeoutWithDelay(50)
|
||||
await runNextTimeoutWithDelay(50)
|
||||
await runNextTimeoutWithDelay(50)
|
||||
await drainTimeoutsWithDelay(0)
|
||||
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(4)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Orca skipped hidden terminal output because main recovery was unavailable.'
|
||||
),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining(live),
|
||||
expect.any(Function)
|
||||
)
|
||||
} finally {
|
||||
disposable?.dispose()
|
||||
globalThis.setTimeout = originalSetTimeout
|
||||
globalThis.clearTimeout = originalClearTimeout
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a newer same-PTY hidden restore after a timed-out snapshot resolves late', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks8 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
|
||||
-390
@@ -26,150 +26,6 @@ import {
|
||||
} from '../test-fixtures/pty-connection-test-environment'
|
||||
import { mockPanePtyArgs } from '../test-fixtures/pty-connection-test-connect-mock-pane'
|
||||
|
||||
export function registerPtyConnectionForegroundBulkAndInteractiveOutputTests(): void {
|
||||
let mockStoreState: StoreState
|
||||
let transportFactoryQueue: MockTransport[] = []
|
||||
let createdTransportOptions: Record<string, unknown>[] = []
|
||||
let storeSubscribers: ((state: StoreState) => void)[] = []
|
||||
|
||||
function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
return buildPaneConnectionDeps(() => mockStoreState, overrides)
|
||||
}
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
bindPtyConnectionTestMocks({
|
||||
getState: () => mockStoreState,
|
||||
getTransportQueue: () => transportFactoryQueue,
|
||||
getTransportOptions: () => createdTransportOptions,
|
||||
getStoreSubscribers: () => storeSubscribers,
|
||||
setStoreSubscribers: (subscribers) => {
|
||||
storeSubscribers = subscribers
|
||||
}
|
||||
})
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('queues visible bulk output off the synchronous xterm write path', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const pane = createPane(1)
|
||||
const transport = createMockTransport('pty-1')
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-1'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks()
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
|
||||
vi.useFakeTimers()
|
||||
capturedDataCallback.current?.('x'.repeat(16 * 1024))
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalled()
|
||||
vi.advanceTimersByTime(0)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('x'.repeat(16 * 1024), expect.any(Function))
|
||||
})
|
||||
|
||||
it('keeps ANSI redraws after terminal input on the immediate xterm write path', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const pane = createPane(1)
|
||||
const transport = createMockTransport('pty-1')
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-1'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks2)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks()
|
||||
const onDataMock = pane.terminal.onData as unknown as {
|
||||
mock: { calls: [[(data: string) => void] | []] }
|
||||
}
|
||||
const terminalInputHandler = onDataMock.mock.calls[0]?.[0]
|
||||
expect(terminalInputHandler).toBeTypeOf('function')
|
||||
terminalInputHandler?.('a')
|
||||
|
||||
const redraw = `\x1b[2J\x1b[H${'codex composer redraw '.repeat(200)}`
|
||||
capturedDataCallback.current?.(redraw)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(redraw, expect.any(Function))
|
||||
})
|
||||
|
||||
it('keeps large ANSI redraws after terminal input on the immediate xterm write path', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks3 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const pane = createPane(1)
|
||||
const transport = createMockTransport('pty-1')
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-1'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks3)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks()
|
||||
const onDataMock = pane.terminal.onData as unknown as {
|
||||
mock: { calls: [[(data: string) => void] | []] }
|
||||
}
|
||||
const terminalInputHandler = onDataMock.mock.calls[0]?.[0]
|
||||
expect(terminalInputHandler).toBeTypeOf('function')
|
||||
terminalInputHandler?.('a')
|
||||
|
||||
const redraw = `\x1b[2J\x1b[H${'codex large composer redraw '.repeat(1_200)}`
|
||||
expect(redraw.length).toBeGreaterThan(16 * 1024)
|
||||
expect(redraw.length).toBeLessThan(128 * 1024)
|
||||
capturedDataCallback.current?.(redraw)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(redraw, expect.any(Function))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function registerPtyConnectionForegroundFrameSchedulingTests(): void {
|
||||
let mockStoreState: StoreState
|
||||
let transportFactoryQueue: MockTransport[] = []
|
||||
@@ -202,194 +58,6 @@ export function registerPtyConnectionForegroundFrameSchedulingTests(): void {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('keeps large ANSI redraws after captured shortcut input on the immediate path', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks6 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const pane = createPane(1)
|
||||
const transport = createMockTransport('pty-1')
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-1'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const binding = connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks6)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks()
|
||||
binding.markShortcutTerminalInputSent()
|
||||
|
||||
const redraw = `\x1b[2J\x1b[H${'pi composer redraw '.repeat(200)}`
|
||||
expect(redraw.length).toBeGreaterThan(2_048)
|
||||
capturedDataCallback.current?.(redraw)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(redraw, expect.any(Function))
|
||||
})
|
||||
|
||||
it('does not let OpenTUI-style small ANSI redraw bursts monopolize foreground writes', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks7 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const pane = createPane(1)
|
||||
const transport = createMockTransport('pty-1')
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-1'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks7)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks()
|
||||
expect(capturedDataCallback.current).not.toBeNull()
|
||||
|
||||
vi.useFakeTimers()
|
||||
|
||||
const frames = Array.from({ length: 270 }, (_, index) =>
|
||||
[
|
||||
'\x1b[?2026h',
|
||||
'\x1b[?25l',
|
||||
`\x1b[2;3H\x1b[38;2;255;138;0m${index % 2 === 0 ? '#' : '*'}${'*'.repeat(7)}\x1b[0m`,
|
||||
`\x1b[2;12H\x1b[38;2;255;138;0mOpenTUI synthetic active TUI redraw ${index}\x1b[0m`,
|
||||
`\x1b[4;6H\x1b[38;2;231;237;247m${'#'.repeat(36)} ${'opentui'.repeat(48)}\x1b[0m`
|
||||
].join('')
|
||||
)
|
||||
expect(frames.every((frame) => frame.length <= 2048 && frame.includes('\x1b['))).toBe(true)
|
||||
expect(frames.join('').length).toBeGreaterThan(128 * 1024)
|
||||
|
||||
for (const frame of frames) {
|
||||
capturedDataCallback.current?.(frame)
|
||||
}
|
||||
|
||||
expect(pane.terminal.write.mock.calls.length).toBeLessThan(frames.length)
|
||||
vi.advanceTimersByTime(0)
|
||||
expect(pane.terminal.write.mock.calls.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('drains a post-submit synchronized frame on the fast path when its end marker arrives late', async () => {
|
||||
// Submit-opened DEC 2026 frames keep the fast ~16ms drain even when ConPTY splits their close past 150ms (STA-1041).
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
||||
)
|
||||
await loadFreshTerminalModules()
|
||||
try {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks8 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks8)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
vi.useFakeTimers()
|
||||
sendTerminalInputThroughPane(pane, '\r')
|
||||
const repaintBody = 'opencode repaint '.repeat(200)
|
||||
expect(repaintBody.length).toBeGreaterThan(2048)
|
||||
capturedDataCallback.current?.(`\x1b[?2026h${repaintBody}`)
|
||||
// The frame body holds, then its hold-safety fallback drains it.
|
||||
vi.advanceTimersByTime(40)
|
||||
pane.terminal.write.mockClear()
|
||||
|
||||
// ConPTY delivers the closing chunk well past the 150ms redraw window.
|
||||
vi.advanceTimersByTime(300)
|
||||
const endChunk = `${repaintBody}\x1b[?25l\x1b[13;14H\x1b[?25h\x1b[?2026l`
|
||||
expect(endChunk.length).toBeGreaterThan(2048)
|
||||
capturedDataCallback.current?.(endChunk)
|
||||
|
||||
vi.advanceTimersByTime(20)
|
||||
expect(pane.terminal.write).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps coalescing a synchronized frame end with no recent input behind the 1s fallback', async () => {
|
||||
// Only keystrokes arm the fast path; background redraws retain the full fallback to hide transient Windows cursors (STA-1041).
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
||||
)
|
||||
await loadFreshTerminalModules()
|
||||
try {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks9 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks9)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
vi.useFakeTimers()
|
||||
const repaintBody = 'opencode repaint '.repeat(200)
|
||||
capturedDataCallback.current?.(`\x1b[?2026h${repaintBody}`)
|
||||
// Let the frame body drain via its 250ms hold-safety fallback before isolating the closing chunk.
|
||||
vi.advanceTimersByTime(300)
|
||||
pane.terminal.write.mockClear()
|
||||
|
||||
const endChunk = `${repaintBody}\x1b[?25l\x1b[13;14H\x1b[?25h\x1b[?2026l`
|
||||
capturedDataCallback.current?.(endChunk)
|
||||
|
||||
vi.advanceTimersByTime(20)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(pane.terminal.write).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('clears the synchronized latch when ConPTY splits the frame end marker', async () => {
|
||||
// Split DEC 2026 closes must release the foreground latch or later output freezes until blur (#8754).
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent(
|
||||
@@ -502,63 +170,5 @@ export function registerPtyConnectionForegroundFrameSchedulingTests(): void {
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
|
||||
it('coalesces a second synchronized frame that opens after the window with no keystroke', async () => {
|
||||
// Why: the second frame opens after the interactive window, so its start is judged independently and stays on the 1s fallback.
|
||||
const restoreNavigator = temporarilySetNavigatorUserAgent(
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
||||
)
|
||||
await loadFreshTerminalModules()
|
||||
try {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks12 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(
|
||||
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
}
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
connectPanePty(
|
||||
...mockPanePtyArgs(
|
||||
pane,
|
||||
createManager(1),
|
||||
bindPaneAgentTitleMocks(createDeps(), agentTitleMocks12)
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
vi.useFakeTimers()
|
||||
sendTerminalInputThroughPane(pane, '\r')
|
||||
const repaintBody = 'opencode repaint '.repeat(200)
|
||||
expect(repaintBody.length).toBeGreaterThan(2048)
|
||||
capturedDataCallback.current?.(`\x1b[?2026h${repaintBody}\x1b[?2026l`)
|
||||
vi.advanceTimersByTime(40)
|
||||
|
||||
// Move past the 400ms window with no further keystroke, then open frame 2.
|
||||
vi.advanceTimersByTime(500)
|
||||
capturedDataCallback.current?.(`\x1b[?2026h${repaintBody}`)
|
||||
vi.advanceTimersByTime(40)
|
||||
pane.terminal.write.mockClear()
|
||||
|
||||
const secondEndChunk = `${repaintBody}\x1b[?25l\x1b[13;14H\x1b[?25h\x1b[?2026l`
|
||||
capturedDataCallback.current?.(secondEndChunk)
|
||||
|
||||
vi.advanceTimersByTime(20)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(pane.terminal.write).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
restoreNavigator()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
-619
@@ -1,619 +0,0 @@
|
||||
import {
|
||||
bindsPaneInStore,
|
||||
expectWindowStartAskedMain,
|
||||
mainStartsPaneAs,
|
||||
expectNoWindowStart,
|
||||
mainLayoutApplyTransport
|
||||
} from '../test-fixtures/pty-connection-test-main-pane-start-fixtures'
|
||||
import {
|
||||
LEAF_2,
|
||||
LEAF_1,
|
||||
createMockTransport,
|
||||
createPane,
|
||||
createManager,
|
||||
type MockTransport
|
||||
} from '../test-fixtures/pty-connection-test-pane-fixtures'
|
||||
import { bindPtyConnectionTestMocks } from '../test-fixtures/pty-connection-mocks.spec'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../../shared/layout-owner-switch'
|
||||
|
||||
import { flushAsyncTicks } from '../test-fixtures/pty-connection-test-async'
|
||||
import { UUID_RE } from '../test-fixtures/pty-connection-test-constants'
|
||||
|
||||
import {
|
||||
buildPaneConnectionDeps,
|
||||
type PtyConnectionAgentTitleMocks,
|
||||
bindPaneAgentTitleMocks
|
||||
} from '../test-fixtures/pty-connection-test-deps'
|
||||
import { createInitialStoreState } from '../test-fixtures/pty-connection-test-store-fixtures'
|
||||
import type { StoreState } from '../test-fixtures/pty-connection-test-store-state'
|
||||
import {
|
||||
installTerminalTestGlobals,
|
||||
restoreTerminalTestGlobals
|
||||
} from '../test-fixtures/pty-connection-test-environment'
|
||||
|
||||
export function registerPtyConnectionSleepingRecordResumeTests(): void {
|
||||
let mockStoreState: StoreState
|
||||
let transportFactoryQueue: MockTransport[] = []
|
||||
let createdTransportOptions: Record<string, unknown>[] = []
|
||||
let storeSubscribers: ((state: StoreState) => void)[] = []
|
||||
|
||||
function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
return buildPaneConnectionDeps(() => mockStoreState, overrides)
|
||||
}
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
bindPtyConnectionTestMocks({
|
||||
getState: () => mockStoreState,
|
||||
getTransportQueue: () => transportFactoryQueue,
|
||||
getTransportOptions: () => createdTransportOptions,
|
||||
getStoreSubscribers: () => storeSubscribers,
|
||||
setStoreSubscribers: (subscribers) => {
|
||||
storeSubscribers = subscribers
|
||||
}
|
||||
})
|
||||
await installTerminalTestGlobals()
|
||||
;(await mainLayoutApplyTransport()).reset()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('uses sleeping-record launch config for pane cold restore after settings change', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('fresh-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
|
||||
if (sessionId) {
|
||||
return {
|
||||
id: 'fresh-pty',
|
||||
coldRestore: { scrollback: 'cold-payload', cwd: '/tmp/wt-1' }
|
||||
}
|
||||
}
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
const launchConfig = {
|
||||
agentCommand: "codex '--model' 'gpt-5' '--reasoning-effort' 'high'",
|
||||
agentArgs: '--model gpt-5 --reasoning-effort high',
|
||||
agentEnv: {
|
||||
CODEX_PROFILE: 'captured',
|
||||
ORCA_PANE_KEY: 'wrong-pane',
|
||||
ORCA_TAB_ID: 'wrong-tab',
|
||||
ORCA_WORKTREE_ID: 'wrong-worktree',
|
||||
ORCA_WORKSPACE_ID: 'wrong-workspace'
|
||||
}
|
||||
}
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }]
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {},
|
||||
agentDefaultArgs: { codex: '--model changed' },
|
||||
agentDefaultEnv: { codex: { CODEX_PROFILE: 'changed' } }
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' },
|
||||
prompt: 'finish the task',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
launchConfig
|
||||
}
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'lost-pty' }
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
await vi.advanceTimersByTimeAsync(70)
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// Main resumes the agent; the window only attaches to the cold-restored terminal and paints it.
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 'lost-pty', attachOnlyView: true })
|
||||
)
|
||||
expectNoWindowStart(transport, mockStoreState)
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
return
|
||||
}
|
||||
const reattachArgs = transport.connect.mock.calls.find(
|
||||
([args]) => args.sessionId === 'lost-pty'
|
||||
)?.[0]
|
||||
const launchToken = (reattachArgs?.env as Record<string, string> | undefined)
|
||||
?.ORCA_AGENT_LAUNCH_TOKEN
|
||||
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
expect(launchToken).toMatch(new RegExp(`^${UUID_RE}$`))
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: 'lost-pty',
|
||||
command:
|
||||
"codex '--model' 'gpt-5' '--reasoning-effort' 'high' '-c' 'disable_paste_burst=true' 'resume' 'codex-session-1'",
|
||||
env: expect.objectContaining({
|
||||
CODEX_PROFILE: 'captured',
|
||||
ORCA_PANE_KEY: paneKey,
|
||||
ORCA_TAB_ID: 'tab-1',
|
||||
ORCA_WORKTREE_ID: 'wt-1',
|
||||
ORCA_WORKSPACE_ID: 'wt-1',
|
||||
ORCA_AGENT_LAUNCH_TOKEN: launchToken
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(mockStoreState.registerAgentLaunchConfig).toHaveBeenCalledWith(paneKey, launchConfig, {
|
||||
agentType: 'codex',
|
||||
launchToken,
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_1
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function registerPtyConnectionLiveResumeIdentityTests(): void {
|
||||
let mockStoreState: StoreState
|
||||
let transportFactoryQueue: MockTransport[] = []
|
||||
let createdTransportOptions: Record<string, unknown>[] = []
|
||||
let storeSubscribers: ((state: StoreState) => void)[] = []
|
||||
|
||||
function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
return buildPaneConnectionDeps(() => mockStoreState, overrides)
|
||||
}
|
||||
|
||||
/** Main's frame binding tab-1's pane 2. */
|
||||
const bindLeaf2 = bindsPaneInStore(() => mockStoreState, 'tab-1', LEAF_2)
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
bindPtyConnectionTestMocks({
|
||||
getState: () => mockStoreState,
|
||||
getTransportQueue: () => transportFactoryQueue,
|
||||
getTransportOptions: () => createdTransportOptions,
|
||||
getStoreSubscribers: () => storeSubscribers,
|
||||
setStoreSubscribers: (subscribers) => {
|
||||
storeSubscribers = subscribers
|
||||
}
|
||||
})
|
||||
await installTerminalTestGlobals()
|
||||
;(await mainLayoutApplyTransport()).reset()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await restoreTerminalTestGlobals()
|
||||
})
|
||||
|
||||
it('prefers live-entry launch config for pane cold restore when status survived PTY loss', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks5 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('fresh-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
|
||||
if (sessionId) {
|
||||
return {
|
||||
id: 'fresh-pty',
|
||||
coldRestore: { scrollback: 'cold-payload', cwd: '/tmp/wt-1' }
|
||||
}
|
||||
}
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
const launchConfig = {
|
||||
agentCommand: "codex '--model' 'gpt-5-mini'",
|
||||
agentArgs: '--model gpt-5-mini',
|
||||
agentEnv: {}
|
||||
}
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }]
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {},
|
||||
agentDefaultArgs: { codex: '--model changed' }
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
state: 'working',
|
||||
prompt: 'finish the task',
|
||||
agentType: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' }
|
||||
}
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {
|
||||
[paneKey]: { launchConfig }
|
||||
},
|
||||
sleepingAgentSessionsByPaneKey: {}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'lost-pty' }
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks5)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
await vi.advanceTimersByTimeAsync(70)
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// Main resumes from the live entry; the window only attaches and paints.
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 'lost-pty', attachOnlyView: true })
|
||||
)
|
||||
expectNoWindowStart(transport, mockStoreState)
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
return
|
||||
}
|
||||
expect(transport.sendInput).not.toHaveBeenCalled()
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: 'lost-pty',
|
||||
command:
|
||||
"codex '--model' 'gpt-5-mini' '-c' 'disable_paste_burst=true' 'resume' 'codex-session-1'",
|
||||
env: expect.objectContaining({
|
||||
ORCA_AGENT_LAUNCH_TOKEN: expect.stringMatching(new RegExp(`^${UUID_RE}$`))
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(mockStoreState.registerAgentLaunchConfig).toHaveBeenCalledWith(paneKey, launchConfig, {
|
||||
agentType: 'codex',
|
||||
launchToken: expect.stringMatching(new RegExp(`^${UUID_RE}$`)),
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_1
|
||||
})
|
||||
expect(mockStoreState.clearSleepingAgentSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['ordinary', 'settled-worker'])(
|
||||
'restores %s through main with one resume command',
|
||||
async (kind) => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks6 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const retainedPtyId = 'wt-1@@lost-pty'
|
||||
const transport = createMockTransport()
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) =>
|
||||
sessionId
|
||||
? {
|
||||
id: 'fresh-pty',
|
||||
coldRestore: { scrollback: 'cold-payload', cwd: '/tmp/wt-1' }
|
||||
}
|
||||
: 'fresh-pty'
|
||||
)
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: retainedPtyId }]
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {}
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
state: 'working',
|
||||
prompt: 'finish the task',
|
||||
agentType: 'claude',
|
||||
providerSession: { key: 'session_id', id: 'claude-session-1' }
|
||||
}
|
||||
},
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'claude',
|
||||
providerSession: { key: 'session_id', id: 'claude-session-1' },
|
||||
prompt: 'finish the task',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
...(kind === 'settled-worker'
|
||||
? { automaticResumeBlockedBy: 'legacy-orchestration-worker' }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
connectPanePty(
|
||||
createPane(1) as never,
|
||||
createManager(1) as never,
|
||||
bindPaneAgentTitleMocks(
|
||||
createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: retainedPtyId }
|
||||
}) as never,
|
||||
agentTitleMocks6
|
||||
)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
await vi.advanceTimersByTimeAsync(70)
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// Main's start carries the one resume; the window's one connect is an attach.
|
||||
expect(transport.connect).toHaveBeenCalledTimes(1)
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: retainedPtyId, attachOnlyView: true })
|
||||
)
|
||||
expectNoWindowStart(transport, mockStoreState)
|
||||
expect(mockStoreState.tabsByWorktree['wt-1']).toHaveLength(1)
|
||||
return
|
||||
}
|
||||
expect(transport.connect).toHaveBeenCalledTimes(1)
|
||||
expect(transport.attach).not.toHaveBeenCalled()
|
||||
const options = transport.connect.mock.calls[0]?.[0] as {
|
||||
sessionId?: string
|
||||
command?: string
|
||||
}
|
||||
expect(options.sessionId).toBe(retainedPtyId)
|
||||
expect(options.command).toContain('claude-session-1')
|
||||
expect(options.command?.match(/--resume/g)).toHaveLength(1)
|
||||
expect(mockStoreState.tabsByWorktree['wt-1']).toHaveLength(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('ignores stale live launch config when cold restore identity lookup rejects it', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks7 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport('fresh-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
|
||||
if (sessionId) {
|
||||
return {
|
||||
id: 'fresh-pty',
|
||||
coldRestore: { scrollback: 'cold-payload', cwd: '/tmp/wt-1' }
|
||||
}
|
||||
}
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: 'lost-pty' }]
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {},
|
||||
agentDefaultArgs: { codex: '--model current' }
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
state: 'working',
|
||||
prompt: 'finish the task',
|
||||
agentType: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' }
|
||||
}
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {
|
||||
[paneKey]: {
|
||||
launchConfig: {
|
||||
agentCommand: "codex '--model' 'stale'",
|
||||
agentArgs: '--model stale',
|
||||
agentEnv: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
getAgentLaunchConfigForStatusEntry: vi.fn(() => undefined),
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'older-codex-session' },
|
||||
prompt: 'older task',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1,
|
||||
launchConfig: {
|
||||
agentCommand: "codex '--model' 'sleeping-stale'",
|
||||
agentArgs: '--model sleeping-stale',
|
||||
agentEnv: { CODEX_PROFILE: 'sleeping-stale' }
|
||||
}
|
||||
}
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'lost-pty' }
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks7)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
await vi.advanceTimersByTimeAsync(70)
|
||||
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// Main picks the resume recipe; the window neither builds nor registers one.
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 'lost-pty', attachOnlyView: true })
|
||||
)
|
||||
expectNoWindowStart(transport, mockStoreState)
|
||||
return
|
||||
}
|
||||
expect(mockStoreState.getAgentLaunchConfigForStatusEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ paneKey, agentType: 'codex' })
|
||||
)
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: 'lost-pty',
|
||||
// Neither stale recipe applies, and settings defaults never stand in for a recorded one.
|
||||
command: "codex '-c' 'disable_paste_burst=true' 'resume' 'codex-session-1'"
|
||||
})
|
||||
)
|
||||
expect(mockStoreState.registerAgentLaunchConfig).toHaveBeenCalledWith(
|
||||
paneKey,
|
||||
expect.objectContaining({
|
||||
agentArgs: ''
|
||||
}),
|
||||
expect.objectContaining({
|
||||
agentType: 'codex',
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_1
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the restored banner when a sleeping resume falls back to a fresh shell', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks8 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const staleSessionId = 'wt-1@@stale-session'
|
||||
const transport = createMockTransport()
|
||||
transport.connect.mockImplementation(async (opts: { sessionId?: string }) => {
|
||||
if (opts.sessionId) {
|
||||
return undefined
|
||||
}
|
||||
const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as
|
||||
| ((ptyId: string) => void)
|
||||
| undefined
|
||||
onPtySpawn?.('fresh-pty')
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_2)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: staleSessionId }]
|
||||
},
|
||||
ptyIdsByTabId: {
|
||||
'tab-1': [staleSessionId]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_2 },
|
||||
activeLeafId: LEAF_2,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_2]: staleSessionId }
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {}
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' },
|
||||
prompt: 'finish the task',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(2)
|
||||
const manager = createManager(2)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_2,
|
||||
restoredPtyIdByLeafId: { [LEAF_2]: staleSessionId }
|
||||
})
|
||||
await mainStartsPaneAs({ ptyId: 'pty-main', bind: bindLeaf2, transport })
|
||||
|
||||
connectPanePty(
|
||||
pane as never,
|
||||
manager as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks8)
|
||||
)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
expect(transport.connect).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ sessionId: staleSessionId })
|
||||
)
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
// A missing reattach asks main to start and preserve the resume record; the window cannot spawn its own shell.
|
||||
await expectWindowStartAskedMain(transport, paneKey, mockStoreState)
|
||||
expect(transport.connect).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ sessionId: 'pty-main', attachOnlyView: true })
|
||||
)
|
||||
expect(mockStoreState.clearSleepingAgentSession).not.toHaveBeenCalled()
|
||||
expect(deps.onPtyErrorRef.current).not.toHaveBeenCalled()
|
||||
return
|
||||
}
|
||||
expect(transport.connect).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.not.objectContaining({ sessionId: expect.any(String) })
|
||||
)
|
||||
expect(deps.clearTabPtyId).toHaveBeenCalledWith('tab-1', staleSessionId)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('--- session restored ---'),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(deps.onShowSessionRestoredBanner).toHaveBeenCalledTimes(1)
|
||||
expect(deps.onShowSessionRestoredBanner).toHaveBeenCalledWith(2, 'restored')
|
||||
expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toMatchObject({
|
||||
paneKey,
|
||||
origin: 'quit',
|
||||
restoreOnTabOpenOnly: true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { registerAgentStatusStartupSnapshot } from '@/hooks/ipc-events/agent-status-startup-snapshot'
|
||||
import { isPaneResumingInPlace } from '@/lib/pane-spawn-deferral-registry'
|
||||
import { bindDeferredColdRestoreAndSnapshot } from '../deferred-cold-restore-and-snapshot'
|
||||
import { startDeferredSessionReattach } from '../reattach/deferred-session-reattach-connect'
|
||||
import type { ConnectPanePtySession } from '../connect-pane-pty-session'
|
||||
import type { ColdRestoreAgentResumeStartup } from './fresh-spawn-types'
|
||||
|
||||
vi.mock('../../../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
vi.mock('../../layout/reconciliation/terminal-lifecycle-diagnostics', () => ({
|
||||
warnTerminalLifecycleAnomaly: vi.fn()
|
||||
}))
|
||||
vi.mock('../instrumentation/pty-connect-diagnostics', () => ({
|
||||
recordPtyConnectDiagnostic: vi.fn()
|
||||
}))
|
||||
vi.mock('../../pty-transport/host-ownership/ssh-session-gone-verdict', () => ({
|
||||
isSshSessionGoneError: () => false
|
||||
}))
|
||||
|
||||
const RECORD_PANE_KEY = 'tab:leaf'
|
||||
|
||||
function resumeStartup(): ColdRestoreAgentResumeStartup {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the starts read only the command, agent, launch config and the record's pane key.
|
||||
return {
|
||||
command: 'claude --resume c1',
|
||||
agent: 'claude',
|
||||
launchConfig: { agentCommand: 'claude', agentArgs: '', agentEnv: {} },
|
||||
launchToken: 'token-1',
|
||||
sleepingRecordEntry: { paneKey: RECORD_PANE_KEY, record: {} }
|
||||
} as unknown as ColdRestoreAgentResumeStartup
|
||||
}
|
||||
|
||||
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
||||
let resolve: (value: T) => void = () => {}
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
/** A paired server's pane, which the window still starts itself; main starts every other one. */
|
||||
const PAIRED_SERVER = 'env-paired'
|
||||
|
||||
function createSession(connect: () => Promise<unknown>, runtimeEnvironmentId: string | null) {
|
||||
const transport = { connect: vi.fn(connect), getPtyId: () => null }
|
||||
const fixture = {
|
||||
pane: { id: 1, leafId: 'leaf' },
|
||||
transport,
|
||||
disposed: false,
|
||||
transportStreamGeneration: 1,
|
||||
runtimeEnvironmentId,
|
||||
deps: {
|
||||
tabId: 'tab',
|
||||
worktreeId: 'wt',
|
||||
paneTransportsRef: { current: new Map([[1, transport]]) },
|
||||
clearTabPtyId: vi.fn()
|
||||
},
|
||||
buildColdRestoreAgentResumeStartup: resumeStartup,
|
||||
captureTransportOutputCallbacks: () => ({ generation: 1, callbacks: {} }),
|
||||
beginReattachLiveDataDeferral: vi.fn(),
|
||||
finishReattachLiveDataDeferral: vi.fn(),
|
||||
shouldDeclareHiddenAtSpawn: () => false,
|
||||
mergeStartupEnvWithPaneIdentity: (env: Record<string, string>) => env,
|
||||
handleReattachResult: vi.fn().mockResolvedValue(true),
|
||||
clearExitedPanePtyLayoutBinding: vi.fn(),
|
||||
startFreshColdRestoreAgentResume: vi.fn().mockResolvedValue('fresh-pty'),
|
||||
reportError: vi.fn(),
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cacheKey: RECORD_PANE_KEY
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture provides every session field the two starts read.
|
||||
return fixture as unknown as ConnectPanePtySession
|
||||
}
|
||||
|
||||
describe('a start that resumes a record marks its pane as resuming in place (layout-owner build)', () => {
|
||||
let bridge: ReturnType<typeof registerAgentStatusStartupSnapshot>
|
||||
beforeEach(() => {
|
||||
bridge = registerAgentStatusStartupSnapshot()
|
||||
bridge.settle()
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { pty: { declarePendingPaneSerializer: vi.fn().mockResolvedValue(null) } }
|
||||
})
|
||||
})
|
||||
afterEach(() => bridge.dispose())
|
||||
|
||||
it('the reattach that carries the resume holds the mark until it settles', async () => {
|
||||
const connected = deferred<unknown>()
|
||||
const session = createSession(() => connected.promise, PAIRED_SERVER)
|
||||
|
||||
startDeferredSessionReattach(session, 'restored-pty')
|
||||
await vi.waitFor(() => expect(session.transport.connect).toHaveBeenCalled())
|
||||
expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(true)
|
||||
|
||||
connected.resolve({ id: 'restored-pty' })
|
||||
await vi.waitFor(() => expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(false))
|
||||
})
|
||||
|
||||
it('the fresh start that resumes the record holds the mark until it settles', async () => {
|
||||
const session = createSession(() => Promise.resolve(null), PAIRED_SERVER)
|
||||
bindDeferredColdRestoreAndSnapshot(session)
|
||||
const spawned = deferred<string | null>()
|
||||
session.startFreshSpawn = vi.fn(() => spawned.promise)
|
||||
|
||||
const start = session.spawnColdRestoreAgentResume(resumeStartup())
|
||||
expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(true)
|
||||
|
||||
spawned.resolve('fresh-pty')
|
||||
await start
|
||||
await vi.waitFor(() => expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(false))
|
||||
})
|
||||
it('a pane main starts: its reattach is a view that carries no resume and holds no mark', async () => {
|
||||
const connected = deferred<unknown>()
|
||||
const session = createSession(() => connected.promise, null)
|
||||
|
||||
startDeferredSessionReattach(session, 'restored-pty')
|
||||
await vi.waitFor(() => expect(session.transport.connect).toHaveBeenCalled())
|
||||
expect(session.transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 'restored-pty', attachOnlyView: true })
|
||||
)
|
||||
expect(session.transport.connect).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ command: expect.anything() })
|
||||
)
|
||||
expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(false)
|
||||
|
||||
connected.resolve({ id: 'restored-pty' })
|
||||
await vi.waitFor(() => expect(session.handleReattachResult).toHaveBeenCalled())
|
||||
expect(isPaneResumingInPlace(RECORD_PANE_KEY)).toBe(false)
|
||||
})
|
||||
})
|
||||
-34
@@ -73,23 +73,6 @@ describe("the window's banner for main's start of a pane", () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('shows the outcome of the terminal the view attached, once per start', async () => {
|
||||
const showMainPaneResumeOutcome = await loadBannerModule()
|
||||
store.publish({
|
||||
terminalPaneResumeOutcomesByPaneKey: {
|
||||
[PANE_KEY]: { ptyId: 'pty-1', ptyGeneration: 1, outcome: 'restored' }
|
||||
}
|
||||
})
|
||||
const first = attachedView()
|
||||
showMainPaneResumeOutcome(first.session, 'pty-1')
|
||||
expect(first.view.showSessionRestoredBanner).toHaveBeenCalledExactlyOnceWith('restored')
|
||||
|
||||
// A remounted view attaching the same start says nothing again.
|
||||
const remounted = attachedView()
|
||||
showMainPaneResumeOutcome(remounted.session, 'pty-1')
|
||||
expect(remounted.view.showSessionRestoredBanner).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows once per start: a later start under the same terminal id shows again', async () => {
|
||||
const showMainPaneResumeOutcome = await loadBannerModule()
|
||||
const publish = (ptyGeneration: number, outcome: TerminalPaneResumeOutcome['outcome']) =>
|
||||
@@ -127,21 +110,4 @@ describe("the window's banner for main's start of a pane", () => {
|
||||
;(await loadBannerModule())(reloaded.session, 'pty-1')
|
||||
expect(reloaded.view.showSessionRestoredBanner).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows nothing for an outcome naming another terminal', async () => {
|
||||
const showMainPaneResumeOutcome = await loadBannerModule()
|
||||
store.publish({
|
||||
terminalPaneResumeOutcomesByPaneKey: {
|
||||
[PANE_KEY]: { ptyId: 'pty-old', ptyGeneration: 1, outcome: 'restored' }
|
||||
}
|
||||
})
|
||||
const { view, session } = attachedView()
|
||||
showMainPaneResumeOutcome(session, 'pty-1')
|
||||
store.publish({
|
||||
terminalPaneResumeOutcomesByPaneKey: {
|
||||
[PANE_KEY]: { ptyId: 'pty-old', ptyGeneration: 2, outcome: 'resume-unavailable' }
|
||||
}
|
||||
})
|
||||
expect(view.showSessionRestoredBanner).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
import type * as React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushAsyncTicks } from '../test-fixtures/pty-connection-test-async'
|
||||
import {
|
||||
createMockTransport,
|
||||
createPane,
|
||||
createManager,
|
||||
type MockTransport
|
||||
} from '../test-fixtures/pty-connection-test-pane-fixtures'
|
||||
import {
|
||||
buildPaneConnectionDeps,
|
||||
type PtyConnectionAgentTitleMocks,
|
||||
bindPaneAgentTitleMocks
|
||||
} from '../test-fixtures/pty-connection-test-deps'
|
||||
import { createInitialStoreState } from '../test-fixtures/pty-connection-test-store-fixtures'
|
||||
import type { StoreState } from '../test-fixtures/pty-connection-test-store-state'
|
||||
import {
|
||||
installTerminalTestGlobals,
|
||||
restoreTerminalTestGlobals
|
||||
} from '../test-fixtures/pty-connection-test-environment'
|
||||
|
||||
const {
|
||||
resetAndRefreshAllTerminalWebglAtlases,
|
||||
scheduleRuntimeGraphSync,
|
||||
shouldSeedCacheTimerOnInitialTitle,
|
||||
toastInfo,
|
||||
notifyCodexPaneBoundForStaleSweep
|
||||
} = vi.hoisted(() => ({
|
||||
resetAndRefreshAllTerminalWebglAtlases: vi.fn(),
|
||||
scheduleRuntimeGraphSync: vi.fn(),
|
||||
shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false),
|
||||
toastInfo: vi.fn(),
|
||||
notifyCodexPaneBoundForStaleSweep: vi.fn()
|
||||
}))
|
||||
|
||||
let mockStoreState: StoreState
|
||||
let transportFactoryQueue: MockTransport[] = []
|
||||
let createdTransportOptions: Record<string, unknown>[] = []
|
||||
let storeSubscribers: ((state: StoreState) => void)[] = []
|
||||
|
||||
vi.mock('@/runtime/sync-runtime-graph/graph-sync-scheduler', () => ({ scheduleRuntimeGraphSync }))
|
||||
|
||||
vi.mock('@/lib/pane-manager/pane-manager-registry', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
resetAndRefreshAllTerminalWebglAtlases
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => mockStoreState,
|
||||
subscribe: (listener: (state: StoreState) => void) => {
|
||||
storeSubscribers.push(listener)
|
||||
return () => {
|
||||
storeSubscribers = storeSubscribers.filter((candidate) => candidate !== listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-status/aggregation/worktree-agent-status', async (importOriginal) => {
|
||||
const { buildAgentStatusModuleMock } =
|
||||
await import('../test-fixtures/pty-connection-test-environment')
|
||||
return buildAgentStatusModuleMock(await importOriginal<Record<string, unknown>>())
|
||||
})
|
||||
|
||||
vi.mock('../../titles/cache-timer-seeding', () => ({ shouldSeedCacheTimerOnInitialTitle }))
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: toastInfo } }))
|
||||
|
||||
vi.mock('@/lib/agent-session/accounts/codex/codex-stale-pane-sweep', () => ({
|
||||
notifyCodexPaneBoundForStaleSweep
|
||||
}))
|
||||
|
||||
vi.mock('react', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof React>()
|
||||
return {
|
||||
...actual,
|
||||
useCallback: <T extends (...args: unknown[]) => unknown>(fn: T): T => fn
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../pty-transport/ipc/pty-transport', () => ({
|
||||
createIpcPtyTransport: vi.fn((options: Record<string, unknown>) => {
|
||||
createdTransportOptions.push(options)
|
||||
const nextTransport = transportFactoryQueue.shift()
|
||||
if (!nextTransport) {
|
||||
throw new Error('No mock transport queued')
|
||||
}
|
||||
return nextTransport
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('../../pty-transport/remote-runtime/remote-runtime-pty-transport', () => ({
|
||||
createRemoteRuntimePtyTransport: vi.fn(
|
||||
(_environmentId: string, options: Record<string, unknown>) => {
|
||||
createdTransportOptions.push(options)
|
||||
const transport = transportFactoryQueue.shift()
|
||||
if (!transport) {
|
||||
throw new Error('No remote mock transport queued')
|
||||
}
|
||||
return transport
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('../../pty-transport/delivery/pty-dispatcher', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>()
|
||||
return { ...actual, getEagerPtyBufferHandle: vi.fn(() => undefined) }
|
||||
})
|
||||
|
||||
function spawnOnConnect(transport: MockTransport, ptyId: string): void {
|
||||
transport.connect.mockImplementation(async () => {
|
||||
const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as ((id: string) => void) | undefined
|
||||
onPtySpawn?.(ptyId)
|
||||
return ptyId
|
||||
})
|
||||
}
|
||||
describe('connectPanePty queued startup consume', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await restoreTerminalTestGlobals()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('spends the queued startup only after the pty is bound to the tab', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks2 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
spawnOnConnect(transport, 'fresh-pty')
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const callOrder: string[] = []
|
||||
const deps = buildPaneConnectionDeps(() => mockStoreState, {
|
||||
startup: { command: 'echo queued' },
|
||||
updateTabPtyId: vi.fn(() => {
|
||||
callOrder.push('updateTabPtyId')
|
||||
}),
|
||||
onQueuedStartupSpawned: vi.fn(() => {
|
||||
callOrder.push('onQueuedStartupSpawned')
|
||||
})
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
createPane(1) as never,
|
||||
createManager(1) as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks2)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(callOrder).toContain('onQueuedStartupSpawned')
|
||||
expect(callOrder.indexOf('updateTabPtyId')).toBeGreaterThanOrEqual(0)
|
||||
expect(callOrder.indexOf('updateTabPtyId')).toBeLessThan(
|
||||
callOrder.indexOf('onQueuedStartupSpawned')
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves the queued startup unspent when the spawn never reaches onPtySpawn', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks3 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
// A retired/reattached spawn resolves without ever announcing a fresh pty.
|
||||
transport.connect.mockImplementation(async () => undefined)
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const onQueuedStartupSpawned = vi.fn()
|
||||
const deps = buildPaneConnectionDeps(() => mockStoreState, {
|
||||
startup: { command: 'echo queued' },
|
||||
onQueuedStartupSpawned
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
createPane(1) as never,
|
||||
createManager(1) as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks3)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
// The next mount must still find the command queued, or it is lost for good.
|
||||
expect(onQueuedStartupSpawned).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps a throwing consume from escaping into the spawn', async () => {
|
||||
const { connectPanePty } = await import('../connect-pane-pty')
|
||||
const agentTitleMocks4 = await vi.importMock<PtyConnectionAgentTitleMocks>(
|
||||
'@/lib/agent-status/aggregation/worktree-agent-status'
|
||||
)
|
||||
const transport = createMockTransport()
|
||||
// onPtySpawn runs inside connect; callback throws would reject spawn and strand the pane, so cleanup must not throw.
|
||||
let escapedIntoSpawn = false
|
||||
transport.connect.mockImplementation(async () => {
|
||||
const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as
|
||||
| ((id: string) => void)
|
||||
| undefined
|
||||
try {
|
||||
onPtySpawn?.('fresh-pty')
|
||||
} catch {
|
||||
escapedIntoSpawn = true
|
||||
return undefined
|
||||
}
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const updateTabPtyId = vi.fn()
|
||||
const deps = buildPaneConnectionDeps(() => mockStoreState, {
|
||||
startup: { command: 'echo queued' },
|
||||
updateTabPtyId,
|
||||
onQueuedStartupSpawned: vi.fn(() => {
|
||||
throw new Error('store blew up')
|
||||
})
|
||||
})
|
||||
|
||||
connectPanePty(
|
||||
createPane(1) as never,
|
||||
createManager(1) as never,
|
||||
bindPaneAgentTitleMocks(deps as never, agentTitleMocks4)
|
||||
)
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(escapedIntoSpawn).toBe(false)
|
||||
expect(updateTabPtyId).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
+1
-5
@@ -3,10 +3,7 @@ import { registerPtyConnectionForegroundAgentRoutingTests } from '../agent-ident
|
||||
import { registerPtyConnectionForegroundAgentSamplingTests } from '../agent-identity/pty-connection-foreground-agent-sampling.spec'
|
||||
import { registerPtyConnectionForegroundProcessExitTests } from '../agent-identity/pty-connection-foreground-process-exit.spec'
|
||||
import { registerPtyConnectionForegroundRoutingConfirmationTests } from '../agent-identity/pty-connection-foreground-routing-confirmation.spec'
|
||||
import {
|
||||
registerPtyConnectionForegroundBulkAndInteractiveOutputTests,
|
||||
registerPtyConnectionForegroundFrameSchedulingTests
|
||||
} from '../output/pty-connection-foreground-write-path.spec'
|
||||
import { registerPtyConnectionForegroundFrameSchedulingTests } from '../output/pty-connection-foreground-write-path.spec'
|
||||
import { registerPtyConnectionInterruptInferenceTests } from '../input/pty-connection-interrupt-inference.spec'
|
||||
import { registerPtyConnectionTypedAgentIdentityTests } from '../agent-identity/pty-connection-typed-agent-identity.spec'
|
||||
|
||||
@@ -24,7 +21,6 @@ describe(
|
||||
registerPtyConnectionForegroundRoutingConfirmationTests
|
||||
)
|
||||
describe('pty-connection-foreground-write-path', () => {
|
||||
registerPtyConnectionForegroundBulkAndInteractiveOutputTests()
|
||||
registerPtyConnectionForegroundFrameSchedulingTests()
|
||||
})
|
||||
describe('pty-connection-interrupt-inference', registerPtyConnectionInterruptInferenceTests)
|
||||
|
||||
-6
@@ -1,8 +1,4 @@
|
||||
import { describe, vi } from 'vitest'
|
||||
import {
|
||||
registerPtyConnectionSleepingRecordResumeTests,
|
||||
registerPtyConnectionLiveResumeIdentityTests
|
||||
} from '../reattach/pty-connection-agent-session-resume.spec'
|
||||
import {
|
||||
registerPtyConnectionNonAgentLaunchRegistrationTests,
|
||||
registerPtyConnectionUnboundAgentLaunchRegistrationTests
|
||||
@@ -30,10 +26,8 @@ vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (importOriginal) =>
|
||||
)
|
||||
|
||||
describe('pty-connection-agent-session-resume', () => {
|
||||
registerPtyConnectionSleepingRecordResumeTests()
|
||||
registerPtyConnectionNonAgentLaunchRegistrationTests()
|
||||
registerPtyConnectionUnboundAgentLaunchRegistrationTests()
|
||||
registerPtyConnectionLiveResumeIdentityTests()
|
||||
})
|
||||
describe(
|
||||
'pty-connection-cold-restore-agent-resume',
|
||||
|
||||
-17
@@ -62,23 +62,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('delayed agent startup subscription', () => {
|
||||
it('delivers when launch registration, PTY ownership, and layout binding arrive', () => {
|
||||
seedPendingState()
|
||||
const deliver = vi.fn().mockResolvedValue(undefined)
|
||||
const startup = {} as never
|
||||
queuePendingAgentStartupDelivery({
|
||||
worktreeId: 'wt-background',
|
||||
tabId: 'tab-background',
|
||||
launchToken: 'target-launch',
|
||||
startup,
|
||||
deliver
|
||||
})
|
||||
|
||||
bindPendingPty()
|
||||
|
||||
expect(deliver).toHaveBeenCalledWith('tab-background', 'pty-background', startup)
|
||||
})
|
||||
|
||||
it('drops a delivery when its tab is removed before PTY binding', () => {
|
||||
seedPendingState()
|
||||
const deliver = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PluginHostListEntry } from '../../../../../shared/plugins/plugin-list-entry'
|
||||
import { collectActivePluginCommands, usePluginPanelsStore } from './plugin-contribution-catalog'
|
||||
import { usePluginPanelsStore } from './plugin-contribution-catalog'
|
||||
|
||||
function plugin(pluginKey: string): PluginHostListEntry {
|
||||
return {
|
||||
@@ -73,32 +73,3 @@ describe('plugin panel list loading', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin names on contributions', () => {
|
||||
it("adds the publisher to a non-official plugin sharing an official plugin's name", () => {
|
||||
const command = {
|
||||
id: 'apply',
|
||||
title: 'Apply',
|
||||
context: 'global' as const,
|
||||
handler: { type: 'worker' as const },
|
||||
keybindings: []
|
||||
}
|
||||
const official = {
|
||||
...plugin('stablyai.orca-catppuccin-theme'),
|
||||
name: 'Catppuccin for Orca',
|
||||
publisher: 'stablyai',
|
||||
official: true,
|
||||
commands: [command]
|
||||
}
|
||||
const community = {
|
||||
...plugin('vale-c.catppuccin'),
|
||||
name: 'Catppuccin for Orca',
|
||||
publisher: 'vale-c',
|
||||
commands: [command]
|
||||
}
|
||||
expect(collectActivePluginCommands([official, community]).map((c) => c.pluginName)).toEqual([
|
||||
'Catppuccin for Orca',
|
||||
'Catppuccin for Orca (vale-c)'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user