test: remove redundant cases and strengthen session boundary coverage (#33129)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-24 03:48:00 -07:00
committed by GitHub
co-authored by OrcaWin
parent 2071e7be88
commit 3ae885be11
17 changed files with 161 additions and 141 deletions
@@ -1,4 +1,8 @@
import { readFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { runProcessSync } from '@orca/process-host'
import { describe, expect, it } from 'vitest'
import { packagedNodePtyFloorDockerArgs } from './run-linux-packaged-node-pty-floor-smoke.mjs'
@@ -24,13 +28,37 @@ describe('packaged node-pty Linux floor smoke', () => {
).toThrow('linux-packaged-node-pty-floor-app-directory-invalid')
})
it('loads node-pty from packaged runtime resources', () => {
const childSource = readFileSync(
new URL('./linux-packaged-node-pty-floor-child.cjs', import.meta.url),
'utf8'
it.each([
['valid output', 0, 'orca-node-pty-floor-ok', 0],
['nonzero exit', 3, 'orca-node-pty-floor-ok', 1],
['wrong output', 0, 'unexpected', 1]
])('validates packaged node-pty %s', (_name, exitCode, output, expectedCode) => {
const resources = mkdtempSync(join(tmpdir(), 'packaged-node-pty-floor-'))
const moduleDirectory = join(resources, 'node_modules', 'node-pty')
mkdirSync(moduleDirectory, { recursive: true })
writeFileSync(
join(moduleDirectory, 'index.js'),
`module.exports = { spawn() { return {
onData(callback) { callback(${JSON.stringify(output)}) },
onExit(callback) { queueMicrotask(() => callback({ exitCode: ${exitCode} })) },
kill() {}
} } }`
)
expect(childSource).toContain("join(resourcesDirectory, 'node_modules', 'node-pty')")
expect(childSource).not.toContain("join(resourcesDirectory, 'app.asar'")
try {
const result = runProcessSync({
program: process.execPath,
args: [
fileURLToPath(new URL('./linux-packaged-node-pty-floor-child.cjs', import.meta.url)),
resources
]
})
expect(result.code, result.stderr).toBe(expectedCode)
expect(result.stdout).toBe(expectedCode === 0 ? 'orca-node-pty-floor-ok\n' : '')
if (expectedCode !== 0) {
expect(result.stderr).toContain('packaged node-pty floor smoke failed:')
}
} finally {
rmSync(resources, { recursive: true, force: true })
}
})
})
@@ -26,18 +26,6 @@ describe('resolveActiveSessionTab', () => {
expect(result.retainSelectedSessionTabId).toBe(false)
})
it('keeps the device on a terminal tab when an agent turn flags another terminal active', () => {
const result = resolveActiveSessionTab(
[terminalTab('agent', true), terminalTab('shell', false)],
{
pendingActiveSessionTabId: null,
selectedSessionTabId: 'shell'
}
)
expect(result.activeTab?.id).toBe('shell')
})
it('follows the host when it explicitly navigates this device', () => {
const result = resolveActiveSessionTab(
[terminalTab('agent', true), browserTab('browser', false)],
@@ -20,81 +20,13 @@ function baseInput(
}
}
/**
* Replays the route state the session screen actually holds
* (`app/h/[hostId]/session/[worktreeId].tsx`): `applySessionTabs` publishes the
* tab list and flips `terminalsLoaded`, `handleCloseSessionTab` prunes the
* closed tab and nulls the active handle.
*/
class SessionRouteState {
newlyCreatedWorkspace = true
tabsLoaded = false
tabIds: string[] = []
activeHandle: string | null = null
sawSessionTabs = false
autoCreatedForWorktree = false
applySessionTabs(tabIds: string[], activeHandle: string | null = tabIds[0] ? 'pty-1' : null) {
this.tabIds = tabIds
this.activeHandle = activeHandle
if (tabIds.length > 0) {
this.sawSessionTabs = true
}
this.tabsLoaded = true
}
closeSessionTab(tabId: string) {
this.tabIds = this.tabIds.filter((candidate) => candidate !== tabId)
this.activeHandle = null
}
gate(): boolean {
return shouldAutoCreateInitialSessionTerminal({
newlyCreatedWorkspace: this.newlyCreatedWorkspace,
connected: true,
tabsLoaded: this.tabsLoaded,
visibleTabCount: this.tabIds.length,
hasActiveTerminalHandle: this.activeHandle !== null,
createInFlight: false,
sawSessionTabs: this.sawSessionTabs,
autoCreatedForWorktree: this.autoCreatedForWorktree
})
}
}
describe('shouldAutoCreateInitialSessionTerminal', () => {
it('creates the first terminal for a workspace that hydrates with nothing', () => {
const route = new SessionRouteState()
route.applySessionTabs([])
expect(route.gate()).toBe(true)
expect(shouldAutoCreateInitialSessionTerminal(baseInput())).toBe(true)
})
it('does not re-create after the user closes the last tab (#9717, #7345)', () => {
const route = new SessionRouteState()
route.applySessionTabs(['tab-1'])
expect(route.gate()).toBe(false)
route.closeSessionTab('tab-1')
expect(route.tabIds).toHaveLength(0)
expect(route.activeHandle).toBeNull()
// Emptiness that follows a populated list is a close, not a cold hydrate.
expect(route.gate()).toBe(false)
})
it('does not re-create after the consumed creation route remounts empty', () => {
const creationRoute = new SessionRouteState()
creationRoute.applySessionTabs([])
expect(creationRoute.gate()).toBe(true)
creationRoute.newlyCreatedWorkspace = false
creationRoute.autoCreatedForWorktree = true
creationRoute.applySessionTabs(['tab-1'])
creationRoute.closeSessionTab('tab-1')
expect(creationRoute.gate()).toBe(false)
const reopenedRoute = new SessionRouteState()
reopenedRoute.newlyCreatedWorkspace = creationRoute.newlyCreatedWorkspace
reopenedRoute.applySessionTabs([])
expect(reopenedRoute.gate()).toBe(false)
it('does not re-create once this route has shown session tabs', () => {
expect(shouldAutoCreateInitialSessionTerminal(baseInput({ sawSessionTabs: true }))).toBe(false)
})
it('does not create from stale empty state while switching ordinary workspace routes', () => {
@@ -109,13 +41,6 @@ describe('shouldAutoCreateInitialSessionTerminal', () => {
).toBe(false)
})
it('does not re-create when the host drops every tab mid-session', () => {
const route = new SessionRouteState()
route.applySessionTabs(['tab-1', 'tab-2'])
route.applySessionTabs([], null)
expect(route.gate()).toBe(false)
})
it('stays armed while an empty workspace is still loading', () => {
expect(shouldAutoCreateInitialSessionTerminal(baseInput({ tabsLoaded: false }))).toBe(false)
})
@@ -1,4 +1,3 @@
import { randomBytes } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import { hookMount, performHookAction } from '../test-support/rpc-recording/hook-mount'
import { mountFixture } from '../test-support/rpc-recording/recorder-fixture-shape'
@@ -82,7 +81,7 @@ function mountDocuments(readReply: () => Promise<RpcResponse>) {
unmount: hook.unmount,
read(id: string) {
const target = tab(id)
tabs.current = [target]
tabs.current = [...tabs.current.filter((current) => current.id !== id), target]
return performHookAction(() => readers?.readMarkdownTab(target))
},
close(id: string) {
@@ -107,15 +106,30 @@ function delayedReply() {
}
describe('mobile Markdown document lifetime', () => {
it('releases 40 MiB of clean content after twenty reads and successful closes', async () => {
const h = mountDocuments(async () => success(randomBytes(1536 * 1024).toString('base64')))
it('releases only the closed clean document and preserves other cached documents', async () => {
const readReply = vi
.fn()
.mockResolvedValueOnce(success('# a'))
.mockResolvedValueOnce(success('# b'))
.mockResolvedValueOnce(success('# c'))
const h = mountDocuments(readReply)
try {
for (let index = 0; index < 20; index += 1) {
await h.read('tab-' + index)
const doc = h.docs.get('tab-' + index)
expect(doc?.status === 'ready' && Buffer.byteLength(doc.content)).toBe(2 * 1024 * 1024)
await h.close('tab-' + index)
}
await h.read('a')
await h.read('b')
await h.read('c')
expect(h.docs.get('a')).toMatchObject({ status: 'ready', content: '# a' })
expect(h.docs.get('b')).toMatchObject({ status: 'ready', content: '# b' })
expect(h.docs.get('c')).toMatchObject({ status: 'ready', content: '# c' })
const a = h.docs.get('a')
const c = h.docs.get('c')
await h.close('b')
expect([...h.docs.keys()]).toEqual(['a', 'c'])
expect(h.docs.get('a')).toBe(a)
expect(h.docs.get('c')).toBe(c)
await h.close('a')
expect([...h.docs.keys()]).toEqual(['c'])
expect(h.docs.get('c')).toBe(c)
await h.close('c')
expect(h.docs.size).toBe(0)
} finally {
h.unmount()
@@ -12,19 +12,28 @@ const readOnlyDoc = {
}
describe('markdownReaderStatusText', () => {
it('names a truncated preview and the full file size the way file tabs do', () => {
it('names a truncated preview with or without the full file size', () => {
expect(
markdownReaderStatusText({ ...readOnlyDoc, truncated: true, byteLength: 3_000_000 })
).toBe('Preview truncated. File size: 2.9 MB.')
expect(markdownReaderStatusText({ ...readOnlyDoc, truncated: true })).toBe('Preview truncated.')
})
it('keeps read-only, stale, and save-error states as before', () => {
it('shows read-only, stale and save-error states, and no status for a clean editable document', () => {
expect(
markdownReaderStatusText({ ...readOnlyDoc, editable: true, readOnlyReason: undefined })
).toBeNull()
expect(markdownReaderStatusText(readOnlyDoc)).toBe('Read only')
expect(
markdownReaderStatusText({ ...readOnlyDoc, readOnlyReason: undefined, stale: true })
).toBe('Changed on desktop')
expect(markdownReaderStatusText({ ...readOnlyDoc, saveError: 'Save failed' })).toBe(
'Save failed'
)
expect(
markdownReaderStatusText({
...readOnlyDoc,
editable: true,
readOnlyReason: undefined,
saveError: 'Save failed'
})
).toBe('Save failed')
})
})
@@ -13,7 +13,7 @@ function clientWith(sendRequest: RpcClient['sendRequest']): Pick<RpcClient, 'sen
}
describe('mobile session tab activation', () => {
it('retries terminal focus once on the authenticated replacement after cutover', async () => {
it('retries terminal focus once after cutover', async () => {
const sendRequest = vi
.fn<RpcClient['sendRequest']>()
.mockRejectedValueOnce(new LogicalClientCutoverError())
@@ -17,6 +17,12 @@ describe('runAcceptedMobileSessionTabsEffects', () => {
const activateBrowserTab = vi.fn()
const options = {
effectiveTabs: [
{
id: 'browser-other',
type: 'browser' as const,
isActive: false,
browserPageId: 'page-other'
},
{
id: 'browser-1',
type: 'browser' as const,
@@ -39,7 +45,12 @@ describe('runAcceptedMobileSessionTabsEffects', () => {
runAcceptedMobileSessionTabsEffects(options)
expect(pendingPageId).toBeNull()
expect(activateBrowserTab).toHaveBeenCalledTimes(1)
expect(activateBrowserTab).toHaveBeenCalledExactlyOnceWith({
id: 'browser-1',
type: 'browser',
isActive: true,
browserPageId: 'page-1'
})
}
)
@@ -86,7 +97,12 @@ describe('runAcceptedMobileSessionTabsEffects', () => {
runAcceptedMobileSessionTabsEffects({
...base,
effectiveTabs: [markdown],
effectiveTabs: [
{ ...markdown, id: 'browser-dirty', type: 'browser' },
{ ...markdown, id: 'markdown-inactive', isActive: false },
{ ...markdown, id: 'markdown-clean', isDirty: false },
markdown
],
source: 'stream'
})
expect(markActiveMarkdownStale).toHaveBeenCalledExactlyOnceWith('markdown-1')
@@ -240,6 +240,13 @@ describe('shouldActivateOpenedMobileSessionTab', () => {
activeTerminalHandle: null
})
).toBe(false)
expect(
shouldActivateOpenedMobileSessionTab({
...currentTerminalState,
activated: false,
activeTerminalHandle: 'pty-2'
})
).toBe(false)
})
it('allows a structured agent-session tab to anchor chat file activation', () => {
@@ -255,5 +262,16 @@ describe('shouldActivateOpenedMobileSessionTab', () => {
activeTabType: 'agent-session'
})
).toBe(true)
expect(
shouldActivateOpenedMobileSessionTab({
...currentTerminalState,
activated: false,
sourceTerminalHandle: null,
activeTerminalHandle: null,
sourceSessionTabId: 'agent-tab-1',
activeSessionTabId: 'agent-tab-2',
activeTabType: 'agent-session'
})
).toBe(false)
})
})
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { MobileSessionTab } from './mobile-session-route-types'
import {
LAUNCHED_SELECTION_SNAPSHOT_BUDGET,
isLaunchedSurfaceListed,
launchedSelection,
pendingSelectionHandle,
@@ -101,13 +100,13 @@ describe('resolveLaunchedSelection', () => {
it('waits without a countdown until the reply, then gives up after the budget', () => {
let selection: PendingSessionSelection | null = launchedSelection('l1', { pane: PANE }, null)
for (let snapshot = 0; snapshot < LAUNCHED_SELECTION_SNAPSHOT_BUDGET * 2; snapshot += 1) {
for (let snapshot = 0; snapshot < 10; snapshot += 1) {
selection = resolveLaunchedSelection(selection, []).selection
}
expect(selection).toEqual(launchedSelection('l1', { pane: PANE }, null))
selection = withLaunchReply(selection, 'l1', { handle: 'term_9' })
for (let snapshot = 1; snapshot < LAUNCHED_SELECTION_SNAPSHOT_BUDGET; snapshot += 1) {
for (let snapshot = 1; snapshot < 5; snapshot += 1) {
const next = resolveLaunchedSelection(selection, [chatTab('other', 'claude_other')])
expect(next.landedTabId).toBeNull()
expect(next.selection?.kind).toBe('launched')
@@ -10,6 +10,7 @@ const native = vi.hoisted(() => {
platform,
remove,
dismiss: vi.fn(),
setLeaveDrafts: vi.fn(),
addEventListener: vi.fn((_event: string, _handler: () => boolean) => ({ remove }))
}
})
@@ -67,7 +68,7 @@ function scopeWith(markdownDocs: Map<string, MarkdownDocState>): MobileSessionMa
setMarkdownDocs: () => {},
discardMarkdownTarget: null,
setDiscardMarkdownTarget: () => {},
setLeaveDrafts: () => {},
setLeaveDrafts: native.setLeaveDrafts,
markdownSaveSeqRef: { current: new Map() },
markdownSaveInFlightRef: { current: new Set() },
showToast: () => {},
@@ -107,6 +108,7 @@ beforeEach(() => {
native.addEventListener.mockClear()
native.remove.mockClear()
native.dismiss.mockClear()
native.setLeaveDrafts.mockClear()
leaves.back = null
leaves.replaced = []
})
@@ -166,6 +168,9 @@ describe("the session's hardware back gate", () => {
})
expect(left).not.toHaveBeenCalled()
expect(native.dismiss).toHaveBeenCalledTimes(1)
expect(native.setLeaveDrafts).toHaveBeenCalledExactlyOnceWith([
{ tabId: 'tab-1', title: 'Markdown', content: 'edited' }
])
})
it('registers once as the drafts change, and the newest drafts are what a press reads', () => {
@@ -88,6 +88,10 @@ describe('applyClosedTabTombstones', () => {
it('stops suppressing and clears an expired tombstone even if still present', () => {
const tombstones = new Map([['a', 5000]])
expect(
applyClosedTabTombstones([tab('a'), tab('b')], tombstones, 4999).map((t) => t.id)
).toEqual(['b'])
expect(tombstones.has('a')).toBe(true)
const result = applyClosedTabTombstones([tab('a'), tab('b')], tombstones, 5000)
expect(result.map((t) => t.id)).toEqual(['a', 'b'])
expect(tombstones.has('a')).toBe(false)
@@ -126,6 +126,9 @@ describe("a launch pane's verdict in the window", () => {
it('never touches a pane no launch laid out', () => {
apply({ kind: 'withdrawn' }, '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d')
expect(launchTab()).toBeDefined()
expect(launchTab()?.agentLaunchPane).toEqual({ leafId: LEAF, operationId: 'op-1' })
expect(agentLaunchPanePrompt(tabId)).toBe('fix the build')
expect(applyClosedTerminalLeafNotice).not.toHaveBeenCalled()
})
})
@@ -227,12 +227,13 @@ describe("a new tab during startup, before this computer's runtime answered", ()
it.each(['claude', 'opencode'] as const)(
'opens the %s chat when the capabilities arrive within the wait, after a failed probe',
async (agent) => {
vi.useFakeTimers()
const route = awaitingRoute(agent)
const relaunch = fakeRelaunch()
const result = launchOnceHostAnswered(route, { ...args, agent }, startupPlan, relaunch)
// The failed probe has answered; the launch is still waiting for one that lands.
await new Promise((resolve) => setTimeout(resolve, 20))
await vi.advanceTimersByTimeAsync(20)
expect(relaunch).not.toHaveBeenCalled()
await capabilitiesLand()
@@ -240,6 +241,7 @@ describe("a new tab during startup, before this computer's runtime answered", ()
kind: 'structured',
sessionId: 'session-1'
})
expect(relaunch).toHaveBeenCalledOnce()
expect(relaunch.mock.calls[0]?.[0]).toMatchObject({
agentSessionLaunchPlan: { route: 'structured-native-chat' }
})
@@ -94,19 +94,37 @@ describe('agentSessionFingerprintConflict', () => {
describe('admitAgentSessionMutation', () => {
const base = { envelope: envelope(), hostFingerprint: 'f'.repeat(64), lease: LEASE }
it('admits a first-time operation under a live lease at the expected fence', () => {
it('admits a matching first-time operation and refuses a mismatched payload', () => {
expect(admitAgentSessionMutation({ ...base, ledger: ADMIT('f'.repeat(64)) }).decision).toBe(
'admit'
)
expect(
admitAgentSessionMutation({
...base,
hostFingerprint: 'a'.repeat(64),
ledger: ADMIT('a'.repeat(64))
})
).toMatchObject({
decision: 'refused',
refusal: {
code: 'agent_session_operation_conflict',
details: { reason: 'fingerprintMismatch' }
}
})
})
it('replays a recorded operation without re-checking the fence', () => {
it('replays a recorded outcome even while the lease is unreconciled', () => {
const recorded = {
...row('f'.repeat(64)),
outcome: { status: 'succeeded' as const, sessionId: 'created-session' }
}
const admission = admitAgentSessionMutation({
...base,
envelope: envelope({ expectedRuntimeFence: 1 }),
ledger: { decision: 'replay', row: row('f'.repeat(64)) }
lease: { ...LEASE, unreconciled: true },
ledger: { decision: 'replay', row: recorded }
})
expect(admission.decision).toBe('replay')
expect(admission).toEqual({ decision: 'replay', row: recorded })
})
it('admits a writer whatever fence the client last saw', () => {
@@ -156,6 +156,7 @@ describe('retention', () => {
const late = agentSessionOperationExpiry(NOW + 10_000, NOW)
expect(late).toBe(agentSessionOperationExpiry(NOW + 10_000, NOW + 10_000))
expect(late).toBeGreaterThan(agentSessionOperationExpiry(NOW, NOW))
expect(agentSessionOperationExpiry(NOW, NOW + 10_000)).toBe(late)
})
it('drops only rows past their own expiry', () => {
+6 -2
View File
@@ -71,11 +71,15 @@ describe('current thread goal', () => {
it('takes the latest transition by sequence for items held unordered', () => {
const paused = goal({ status: 'paused' })
const latest = goalRow(5, 'set', paused)
const unordered = new Map([
['b', goalRow(5, 'set', paused)],
['b', latest],
['a', goalRow(2, 'set')]
])
expect(currentAgentSessionThreadGoalSourceBySequence(unordered.values())?.goal).toEqual(paused)
expect(currentAgentSessionThreadGoalSourceBySequence(unordered.values())).toEqual({
item: latest,
goal: paused
})
expect(currentAgentSessionThreadGoalSourceBySequence([])?.goal).toBeUndefined()
})
@@ -165,20 +165,6 @@ describe('a refusal a failed request carried in its error', () => {
})
})
it("keeps a journal refusal's reason", () => {
const corrupt = {
code: 'agent_session_journal_unreadable',
details: { reason: 'journalCorrupt' }
}
// Not "Orca couldn't confirm what happened": the host refused it before running it.
expect(
agentSessionThrownFailure(saved({ ...payload, data: { refusal: corrupt } }), 'runtime_error')
).toEqual({
kind: 'refused',
...corrupt
})
})
it("degrades a reason another build added to the code's own words", () => {
const newer = {
code: 'agent_session_journal_unreadable',