diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 56c73bbf756..1cba1e9f13d 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -14406,7 +14406,7 @@ "providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "Deterministic main/renderer tests run on macOS and exercise simulated Windows plus POSIX/Linux process-confirmation behavior. They cover exact local-ConPTY membership, detached-child rejection, SSH and paired-runtime host-platform routing including legacy runtime PTY IDs, active-PTY ownership after worktree host changes, Windows-to-WSL routing, unknown-metadata fallback, production handler composition, KKP authorization on every host, process-confirmed Droid routing and launch-triggered confirmation, typed-alias and forged-OSC isolation, shells without OSC 133, split/detach ownership with preserved shell classification, stale PTY exit/rebind rejection, command-generation revocation, daemon v21 warm-reattach identity, unavailable inspection, and lazy subprocess/RPC callback counts. A live linux-arm64 Docker target additionally proved real relay deployment, SSH PTY host detection, and exact inactive/active KKP bytes. The Electron byte test runs only on Windows; live Windows evidence remains uncollected. Test failures report exact expected bytes; no product telemetry or raw terminal logging is added. Protocol-version constant inventories are excluded from handshake and foreground-confirmation evidence. The test-value audit narrows store-active-worktree-terminal-creation.test.ts to selected same-ID local/SSH folder creation and owner-context propagation plus split-survivor relay-drop/orphan protection. Immediate detach PTY/relay/tab-map synchronization, plain root-group inventory, quick-command/startup-cwd field normalization, and Windows default/project-runtime/UNC/SSH/Git-Bash shell-metadata examples are intentionally removed. Those direct state and metadata alarms are lost; remaining cases are not equivalent replacements. Historical executed commands, counts and results remain unchanged; no new shell-spawn, input-byte, live platform, remote or mobile evidence is claimed.", + "coverageNotes": "Deterministic main/renderer tests run on macOS and exercise simulated Windows plus POSIX/Linux process-confirmation behavior. They cover exact local-ConPTY membership, detached-child rejection, SSH and paired-runtime host-platform routing including legacy runtime PTY IDs, active-PTY ownership after worktree host changes, Windows-to-WSL routing, unknown-metadata fallback, production handler composition, KKP authorization on every host, process-confirmed Droid routing and launch-triggered confirmation, typed-alias and forged-OSC isolation, shells without OSC 133, split/detach ownership with preserved shell classification, stale PTY exit/rebind rejection, command-generation revocation, daemon v21 warm-reattach identity, unavailable inspection, and lazy subprocess/RPC callback counts. A live linux-arm64 Docker target additionally proved real relay deployment, SSH PTY host detection, and exact inactive/active KKP bytes. The Electron byte test runs only on Windows; live Windows evidence remains uncollected. Test failures report exact expected bytes; no product telemetry or raw terminal logging is added. Protocol-version constant inventories are excluded from handshake and foreground-confirmation evidence. The test-value audit narrows store-active-worktree-terminal-creation.test.ts to selected same-ID local/SSH folder creation and owner-context propagation plus split-survivor relay-drop/orphan protection. Immediate detach PTY/relay/tab-map synchronization, plain root-group inventory, quick-command/startup-cwd field normalization, and Windows default/project-runtime/UNC/SSH/Git-Bash shell-metadata examples are intentionally removed. Those direct state and metadata alarms are lost; remaining cases are not equivalent replacements. Historical executed commands, counts and results remain unchanged; no new shell-spawn, input-byte, live platform, remote or mobile evidence is claimed. The test-value audit removes the direct Windows installed-path case for Droid npm cmd basename recognition. That exact unit alarm is lost; retained command-line recognition cases are not equivalent replacements. The direct Droid recognition assertion remains a deferred obligation. Historical evidence and broader process-confirmation/input-routing obligations remain unchanged; no new executed coverage, owner-gap closure or credit is claimed.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/7620", "https://github.com/stablyai/orca/pull/7668" @@ -14617,7 +14617,7 @@ { "file": "src/shared/agent-process-recognition.test.ts", "assertions": [ - "Droid's process name resolves to the allowlisted Droid agent identity without substring matching" + "Deferred unit obligation after removal of the direct Windows basename case: Droid's process name resolves to the allowlisted Droid agent identity without substring matching. Retained cases are not equivalent coverage." ] }, { @@ -14988,7 +14988,7 @@ "providers": ["local", "daemon", "ssh", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local"], - "coverageNotes": "Local macOS evidence covers the runtime PTY write contract, live Codex orchestration, a dev-runtime CLI repro, and Claude and Codex composer traces. A deterministic worker-start RPC/SQLite contract proves delayed Codex composer acceptance, exactly one submitted turn, durable Dispatch success, and explicit stalled failure when Enter is swallowed without a rescue send. The render gate stays on the PTY-owning runtime, so paired and SSH callers use the same provider write/output path without a wire change. SSH, daemon, remote-runtime, Linux, and Windows live journeys remain gaps.", + "coverageNotes": "Local macOS evidence covers the runtime PTY write contract, live Codex orchestration, a dev-runtime CLI repro, and Claude and Codex composer traces. A deterministic worker-start RPC/SQLite contract proves delayed Codex composer acceptance, exactly one submitted turn, durable Dispatch success, and explicit stalled failure when Enter is swallowed without a rescue send. The render gate stays on the PTY-owning runtime, so paired and SSH callers use the same provider write/output path without a wire change. SSH, daemon, remote-runtime, Linux, and Windows live journeys remain gaps. The test-value audit intentionally removes these direct unit alarms: Only OMP joins submit to paste frame. Exact bracketed multiline prompt frame. Claude/team/unknown lead-line positives and Codex/OpenCode negatives. DSB generic lead/submit/delay fallback on three platform strings. 2KB/8KB Windows and 8KB Darwin delay below700ms. 320KB ingest estimate greater than1.4x3342ms. 145KB delay greater than1500ms. Darwin8KB <=2ms, zero/NaN Linux and negative Windows return zero. 400KB greater than40KB across platforms and Windows slower than Darwin. Antigravity100-line settle addition and Aider one-line delta. Retained unit cases still assert safe typed-lead normalization, pinned measured delay floors and ceiling/budget bounds, ESC sanitization, exact framed UTF-8 chunk reassembly, and held-draft discrimination. They are not equivalent replacements for the removed unit alarms. The OMP-versus-Claude/Codex/unidentified-agent submit-join predicate obligation remains deferred. Runtime PTY-writer, worker-start and live evidence above are unchanged; no runtime-writer coverage loss, new execution, owner-gap closure or credit is claimed.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-3800", "https://linear.app/stably/issue/STA-3904", @@ -15017,7 +15017,7 @@ "file": "src/shared/agent-prompt-injection.test.ts", "assertions": [ "agent prompts are always framed as bracketed paste", - "submit stays separate from the paste frame", + "Deferred unit obligation: OMP joins submit to the paste frame while Claude, Codex and unidentified agents keep submit separate; the direct predicate table was removed. Runtime-writer coverage is unchanged.", "embedded ESC bytes are made inert before framing", "chunk reconstruction preserves the paste frame" ] diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 14e343793c1..386d0f4cc57 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -11607,13 +11607,9 @@ "src/shared/agent-launch-operation.test.ts": 176, "src/shared/agent-launch-picker-legacy-permission.test.ts": 299, "src/shared/agent-launch-picker.test.ts": 303, - "src/shared/agent-launch-preferences.test.ts": 193, - "src/shared/agent-launch-remote.test.ts": 41, "src/shared/agent-launch-settings-auto-projection.test.ts": 219, "src/shared/agent-launch-settings-permissions.test.ts": 199, "src/shared/agent-lead-status-fold.test.ts": 59, - "src/shared/agent-main-agent-verdict.test.ts": 55, - "src/shared/agent-notification-id.test.ts": 46, "src/shared/agent-pending-prompt.test.ts": 99, "src/shared/agent-process-presence-platform.test.ts": 64, "src/shared/agent-process-presence-probe.test.ts": 88, @@ -11626,9 +11622,7 @@ "src/shared/agent-session-handoff-context.test.ts": 216, "src/shared/agent-session/agent-message-display-text.test.ts": 147, "src/shared/agent-session/agent-session-approval-blocked-path.test.ts": 49, - "src/shared/agent-session/agent-session-approval-subject.test.ts": 112, "src/shared/agent-session/agent-session-attachments.test.ts": 46, - "src/shared/agent-session/agent-session-background-task-state-admission.test.ts": 49, "src/shared/agent-session/agent-session-context-clear.test.ts": 222, "src/shared/agent-session/agent-session-conversation-name.test.ts": 71, "src/shared/agent-session/agent-session-message-source.test.ts": 166, @@ -11643,17 +11637,11 @@ "src/shared/agent-session/failures/agent-session-wire-refusals.test.ts": 144, "src/shared/agent-session/failures/agent-session-write-failure.test.ts": 133, "src/shared/agent-session/failures/structured-agent-session-rejection-words.test.ts": 167, - "src/shared/agent-session/journal/agent-session-journal-body-admission.test.ts": 109, - "src/shared/agent-session/journal/agent-session-journal-producer.test.ts": 42, - "src/shared/agent-session/lifecycle/agent-session-account-home.test.ts": 163, "src/shared/agent-session/lifecycle/agent-session-lease-adjudication.test.ts": 66, - "src/shared/agent-session/lifecycle/agent-session-legacy-handoff-lease.test.ts": 157, "src/shared/agent-session/lifecycle/agent-session-mutation-envelope.test.ts": 170, "src/shared/agent-session/lifecycle/agent-session-operation-ledger.test.ts": 169, "src/shared/agent-session/lifecycle/agent-session-provider-handle.test.ts": 90, "src/shared/agent-session/lifecycle/agent-session-record-stored-form.test.ts": 192, - "src/shared/agent-session/lifecycle/agent-session-resume-cursor.test.ts": 219, - "src/shared/agent-session/lifecycle/agent-session-resume.test.ts": 76, "src/shared/agent-session/lifecycle/agent-session-sign-in.test.ts": 144, "src/shared/agent-session/lifecycle/agent-session-stopped-history.test.ts": 164, "src/shared/agent-session/lifecycle/agent-session-stored-agent.test.ts": 160, @@ -11665,7 +11653,6 @@ "src/shared/agent-session/options/agent-session-option-catalog-removers.test.ts": 199, "src/shared/agent-session/options/agent-session-option-catalog.test.ts": 210, "src/shared/agent-session/options/structured-agent-session-model-catalog-application.test.ts": 235, - "src/shared/agent-session/options/structured-agent-session-option-pick-book.test.ts": 37, "src/shared/agent-session/options/structured-agent-session-option-picks.test.ts": 211, "src/shared/agent-session/options/structured-agent-session-option-view.test.ts": 275, "src/shared/agent-session/options/structured-agent-session-options-read-owner.test.ts": 234, @@ -11675,7 +11662,6 @@ "src/shared/agent-session/options/structured-agent-session-selection-operations.test.ts": 202, "src/shared/agent-session/requests/structured-agent-session-dispatch-rejection.test.ts": 169, "src/shared/agent-session/requests/structured-agent-session-latest-request.test.ts": 218, - "src/shared/agent-session/requests/structured-agent-session-opening-send.test.ts": 111, "src/shared/agent-session/requests/structured-agent-session-send-evidence.test.ts": 148, "src/shared/agent-session/structured-agent-session-child-work-legacy.test.ts": 47, "src/shared/agent-session/structured-agent-session-coalescer.test.ts": 387, diff --git a/src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx b/src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx index 92b181dd82f..e69fe9af914 100644 --- a/src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx @@ -56,12 +56,11 @@ function renderHeader(handOff?: NativeChatHeaderHandOff, focused = true) { return source } -it('offers the terminal header action order and targets Resume and Close at the displayed chat', () => { +it('offers chat actions without a duplicate close button and targets Resume at the displayed chat', () => { const source = renderHeader() expect(screen.getAllByRole('button').map((button) => button.getAttribute('aria-label'))).toEqual([ 'Continue with Terminal Agent', - 'Split Chat Right', - 'Close tab' + 'Split Chat Right' ]) const folder = useAppStore.getState().folderWorkspaces[0] act(() => useAppStore.setState({ folderWorkspaces: [{ ...folder, executionHostId: 'ssh:dev' }] })) @@ -80,11 +79,6 @@ it('offers the terminal header action order and targets Resume and Close at the agent: 'codex' }) ) - fireEvent.click(screen.getByRole('button', { name: 'Close tab' })) - expect(mocks.close).toHaveBeenCalledExactlyOnceWith({ - type: 'close', - target: { kind: 'tab', worktreeId: source.worktreeId, tabId: source.id } - }) }) async function splitTooltipText(): Promise { diff --git a/src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx b/src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx index 8123e70b2e6..4a603b9db6d 100644 --- a/src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx +++ b/src/renderer/src/components/native-chat/NativeChatPaneHeader.tsx @@ -1,5 +1,5 @@ import { Fragment } from 'react' -import { MessageSquarePlus, SquareSplitVertical, X } from 'lucide-react' +import { MessageSquarePlus, SquareSplitVertical } from 'lucide-react' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -119,27 +119,6 @@ export function NativeChatPaneHeader({ - {actions.canClosePane ? ( - - - - - - {actions.closeLabel} - - - ) : null} ) diff --git a/src/renderer/src/components/native-chat/native-chat-header-ownership.test.tsx b/src/renderer/src/components/native-chat/native-chat-header-ownership.test.tsx index cf7f549ab75..ffe4472884a 100644 --- a/src/renderer/src/components/native-chat/native-chat-header-ownership.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-header-ownership.test.tsx @@ -145,7 +145,7 @@ it.each([10, 100])( const view = render(overlay(true)) const retained = chat(view.container, 'chat-0').querySelector('output') const unfocused = chat(view.container, `chat-${size - 1}`) - expect(within(unfocused).getByRole('button', { name: 'Close Pane' })).toBeTruthy() + expect(within(unfocused).queryByRole('button', { name: 'Close Pane' })).toBeNull() for (const operation of [ () => useAppStore.getState().setTabLabel('chat-0', 'Changed title'), () => useAppStore.getState().activateTab('chat-1', { worktreeId: workspace }) @@ -174,11 +174,7 @@ it.each([10, 100])( const shown = chat(view.container, 'chat-1') expect(within(shown).queryByRole('button', { name: 'Close tab' })).toBeNull() act(() => useAppStore.getState().unpinTab('chat-1', { recordInteraction: false })) - fireEvent.click(within(shown).getByRole('button', { name: 'Close tab' })) - expect(mocks.close).toHaveBeenCalledExactlyOnceWith({ - type: 'close', - target: { kind: 'tab', worktreeId: workspace, tabId: 'chat-1' } - }) + expect(within(shown).queryByRole('button', { name: 'Close tab' })).toBeNull() expect(chat(view.container, 'chat-0').querySelector('output')).toBe(retained) expect(fullRequest).not.toHaveBeenCalled() } diff --git a/src/renderer/src/store/slices/agent-status/agent-status-provider-session.test.ts b/src/renderer/src/store/slices/agent-status/agent-status-provider-session.test.ts index d4b6ae65bf0..cdb3bee787b 100644 --- a/src/renderer/src/store/slices/agent-status/agent-status-provider-session.test.ts +++ b/src/renderer/src/store/slices/agent-status/agent-status-provider-session.test.ts @@ -338,7 +338,7 @@ it('retains recorded Jcode session-start identity without a status row', () => { const store = createTestStore() seedStore(store, { tabsByWorktree: { 'wt-1': [makeTab({ id: 'tab-1', worktreeId: 'wt-1' })] } }) const paneKey = 'tab-1:11111111-1111-4111-8111-111111111111' - // Jcode 0.87.1 captured input; agent-hook-listener-jcode.test.ts pins provenance. + // Jcode 0.87.1 captured input. const payload = { hook_event_name: 'session_start', event: 'session_start', diff --git a/src/shared/agent-hook-listener-extraction-characterization.test.ts b/src/shared/agent-hook-listener-extraction-characterization.test.ts index aada0f643d1..1f5a835edab 100644 --- a/src/shared/agent-hook-listener-extraction-characterization.test.ts +++ b/src/shared/agent-hook-listener-extraction-characterization.test.ts @@ -1,91 +1,21 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { normalizeHookPayload } from './agent-hook-listener' -import type { AgentHookSource } from './agent-hook-relay' + import { clearAllListenerCaches, - clearPaneCacheState, createHookListenerState, - movePaneCacheState, - seedLegacyAgentStatusForTests + movePaneCacheState } from './agent-hook-listener/listener-state' import { warnOnHookEnvOrVersionMismatch } from './agent-hook-listener/listener-limits' -import { resolveHookSource } from './agent-hook-listener/source-routing' +import './agent-hook-listener/source-routing' import { bindOpenCodeSession } from './agent-hook-listener/opencode-session-registry' import { makePaneKey } from './stable-pane-id' const PANE = makePaneKey('tab-hooks', '11111111-1111-4111-8112-111111111111') const MOVED_PANE = makePaneKey('tab-hooks', '22222222-2222-4222-8222-222222222222') -function normalizeProviderState( - source: AgentHookSource, - eventName: string, - payload: Record = {} -) { - const routedSource = resolveHookSource(`/hook/${source}`) - if (routedSource === null) { - return null - } - return ( - normalizeHookPayload( - createHookListenerState(), - routedSource, - { paneKey: PANE, hook_event_name: eventName, payload }, - 'production' - )?.payload ?? null - ) -} afterEach(() => vi.restoreAllMocks()) describe('agent hook extraction boundaries', () => { - it.each([ - ['qoder', 'UserPromptSubmit'], - ['qoder-cn', 'UserPromptSubmit'], - ['qwen-code', 'UserPromptSubmit'], - ['codebuddy', 'UserPromptSubmit'], - ['opencode2', 'SessionBusy'], - ['muse', 'UserPromptSubmit'], - ['zcode', 'UserPromptSubmit'], - ['dsh', 'UserPromptSubmit'], - ['kiro', 'userPromptSubmit'] - ] as const)('normalizes %s hook traffic through its routed source', (source, eventName) => { - expect(normalizeProviderState(source, eventName, { prompt: 'routed prompt' })).toMatchObject({ - state: 'working', - agentType: source, - prompt: 'routed prompt' - }) - }) - - it('rejects unknown and malformed hook paths', () => { - expect(resolveHookSource('/hook/unknown')).toBeNull() - expect(resolveHookSource('/hook/devin/')).toBeNull() - expect(resolveHookSource('/HOOK/DEVIN')).toBeNull() - }) - - it('preserves thin provider lifecycle branches and source attribution', () => { - const cases = [ - ['devin', 'UserPromptSubmit', 'working'], - ['devin', 'PermissionRequest', 'waiting'], - ['devin', 'Stop', 'done'], - ['mimo-code', 'MessagePart', 'working'], - ['mimo-code', 'AskUserQuestion', 'waiting'], - ['prime-agent', 'before_agent_start', 'working'], - ['prime-agent', 'agent_end', 'done'], - ['kimi', 'PermissionRequest', 'waiting'], - ['kimi', 'StopFailure', 'done'], - ['hermes', 'on_session_finalize', 'done'], - ['hermes', 'on_session_reset', 'done'] - ] as const - - for (const [source, eventName, expectedState] of cases) { - expect(normalizeProviderState(source, eventName)).toMatchObject({ - state: expectedState, - agentType: source - }) - } - for (const source of ['devin', 'mimo-code', 'prime-agent', 'kimi', 'hermes'] as const) { - expect(normalizeProviderState(source, 'UnknownEvent')).toBeNull() - } - }) - it('warns before tab rejection and bounds distinct version and environment warnings', () => { const orderState = createHookListenerState() const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) @@ -136,78 +66,6 @@ describe('agent hook extraction boundaries', () => { expect(warn).toHaveBeenCalledTimes(64) }) - it('moves selected pane-owned collections and preserves NUL suffixes', () => { - const state = createHookListenerState() - const scoped = `${PANE}\0child` - const movedScoped = `${MOVED_PANE}\0child` - const sibling = `${PANE}-sibling` - const paneMaps = [ - state.lastPromptByPaneKey, - state.lastToolByPaneKey, - state.antigravityCompletedTranscriptByPaneKey, - state.claudeSubagentRosterByPaneKey, - state.claudeLeadStateByPaneKey, - state.codexSubagentRosterByPaneKey, - state.codexSubagentTranscriptByPaneKey, - state.codexLeadStateByPaneKey, - state.grokActiveTurnByPaneKey - ] - for (const map of paneMaps) { - const cache = map as Map - cache.set(PANE, 'exact') - cache.set(scoped, 'scoped') - cache.set(sibling, 'sibling') - } - for (const [paneKey, prompt] of [ - [PANE, 'exact'], - [scoped, 'scoped'], - [sibling, 'sibling'] - ]) { - seedLegacyAgentStatusForTests(state, { - paneKey, - connectionId: null, - payload: { state: 'working', prompt } - }) - } - const paneSets = [ - state.ampCompletedCacheKeys, - state.claudeUnconfirmedRestoredStatusPaneKeys, - state.claudeRunningNonAgentTaskPaneKeys, - state.claudeActiveSessionCronPaneKeys - ] - for (const set of paneSets) { - set.add(PANE) - set.add(scoped) - set.add(sibling) - } - - movePaneCacheState(state, PANE, MOVED_PANE) - - for (const map of paneMaps) { - const cache = map as Map - expect(cache.get(MOVED_PANE)).toBe('exact') - expect(cache.get(movedScoped)).toBe('scoped') - expect(cache.get(sibling)).toBe('sibling') - expect(cache.has(PANE)).toBe(false) - expect(cache.has(scoped)).toBe(false) - } - expect(state.lastStatusByPaneKey.get(MOVED_PANE)?.payload.prompt).toBe('exact') - expect(state.lastStatusByPaneKey.get(movedScoped)?.payload.prompt).toBe('scoped') - expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling') - expect(state.lastStatusByPaneKey.has(PANE)).toBe(false) - expect(state.lastStatusByPaneKey.has(scoped)).toBe(false) - for (const set of paneSets) { - expect(set.has(MOVED_PANE)).toBe(true) - expect(set.has(movedScoped)).toBe(true) - expect(set.has(sibling)).toBe(true) - expect(set.has(PANE)).toBe(false) - expect(set.has(scoped)).toBe(false) - } - - movePaneCacheState(state, MOVED_PANE, MOVED_PANE) - expect((state.lastPromptByPaneKey as Map).get(MOVED_PANE)).toBe('exact') - }) - it('preserves bound OpenCode session ownership and launch identity after a pane move', () => { const state = createHookListenerState() normalizeHookPayload( @@ -242,81 +100,6 @@ describe('agent hook extraction boundaries', () => { expect(state.lastLaunchTokenByPaneKey.has(PANE)).toBe(false) }) - it('clears exact and NUL-scoped cache keys but not sibling prefixes', () => { - const state = createHookListenerState() - const scoped = `${PANE}\0thread` - const sibling = `${PANE}-sibling` - const paneMaps = [ - state.lastPromptByPaneKey, - state.lastToolByPaneKey, - state.antigravityCompletedTranscriptByPaneKey - ] - for (const map of paneMaps) { - const cache = map as Map - cache.set(PANE, 'exact') - cache.set(scoped, 'scoped') - cache.set(sibling, 'sibling') - } - for (const [paneKey, prompt] of [ - [PANE, 'exact'], - [scoped, 'scoped'], - [sibling, 'sibling'] - ]) { - seedLegacyAgentStatusForTests(state, { - paneKey, - connectionId: null, - payload: { state: 'working', prompt } - }) - } - state.ampCompletedCacheKeys.add(PANE) - state.ampCompletedCacheKeys.add(scoped) - state.ampCompletedCacheKeys.add(sibling) - state.claudeLeadStateByPaneKey.set(PANE, { state: 'working', stateStartedAt: 1 }) - state.codexLeadStateByPaneKey.set(PANE, { state: 'working', stateStartedAt: 1 }) - state.grokActiveTurnByPaneKey.set(PANE, { promptId: 'prompt-1' }) - - clearPaneCacheState(state, PANE) - - for (const map of paneMaps) { - const cache = map as Map - expect(cache.has(PANE)).toBe(false) - expect(cache.has(scoped)).toBe(false) - expect(cache.get(sibling)).toBe('sibling') - } - expect(state.lastStatusByPaneKey.has(PANE)).toBe(false) - expect(state.lastStatusByPaneKey.has(scoped)).toBe(false) - expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling') - expect(state.ampCompletedCacheKeys.has(scoped)).toBe(false) - expect(state.ampCompletedCacheKeys.has(PANE)).toBe(false) - expect(state.ampCompletedCacheKeys.has(sibling)).toBe(true) - expect(state.claudeLeadStateByPaneKey.has(PANE)).toBe(false) - expect(state.codexLeadStateByPaneKey.has(PANE)).toBe(false) - expect(state.grokActiveTurnByPaneKey.has(PANE)).toBe(false) - }) - - it('preserves cache mutation from a provider reset that emits no row', () => { - const state = createHookListenerState() - state.lastPromptByPaneKey.set(PANE, 'old prompt') - state.lastToolByPaneKey.set(PANE, { toolName: 'old tool' }) - seedLegacyAgentStatusForTests(state, { - paneKey: PANE, - connectionId: null, - payload: { state: 'working', prompt: 'old status' } - }) - - const event = normalizeHookPayload( - state, - 'droid', - { paneKey: PANE, payload: { hook_event_name: 'SessionStart' } }, - 'production' - ) - - expect(event).toBeNull() - expect(state.lastPromptByPaneKey.has(PANE)).toBe(false) - expect(state.lastToolByPaneKey.has(PANE)).toBe(false) - expect(state.lastStatusByPaneKey.has(PANE)).toBe(true) - }) - it('clears warning, provider, and lifecycle caches together', () => { const state = createHookListenerState() state.warnedVersions.add('old-version') diff --git a/src/shared/agent-hook-listener-grok-completion.test.ts b/src/shared/agent-hook-listener-grok-completion.test.ts index c234b6a15b8..a922732aa73 100644 --- a/src/shared/agent-hook-listener-grok-completion.test.ts +++ b/src/shared/agent-hook-listener-grok-completion.test.ts @@ -69,113 +69,6 @@ describe('Grok completion observations', () => { }) }) - it('marks the captured shutdown SessionEnd and trailing Stop as session boundaries', () => { - const sessionEnd = normalize( - capturedHook((hook) => hook.hookEventName === 'session_end' && hook.reason === 'shutdown') - ) - const shutdownStop = normalize( - capturedHook( - (hook) => - hook.hookEventName === 'stop' && - hook.reason === 'shutdown' && - !Object.hasOwn(hook, 'backgroundTasks') - ) - ) - - expect(sessionEnd).toMatchObject({ - state: 'done', - sessionBoundary: true - }) - expect(shutdownStop).toMatchObject({ - state: 'done', - sessionBoundary: true - }) - expect( - normalize({ - hookEventName: 'Stop', - reason: 'shutdown', - stopHookActive: true, - backgroundTasks: [{ id: 'task-1', type: 'shell', status: 'running' }] - }) - ).toMatchObject({ state: 'done', sessionBoundary: true }) - }) - - it('settles a lead StopFailure without requiring background inventory', () => { - expect( - normalize({ - hookEventName: 'StopFailure', - error: 'server_error', - errorDetails: 'upstream failed', - lastAssistantMessage: 'The request failed.' - }) - ).toMatchObject({ - state: 'done' - }) - }) - - it('settles a lead StopCancelled as interrupted', () => { - expect( - normalize({ - hookEventName: 'StopCancelled', - reason: 'user_interrupt', - cancelledBy: 'user', - cancelTrigger: 'stop_gesture', - reasonDetails: 'user interrupted the turn', - lastAssistantMessage: 'Stopped by user.' - }) - ).toMatchObject({ - state: 'done', - interrupted: true, - lastAssistantMessage: 'Stopped by user.' - }) - }) - - // Pins grok-events.ts: finite task predicate and sessionCrons omission; treating monitors/crons as finite must redden. - it.each([ - { - label: 'monitor-only', - backgroundTasks: [ - { id: 'monitor-1', type: 'monitor', status: 'running', description: 'watch the build' } - ], - sessionCrons: [] - }, - { - label: 'cron-only', - backgroundTasks: [], - sessionCrons: [ - { id: 'cron-1', schedule: 'every minute', recurring: true, prompt: 'check the build' } - ] - } - ])('settles when outstanding work is $label', ({ backgroundTasks, sessionCrons }) => { - expect( - normalize({ - hookEventName: 'Stop', - reason: 'end_turn', - stopHookActive: false, - backgroundTasks, - sessionCrons - })?.state - ).toBe('done') - }) - - // Legacy Grok versions omit both fields, so only positive finite-work evidence may hold the row. - it.each([ - {}, - { backgroundTasks: { unexpected: true } }, - { backgroundTasks: [{ type: 'future-task', status: 'running' }] }, - { backgroundTasks: [], stopHookActive: 'true' }, - { background_tasks: [], stop_hook_active: false } - ])('fails open for unknown or legacy optional fields %#', (payload) => { - expect( - normalize({ - hookEventName: 'Stop', - reason: 'end_turn', - stopHookActive: false, - ...payload - })?.state - ).toBe('done') - }) - it('keeps ambiguous continuation Stops working and settles on the idle backstop', () => { normalize({ hookEventName: 'UserPromptSubmit', @@ -208,15 +101,6 @@ describe('Grok completion observations', () => { ).toBe('done') }) - it('keeps a starting finite task working', () => { - expect( - normalize({ - hookEventName: 'Stop', - backgroundTasks: [{ id: 'task-1', type: 'shell', status: 'starting' }] - }) - ).toMatchObject({ state: 'working', workingMode: 'monitoring' }) - }) - // Real Grok 1.0.41 Stop entry for a background `spawn_subagent`, identifiers dropped. const backgroundSubagent = { id: 'task-1', @@ -253,17 +137,6 @@ describe('Grok completion observations', () => { } ) - it('reads the snake_case inventory the same way', () => { - const row = normalize({ - hookEventName: 'Stop', - reason: 'end_turn', - stop_hook_active: false, - background_tasks: [{ id: 'task-1', type: 'subagent', status: 'running' }] - }) - expect(row).toMatchObject({ state: 'working', mainAgent: { state: 'done' } }) - expect(row?.workingMode).toBeUndefined() - }) - it('keeps a background subagent working past a cancel, and settles only at a session boundary', () => { const cancelled = normalize({ hookEventName: 'StopCancelled', @@ -436,30 +309,4 @@ describe('Grok completion observations', () => { ).toBeUndefined() } ) - - it('settles an unseen prompt id and a session-scoped idle event', () => { - expect( - normalize({ - hookEventName: 'StopCancelled', - sessionId: 'session-1', - promptId: 'bash-mode-prompt', - reason: 'user_interrupt' - })?.state - ).toBe('done') - expect( - normalize({ hookEventName: 'Notification', notificationType: 'idle_prompt' })?.state - ).toBe('done') - }) - - // Pins grok-events.ts: permission/question branch ordering; gating waiting notifications on background work must redden. - it('leaves the real ask-user-question notification path ungated', () => { - expect( - normalize({ - hookEventName: 'Notification', - notificationType: 'elicitation_dialog', - message: 'User question requested', - level: 'info' - }) - ).toMatchObject({ state: 'waiting', agentType: 'grok' }) - }) }) diff --git a/src/shared/agent-hook-listener-grok.test.ts b/src/shared/agent-hook-listener-grok.test.ts index ad603d9522d..95a26a97b75 100644 --- a/src/shared/agent-hook-listener-grok.test.ts +++ b/src/shared/agent-hook-listener-grok.test.ts @@ -72,47 +72,6 @@ describe('shared agent-hook-listener', () => { }) }) - it('previews Grok-native tool names (run_terminal_command / search_replace)', () => { - const shell = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { - hookEventName: 'PreToolUse', - tool_name: 'run_terminal_command', - tool_input: { command: 'git status' } - } - }, - 'production' - ) - const edit = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { - hookEventName: 'PreToolUse', - toolName: 'search_replace', - toolInput: { path: 'src/app.ts', old_string: 'a', new_string: 'b' } - } - }, - 'production' - ) - expect(shell?.payload).toMatchObject({ - agentType: 'grok', - state: 'working', - toolName: 'run_terminal_command', - toolInput: 'git status' - }) - expect(edit?.payload).toMatchObject({ - agentType: 'grok', - state: 'working', - toolName: 'search_replace', - toolInput: 'src/app.ts' - }) - }) - it('maps Grok ask_user_question PreToolUse to waiting with interactivePrompt', () => { const questions = [ { @@ -161,26 +120,6 @@ describe('shared agent-hook-listener', () => { expect(answered?.payload.interactivePrompt).toBeUndefined() }) - it('does not recreate a Grok question card on post_tool_use_failure', () => { - const event = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { - hookEventName: 'post_tool_use_failure', - toolName: 'ask_user_question', - toolInput: { questions: [{ question: 'Retry?', options: ['yes', 'no'] }] }, - error: 'cancelled' - } - }, - 'production' - ) - - expect(event?.payload.state).toBe('working') - expect(event?.payload.interactivePrompt).toBeUndefined() - }) - it('surfaces the Grok tool-failure error and clears stale tool fields', () => { normalizeHookPayload( state, @@ -218,76 +157,6 @@ describe('shared agent-hook-listener', () => { expect(failed?.payload.toolInput).toBeUndefined() }) - it('maps Grok StopFailure to done', () => { - normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { hookEventName: 'UserPromptSubmit', prompt: 'do work' } - }, - 'production' - ) - const done = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { hookEventName: 'StopFailure', error: 'api timeout' } - }, - 'production' - ) - expect(done?.payload).toMatchObject({ - agentType: 'grok', - state: 'done', - prompt: 'do work' - }) - }) - - it('strips Grok internal user_query wrapper before caching the prompt', () => { - const prompt = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { - hookEventName: 'user_prompt_submit', - prompt: '\nFind recent PR\n' - } - }, - 'production' - ) - expect(prompt?.payload.prompt).toBe('Find recent PR') - - const tool = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { - hookEventName: 'pre_tool_use', - toolName: 'web_search', - toolInput: { query: 'recent PR' } - } - }, - 'production' - ) - expect(tool?.payload.prompt).toBe('Find recent PR') - }) - - it('strips Grok opening user_query wrapper even when the closing tag is absent', () => { - const event = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { hookEventName: 'user_prompt_submit', prompt: 'Find recent PR' } - }, - 'production' - ) - expect(event?.payload.prompt).toBe('Find recent PR') - }) - it('preserves both ends of newline-heavy Grok user_query prompts', () => { const promptText = `Find recent PR${'\n'.repeat(300)}Keep the final requirement` const event = normalizeHookPayload( @@ -317,32 +186,6 @@ describe('shared agent-hook-listener', () => { expect(tool?.payload.prompt).toBe('Find recent PR Keep the final requirement') }) - it('maps Grok feedback notifications to waiting without overwriting the prompt', () => { - normalizeHookPayload( - state, - 'grok', - { paneKey: PANE_KEY, payload: { hookEventName: 'UserPromptSubmit', prompt: 'ship it' } }, - 'production' - ) - - const event = normalizeHookPayload( - state, - 'grok', - { - paneKey: PANE_KEY, - payload: { hookEventName: 'Notification', message: 'Grok needs your feedback to proceed' } - }, - 'production' - ) - - expect(event).not.toBeNull() - expect(event!.payload).toMatchObject({ - state: 'waiting', - prompt: 'ship it', - agentType: 'grok' - }) - }) - it('ignores Grok routine permission prompt notifications during tool use', () => { normalizeHookPayload( state, @@ -517,18 +360,6 @@ describe('shared agent-hook-listener', () => { } }) - it('treats Grok SessionEnd chat history as pending result text', () => { - expect( - hasPendingAgentResultText('grok', { - payload: { - hookEventName: 'SessionEnd', - sessionId: '019e37f4-5135-7b63-a4ab-6d13aa6bf528', - cwd: '/tmp/workspace' - } - }) - ).toBe(true) - }) - it('enriches a long-cwd Grok result after async discovery completes', async () => { const tmpDir = mkdtempSync(join(tmpdir(), 'orca-grok-pending-home-')) const hookGrokHome = join(tmpDir, 'hook-grok') @@ -560,16 +391,4 @@ describe('shared agent-hook-listener', () => { rmSync(tmpDir, { recursive: true, force: true }) } }) - - it('does not schedule Grok discovery for an invalid session id', () => { - expect( - hasPendingAgentResultText('grok', { - payload: { - hookEventName: 'SessionEnd', - sessionId: '../escape', - cwd: '/tmp/workspace' - } - }) - ).toBe(false) - }) }) diff --git a/src/shared/agent-hook-listener-hermes-codex-droid.test.ts b/src/shared/agent-hook-listener-hermes-codex-droid.test.ts index 69d91f8a3b8..d6fbc516f4e 100644 --- a/src/shared/agent-hook-listener-hermes-codex-droid.test.ts +++ b/src/shared/agent-hook-listener-hermes-codex-droid.test.ts @@ -19,112 +19,6 @@ describe('shared agent-hook-listener', () => { vi.unstubAllEnvs() }) - it('normalizes Hermes pre_llm_call to a working turn with prompt text', () => { - const event = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - tabId: 'tab-1', - worktreeId: 'wt', - env: 'production', - version: '1', - payload: { - hook_event_name: 'pre_llm_call', - user_message: 'ship the Hermes support' - } - }, - 'production' - ) - expect(event).not.toBeNull() - expect(event!.payload.state).toBe('working') - expect(event!.payload.prompt).toBe('ship the Hermes support') - expect(event!.payload.agentType).toBe('hermes') - }) - - it('normalizes Hermes tool calls and approval hooks', () => { - normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_llm_call', - user_message: 'run tests' - } - }, - 'production' - ) - const tool = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_tool_call', - tool_name: 'terminal', - args: { command: 'pnpm test' } - } - }, - 'production' - ) - expect(tool?.payload.state).toBe('working') - expect(tool?.payload.toolName).toBe('terminal') - expect(tool?.payload.toolInput).toBe('pnpm test') - expect(tool?.payload.prompt).toBe('run tests') - - const approval = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_approval_request', - command: 'rm -rf build', - description: 'Remove stale build output' - } - }, - 'production' - ) - expect(approval?.payload.state).toBe('waiting') - expect(approval?.payload.toolName).toBe('approval') - expect(approval?.payload.toolInput).toBe('rm -rf build') - }) - - it('normalizes Hermes first-party tool argument previews', () => { - const execute = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_tool_call', - tool_name: 'execute_code', - args: { code: 'print("ok")' } - } - }, - 'production' - ) - expect(execute?.payload.toolName).toBe('execute_code') - expect(execute?.payload.toolInput).toBe('print("ok")') - - const pluginTool = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_tool_call', - tool_name: 'custom_plugin_tool', - args: { query: 'agent hooks' } - } - }, - 'production' - ) - expect(pluginTool?.payload.toolName).toBe('custom_plugin_tool') - expect(pluginTool?.payload.toolInput).toBe('agent hooks') - }) - it('clears stale Codex tool input when a same-tool update has explicit unpreviewable input', () => { normalizeHookPayload( state, @@ -216,24 +110,6 @@ describe('shared agent-hook-listener', () => { expect(stop?.payload.state).toBe('done') }) - it('keeps ordinary Codex PreToolUse mapped to working', () => { - const working = normalizeHookPayload( - state, - 'codex', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PreToolUse', - tool_name: 'shell', - tool_input: { command: 'ls' } - } - }, - 'production' - ) - expect(working?.payload.state).toBe('working') - expect(working?.payload.interactivePrompt).toBeUndefined() - }) - it('records the new folder when Codex forks a thread elsewhere (/cd, /worktree)', () => { normalizeAndAccept(state, 'codex', { hook_event_name: 'UserPromptSubmit', @@ -284,36 +160,6 @@ describe('shared agent-hook-listener', () => { expect(next?.payload.toolInput).toBeUndefined() }) - it('normalizes Hermes post_llm_call to done with assistant text', () => { - normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'pre_llm_call', - user_message: 'summarize' - } - }, - 'production' - ) - const done = normalizeHookPayload( - state, - 'hermes', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'post_llm_call', - assistant_response: 'Hermes is wired up.' - } - }, - 'production' - ) - expect(done?.payload.state).toBe('done') - expect(done?.payload.prompt).toBe('summarize') - expect(done?.payload.lastAssistantMessage).toBe('Hermes is wired up.') - }) - // Order captured from a real Hermes `delegate_task` run (session ids shortened). it("keeps the Hermes pane's provider session on the parent, never a delegate child", () => { const parent = { session_id: 'local-parent' } diff --git a/src/shared/agent-hook-listener-interactive-prompts.test.ts b/src/shared/agent-hook-listener-interactive-prompts.test.ts index 34a14d19b0d..f6d3ba74297 100644 --- a/src/shared/agent-hook-listener-interactive-prompts.test.ts +++ b/src/shared/agent-hook-listener-interactive-prompts.test.ts @@ -19,28 +19,6 @@ describe('shared agent-hook-listener', () => { vi.unstubAllEnvs() }) - it('normalizes a Claude UserPromptSubmit body to a working state', () => { - const event = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - tabId: 'tab-1', - worktreeId: 'wt', - env: 'production', - version: '1', - payload: { hook_event_name: 'UserPromptSubmit', prompt: 'hello' } - }, - 'production' - ) - expect(event).not.toBeNull() - expect(event!.paneKey).toBe(PANE_KEY) - expect(event!.connectionId).toBeNull() - expect(event!.payload.state).toBe('working') - expect(event!.payload.prompt).toBe('hello') - expect(event!.payload.agentType).toBe('claude') - }) - it.each([0, 1, 2, 3])( 'normalizes %i leading Cursor BOMs to a working state (#21421)', (count) => { @@ -82,27 +60,6 @@ describe('shared agent-hook-listener', () => { } }) - it('normalizes Gemini BeforeTool to working with tool fields', () => { - const event = normalizeHookPayload( - state, - 'gemini', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'BeforeTool', - tool_name: 'read_file', - args: { file_path: 'src/index.ts' } - } - }, - 'production' - ) - - expect(event?.payload.state).toBe('working') - expect(event?.payload.agentType).toBe('gemini') - expect(event?.payload.toolName).toBe('read_file') - expect(event?.payload.toolInput).toBe('src/index.ts') - }) - it('captures the full AskUserQuestion tool input as interactivePrompt (untruncated)', () => { const questions = { questions: Array.from({ length: 4 }, (_, i) => ({ @@ -134,120 +91,6 @@ describe('shared agent-hook-listener', () => { expect(event?.payload.interactivePrompt!.length).toBe(expected.length) }) - it('maps Claude AskUserQuestion PreToolUse to waiting, then back to working on answer', () => { - const question = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PreToolUse', - tool_name: 'AskUserQuestion', - tool_input: { questions: [{ question: 'Pick', options: ['a', 'b'] }] } - } - }, - 'production' - ) - const answered = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PostToolUse', - tool_name: 'AskUserQuestion', - tool_response: { selected: ['a'] } - } - }, - 'production' - ) - - expect(question?.payload).toMatchObject({ - agentType: 'claude', - state: 'waiting', - toolName: 'AskUserQuestion' - }) - expect(answered?.payload).toMatchObject({ - agentType: 'claude', - state: 'working', - toolName: 'AskUserQuestion' - }) - }) - - it('keeps a normal Claude PreToolUse tool call as working', () => { - const event = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PreToolUse', - tool_name: 'Bash', - tool_input: { command: 'ls' } - } - }, - 'production' - ) - expect(event?.payload.state).toBe('working') - expect(event?.payload.toolName).toBe('Bash') - }) - - it('leaves interactivePrompt undefined for a normal tool call', () => { - const event = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PreToolUse', - tool_name: 'Edit', - tool_input: { file_path: '/tmp/x.ts' } - } - }, - 'production' - ) - expect(event?.payload.toolName).toBe('Edit') - expect(event?.payload.interactivePrompt).toBeUndefined() - }) - - it('captures an approval envelope as interactivePrompt on a PermissionRequest', () => { - const event = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PermissionRequest', - tool_name: 'Bash', - tool_input: { command: 'rm -rf build' } - } - }, - 'production' - ) - expect(event?.payload.interactivePrompt).toBe( - JSON.stringify({ approval: { tool: 'Bash', summary: 'rm -rf build' } }) - ) - }) - - it('captures an approval envelope for a Codex PermissionRequest', () => { - const event = normalizeHookPayload( - state, - 'codex', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PermissionRequest', - tool_name: 'shell', - input: { command: 'git push --force' } - } - }, - 'production' - ) - expect(event?.payload.interactivePrompt).toBe( - JSON.stringify({ approval: { tool: 'shell', summary: 'git push --force' } }) - ) - }) - it('clears interactivePrompt on the next tool event after AskUserQuestion', () => { normalizeHookPayload( state, @@ -363,38 +206,4 @@ describe('shared agent-hook-listener', () => { expect(answered?.payload.state).toBe('working') expect(answered?.payload.interactivePrompt).toBeUndefined() }) - - it('does not re-assert the AskUserQuestion prompt on PostToolUse', () => { - // Answered questions must clear the card instead of rebuilding it from carried tool input. - const event = normalizeHookPayload( - state, - 'claude', - { - paneKey: PANE_KEY, - payload: { - hook_event_name: 'PostToolUse', - tool_name: 'AskUserQuestion', - tool_input: { questions: [{ question: 'Pick', options: ['a'] }] } - } - }, - 'production' - ) - expect(event?.payload.toolName).toBe('AskUserQuestion') - expect(event?.payload.interactivePrompt).toBeUndefined() - }) - - it('captures interactivePrompt for the OpenCode AskUserQuestion route', () => { - const properties = { questions: [{ question: 'Choose', options: ['x', 'y'] }] } - const event = normalizeHookPayload( - state, - 'opencode', - { - paneKey: PANE_KEY, - payload: { hook_event_name: 'AskUserQuestion', ...properties } - }, - 'production' - ) - expect(event?.payload.state).toBe('waiting') - expect(event?.payload.interactivePrompt).toBe(JSON.stringify(properties)) - }) }) diff --git a/src/shared/agent-hook-listener-jcode.test.ts b/src/shared/agent-hook-listener-jcode.test.ts index 0a3f250c961..d541b5f8ed6 100644 --- a/src/shared/agent-hook-listener-jcode.test.ts +++ b/src/shared/agent-hook-listener-jcode.test.ts @@ -33,46 +33,6 @@ describe('shared agent-hook-listener: jcode', () => { rmSync(jcodeHome, { recursive: true, force: true }) }) - it('maps turn_start to working before any tool has run', () => { - const event = ingest(state, { - event: 'turn_start', - session_id: 'session_jc_1', - model: 'claude-haiku-4-5', - source: 'chat' - }) - expect(event?.payload).toMatchObject({ - agentType: 'jcode', - state: 'working', - model: 'claude-haiku-4-5' - }) - expect(event?.payload?.toolName).toBeUndefined() - }) - - it('reports the live tool from pre_tool, before the tool has finished', () => { - const event = ingest(state, { - event: 'pre_tool', - session_id: 'session_jc_1', - tool_name: 'read', - tool_input: '{"file_path":"sample.txt","intent":"Read sample.txt to get its contents"}' - }) - expect(event?.payload).toMatchObject({ - agentType: 'jcode', - state: 'working', - toolName: 'read', - toolInput: 'sample.txt' - }) - }) - - it('falls back to the tool intent when no tool-specific key matches', () => { - const event = ingest(state, { - event: 'pre_tool', - session_id: 'session_jc_1', - tool_name: 'swarm', - tool_input: '{"action":"status","intent":"Check on the workers"}' - }) - expect(event?.payload).toMatchObject({ toolName: 'swarm', toolInput: 'Check on the workers' }) - }) - it('keeps the pre_tool input visible when post_tool reports completion', () => { ingest(state, { event: 'pre_tool', @@ -95,24 +55,6 @@ describe('shared agent-hook-listener: jcode', () => { }) }) - it('maps a pending request_permission to waiting with the full question', () => { - const event = ingest(state, { - event: 'pre_tool', - session_id: 'session_jc_2', - tool_name: 'request_permission', - tool_input: '{"action":"delete the staging bucket","reason":"Why this needs approval"}' - }) - expect(event?.payload).toMatchObject({ - agentType: 'jcode', - state: 'waiting', - toolName: 'request_permission' - }) - expect(JSON.parse(event?.payload?.interactivePrompt ?? '{}')).toMatchObject({ - action: 'delete the staging bucket', - reason: 'Why this needs approval' - }) - }) - it('does not re-open a question on post_tool, which fires after the answer', () => { const pending = ingest(state, { event: 'pre_tool', @@ -132,32 +74,6 @@ describe('shared agent-hook-listener: jcode', () => { expect(event?.payload.interactivePrompt).toBeUndefined() }) - it('leaves unrelated tool names working even when they read like a question', () => { - const event = ingest(state, { - event: 'pre_tool', - session_id: 'session_jc_2', - tool_name: 'conversation_search', - tool_input: '{"query":"what did we confirm about the ask flow"}' - }) - expect(event?.payload).toMatchObject({ state: 'working', toolName: 'conversation_search' }) - }) - - it('maps turn_end to done with the last assistant text', () => { - const event = ingest(state, { - event: 'turn_end', - session_id: 'session_jc_3', - status: 'ok', - duration_ms: '6868', - model: 'claude-haiku-4-5', - last_assistant_text: 'Done.' - }) - expect(event?.payload).toMatchObject({ - agentType: 'jcode', - state: 'done', - lastAssistantMessage: 'Done.' - }) - }) - it('surfaces the turn error instead of a stale reply when a turn fails', () => { const previous = ingest(state, { event: 'turn_end', @@ -179,29 +95,6 @@ describe('shared agent-hook-listener: jcode', () => { }) }) - it('keeps the finished turn detail a completion notification needs', () => { - // Done banners require reply/tool detail; retain the last tool when the turn ends without prose. - ingest(state, { - event: 'turn_start', - session_id: 'session_jc_6', - model: 'claude-haiku-4-5', - source: 'chat' - }) - ingest(state, { - event: 'pre_tool', - session_id: 'session_jc_6', - tool_name: 'write', - tool_input: '{"file_path":"SUMMARY.md","content":"# Summary"}' - }) - const event = ingest(state, { - event: 'turn_end', - session_id: 'session_jc_6', - status: 'ok', - duration_ms: '18000' - }) - expect(event?.payload).toMatchObject({ state: 'done', toolName: 'write' }) - }) - it('clears the previous turn tool when a new turn starts', () => { const previous = ingest(state, { event: 'pre_tool', @@ -219,41 +112,4 @@ describe('shared agent-hook-listener: jcode', () => { expect(event?.payload?.toolName).toBeUndefined() expect(event?.payload?.toolInput).toBeUndefined() }) - - it('treats session_start as identity-only (no status row)', () => { - const event = ingest(state, { - event: 'session_start', - session_id: 'session_jc_5', - model: 'claude-haiku-4-5', - source: 'create' - }) - expect(event?.payload).toMatchObject({ agentType: 'jcode', state: 'done' }) - expect(event?.providerSession).toEqual({ key: 'session_id', id: 'session_jc_5' }) - expect(event?.providerSessionOnly).toBe(true) - }) - - it('reads the lifecycle point from jcode\u2019s own `event` key', () => { - // Managed transport posts JCODE_HOOK_PAYLOAD verbatim; accept its event field without form restatement. - const event = normalizeHookPayload( - state, - 'jcode', - { - paneKey: PANE_KEY, - payload: { event: 'turn_start', session_id: 'session_jc_7', model: 'claude-haiku-4-5' } - }, - 'production' - ) - expect(event?.payload).toMatchObject({ agentType: 'jcode', state: 'working' }) - }) - - it('does not count a direct jcode prompt without journal evidence as explicit', () => { - // Hook prompt fields lack journal backing and must not imply explicit submission. - const event = ingest(state, { - event: 'post_tool', - tool_name: 'read', - prompt: 'fix the bug' - }) - expect(event?.payload).toMatchObject({ agentType: 'jcode', state: 'working' }) - expect(event?.hasExplicitPrompt).toBeFalsy() - }) }) diff --git a/src/shared/agent-hook-listener-omp-approval-ownership.test.ts b/src/shared/agent-hook-listener-omp-approval-ownership.test.ts index 2a751c6e64b..6ae47462d2f 100644 --- a/src/shared/agent-hook-listener-omp-approval-ownership.test.ts +++ b/src/shared/agent-hook-listener-omp-approval-ownership.test.ts @@ -32,34 +32,6 @@ describe('OMP approval ownership', () => { }) } - // omp 17.0.5's `tools.approvalMode` enum, captured off the wire from a real run of each mode. - it.each(['always-ask', 'write', 'yolo'])( - 'keeps an approval request blocked under approval_mode %s', - (approvalMode) => { - expect(approvalRequest({ approval_mode: approvalMode })?.payload).toMatchObject({ - state: 'blocked', - agentType: 'omp', - toolName: 'bash' - }) - } - ) - - it.each([ - ['missing', {}], - ['unrecognised', { approval_mode: 'some-future-mode' }], - ['non-string', { approval_mode: 3 }], - ['null', { approval_mode: null }] - ])('treats a %s approval_mode exactly like today', (_name, extra) => { - expect(approvalRequest(extra)?.payload.state).toBe('blocked') - }) - - it('reports the reason omp sends as the tool input preview', () => { - // omp's bash tool returns this reason for its critical-pattern override. - expect( - approvalRequest({ reason: 'Critical pattern detected', approval_mode: 'write' })?.payload - ).toMatchObject({ state: 'blocked', toolInput: 'Critical pattern detected' }) - }) - it('clears the previous tool input when omp sends no reason', () => { expect( approvalRequest({ reason: 'Critical pattern detected', approval_mode: 'always-ask' })?.payload @@ -72,17 +44,6 @@ describe('OMP approval ownership', () => { }) }) - it('clears the wait on resolution regardless of the mode that requested it', () => { - expect(approvalRequest({ approval_mode: 'yolo' })?.payload.state).toBe('blocked') - expect( - post('omp', { - hook_event_name: 'tool_approval_resolved', - tool_name: 'bash', - approved: true - })?.payload - ).toMatchObject({ state: 'working', toolName: 'bash' }) - }) - /** Normalizes the post and admits its row, as the hook server does, so the next post sees it. */ function deliver(payload: Record): string { const event = post('omp', payload) @@ -119,28 +80,4 @@ describe('OMP approval ownership', () => { expect(deliver({ hook_event_name: 'tool_execution_start', tool_name: 'ask' })).toBe('blocked') expect(deliver({ hook_event_name: 'tool_execution_end', tool_name: 'ask' })).toBe('working') }) - - // Pi and prime-agent share this normalizer but have no approval lifecycle, so the field must not - // give their panes a status row. - it.each(['always-ask', 'write', 'yolo'])( - 'still ignores a Pi approval request carrying approval_mode %s', - (approvalMode) => { - expect( - post('pi', { - hook_event_name: 'tool_approval_requested', - tool_name: 'bash', - approval_mode: approvalMode - }) - ).toBeNull() - } - ) - - it('leaves a Pi input modal waiting, not blocked, when approval_mode is present', () => { - expect( - post('pi', { - hook_event_name: 'ui_prompt_start', - approval_mode: 'yolo' - })?.payload.state - ).toBe('waiting') - }) }) diff --git a/src/shared/agent-hook-listener-omp-plan-review.test.ts b/src/shared/agent-hook-listener-omp-plan-review.test.ts index 046aac7d47e..1b723075a90 100644 --- a/src/shared/agent-hook-listener-omp-plan-review.test.ts +++ b/src/shared/agent-hook-listener-omp-plan-review.test.ts @@ -7,7 +7,7 @@ import { import { normalizeHookPayload } from './agent-hook-listener' import { PANE_KEY } from './agent-hook-listener-test-harness' import { AGENT_STATUS_INTERACTIVE_PROMPT_MAX_LENGTH } from './agent-status-types' -import { buildOmpPlanReviewAnswer, parseOmpPlanReviewPrompt } from './omp-plan-review-prompt' +import { parseOmpPlanReviewPrompt } from './omp-plan-review-prompt' const REVIEW_ID = '86949d10-20aa-46d4-9359-4e1e71467d8f' @@ -35,21 +35,6 @@ describe('OMP plan review status', () => { state = createHookListenerState() }) - it('blocks on the review with a parsed plan card and answer choices', () => { - const blocked = requestReview(state, '# Add a greeting') - - expect(blocked?.state).toBe('blocked') - expect(parseOmpPlanReviewPrompt(blocked?.interactivePrompt)).toEqual({ - reviewId: REVIEW_ID, - plan: '# Add a greeting', - options: [ - { label: 'Approve and execute', send: buildOmpPlanReviewAnswer(REVIEW_ID, 'execute') }, - { label: 'Refine plan', send: buildOmpPlanReviewAnswer(REVIEW_ID, 'dismiss') }, - { label: 'Save and quit', send: buildOmpPlanReviewAnswer(REVIEW_ID, 'save') } - ] - }) - }) - it('clips a long plan so the card still parses', () => { const blocked = requestReview(state, `"quoted"\n${'x'.repeat(40_000)}`) @@ -61,17 +46,6 @@ describe('OMP plan review status', () => { ) }) - it.each([ - [true, 'done'], - [false, 'working'] - ])('ends the review wait when OMP closes it (idle %s)', (isIdle, expected) => { - requestReview(state, '# Plan') - const resolved = post(state, { hook_event_name: 'plan_review_resolved', is_idle: isIdle }) - - expect(resolved?.state).toBe(expected) - expect(resolved?.interactivePrompt).toBeUndefined() - }) - it("keeps the card when an earlier turn's last async child ends under the review", () => { const deliver = (payload: Record) => { const event = normalizeHookPayload(state, 'omp', { paneKey: PANE_KEY, payload }, 'production') diff --git a/src/shared/agent-hook-listener-opencode-reattribution.test.ts b/src/shared/agent-hook-listener-opencode-reattribution.test.ts index b7b0f05ee4c..c6cd624ee01 100644 --- a/src/shared/agent-hook-listener-opencode-reattribution.test.ts +++ b/src/shared/agent-hook-listener-opencode-reattribution.test.ts @@ -39,12 +39,6 @@ describe('OpenCode 1 shared-server reattribution (#21359)', () => { expect(result?.payload.state).toBe('working') }) - it('keeps the stamped pane for unbound sessions', () => { - const state = createHookListenerState() - const result = opencodeBusy(state, PANE_A, 'ses_unknown') - expect(result?.paneKey).toBe(PANE_A) - }) - it('uses the bound pane live launch token', () => { const state = createHookListenerState() // A tokened post teaches the listener pane B's live token. @@ -86,22 +80,6 @@ describe('OpenCode 1 shared-server reattribution (#21359)', () => { expect(result?.launchToken).toBe('token-a') }) - it('leaves other sources untouched', () => { - const state = createHookListenerState() - bindOpenCodeSession(state, 'ses_1', { - paneKey: PANE_B, - boundAt: 1, - basis: 'argv' - }) - const result = normalizeHookPayload( - state, - 'claude', - { paneKey: PANE_A, payload: { hook_event_name: 'Stop', session_id: 'ses_1' } }, - 'production' - ) - expect(result?.paneKey).toBe(PANE_A) - }) - it('never lets a stale same-pane stamp overwrite the live token', () => { const state = createHookListenerState() // A tokened post teaches the listener pane B's live token. diff --git a/src/shared/agent-launch-choices.test.ts b/src/shared/agent-launch-choices.test.ts index 23dc925b8ac..923d5a31d0d 100644 --- a/src/shared/agent-launch-choices.test.ts +++ b/src/shared/agent-launch-choices.test.ts @@ -15,45 +15,15 @@ import { tokenizeStartupCommand } from './tui-agent-startup-shell' import { describe, expect, it, vi } from 'vitest' import * as choiceArgs from './agent-choice-args' import { reconcileAgentChoiceArgs } from './agent-choice-reconciliation' -import { resolveAgentChoiceLayers } from './agent-choice-layers' +import './agent-choice-layers' import { projectAgentLaunchProfile } from './agent-launch-profile' -import { agentLaunchNeedsPreparedCommand } from './agent-launch-preferences' +import './agent-launch-preferences' const keys = new Set(['model', 'effort', 'permissionMode']) const load = (agent: 'claude' | 'codex', userArgs: string) => reconcileAgentChoiceArgs({ agent, userArgs, choices: {}, mode: 'load', keys }) describe('dedicated agent choices', () => { - it.each([ - ['-c modelx', undefined, false], - ['--config=modelx', undefined, false], - ['-c model_reasoning_effortx', undefined, false], - ['-c model=value', { model: 'value' }, true], - ['-c model_reasoning_effort=low', { options: { effort: 'low' } }, true], - ['-c model=42', undefined, true] - ] as const)('requires a config assignment for %s ownership', (userArgs, choices, owned) => { - const migrated = load('codex', userArgs) - const patch = { model: 'gpt-5.5', options: { effort: 'high' } } - const edited = reconcileAgentChoiceArgs({ - agent: 'codex', - userArgs, - choices: {}, - mode: 'choice-edit', - keys, - patch - }) - const projected = projectAgentLaunchProfile( - 'codex', - { agentPermissionDefault: 'own' }, - { target: 'terminal', userArgs, choices: patch } - ) - expect.soft(migrated).toEqual(choices ? { userArgs: '', choices } : { userArgs, choices: {} }) - expect.soft(edited).toEqual({ userArgs: owned ? '' : userArgs, choices: patch }) - expect(projected.args).toBe( - ['-m gpt-5.5 -c model_reasoning_effort=high', owned ? '' : userArgs].filter(Boolean).join(' ') - ) - }) - it('bounds range comparisons and copied bytes on repeated-flag choice edits', () => { const analyze = choiceArgs.analyzeAgentChoiceArgs const slice = String.prototype.slice @@ -143,26 +113,6 @@ describe('dedicated agent choices', () => { } }) - it('lifts a complete tuple atomically and keeps surviving raw bytes', () => { - expect( - load('claude', '--model saved-unlisted-id --effort high --permission-mode auto') - ).toEqual({ - userArgs: '', - choices: { model: 'saved-unlisted-id', options: { effort: 'high' }, permissionMode: 'auto' } - }) - expect(load('codex', '-c model=opus --search -c model_reasoning_effort=high')).toEqual({ - userArgs: '--search', - choices: { model: 'opus', options: { effort: 'high' } } - }) - expect(load('claude', '--effort high')).toEqual({ - userArgs: '', - choices: { options: { effort: 'high' } } - }) - expect(load('claude', '--model opus --append-system-prompt "keep spacing"').userArgs).toBe( - '--append-system-prompt "keep spacing"' - ) - }) - it('keeps duplicates, operands, uncertain regions and unproved TOML literal', () => { for (const text of [ '-m old --model new', @@ -276,95 +226,6 @@ describe('dedicated agent choices', () => { ).args ).toBe('--model claude-opus-4-8-thinking --verbose') }) - - it('does not revive remembered model/options when raw text defeats a yielding layer', () => { - const input = { - agent: 'claude' as const, - remembered: { model: 'sonnet', options: { effort: 'high' } }, - action: { permissionMode: 'ask' as const }, - argumentOwner: 'launch' as const, - launchPreferencesYieldToAgentArgs: true - } - expect(resolveAgentChoiceLayers({ ...input, userArgs: '--model haiku' })).toEqual({ - permissionMode: 'ask' - }) - expect(resolveAgentChoiceLayers({ ...input, userArgs: '--effort medium' })).toEqual({ - model: 'sonnet', - permissionMode: 'ask' - }) - expect( - resolveAgentChoiceLayers({ ...input, userArgs: '--model haiku', explicit: { model: 'opus' } }) - ).toEqual({ model: 'opus', permissionMode: 'ask' }) - }) - - it('resolves raw inheritance before projecting and never imports another model’s options', () => { - expect( - resolveAgentChoiceLayers({ - agent: 'claude', - defaults: { model: 'sonnet' }, - action: { model: 'opus' }, - userArgs: '--append-system-prompt hello', - argumentOwner: 'launch', - launchPreferencesYieldToAgentArgs: true - }) - ).toEqual({ model: 'opus' }) - expect( - resolveAgentChoiceLayers({ - agent: 'claude', - remembered: { model: 'sonnet', options: { effort: 'high' } }, - explicit: { model: 'opus' }, - argumentOwner: 'defaults' - }) - ).toEqual({ model: 'opus' }) - expect( - resolveAgentChoiceLayers({ - agent: 'claude', - remembered: { model: 'sonnet' }, - defaults: { options: { effort: 'high' } }, - argumentOwner: 'defaults' - }) - ).toEqual({ model: 'sonnet', options: { effort: 'high' } }) - expect( - projectAgentLaunchProfile( - 'claude', - { agentPermissionDefault: 'ask' }, - { target: 'terminal', choices: { options: { effort: 'high' } }, userArgs: '' } - ).args - ).toContain('--effort') - expect( - projectAgentLaunchProfile( - 'claude', - { agentPermissionDefault: 'ask' }, - { target: 'structured', choices: { model: 'opus' }, userArgs: '' } - ) - ).toEqual({ - args: '', - choices: { model: 'opus', permissionMode: 'ask' }, - launch: { args: ['--model', 'opus'], appliedValues: { model: 'opus' } } - }) - expect(agentLaunchNeedsPreparedCommand('codex', { model: 'unlisted', effort: 'high' })).toBe( - false - ) - }) -}) - -it('adds one Claude settings carrier only for deliberate Fast, including explicit off', () => { - for (const value of ['true', 'false']) { - const launch = projectAgentLaunchProfile('claude', undefined, { - target: 'terminal', - userArgs: '--add-dir docs', - choices: { options: { fastMode: value } } - }) - expect(launch.args).toContain(`"fastMode":${value}`) - expect(launch.args.match(/--settings/g)).toHaveLength(1) - } - expect( - projectAgentLaunchProfile('claude', undefined, { - target: 'terminal', - userArgs: '--add-dir docs', - choices: { model: 'opus' } - }).args - ).not.toContain('--settings') }) it('edits only the last inline Claude settings object and never unshadows earlier settings on removal', () => { @@ -415,27 +276,6 @@ it('leaves effective file and unparseable Claude settings literal and never repo } }) -it('round-trips arbitrary host-owned Codex tier ids through the common config carrier without a release list', () => { - const userArgs = '-c service_tier=account_fast_v9 --add-dir docs' - const read = reconcileAgentChoiceArgs({ - agent: 'codex', - userArgs, - choices: {}, - mode: 'arguments-edit', - keys: new Set(['serviceTier']) - }) - expect(read).toEqual({ - userArgs: '--add-dir docs', - choices: { options: { serviceTier: 'account_fast_v9' } } - }) - const launch = projectAgentLaunchProfile('codex', undefined, { - target: 'terminal', - userArgs: read.userArgs, - choices: read.choices - }) - expect(launch.args).toBe('-c service_tier=account_fast_v9 --add-dir docs') -}) - it.each([ ['claude', '--add-dir /tmp/😀 --model "opus" --effort high -- tail'], ['claude', '--add-dir /tmp --model=opus --effort=high -- tail'], diff --git a/src/shared/agent-launch-default-choices.test.ts b/src/shared/agent-launch-default-choices.test.ts index e02d239cf9b..f0268a65fa2 100644 --- a/src/shared/agent-launch-default-choices.test.ts +++ b/src/shared/agent-launch-default-choices.test.ts @@ -7,7 +7,7 @@ import { type AgentLaunchSettingsSource } from './agent-launch-settings' import { migrateAgentPermissionChoice } from './tui-agent-permission-lift' -import { structuredLaunchSeedOptions } from './native-chat/input/native-chat-session-option-defaults' +import './native-chat/input/native-chat-session-option-defaults' const baseline: AgentLaunchSettingsSource = { agentPermissionDefault: 'ask', @@ -163,48 +163,3 @@ it('uses original raw ownership before compiling authored terminal defaults', () expect(input(raw).sessionOptions).toEqual({ model: 'opus', effort: 'high' }) expect(launch(input(raw))).toContain('{"language":"french","fastMode":false}') }) - -it.each(['claude', 'codex'] as const)( - 'preserves encoded boolean seeds through authored/remembered layering for %s', - (agent) => { - const remembered = { - model: 'remembered-model', - valuesByModel: { 'remembered-model': { effort: 'low', fastMode: true } } - } - const settings = { - agentPermissionDefault: 'ask' as const, - nativeChatSessionOptions: { [agent]: remembered }, - agentLaunchChoices: {} - } - expect(structuredLaunchSeedOptions(settings, agent)).toMatchObject({ - model: 'remembered-model', - effort: 'low', - fastMode: 'true' - }) - const authored = { - ...settings, - agentLaunchChoices: { - [agent]: { model: 'authored-model', options: { effort: 'high', fastMode: 'false' } } - } - } - expect(structuredLaunchSeedOptions(authored, agent)).toMatchObject({ - model: 'authored-model', - effort: 'high', - fastMode: 'false' - }) - expect( - structuredLaunchSeedOptions({ ...authored, agentLaunchChoices: {} }, agent) - ).toMatchObject({ model: 'remembered-model', effort: 'low', fastMode: 'true' }) - expect(settings.nativeChatSessionOptions[agent]).toEqual(remembered) - expect( - structuredLaunchSeedOptions( - { - agentLaunchChoices: { - [agent]: { options: { effort: 'high', fastMode: 'false' } } - } - }, - agent - ) - ).toMatchObject({ effort: 'high', fastMode: 'false' }) - } -) diff --git a/src/shared/agent-launch-legacy-publication.test.ts b/src/shared/agent-launch-legacy-publication.test.ts index 37fb07cb35b..1cc08f4de5e 100644 --- a/src/shared/agent-launch-legacy-publication.test.ts +++ b/src/shared/agent-launch-legacy-publication.test.ts @@ -119,29 +119,6 @@ it('handles a whole-map legacy no-op, removed sibling and permission carrier in expect(changed.agentDefaultArgs?.claude).toBe('--verbose --debug') }) -it('does not conserve generated ownership through duplicate, operand, terminator or changed model carriers', () => { - const settings: AgentLaunchSettingsSource = { - agentPermissionDefault: 'own', - terminalWindowsShell: 'powershell.exe', - agentLaunchChoices: { claude: { model: String.raw`org\model`, options: { effort: 'high' } } }, - agentDefaultArgs: { claude: '--verbose' } - } - const text = projectAgentLaunchSettings(settings, 'win32').agentDefaultArgs.claude! - for (const value of [ - `${text} --model other`, - `-- ${text}`, - `--append-system-prompt ${text}`, - '--model different --verbose' - ]) { - const update = agentLaunchSettingsMutationUpdates( - settings, - { type: 'arguments', agent: 'claude', value }, - 'win32' - ) - expect(update.agentLaunchChoices?.claude?.model).not.toBe(String.raw`org\model`) - } -}) - it('keeps native-only unrepresented choices and cuts Windows settings with their actual grammar', () => { const native: AgentLaunchSettingsSource = { agentPermissionDefault: 'own', diff --git a/src/shared/agent-launch-operation.test.ts b/src/shared/agent-launch-operation.test.ts index b2cb77b603e..c4868e54a1d 100644 --- a/src/shared/agent-launch-operation.test.ts +++ b/src/shared/agent-launch-operation.test.ts @@ -1,127 +1,9 @@ /** Fingerprint behavioral fields, excluding telemetry: omissions can replay a different launch; extra attribution fields can reject an honest retry. */ import { describe, expect, it } from 'vitest' -import { - computeAgentLaunchFingerprint, - type AgentLaunchFingerprintInput -} from './agent-launch-operation' +import { computeAgentLaunchFingerprint } from './agent-launch-operation' import { canonicalAgentSessionDigest } from './agent-session/lifecycle/agent-session-mutation-envelope' -const BASE = { - agent: 'claude', - target: { kind: 'existing' as const, worktree: 'wt-1' } -} -const PANE_KEY = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d:3f2504e0-4f89-41d3-9a0c-0305e82c3301' -const OTHER_PANE_KEY = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d:6fa459ea-ee8a-4ca4-894e-db77e160355e' - -/** The handler passes whole wire objects; test runtime exclusion of launchSource rather than its absence from the input type. */ -function fingerprintOfWirePayload( - params: AgentLaunchFingerprintInput & { launchSource?: string } -): string { - return computeAgentLaunchFingerprint(params) -} - -describe('fields the launch fingerprint covers', () => { - it('covers encoded chat choices while omission keeps the previous digest', () => { - expect( - computeAgentLaunchFingerprint({ ...BASE, chatOptions: { model: 'opus', fastMode: 'false' } }) - ).not.toBe(computeAgentLaunchFingerprint(BASE)) - expect( - computeAgentLaunchFingerprint({ ...BASE, chatOptions: { model: 'opus', fastMode: 'false' } }) - ).not.toBe( - computeAgentLaunchFingerprint({ ...BASE, chatOptions: { model: 'opus', fastMode: 'true' } }) - ) - }) - - it('separates two launches that differ only in agentArgs', () => { - expect(computeAgentLaunchFingerprint({ ...BASE, agentArgs: '--model opus' })).not.toBe( - computeAgentLaunchFingerprint({ ...BASE, agentArgs: '--model sonnet' }) - ) - }) - - it('treats an explicit "no arguments" as different from falling back to the settings default', () => { - // Preserve explicit-clear versus default-arguments identity across retries. - expect(computeAgentLaunchFingerprint({ ...BASE, agentArgs: null })).not.toBe( - computeAgentLaunchFingerprint(BASE) - ) - }) - - it('separates two launches that differ only in cwd', () => { - expect(computeAgentLaunchFingerprint({ ...BASE, cwd: '/repo/packages/a' })).not.toBe( - computeAgentLaunchFingerprint({ ...BASE, cwd: '/repo/packages/b' }) - ) - }) - - it('separates two launches that reserved different panes or sessions', () => { - // The key is baked into the pane's env; replaying another pane's key strands the new reservation. - expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe( - computeAgentLaunchFingerprint({ ...BASE, paneKey: OTHER_PANE_KEY }) - ) - expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe( - computeAgentLaunchFingerprint(BASE) - ) - const sessionId = '11111111-1111-4111-8111-111111111111' - expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe( - computeAgentLaunchFingerprint({ ...BASE, sessionId: '22222222-2222-4222-8222-222222222222' }) - ) - expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe( - computeAgentLaunchFingerprint(BASE) - ) - }) - - it('separates a retry that picked another surface, or none, from the first launch', () => { - const terminal = computeAgentLaunchFingerprint({ ...BASE, surface: 'terminal' }) - expect(terminal).not.toBe(computeAgentLaunchFingerprint({ ...BASE, surface: 'chat' })) - expect(terminal).not.toBe(computeAgentLaunchFingerprint(BASE)) - }) -}) - -describe('fields the launch fingerprint deliberately ignores', () => { - it('does not separate two launches that differ only in launchSource', () => { - // Attribution changes must not turn an identical launch retry into a conflict. - expect(fingerprintOfWirePayload({ ...BASE, launchSource: 'shortcut' })).toBe( - fingerprintOfWirePayload({ ...BASE, launchSource: 'tab_bar_quick_launch' }) - ) - }) - - it('ignores launchSource entirely, so sending one matches sending none', () => { - expect(fingerprintOfWirePayload({ ...BASE, launchSource: 'sidebar' })).toBe( - computeAgentLaunchFingerprint(BASE) - ) - }) -}) - -describe('compatibility with rows written before these fields existed', () => { - /** Preserve previous-build digests when new fields are absent so host upgrades do not refuse in-flight retries. */ - it('matches the pre-existing digest when no new field is sent', () => { - const previousBuild = canonicalAgentSessionDigest({ - method: 'agent.launch', - agent: BASE.agent, - target: BASE.target, - prompt: undefined, - sessionOptions: undefined, - reuseTerminal: undefined - }) - expect(computeAgentLaunchFingerprint(BASE)).toBe(previousBuild) - }) - - it('matches the digest from before the pane key existed when no pane is reserved', () => { - const beforePaneKey = canonicalAgentSessionDigest({ - method: 'agent.launch', - agent: BASE.agent, - target: BASE.target, - prompt: undefined, - sessionOptions: undefined, - reuseTerminal: undefined, - agentArgs: '--model opus', - cwd: '/repo/packages/a' - }) - expect( - computeAgentLaunchFingerprint({ ...BASE, agentArgs: '--model opus', cwd: '/repo/packages/a' }) - ).toBe(beforePaneKey) - }) -}) - describe('transient cwd observation is not launch intent', () => { const create = { repo: 'id:00000000-0000-4000-8000-000000000099', @@ -183,35 +65,6 @@ describe('transient cwd observation is not launch intent', () => { }) ).not.toBe(computeAgentLaunchFingerprint(input)) }) - it.each>([ - { agent: 'claude' }, - { prompt: { text: 'hello', delivery: 'submit' } }, - { sessionOptions: { model: 'model' } }, - { chatOptions: { model: 'model' } }, - { reuseTerminal: { handle: 'term_other' } }, - { agentArgs: null }, - { agentArgs: '--verbose' }, - { cwd: '/another' }, - { paneKey: PANE_KEY }, - { sessionId: 'session_other' }, - { surface: 'terminal' } - ])('retains outer launch intent and explicit clear with the observation: %j', (change) => { - const input: AgentLaunchFingerprintInput = { - agent: 'codex', - target: { kind: 'create-worktree', create } - } - const changed = { ...input, ...change } - expect(computeAgentLaunchFingerprint(changed)).not.toBe(computeAgentLaunchFingerprint(input)) - expect(computeAgentLaunchFingerprint(changed)).toBe( - canonicalAgentSessionDigest({ method: 'agent.launch', ...changed }) - ) - expect( - computeAgentLaunchFingerprint({ - ...changed, - target: { kind: 'create-worktree', create: { ...create, cwdParentAmbiguous: true } } - }) - ).toBe(computeAgentLaunchFingerprint(changed)) - }) it.each(['existing', 'create-folder-workspace'] as const)( 'does not project %s targets', (kind) => { diff --git a/src/shared/agent-launch-picker-legacy-permission.test.ts b/src/shared/agent-launch-picker-legacy-permission.test.ts index 232c1e17c4d..037d582d185 100644 --- a/src/shared/agent-launch-picker-legacy-permission.test.ts +++ b/src/shared/agent-launch-picker-legacy-permission.test.ts @@ -8,9 +8,9 @@ import { } from './agent-launch-picker' import { agentPickerArgumentPermission } from './agent-launch-picker-permission' import { projectAgentLaunchProfile } from './agent-launch-profile' -import { structuredAgentConfiguredArgs } from '../main/native-chat/structured-agent-configured-args' -import { claudeStructuredLaunchArgs } from '../main/claude/structured/launch/claude-structured-launch-args' -import { codexStructuredLaunchArgs } from '../main/codex/codex-structured-launch-args' +import '../main/native-chat/structured-agent-configured-args' +import '../main/claude/structured/launch/claude-structured-launch-args' +import '../main/codex/codex-structured-launch-args' const settings = Object.freeze({ agentPermissionDefault: 'ask' as const, @@ -57,25 +57,6 @@ it('retains inherited raw permission precedence despite a typed choice; removal ) }) -it('shows unliftable launch-owned permission bytes without guessing a native choice and preserves unrelated Arguments', () => { - const draft = editAgentPickerArguments( - agentPickerDraft(entry), - '--permission-mode plan --add-dir docs' - ) - expect(draft.choices.permissionMode).toBeUndefined() - const permission = agentPickerArgumentPermission(entry, draft) - expect(permission).toEqual({ - flag: '--permission-mode plan', - remainingArguments: '--add-dir docs', - value: 'plan', - choice: undefined, - opaque: false - }) - const removed = editAgentPickerArguments(draft, permission?.remainingArguments ?? '') - expect(removed.arguments).toBe('--add-dir docs') - expect(agentPickerArgumentPermission(entry, removed)).toBeUndefined() -}) - it('keeps shell-ambiguous Arguments together rather than inventing a removable permission boundary', () => { const draft = { ...agentPickerDraft(entry), @@ -89,24 +70,3 @@ it('keeps shell-ambiguous Arguments together rather than inventing a removable p opaque: true }) }) - -it('retains raw CLI permission bytes while native configured-Arguments filtering stays unchanged', () => { - const chat = { ...agentPickerDraft(entry, 'chat'), arguments: '--permission-mode plan' } - expect(agentPickerArgumentPermission(entry, chat)).toMatchObject({ - value: 'plan', - choice: undefined, - opaque: false - }) - expect( - claudeStructuredLaunchArgs(structuredAgentConfiguredArgs('claude', settings)).extraArgs - ).toEqual({}) - expect( - codexStructuredLaunchArgs( - structuredAgentConfiguredArgs('codex', { - agentDefaultArgs: { - codex: '-a never -s danger-full-access -c approval_policy=never --search' - } - }) - ) - ).toEqual(['--search']) -}) diff --git a/src/shared/agent-launch-picker.test.ts b/src/shared/agent-launch-picker.test.ts index 7276d74180b..b831e7a5a8a 100644 --- a/src/shared/agent-launch-picker.test.ts +++ b/src/shared/agent-launch-picker.test.ts @@ -1,16 +1,13 @@ import { buildNewTabAgentMenuRows } from './agent-launch-picker-rows' -import { readFileSync } from 'node:fs' -import { z } from 'zod' -import { describe, expect, it } from 'vitest' +import 'node:fs' +import 'zod' +import { expect, it } from 'vitest' import { agentPickerLaunchInputs, agentPickerEntriesFromRows, agentPickerChoiceSeeds, agentPickerDraft, - agentPickerHydrationQueue, - agentPickerEntryOptions, agentPickerEffectiveChoices, - searchAgentPickerSetups, editAgentPickerDraft, editAgentPickerArguments, type AgentPickerEntry @@ -43,109 +40,6 @@ const entry: AgentPickerEntry = { } } -describe('shared agent launch picker', () => { - it('derives setup matches from every query word, folds extra high and leaves plain agent searches plain', () => { - expect(searchAgentPickerSetups([entry], 'codex')).toEqual([]) - expect(searchAgentPickerSetups([entry], 'astra low')[0]?.choices).toEqual({ - model: 'astra', - options: { effort: 'low' } - }) - expect( - searchAgentPickerSetups([entry], 'codex astra extra high')[0]?.choices.options?.effort - ).toBe('xhigh') - expect(searchAgentPickerSetups([entry], 'astra unsupported')).toEqual([]) - expect(searchAgentPickerSetups([entry], 'astra')).toHaveLength(1) - }) - it('keeps unknown saved models launchable while current search excludes a classified legacy model', () => { - const legacy = { - ...entry, - catalog: { - origin: 'probe' as const, - fetchedAt: 1, - models: [ - { - id: 'retired', - label: 'Retired', - isDefault: false, - efforts: [], - isLegacy: true as const - } - ] - } - } - expect(searchAgentPickerSetups([legacy], 'retired')).toEqual([]) - const draft = editAgentPickerDraft(agentPickerDraft(entry), { model: 'my-private-alias' }) - expect(editAgentPickerDraft(draft, { permissionMode: 'own' }).choices).toEqual({ - model: 'my-private-alias', - permissionMode: 'own' - }) - }) - it('reconciles deliberate Arguments edits once and preserves explicit empty versus inheritance', () => { - const draft = editAgentPickerArguments( - agentPickerDraft(entry, 'terminal'), - '--model astra --add-dir docs' - ) - expect(draft.choices.model).toBeUndefined() - expect(agentPickerLaunchInputs(draft, entry).sessionOptions?.model).toBe('astra') - expect(draft.arguments).toBe('--model astra --add-dir docs') - expect(editAgentPickerArguments(draft, '').arguments).toBeNull() - expect(agentPickerDraft(entry).arguments).toBeUndefined() - }) -}) - -it('prioritizes the selected pending catalog and reads each offered agent only once', () => { - let selected: AgentPickerEntry['agent'] | undefined - const entries = [ - entry, - { ...entry, agent: 'claude' as const }, - { ...entry, agent: 'gemini' as const }, - { ...entry, agent: 'grok' as const, surfaces: [] } - ] - const next = agentPickerHydrationQueue(entries, () => selected) - expect(next()).toBe(0) - selected = 'gemini' - expect(next()).toBe(2) - expect(next()).toBe(1) - expect(next()).toBeUndefined() -}) -it('retains a model named by a captured Claude init and honors its disabled fast mode', () => { - const initSchema = z.object({ - frame: z.object({ - type: z.literal('system'), - subtype: z.literal('init'), - model: z.string(), - fast_mode_disabled_reason: z.literal('sdk_opt_in_required') - }) - }) - const capture = readFileSync( - new URL('../main/claude/__fixtures__/claude-adapter-capture-fold.jsonl', import.meta.url), - 'utf8' - ) - const init = capture - .trim() - .split('\n') - .map((line) => initSchema.safeParse(JSON.parse(line))) - .find((result) => result.success) - if (!init?.success) { - throw new Error('Captured init is missing') - } - const model = init.data.frame.model - const captured: AgentPickerEntry = { - ...entry, - agent: 'claude', - catalog: { - origin: 'live-session', - fetchedAt: 1, - fastModeSupport: { supported: false, reason: init.data.frame.fast_mode_disabled_reason }, - models: [{ id: model, label: model, isDefault: false, efforts: [], effortsKnown: false }] - } - } - expect( - agentPickerEntryOptions(captured, model, 'chat').some((option) => option.id === 'fastMode') - ).toBe(false) - expect(editAgentPickerDraft(agentPickerDraft(captured), { model }).choices.model).toBe(model) -}) - it.each(['chat', 'terminal'] as const)( 'keeps CLI edits separate from shared intent with %s as the default', (surface) => { @@ -218,47 +112,6 @@ it('shows inherited terminal Arguments choices without promoting them into launc ).toBe('opus') }) -it('only suggests host-advertised model words and preserves the mixed omnibox short-prefix boundary', () => { - expect( - searchAgentPickerSetups([{ ...entry, catalog: { origin: 'unknown' } }], 'gpt low') - ).toEqual([]) - for (const query of ['a', 'low', 'high', 'codex low']) { - expect(searchAgentPickerSetups([entry], query)).toEqual([]) - } - expect(searchAgentPickerSetups([entry], 'as low')).toHaveLength(1) -}) - -it('bounds real setup search work at N and 10N for no, broad and effort matches', () => { - const measure = (count: number, query: string) => { - let visits = 0 - const models = Array.from({ length: count }, (_, index) => ({ - ...(entry.catalog.origin !== 'unknown' - ? entry.catalog.models[0] - : { isDefault: false, efforts: [] }), - get id() { - visits++ - return `astra-${index}` - }, - get label() { - visits++ - return `Astra ${index}` - } - })) - const result = searchAgentPickerSetups( - [{ ...entry, catalog: { origin: 'probe', fetchedAt: 1, models } }], - query - ) - return { visits, length: result.length } - } - for (const query of ['absent', 'astra', 'astra low', 'low']) { - const small = measure(30, query), - large = measure(300, query) - expect(large.visits).toBeLessThanOrEqual(10 * small.visits) - expect(large.visits).toBeGreaterThan(0) - expect(large.length).toBeLessThanOrEqual(8) - } -}) - it('clears an Arguments-owned model/effort through deliberate controls without copying other CLI choices into Chat', () => { const own = { ...entry, agent: 'claude' as const } const original = editAgentPickerDraft(agentPickerDraft(own), { @@ -337,20 +190,6 @@ it('reports opaque permission literally when the actual compiler ignores a diffe }) }) -it('retains a raw permission spelling even when its literal value happens to name a typed choice', async () => { - const { agentPickerPanelArguments } = await import('./agent-launch-picker-arguments') - const own: AgentPickerEntry = { - ...entry, - agent: 'claude', - defaultSurface: 'terminal', - arguments: '--permission-mode ask --add-dir docs' - } - const view = agentPickerPanelArguments(own, agentPickerDraft(own)) - expect(view.permission).toBe('ask') - expect(view.selectedPermission).toBeUndefined() - expect(view.chips[0].flag).toBe('--permission-mode ask') -}) - it('keeps offered host Chat independent of preference through the actual row builder and adapter', () => { for (const scenario of [ { @@ -513,23 +352,3 @@ it('keeps offered host Chat independent of preference through the actual row bui }) ).toEqual([]) }) - -it('orders setups by agent, not by whether each opens as a chat or its CLI', () => { - const rows = buildNewTabAgentMenuRows({ - settings: { nativeChatProviders: { codex: true, grok: true, claude: false } }, - detectedOnHost: ['claude', 'codex', 'grok', 'gemini'], - chatAgents: ['claude', 'codex', 'grok'], - hostChatAnswer: () => 'yes', - chatProgram: () => 'available' - }) - const order = (defaultAgent: 'codex' | 'blank') => - agentPickerEntriesFromRows( - rows, - ['claude', 'codex', 'grok', 'gemini'], - () => ({ terminal: {}, structured: {} }), - (agent) => agent, - defaultAgent - ).map((entry) => entry.agent) - expect(order('blank')).toEqual(['claude', 'codex', 'grok', 'gemini']) - expect(order('codex')).toEqual(['codex', 'claude', 'grok', 'gemini']) -}) diff --git a/src/shared/agent-launch-preferences.test.ts b/src/shared/agent-launch-preferences.test.ts deleted file mode 100644 index 22c48ecefbe..00000000000 --- a/src/shared/agent-launch-preferences.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { toAgentLaunchPreferences } from './agent-launch-preferences' -import { getAgentSessionOptionCatalog } from './agent-session/options/agent-session-option-catalog' -import { resolveAgentSessionOptionLaunch } from './agent-session/options/agent-session-option-launch' -import { - decodeCatalogOptionValue, - encodeCatalogOptionValue -} from './agent-session/options/structured-agent-session-option-codec' - -describe('toAgentLaunchPreferences', () => { - it('keeps the strict preferences, with a mode only where the agent has a terminal flag', () => { - const options = { model: ' gpt-5 ', effort: 'high', permissionMode: 'ask', fastMode: true } - expect(toAgentLaunchPreferences(options, 'codex')).toEqual({ - model: 'gpt-5', - effort: 'high', - mode: 'ask' - }) - // OpenCode's terminal has no permission flag; a released host refuses any option it gets. - expect(toAgentLaunchPreferences({ permissionMode: 'ask' }, 'opencode')).toBeUndefined() - expect(toAgentLaunchPreferences({ permissionMode: 'own' }, 'opencode')).toBeUndefined() - expect(toAgentLaunchPreferences({ permissionMode: 'own' }, 'codex')).toEqual({ mode: 'own' }) - expect(toAgentLaunchPreferences({ permissionMode: 'own' }, 'goose')).toEqual({ mode: 'own' }) - expect(toAgentLaunchPreferences({ permissionMode: 'ask' }, 'antigravity')).toBeUndefined() - }) - - it('answers nothing when no preference survives', () => { - expect(toAgentLaunchPreferences({ model: ' ', fastMode: true }, 'codex')).toBeUndefined() - expect(toAgentLaunchPreferences(undefined, 'codex')).toBeUndefined() - }) -}) - -describe('a saved boolean option across the wire', () => { - it('round-trips Cursor Thinking into the composed terminal model', () => { - const thinking = getAgentSessionOptionCatalog('cursor')! - .models.find((model) => model.id === 'claude-opus-4-8')! - .options.find((option) => option.id === 'thinking')! - const saved = encodeCatalogOptionValue(thinking.kind, true) - expect(saved).toBe('true') - expect(decodeCatalogOptionValue(thinking.kind, saved!)).toBe(true) - const options = { model: 'claude-opus-4-8', thinking: saved!, effort: 'high' } - expect(toAgentLaunchPreferences(options, 'cursor')?.model).toBe('claude-opus-4-8-thinking-high') - expect(resolveAgentSessionOptionLaunch('cursor', options).args).toEqual([ - '--model', - 'claude-opus-4-8-thinking-high' - ]) - }) - - it('carries a saved permission mode on the existing mode key', () => { - expect(toAgentLaunchPreferences({ permissionMode: 'full-access' }, 'claude')).toEqual({ - mode: 'full-access' - }) - }) -}) diff --git a/src/shared/agent-launch-remote.test.ts b/src/shared/agent-launch-remote.test.ts deleted file mode 100644 index 54b621e03e5..00000000000 --- a/src/shared/agent-launch-remote.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { repoIsRemote } from './agent-launch-remote' - -describe('repoIsRemote', () => { - it('reads both spellings of SSH ownership on two different hosts', () => { - // Distinct SSH hosts catch lookups accidentally reading another row's owner (ssh:m4air to openclaw). - expect(repoIsRemote({ connectionId: 'm4air', executionHostId: null })).toBe(true) - expect(repoIsRemote({ connectionId: null, executionHostId: 'ssh:openclaw' })).toBe(true) - expect(repoIsRemote({ connectionId: 'm4air', executionHostId: 'ssh:m4air' })).toBe(true) - }) - - it('answers local for a row that declares itself local with a stale connection', () => { - expect(repoIsRemote({ connectionId: 'm4air', executionHostId: 'local' })).toBe(false) - }) - - it('keeps a runtime host with a nested SSH target remote', () => { - expect(repoIsRemote({ connectionId: 'nested-target', executionHostId: 'runtime:vm-1' })).toBe( - true - ) - }) - - it('treats a headless runtime with no nested SSH target as remote', () => { - expect(repoIsRemote({ connectionId: null, executionHostId: 'runtime:vm-1' })).toBe(true) - }) - - it('keeps a WSL workspace on a Windows server on the WSL CLI name', () => { - expect( - repoIsRemote({ - connectionId: null, - executionHostId: 'runtime:win-1', - path: '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo' - }) - ).toBe(false) - }) - - it('keeps plain local rows local', () => { - expect(repoIsRemote({ connectionId: null, executionHostId: null })).toBe(false) - expect(repoIsRemote({ connectionId: null, executionHostId: 'local' })).toBe(false) - }) -}) diff --git a/src/shared/agent-launch-settings-auto-projection.test.ts b/src/shared/agent-launch-settings-auto-projection.test.ts index bfee3db901b..0dd9e49e521 100644 --- a/src/shared/agent-launch-settings-auto-projection.test.ts +++ b/src/shared/agent-launch-settings-auto-projection.test.ts @@ -1,48 +1,8 @@ import { describe, expect, it } from 'vitest' -import { - agentLaunchSettingsMutationUpdates, - projectAgentLaunchSettings -} from './agent-launch-settings' +import { agentLaunchSettingsMutationUpdates } from './agent-launch-settings' import { applyAgentPermissionMode, AUTO_TUI_AGENT_ARGS } from './tui-agent-permissions' describe('agent launch permission projection compatibility', () => { - it('publishes Auto through an optional marker alongside the existing Mixed value', () => { - const settings = projectAgentLaunchSettings(applyAgentPermissionMode({ mode: 'auto' })) - expect(settings.permissionMode).toBe('mixed') - expect(settings.autoPermissionMode).toBe(true) - expect(settings.agentDefaultArgs.codex).toBe('--approve-for-me') - expect(settings.agentDefaultArgs.claude).toBe('--permission-mode auto') - }) - - it.each(['yolo', 'manual'] as const)('preserves the existing %s projection', (mode) => { - const settings = projectAgentLaunchSettings(applyAgentPermissionMode({ mode })) - expect(settings.permissionMode).toBe(mode) - expect(settings).not.toHaveProperty('autoPermissionMode') - }) - - it('does not label a partly configured automatic profile as Auto', () => { - const settings = projectAgentLaunchSettings({ agentDefaultArgs: { codex: '--approve-for-me' } }) - expect(settings.permissionMode).toBe('mixed') - expect(settings).not.toHaveProperty('autoPermissionMode') - }) - - it('publishes the mode of agents Auto does not cover, never Auto itself', () => { - const yolo = applyAgentPermissionMode({ mode: 'yolo' }) - const settings = projectAgentLaunchSettings(applyAgentPermissionMode({ mode: 'auto', ...yolo })) - expect(settings.nonAutoPermissionMode).toBe('yolo') - const manual = projectAgentLaunchSettings(applyAgentPermissionMode({ mode: 'auto' })) - expect(manual.nonAutoPermissionMode).toBe('manual') - }) - - it('keeps one custom agent from making the profile mixed', () => { - const yolo = applyAgentPermissionMode({ mode: 'yolo' }) - const settings = projectAgentLaunchSettings({ - ...yolo, - agentDefaultArgs: { ...yolo.agentDefaultArgs, codex: '--model gpt-5' } - }) - expect(settings.permissionMode).toBe('yolo') - }) - it('changes only agents Auto does not cover', () => { const automatic = applyAgentPermissionMode({ mode: 'auto', diff --git a/src/shared/agent-lead-status-fold.test.ts b/src/shared/agent-lead-status-fold.test.ts index c7c6c099343..5295ec22e2d 100644 --- a/src/shared/agent-lead-status-fold.test.ts +++ b/src/shared/agent-lead-status-fold.test.ts @@ -1,128 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - continueMainAgentStatus, - mainAgentTurnInterrupted, - foldAgentLeadStatus, - isAgentTimeAccruing, - isAgentStatusHeldOpenByChildWork -} from './agent-lead-status-fold' - -describe('foldAgentLeadStatus', () => { - it('keeps a blocked lead while child work is working', () => { - expect(foldAgentLeadStatus({ leadState: 'blocked', childWorkLiveness: 'working' })).toEqual({ - stateName: 'blocked' - }) - }) - - it('reads a settled lead with live agent work as working', () => { - expect(foldAgentLeadStatus({ leadState: 'done', childWorkLiveness: 'working' })).toEqual({ - stateName: 'working' - }) - }) - - it('reads a settled lead with only watch loops as monitoring', () => { - expect(foldAgentLeadStatus({ leadState: 'done', childWorkLiveness: 'monitoring' })).toEqual({ - stateName: 'working', - workingMode: 'monitoring' - }) - }) - - it('settles when nothing is running', () => { - expect(foldAgentLeadStatus({ leadState: 'done', childWorkLiveness: null })).toEqual({ - stateName: 'done' - }) - }) - - describe('a child waiting on a human', () => { - it('makes a working or settled main agent wait', () => { - for (const leadState of ['working', 'done'] as const) { - expect(foldAgentLeadStatus({ leadState, childWorkLiveness: 'waiting' })).toEqual({ - stateName: 'waiting' - }) - } - }) - - it("yields to the main agent's own request for a human, in the main agent's own vocabulary", () => { - for (const leadState of ['waiting', 'blocked'] as const) { - expect(foldAgentLeadStatus({ leadState, childWorkLiveness: 'waiting' })).toEqual({ - stateName: leadState - }) - } - }) - }) -}) - -describe('mainAgentTurnInterrupted', () => { - it('reads only a cancellation verdict as an interrupt', () => { - expect(mainAgentTurnInterrupted({ outcome: 'cancellation' })).toBe(true) - expect(mainAgentTurnInterrupted({ outcome: 'failure' })).toBe(false) - // The legacy flag means the user stopped it; a newer request's replacement names no one. - expect(mainAgentTurnInterrupted({ outcome: 'superseded' })).toBe(false) - expect(mainAgentTurnInterrupted({})).toBe(false) - expect(mainAgentTurnInterrupted(undefined)).toBe(false) - }) - - // Older readers take the flag as "the user stopped it": a crash or an unproven end never sets it. - it('stays down for a turn cut off by a crash and for an end nobody can prove', () => { - expect(mainAgentTurnInterrupted({ outcome: 'interruption' })).toBe(false) - expect(mainAgentTurnInterrupted({ outcome: 'unconfirmed' })).toBe(false) - }) -}) - -describe('isAgentStatusHeldOpenByChildWork', () => { - it('is true only when a settled main agent sits under a row that is not settled', () => { - expect( - isAgentStatusHeldOpenByChildWork({ state: 'working', mainAgent: { state: 'done' } }) - ).toBe(true) - expect(isAgentStatusHeldOpenByChildWork({ state: 'done', mainAgent: { state: 'done' } })).toBe( - false - ) - expect( - isAgentStatusHeldOpenByChildWork({ state: 'working', mainAgent: { state: 'working' } }) - ).toBe(false) - // No main agent fact means no claim: an old host's row is never read as child-held. - expect(isAgentStatusHeldOpenByChildWork({ state: 'working' })).toBe(false) - }) -}) - -describe('isAgentTimeAccruing', () => { - it('accrues while the row works for the main agent or its live agent child work', () => { - expect(isAgentTimeAccruing({ state: 'working' })).toBe(true) - }) - - it('pauses while the row waits on the user, whoever raised the prompt', () => { - expect(isAgentTimeAccruing({ state: 'waiting' })).toBe(false) - expect(isAgentTimeAccruing({ state: 'blocked' })).toBe(false) - }) - - it('does not accrue for a watch loop or a settled row', () => { - expect(isAgentTimeAccruing({ state: 'working', workingMode: 'monitoring' })).toBe(false) - expect(isAgentTimeAccruing({ state: 'done' })).toBe(false) - }) - - it('relies on the fold emitting monitoring only for a settled main agent', () => { - // Monitoring requires a settled lead; live lead turns must still accrue time. - const leadStates = ['working', 'waiting', 'blocked', 'done'] as const - const liveness = ['waiting', 'working', 'monitoring', null] as const - for (const leadState of leadStates) { - for (const childWorkLiveness of liveness) { - const folded = foldAgentLeadStatus({ leadState, childWorkLiveness }) - if (folded.workingMode === 'monitoring') { - expect(leadState).toBe('done') - } - const accrues = isAgentTimeAccruing({ - state: folded.stateName, - workingMode: folded.workingMode - }) - // A child waiting on a human pauses the row, whatever the main agent is doing. - expect(accrues).toBe( - childWorkLiveness !== 'waiting' && - (leadState === 'working' || (leadState === 'done' && childWorkLiveness === 'working')) - ) - } - } - }) -}) +import { continueMainAgentStatus } from './agent-lead-status-fold' describe('continueMainAgentStatus', () => { it('keeps the clock across an unchanged state and restarts it on a change', () => { diff --git a/src/shared/agent-main-agent-verdict.test.ts b/src/shared/agent-main-agent-verdict.test.ts deleted file mode 100644 index a861e521872..00000000000 --- a/src/shared/agent-main-agent-verdict.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - agentMainAgentVerdict, - agentTurnEndedUncleanly, - agentTurnEndedOnPurpose, - agentVerdictDisplayMark, - agentVerdictFields -} from './agent-main-agent-verdict' -import type { AgentTurnOutcome } from './agent-turn-outcome' - -describe('agentMainAgentVerdict', () => { - it.each(['failure', 'interruption'] as const)( - 'reads a main agent whose turn ended in %s while its subagent still works as failed', - (outcome) => { - const row = { state: 'working' as const, mainAgent: { state: 'done' as const, outcome } } - expect(agentMainAgentVerdict(row)).toBe(outcome) - expect(agentVerdictDisplayMark(row)).toBe('failed') - } - ) - - it('keeps a success or a stop with live subagent work reading working', () => { - for (const outcome of ['success', 'cancellation'] as const) { - const row = { state: 'working' as const, mainAgent: { state: 'done' as const, outcome } } - expect(agentVerdictDisplayMark(row)).toBeNull() - } - }) - - it('has no verdict while the main agent itself is not done, whatever the combined row says', () => { - for (const state of ['working', 'blocked', 'waiting'] as const) { - expect( - agentMainAgentVerdict({ state: 'done', interrupted: true, mainAgent: { state } }) - ).toBeNull() - } - }) - - it('reads the legacy flag only on a done row, and under a done main agent with no outcome', () => { - for (const state of ['working', 'blocked', 'waiting'] as const) { - expect(agentMainAgentVerdict({ state, interrupted: true })).toBeNull() - expect( - agentMainAgentVerdict({ state, interrupted: true, mainAgent: { state: 'done' } }) - ).toBeNull() - } - expect(agentMainAgentVerdict({ state: 'done' })).toBeNull() - expect(agentMainAgentVerdict({ state: 'done', interrupted: false })).toBeNull() - expect(agentMainAgentVerdict({ state: 'done', mainAgent: { state: 'done' } })).toBeNull() - expect( - agentMainAgentVerdict({ state: 'done', interrupted: true, mainAgent: { state: 'done' } }) - ).toBe('cancellation') - expect(agentMainAgentVerdict({ state: 'done', interrupted: true })).toBe('cancellation') - }) - - it("marks a done row by its verdict: a user's Stop reads interrupted, any other cut failed", () => { - for (const [outcome, mark, unclean, onPurpose] of [ - ['success', null, false, false], - ['failure', 'failed', true, false], - ['cancellation', 'interrupted', true, true], - // A newer request replaced it: not news, and no one to name. - ['superseded', 'interrupted', true, true], - ['interruption', 'failed', true, false], - ['unconfirmed', 'unconfirmed', true, false] - ] as const) { - const row = { state: 'done' as const, mainAgent: { state: 'done' as const, outcome } } - expect(agentVerdictDisplayMark(row), outcome).toBe(mark) - expect(agentTurnEndedUncleanly(row), outcome).toBe(unclean) - expect(agentTurnEndedOnPurpose(row), outcome).toBe(onPurpose) - } - // An old host's legacy flag is a user's Stop too. - expect(agentVerdictDisplayMark({ state: 'done', interrupted: true })).toBe('interrupted') - }) - - it('reads a crash-cut turn as failed and an unproven end as unconfirmed, neither a stop', () => { - for (const [outcome, mark] of [ - ['interruption', 'failed'], - ['unconfirmed', 'unconfirmed'] - ] as const) { - const row = { state: 'done' as const, mainAgent: { state: 'done' as const, outcome } } - expect(agentVerdictDisplayMark(row)).toBe(mark) - expect(agentTurnEndedUncleanly(row)).toBe(true) - // Nobody asked for it, so attention ranks it as news, like a completion or a failure. - expect(agentTurnEndedOnPurpose(row)).toBe(false) - } - }) - - it('reads an arm from a newer host as no verdict, so the row reads done as it did', () => { - const row = { - state: 'done' as const, - mainAgent: { - state: 'done' as const, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a mirrored row reaches the reader unparsed, carrying an arm this build cannot name. - outcome: 'from-a-newer-host' as AgentTurnOutcome - } - } - expect(agentMainAgentVerdict(row)).toBeNull() - expect(agentVerdictDisplayMark(row)).toBeNull() - expect(agentTurnEndedUncleanly(row)).toBe(false) - }) - - it('prefers the recorded verdict over the legacy flag', () => { - expect( - agentMainAgentVerdict({ - state: 'done', - interrupted: true, - mainAgent: { state: 'done', outcome: 'failure' } - }) - ).toBe('failure') - }) -}) - -describe('agentVerdictFields', () => { - const mainAgent = { state: 'done' as const, outcome: 'failure' as const, stateStartedAt: 7 } - - it('copies the whole main agent status and a true legacy flag together', () => { - expect(agentVerdictFields({ interrupted: true, mainAgent })).toEqual({ - interrupted: true, - mainAgent - }) - }) - - it('copies nothing a row does not carry, and never a false flag', () => { - expect(agentVerdictFields({ interrupted: false })).toEqual({}) - expect(agentVerdictFields({})).toEqual({}) - }) -}) diff --git a/src/shared/agent-model-probe-spec.spec.ts b/src/shared/agent-model-probe-spec.spec.ts deleted file mode 100644 index cc5e73c4b19..00000000000 --- a/src/shared/agent-model-probe-spec.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getAgentModelProbeSpec } from './agent-model-probe-spec' -import { GROK_SESSION_OPTION_CATALOG } from './agent-session/options/agent-session-option-catalog-grok' -import { getCommitMessageAgentSpec, listCommitMessageAgentIds } from './commit-message-agent-spec' - -describe('getAgentModelProbeSpec', () => { - it('resolves grok as a discovery-only probe with no static model list', () => { - const spec = getAgentModelProbeSpec('grok') - expect(spec).toMatchObject({ - id: 'grok', - binary: 'grok', - modelSource: 'dynamic', - models: [], - defaultModelId: 'grok-4.7' - }) - expect(spec?.modelDiscovery).toMatchObject({ binary: 'grok', args: ['models'] }) - }) - - it('wires that probe to the parser that reads a real listing', () => { - // Binary/argv assertions alone miss a wrong parser publishing no models. - const parsed = getAgentModelProbeSpec('grok')!.modelDiscovery!.parse( - 'You are logged in with grok.com.\n\nDefault model: grok-4.5\n\nAvailable models:\n * grok-4.5 (default)\n' - ) - expect(parsed).toEqual([{ id: 'grok-4.5', label: 'Grok 4.5', isDefault: true }]) - }) - - it('keeps the grok probe default in step with the catalog seed', () => { - expect(getAgentModelProbeSpec('grok')!.defaultModelId).toBe( - GROK_SESSION_OPTION_CATALOG.models[0].id - ) - }) - - it('aliases agents without a generation-only default by identity', () => { - // Adapters must not lose discovery fields such as Claude's stdinPayload. - for (const id of listCommitMessageAgentIds().filter((agentId) => agentId !== 'omp')) { - expect(getAgentModelProbeSpec(id)).toBe(getCommitMessageAgentSpec(id)) - } - }) - - it('removes only the OMP generation sentinel while preserving discovery', () => { - const generation = getCommitMessageAgentSpec('omp')! - const probe = getAgentModelProbeSpec('omp')! - expect(generation.defaultModelId).toBe('default') - expect(probe.defaultModelId).toBe('') - expect(probe.models).toEqual([]) - expect(probe.modelDiscovery).toBe(generation.modelDiscovery) - }) - - it('keeps grok out of the commit-message registry', () => { - expect(getCommitMessageAgentSpec('grok')).toBeUndefined() - expect(listCommitMessageAgentIds()).not.toContain('grok') - }) - - it('is undefined for an agent in neither registry', () => { - expect(getAgentModelProbeSpec('aider')).toBeUndefined() - }) -}) diff --git a/src/shared/agent-notification-id.test.ts b/src/shared/agent-notification-id.test.ts deleted file mode 100644 index bd32a616cdb..00000000000 --- a/src/shared/agent-notification-id.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { buildAgentNotificationId } from './agent-notification-id' - -describe('buildAgentNotificationId', () => { - it('builds a stable escaped id for the same agent event metadata', () => { - const args = { - worktreeId: 'repo::/Users/me/orca/workspaces/feature', - paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', - stateStartedAt: 1780000000123 - } - - const id = buildAgentNotificationId(args) - expect(id).toBe( - 'agent:repo%3A%3A%2FUsers%2Fme%2Forca%2Fworkspaces%2Ffeature:tab-1%3A11111111-1111-4111-8111-111111111111:1780000000123' - ) - expect(buildAgentNotificationId(args)).toBe(id) - }) - - it('changes when the agent state start time changes', () => { - const base = { - worktreeId: 'repo::/Users/me/orca/workspaces/feature', - paneKey: 'tab-1:11111111-1111-4111-8111-111111111111' - } - - expect(buildAgentNotificationId({ ...base, stateStartedAt: 1780000000123 })).not.toBe( - buildAgentNotificationId({ ...base, stateStartedAt: 1780000000456 }) - ) - }) - - it('returns null when required fields are missing', () => { - expect( - buildAgentNotificationId({ - paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', - stateStartedAt: 1780000000123 - }) - ).toBeNull() - expect( - buildAgentNotificationId({ - worktreeId: 'repo::/Users/me/orca/workspaces/feature', - stateStartedAt: 1780000000123 - }) - ).toBeNull() - expect( - buildAgentNotificationId({ - worktreeId: 'repo::/Users/me/orca/workspaces/feature', - paneKey: 'tab-1:11111111-1111-4111-8111-111111111111' - }) - ).toBeNull() - }) -}) diff --git a/src/shared/agent-pending-prompt.test.ts b/src/shared/agent-pending-prompt.test.ts index ba632038d10..13a3167356c 100644 --- a/src/shared/agent-pending-prompt.test.ts +++ b/src/shared/agent-pending-prompt.test.ts @@ -19,43 +19,6 @@ function waiting(interactivePrompt: string, toolName?: string) { } describe('readAgentPromptAnswers', () => { - it('reads an approval with the cards’ own allow and deny keys', () => { - for (const agent of ['claude', 'codex']) { - const read = readAgentPromptAnswers(waiting(approval), agent, 'p1', true) - expect(read?.prompt).toEqual({ - kind: 'approval', - promptId: 'p1', - tool: 'Bash', - summary: 'rm -rf build', - options: [ - { id: 'allow_once', label: 'Allow once' }, - { id: 'reject_once', label: 'Reject' } - ] - }) - expect(Object.fromEntries(read!.keysByOption)).toEqual({ - allow_once: '1', - reject_once: '\x1b' - }) - } - }) - - it('reads a one-question single choice with each option’s selector key', () => { - for (const agent of ['claude', 'codex']) { - const read = readAgentPromptAnswers(waiting(question(), 'AskUserQuestion'), agent, 'p2', true) - expect(read?.prompt).toEqual({ - kind: 'question', - promptId: 'p2', - question: 'Which color?', - header: 'Color', - options: [ - { id: '1', label: 'Red', description: 'warm' }, - { id: '2', label: 'Blue' } - ] - }) - expect(Object.fromEntries(read!.keysByOption)).toEqual({ '1': '1', '2': '2' }) - } - }) - it('leaves to Orca what one keystroke cannot answer', () => { const unsupported = { prompt: { kind: 'unsupported', promptId: 'p' }, @@ -92,11 +55,4 @@ describe('readAgentPromptAnswers', () => { readAgentPromptAnswers(waiting(question(), 'AskUserQuestion'), 'grok', 'p', true) ).toEqual(unsupported) }) - - it('reads nothing while the agent is not waiting on the user', () => { - expect( - readAgentPromptAnswers({ state: 'working', interactivePrompt: approval }, 'claude', 'p', true) - ).toBeNull() - expect(readAgentPromptAnswers({ state: 'done' }, 'claude', 'p', true)).toBeNull() - }) }) diff --git a/src/shared/agent-process-presence-platform.test.ts b/src/shared/agent-process-presence-platform.test.ts index 98d3064bb77..b37178321c9 100644 --- a/src/shared/agent-process-presence-platform.test.ts +++ b/src/shared/agent-process-presence-platform.test.ts @@ -15,19 +15,6 @@ afterEach(() => { }) describe('platform process evidence', () => { - it('leaves Windows unanswered until a Windows hook captures an identity', async () => { - platform('win32') - expect(await readAgentProcess(4242)).toEqual({ verdict: 'unverifiable' }) - expect(run).not.toHaveBeenCalled() - expect(read).not.toHaveBeenCalled() - }) - - it('treats a missing macOS utility as unanswered', async () => { - platform('darwin') - run.mockRejectedValue(Object.assign(new Error('missing ps'), { code: 'ENOENT' })) - expect(await readAgentProcess(4242)).toEqual({ verdict: 'unverifiable' }) - }) - it('requires Linux process-file absence rather than an unreadable boot identity', async () => { platform('linux') read.mockRejectedValueOnce(Object.assign(new Error('gone'), { code: 'ENOENT' })) diff --git a/src/shared/agent-process-presence-probe.test.ts b/src/shared/agent-process-presence-probe.test.ts index ad21ab3e0cd..236d812640d 100644 --- a/src/shared/agent-process-presence-probe.test.ts +++ b/src/shared/agent-process-presence-probe.test.ts @@ -31,31 +31,4 @@ describe('single-process presence evidence', () => { expect(await probeAgentProcessPresence(identity, read, 'linux', observe)).toBe('exited') expect(observe).not.toHaveBeenCalled() }) - - it.each<[AgentProcessObservation, string]>([ - [{ verdict: 'live', startTime: 'boot:100', zombie: false }, 'live'], - [{ verdict: 'live', startTime: 'boot:101', zombie: false }, 'exited'], - [{ verdict: 'live', startTime: 'boot:100', zombie: true }, 'exited'], - [{ verdict: 'live', startTime: 'boot:100', zombie: false, stopped: true }, 'unverifiable'], - [{ verdict: 'exited' }, 'exited'], - [{ verdict: 'unverifiable' }, 'unverifiable'] - ])('judges %j as %s', async (observation, verdict) => { - const read = vi.fn(async () => observation) - expect(await probeAgentProcessPresence(identity, read, 'linux')).toBe(verdict) - expect(read).toHaveBeenCalledExactlyOnceWith(4242) - }) - - it('keeps a timeout or permission error unverifiable', async () => { - const read = vi.fn(async () => { - throw new Error('EPERM') - }) - expect(await probeAgentProcessPresence(identity, read, 'linux')).toBe('unverifiable') - }) - - it('does not inspect a missing identity or a different execution platform', async () => { - const read = vi.fn() - expect(await probeAgentProcessPresence(undefined, read, 'linux')).toBe('unverifiable') - expect(await probeAgentProcessPresence(identity, read, 'win32')).toBe('unverifiable') - expect(read).not.toHaveBeenCalled() - }) }) diff --git a/src/shared/agent-process-recognition.test.ts b/src/shared/agent-process-recognition.test.ts index 3f09974170b..871e825bcd0 100644 --- a/src/shared/agent-process-recognition.test.ts +++ b/src/shared/agent-process-recognition.test.ts @@ -2,47 +2,9 @@ import { describe, expect, it } from 'vitest' import { withFreshOmpLaunch } from './omp-fresh-launch' import { buildAgentDraftLaunchPlan } from './tui-agent-startup' import { detectExplicitPiAgentKindFromCommand } from './pi-agent-kind' -import { - isAgentForegroundWrapperProcess, - isExpectedAgentProcess, - isRecognizedAgentType, - recognizeAgentProcess, - recognizeAgentProcessFromCommandLine -} from './agent-process-recognition' +import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition' describe('agent process recognition', () => { - it.each([ - '/usr/local/bin/dsb', - '/usr/bin/deepseek-build', - 'C:\\Users\\dev\\bin\\dsb.cmd', - 'C:\\Users\\dev\\bin\\deepseek-build-agent.exe' - ])('recognizes manually started DeepSeek Build at %s', (processName) => { - expect(recognizeAgentProcess(processName)?.agent).toBe('dsb') - }) - - it('recognizes DeepSeek Build by binary name and its npm shim', () => { - expect(recognizeAgentProcess('dsb')).toEqual({ agent: 'dsb', processName: 'dsb' }) - expect(recognizeAgentProcess('deepseek-build')).toEqual({ - agent: 'dsb', - processName: 'deepseek-build' - }) - expect(recognizeAgentProcess('deepseek-build-agent')).toEqual({ - agent: 'dsb', - processName: 'deepseek-build-agent' - }) - expect( - recognizeAgentProcessFromCommandLine( - 'node /usr/lib/node_modules/@innocarpe/deepseek-build/npm/bin/dsb.js' - ) - ).toEqual({ agent: 'dsb', processName: 'dsb' }) - expect( - recognizeAgentProcessFromCommandLine( - 'node /usr/lib/node_modules/@innocarpe/deepseek-build/npm/bin/dsb.js run "explain this"' - ) - ).toBeNull() - expect(recognizeAgentProcessFromCommandLine('dsb run "explain this"')).toBeNull() - }) - it.each([ '-r ./preload.js', '--require "./preload path.js"', @@ -61,14 +23,6 @@ describe('agent process recognition', () => { expect(recognizeAgentProcessFromCommandLine(`${shim} agent`)?.agent).toBe('dsb') }) - it('recognizes packaged Codex foreground process names', () => { - expect(recognizeAgentProcess('codex-aarch64-ap')).toEqual({ - agent: 'codex', - processName: 'codex-aarch64-ap' - }) - expect(isRecognizedAgentType('codex-aarch64-ap')).toBe(true) - }) - it.each(['posix', 'powershell', 'cmd'] as const)( 'recognizes fresh OMP commands and drafts in %s', (shell) => { @@ -95,12 +49,6 @@ describe('agent process recognition', () => { } ) - it('recognizes cmd fresh OMP lines built before the by-name guard', () => { - const legacy = - 'if exist "%ORCA_OMP_FRESH_CONFIG%" (if not exist "%ORCA_OMP_FRESH_CONFIG%\\*" (omp --config "%ORCA_OMP_FRESH_CONFIG%") else (x)) else (x)' - expect(recognizeAgentProcessFromCommandLine(legacy)?.agent).toBe('omp') - }) - it('does not classify embedded fresh-launch text as an agent command', () => { for (const command of [ `echo 'omp --config "$ORCA_OMP_FRESH_CONFIG"'`, @@ -111,41 +59,6 @@ describe('agent process recognition', () => { } }) - it('recognizes both published DeepSeek Build npm entrypoints and excludes run with cwd', () => { - const shim = '/usr/lib/node_modules/@innocarpe/deepseek-build/npm/bin/deepseek-build.js' - expect(recognizeAgentProcessFromCommandLine(`node ${shim}`)?.agent).toBe('dsb') - expect(recognizeAgentProcessFromCommandLine(`node ${shim} --cwd folder run task`)).toBeNull() - expect(recognizeAgentProcessFromCommandLine('dsb --cwd folder run task')).toBeNull() - }) - - it('recognizes the OpenClaude foreground process', () => { - expect(recognizeAgentProcess('/usr/local/bin/openclaude')).toEqual({ - agent: 'openclaude', - processName: 'openclaude' - }) - expect(isRecognizedAgentType('openclaude')).toBe(true) - expect(isExpectedAgentProcess('/usr/local/bin/openclaude', 'claude')).toBe(false) - }) - - it('recognizes the Droid foreground process on Windows', () => { - expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\droid.cmd`)).toEqual({ - agent: 'droid', - processName: 'droid' - }) - }) - - it('matches expected agents from platform-specific foreground process paths', () => { - expect(recognizeAgentProcess('claude')).toEqual({ - agent: 'claude', - processName: 'claude' - }) - expect( - isExpectedAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\claude.exe`, 'claude') - ).toBe(true) - expect(isExpectedAgentProcess('/usr/local/bin/claude', 'claude')).toBe(true) - expect(isExpectedAgentProcess('powershell.exe', 'claude')).toBe(false) - }) - it('does not recognize Claude print-mode hook subprocesses as interactive agents', () => { expect( recognizeAgentProcessFromCommandLine( @@ -168,39 +81,6 @@ describe('agent process recognition', () => { }) }) - it('recognizes Command Code without classifying Windows cmd.exe as an agent', () => { - expect(recognizeAgentProcess('command-code')).toEqual({ - agent: 'command-code', - processName: 'command-code' - }) - expect( - recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\command-code.cmd`) - ).toEqual({ - agent: 'command-code', - processName: 'command-code' - }) - expect(isRecognizedAgentType('command-code')).toBe(true) - expect(isRecognizedAgentType('cmd.exe')).toBe(false) - expect(recognizeAgentProcess('cmd.exe')).toBeNull() - }) - - it('recognizes Ante without classifying ante-prefixed path fragments as the agent', () => { - expect(recognizeAgentProcess('ante')).toEqual({ - agent: 'ante', - processName: 'ante' - }) - expect(recognizeAgentProcess('/Users/dev/.ante/bin/ante')).toEqual({ - agent: 'ante', - processName: 'ante' - }) - expect(isExpectedAgentProcess('/Users/dev/.ante/bin/ante', 'ante')).toBe(true) - expect(isRecognizedAgentType('ante')).toBe(true) - // Ante occurs in ordinary names; require its exact normalized basename. - expect(recognizeAgentProcess('ante-obsidian')).toBeNull() - expect(recognizeAgentProcess('antechamber')).toBeNull() - expect(isExpectedAgentProcess('ante-obsidian', 'ante')).toBe(false) - }) - it('does not recognize Ante headless one-shot commands as interactive agents', () => { expect(recognizeAgentProcessFromCommandLine('ante -p "summarize this diff"')).toBeNull() expect(recognizeAgentProcessFromCommandLine('ante -psummarize')).toBeNull() @@ -231,22 +111,6 @@ describe('agent process recognition', () => { }) }) - it('recognizes Trae by its traecli binary, not the ambiguous trae-cli name', () => { - expect(recognizeAgentProcess('traecli')).toEqual({ - agent: 'trae', - processName: 'traecli' - }) - expect(recognizeAgentProcess('/Users/dev/.local/bin/traecli')).toEqual({ - agent: 'trae', - processName: 'traecli' - }) - expect(isExpectedAgentProcess('/Users/dev/.local/bin/traecli', 'traecli')).toBe(true) - expect(isRecognizedAgentType('traecli')).toBe(true) - // Why: `trae-cli` and `trae-agent` both name the unrelated open-source bytedance/trae-agent. - expect(recognizeAgentProcess('trae-cli')).toBeNull() - expect(recognizeAgentProcess('trae-agent')).toBeNull() - }) - it('does not recognize Trae headless one-shot commands as interactive agents', () => { expect(recognizeAgentProcessFromCommandLine('traecli -p "summarize this diff"')).toBeNull() expect(recognizeAgentProcessFromCommandLine('traecli --print "review this"')).toBeNull() @@ -267,44 +131,6 @@ describe('agent process recognition', () => { }) }) - it('recognizes Muse by its muse binary', () => { - expect(recognizeAgentProcess('muse')).toEqual({ - agent: 'muse', - processName: 'muse' - }) - expect(recognizeAgentProcess('/Users/dev/.local/bin/muse')).toEqual({ - agent: 'muse', - processName: 'muse' - }) - expect(isExpectedAgentProcess('/Users/dev/.local/bin/muse', 'muse')).toBe(true) - expect(isRecognizedAgentType('muse')).toBe(true) - }) - - it('recognizes Mistral Vibe by its installed executable and legacy alias', () => { - expect(recognizeAgentProcess('/home/dev/.local/bin/vibe')).toEqual({ - agent: 'mistral-vibe', - processName: 'vibe' - }) - expect(recognizeAgentProcess('mistral-vibe')).toEqual({ - agent: 'mistral-vibe', - processName: 'mistral-vibe' - }) - expect(isRecognizedAgentType('vibe')).toBe(true) - }) - - it('recognizes Kimi Code by the kimi-code process its launcher becomes', () => { - expect(recognizeAgentProcess('/home/dev/.kimi-code/bin/kimi')).toEqual({ - agent: 'kimi', - processName: 'kimi' - }) - expect(recognizeAgentProcess('kimi-code')).toEqual({ - agent: 'kimi', - processName: 'kimi-code' - }) - expect(isExpectedAgentProcess('/home/dev/.kimi-code/bin/kimi', 'kimi')).toBe(true) - expect(isRecognizedAgentType('kimi-code')).toBe(true) - }) - // #31311: the npm install on Windows runs as node.exe, observed with this exact .bin shim path. it('recognizes Kimi Code launched by its npm shim on Windows', () => { expect( @@ -315,19 +141,6 @@ describe('agent process recognition', () => { expect(recognizeAgentProcessFromCommandLine('node C:\\tools\\app\\dist\\main.mjs')).toBeNull() }) - it('recognizes Qwen Code by its installed qwen executable', () => { - expect(recognizeAgentProcess('/home/dev/.local/bin/qwen')).toEqual({ - agent: 'qwen-code', - processName: 'qwen' - }) - expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Roaming\npm\qwen.cmd`)).toEqual({ - agent: 'qwen-code', - processName: 'qwen' - }) - expect(isExpectedAgentProcess('/usr/local/bin/qwen', 'qwen')).toBe(true) - expect(isRecognizedAgentType('qwen')).toBe(true) - }) - it('recognizes agent CLIs launched through interpreter wrappers', () => { expect( recognizeAgentProcessFromCommandLine('node /Users/dev/.nvm/versions/node/bin/codex') @@ -471,27 +284,6 @@ describe('agent process recognition', () => { expect(recognizeAgentProcessFromCommandLine(command)).toBeNull() }) - it('recognizes Prime Agent by its binary and npm entrypoint', () => { - expect(recognizeAgentProcess('prime-agent')).toEqual({ - agent: 'prime-agent', - processName: 'prime-agent' - }) - expect(recognizeAgentProcess('/opt/homebrew/bin/prime-agent')).toEqual({ - agent: 'prime-agent', - processName: 'prime-agent' - }) - expect( - recognizeAgentProcessFromCommandLine( - 'node /opt/homebrew/lib/node_modules/prime-agent/dist/bundle/cli.js' - ) - ).toEqual({ agent: 'prime-agent', processName: 'prime-agent' }) - expect( - recognizeAgentProcessFromCommandLine( - String.raw`node.exe C:\Users\dev\AppData\Roaming\npm\node_modules\prime-agent\dist\bundle\cli.js` - ) - ).toEqual({ agent: 'prime-agent', processName: 'prime-agent' }) - }) - it('does not recognize Prime Agent headless one-shot commands as interactive agents', () => { expect(recognizeAgentProcessFromCommandLine('prime-agent -p "summarize this diff"')).toBeNull() expect(recognizeAgentProcessFromCommandLine('prime-agent --print "review this"')).toBeNull() @@ -580,64 +372,6 @@ describe('agent process recognition', () => { expect(recognizeAgentProcessFromCommandLine('python -m not_aider')).toBeNull() }) - it('identifies only foreground processes that can wrap agent entrypoints', () => { - expect(isAgentForegroundWrapperProcess('node.exe')).toBe(true) - expect(isAgentForegroundWrapperProcess('/usr/bin/python3')).toBe(true) - expect(isAgentForegroundWrapperProcess('python3.12.exe')).toBe(true) - expect(isAgentForegroundWrapperProcess('bash')).toBe(false) - expect(isAgentForegroundWrapperProcess('vim.exe')).toBe(false) - }) - - it('recognizes the Antigravity CLI from bare, POSIX and Windows command lines', () => { - const agy = { agent: 'antigravity', processName: 'agy' } - - expect(recognizeAgentProcess('agy')).toEqual(agy) - expect(recognizeAgentProcess('/Users/dev/.local/bin/agy')).toEqual(agy) - expect(recognizeAgentProcess(String.raw`C:\Users\dev\AppData\Local\agy\bin\agy.exe`)).toEqual( - agy - ) - expect( - recognizeAgentProcessFromCommandLine( - String.raw`"C:\Users\dev\AppData\Local\agy\bin\agy.exe" --dangerously-skip-permissions` - ) - ).toEqual(agy) - expect(recognizeAgentProcessFromCommandLine('agy --dangerously-skip-permissions')).toEqual(agy) - }) - - it('recognizes versioned Grok process names observed from the installed CLI', () => { - expect(recognizeAgentProcess('grok-0.2.51')).toEqual({ - agent: 'grok', - processName: 'grok-0.2.51' - }) - }) - - it('recognizes the versioned Muse binary execed by the launcher', () => { - expect(recognizeAgentProcess('muse-bin-1.3.0-r3401.1')).toEqual({ - agent: 'muse', - processName: 'muse-bin-1.3.0-r3401.1' - }) - expect( - recognizeAgentProcessFromCommandLine('/Users/dev/.local/bin/muse-bin-1.3.0-R3401.1') - ).toEqual({ - agent: 'muse', - processName: 'muse-bin-1.3.0-r3401.1' - }) - // Why: Linux truncates comm to 15 chars, so `muse-bin-1.0.3-R…` rows still match. - expect(recognizeAgentProcess('muse-bin-1.0.3-R')).toEqual({ - agent: 'muse', - processName: 'muse-bin-1.0.3-r' - }) - expect(recognizeAgentProcess('muse-workbench')).toBeNull() - expect(isRecognizedAgentType('muse-bin-1.3.0-R3401.1')).toBe(true) - expect(isExpectedAgentProcess('muse', 'muse')).toBe(true) - expect(isExpectedAgentProcess('muse-bin-1.3.0-R3401.1', 'muse')).toBe(true) - expect(isExpectedAgentProcess('/Users/dev/.local/bin/muse-bin-1.3.0-R3401.1', 'muse')).toBe( - true - ) - expect(isExpectedAgentProcess('not-muse', 'muse')).toBe(false) - expect(isExpectedAgentProcess('muse-workbench', 'muse')).toBe(false) - }) - it('does not recognize Muse headless exec runs as interactive agents', () => { expect(recognizeAgentProcessFromCommandLine("muse 'exec'")).toBeNull() for (const command of ['muse resume', 'muse -- resume', 'muse -- "exec the release notes"']) { diff --git a/src/shared/agent-prompt-injection.test.ts b/src/shared/agent-prompt-injection.test.ts index 4989c07b3a9..c8cd8b11334 100644 --- a/src/shared/agent-prompt-injection.test.ts +++ b/src/shared/agent-prompt-injection.test.ts @@ -3,41 +3,17 @@ import { AGENT_PROMPT_BRACKETED_PASTE_END, AGENT_PROMPT_BRACKETED_PASTE_START, buildAgentPromptPasteBytes, - agentPromptSubmitJoinsPasteFrame, - agentPromptTakesLeadLine, getAgentPromptSubmitDelayMs, getMaxTerminalPasteBytesForIngestMs, getTerminalPasteIngestMs, isHeldAgentPromptDraft, - iterateAgentPromptPasteChunks, - resolveAgentPromptSubmitDelayForAgent + iterateAgentPromptPasteChunks } from './agent-prompt-injection' const BEGIN = AGENT_PROMPT_BRACKETED_PASTE_START const END = AGENT_PROMPT_BRACKETED_PASTE_END describe('agent prompt injection bytes', () => { - it('joins submit only for OMP', () => { - expect(agentPromptSubmitJoinsPasteFrame('omp')).toBe(true) - expect(agentPromptSubmitJoinsPasteFrame('claude')).toBe(false) - expect(agentPromptSubmitJoinsPasteFrame('codex')).toBe(false) - expect(agentPromptSubmitJoinsPasteFrame(undefined)).toBe(false) - }) - - it('always bracket-pastes prompts so agent TUIs treat newlines as content', () => { - expect(buildAgentPromptPasteBytes('line one\nline two')).toBe( - '\x1b[200~line one\nline two\x1b[201~' - ) - }) - - it('types the lead line only for Claude agents and unidentified ones', () => { - expect(agentPromptTakesLeadLine('claude')).toBe(true) - expect(agentPromptTakesLeadLine('claude-agent-teams')).toBe(true) - expect(agentPromptTakesLeadLine(null)).toBe(true) - expect(agentPromptTakesLeadLine('codex')).toBe(false) - expect(agentPromptTakesLeadLine('opencode')).toBe(false) - }) - it('types the lead line ahead of the paste frame on one line', () => { expect(buildAgentPromptPasteBytes('brief', 'Please\r\nfollow\x03\x1b[201~\x7f')).toBe( `Please follow [201~ ${BEGIN}brief${END}` @@ -45,23 +21,6 @@ describe('agent prompt injection bytes', () => { expect(buildAgentPromptPasteBytes('brief', '')).toBe(`${BEGIN}brief${END}`) }) - it('keeps generic prompt behavior for recognition-only agents', () => { - expect(agentPromptTakesLeadLine('dsb')).toBe(agentPromptTakesLeadLine(undefined)) - expect(agentPromptSubmitJoinsPasteFrame('dsb')).toBe(false) - for (const platform of ['darwin', 'linux', 'win32'] as const) { - expect(resolveAgentPromptSubmitDelayForAgent(platform, 'first\nsecond', 'dsb')).toBe( - resolveAgentPromptSubmitDelayForAgent(platform, 'first\nsecond', undefined) - ) - } - }) - - it('costs a common-sized prompt far less than the old flat Windows delay', () => { - // Measured ingest: 2 KB 14-25 ms, 8 KB 60-89 ms. The old constant charged 1_500 ms for both. - expect(getAgentPromptSubmitDelayMs('win32', 2_000)).toBeLessThan(700) - expect(getAgentPromptSubmitDelayMs('win32', 8_000)).toBeLessThan(700) - expect(getAgentPromptSubmitDelayMs('darwin', 8_000)).toBeLessThan(700) - }) - it.each([ // Pin the slower measured Win11 ingest samples so Enter cannot race incomplete paste. [2_000, 25], @@ -75,16 +34,6 @@ describe('agent prompt injection bytes', () => { expect(getAgentPromptSubmitDelayMs('win32', bytes)).toBeGreaterThan(measuredMs) }) - it('keeps a real margin over the slowest measured slope', () => { - // 1.5x of 0.0104 ms/byte, so a host ~50% slower than either measured one is still covered. - expect(getTerminalPasteIngestMs('win32', 320_000)).toBeGreaterThan(3_342 * 1.4) - }) - - it('outgrows the old 1_500 ms constant before ConPTY ingest does', () => { - // Ingest crossed 1_500 ms at ~145 KB; the delay must already exceed it there. - expect(getAgentPromptSubmitDelayMs('win32', 145_000)).toBeGreaterThan(1_500) - }) - it('scales without a cap all the way to the terminal input ceiling', () => { const ceilingBytes = 16 * 1024 * 1024 expect(getAgentPromptSubmitDelayMs('win32', ceilingBytes)).toBeGreaterThan(250_000) @@ -92,35 +41,6 @@ describe('agent prompt injection bytes', () => { expect(getAgentPromptSubmitDelayMs('linux', ceilingBytes)).toBeGreaterThan(4_000) }) - it('charges non-Windows hosts nothing measurable for a real prompt', () => { - expect(getTerminalPasteIngestMs('darwin', 8_000)).toBeLessThanOrEqual(2) - expect(getTerminalPasteIngestMs('linux', 0)).toBe(0) - expect(getTerminalPasteIngestMs('linux', Number.NaN)).toBe(0) - expect(getTerminalPasteIngestMs('win32', -5)).toBe(0) - }) - - it('grows monotonically with payload size on every platform', () => { - for (const platform of ['win32', 'darwin', 'linux'] as const) { - expect(getAgentPromptSubmitDelayMs(platform, 400_000)).toBeGreaterThan( - getAgentPromptSubmitDelayMs(platform, 40_000) - ) - } - expect(getTerminalPasteIngestMs('win32', 320_000)).toBeGreaterThan( - getTerminalPasteIngestMs('darwin', 320_000) - ) - }) - - it('adds per-line settle time for antigravity multiline prompts', () => { - const short = resolveAgentPromptSubmitDelayForAgent('darwin', 'one line', 'antigravity') - const long = resolveAgentPromptSubmitDelayForAgent( - 'darwin', - `${'Filler line\n'.repeat(100)}AGY_LONG_OK`, - 'antigravity' - ) - expect(long - short).toBeGreaterThanOrEqual(100 * 45) - expect(resolveAgentPromptSubmitDelayForAgent('darwin', 'one line', 'aider')).toBe(short - 45) - }) - it('inverts the host ingest budget without crossing it', () => { const bytes = getMaxTerminalPasteBytesForIngestMs('win32', 20_000) expect(getTerminalPasteIngestMs('win32', bytes)).toBe(20_000) diff --git a/src/shared/agent-resume-argv-drop.spec.ts b/src/shared/agent-resume-argv-drop.spec.ts index 008ba4fab41..b8357e6c6b1 100644 --- a/src/shared/agent-resume-argv-drop.spec.ts +++ b/src/shared/agent-resume-argv-drop.spec.ts @@ -70,16 +70,6 @@ describe('dropAgentResumeArgvFromCommand', () => { ).toEqual({ status: 'dropped', command: "codex '-c' 'disable_paste_burst=true' '--search'" }) }) - it('drops an unquoted resume argv from a legacy persisted command', () => { - expect( - dropAgentResumeArgvFromCommand({ - command: `codex resume ${CODEX_SESSION.id}`, - agent: 'codex', - providerSession: CODEX_SESSION - }) - ).toEqual({ status: 'dropped', command: 'codex' }) - }) - it('drops the joined Copilot resume token and keeps the user args', () => { const providerSession = { key: 'session_id' as const, @@ -101,16 +91,6 @@ describe('dropAgentResumeArgvFromCommand', () => { ).toEqual({ status: 'dropped', command: "copilot '--yolo'" }) }) - it('reports absent when the command never carried the resume locator', () => { - expect( - dropAgentResumeArgvFromCommand({ - command: 'codex --search', - agent: 'codex', - providerSession: CODEX_SESSION - }) - ).toEqual({ status: 'absent' }) - }) - it('refuses a command whose locator is not the trailing argv', () => { // Why: the caller must fail the spawn rather than launch a resume it could not cancel. expect( @@ -142,18 +122,6 @@ describe('dropAgentResumeArgvFromCommand', () => { ).toEqual({ status: 'unrecognized' }) }) - it('drops a resume argv left with trailing whitespace by a persisted command', () => { - // Why: the suffix match is anchored to the end, so an unnormalized command would - // fall through to `unrecognized` and refuse an otherwise strippable launch. - expect( - dropAgentResumeArgvFromCommand({ - command: `codex 'resume' '${CODEX_SESSION.id}' \n`, - agent: 'codex', - providerSession: CODEX_SESSION - }) - ).toEqual({ status: 'dropped', command: 'codex' }) - }) - it('refuses when the locator also appears outside the trailing resume argv', () => { // Why: stripping the suffix would leave the id behind in the cwd, so the launch is // not actually resume-free and must not be reported as dropped. @@ -165,14 +133,4 @@ describe('dropAgentResumeArgvFromCommand', () => { }) ).toEqual({ status: 'unrecognized' }) }) - - it('drops the resume argv for the other resumable agents too', () => { - expect( - dropAgentResumeArgvFromCommand({ - command: `claude '--resume' '${CODEX_SESSION.id}'`, - agent: 'claude', - providerSession: CODEX_SESSION - }) - ).toEqual({ status: 'dropped', command: 'claude' }) - }) }) diff --git a/src/shared/agent-resume-launch-command.spec.ts b/src/shared/agent-resume-launch-command.spec.ts index 437fd024759..bd4ed88aeba 100644 --- a/src/shared/agent-resume-launch-command.spec.ts +++ b/src/shared/agent-resume-launch-command.spec.ts @@ -36,18 +36,6 @@ function expectSingleAuthoritativeResume(command: string, shell: AgentStartupShe } describe('buildClaudeResumeLaunchCommand', () => { - it.each(SHELLS)('appends the authoritative selector to a plain base ($shell)', ({ shell }) => { - const command = buildClaudeResumeLaunchCommand('claude', RESUME, shell) - expectSingleAuthoritativeResume(command, shell) - expect(command.startsWith('claude ')).toBe(true) - }) - - it.each(SHELLS)('strips a bare persisted --resume picker default ($shell)', ({ shell }) => { - const base = shell === 'cmd' ? 'claude "--resume"' : "claude '--resume'" - const command = buildClaudeResumeLaunchCommand(base, RESUME, shell) - expectSingleAuthoritativeResume(command, shell) - }) - it.each([ 'claude --resume stale-session', 'claude --resume=stale-session', @@ -64,12 +52,6 @@ describe('buildClaudeResumeLaunchCommand', () => { expectSingleAuthoritativeResume(command, 'posix') }) - it('keeps surviving options when stripping selectors', () => { - expect( - buildClaudeResumeLaunchCommand('claude --resume stale --model sonnet', RESUME, 'posix') - ).toBe(`claude --model sonnet '--resume' '${SESSION_ID}'`) - }) - it('never mistakes a dash-leading option value for a selector', () => { expect(buildClaudeResumeLaunchCommand('claude --model -recent', RESUME, 'posix')).toBe( `claude --model -recent '--resume' '${SESSION_ID}'` @@ -85,37 +67,12 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it('leaves the ambiguous joined -r form alone (degrades to pre-guard behavior)', () => { - expect(buildClaudeResumeLaunchCommand('claude -rstale-session', RESUME, 'posix')).toBe( - `claude -rstale-session '--resume' '${SESSION_ID}'` - ) - }) - - it('leaves wrapper commands untouched and appends at the end', () => { - expect(buildClaudeResumeLaunchCommand('bash -c claude', RESUME, 'posix')).toBe( - `bash -c claude '--resume' '${SESSION_ID}'` - ) - expect(buildClaudeResumeLaunchCommand('mise exec -- claude', RESUME, 'posix')).toBe( - `mise exec -- claude '--resume' '${SESSION_ID}'` - ) - expect(buildClaudeResumeLaunchCommand('sudo -u dev -- claude', RESUME, 'posix')).toBe( - `sudo -u dev -- claude '--resume' '${SESSION_ID}'` - ) - }) - it('strips selectors that follow claude after a wrapper terminator', () => { expect( buildClaudeResumeLaunchCommand("mise exec -- claude '--resume' stale", RESUME, 'posix') ).toBe(`mise exec -- claude '--resume' '${SESSION_ID}'`) }) - it('fails open for claude outside command position (wrapper without --)', () => { - // Without a wrapper -- boundary, claude may be an argument; retain append-only behavior. - expect(buildClaudeResumeLaunchCommand("npx claude '--resume'", RESUME, 'posix')).toBe( - `npx claude '--resume' '--resume' '${SESSION_ID}'` - ) - }) - it('never mistakes a claude-suffixed argument for the executable', () => { expect( buildClaudeResumeLaunchCommand( @@ -141,21 +98,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ).toBe(`nix develop /Users/me/src/claude -c claude --resume OLD '--resume' '${SESSION_ID}'`) }) - it.each(['claude "\\--resume" old', 'claude "\\-r" old', 'claude --model "\\--resume"'])( - 'fails open on a double-quoted backslash the shell keeps literal: %s', - (base) => { - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - } - ) - - it('still strips when a double-quoted backslash is shell-consumed', () => { - expect( - buildClaudeResumeLaunchCommand('claude --model "a\\"b" --resume old', RESUME, 'posix') - ).toBe(`claude --model "a\\"b" '--resume' '${SESSION_ID}'`) - }) - it.each([ 'claude "--resu\\\nme" --resume stale', "claude $'-c' --resume stale", @@ -166,35 +108,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it.each([ - 'claude --resume old --append-system-prompt "spend under 5$"', - 'claude --resume old --note x$', - 'claude --resume old --note "a$"' - ])('still strips when a $ is not an expansion opener: %s', (base) => { - const command = buildClaudeResumeLaunchCommand(base, RESUME, 'posix') - expect(command).not.toContain('--resume old') - expectSingleAuthoritativeResume(command, 'posix') - }) - - it.each(['claude a^" --resume ^"b', 'claude ^"x --resume^" --resume old'])( - 'fails open when a cmd caret escapes a quote: %s', - (base) => { - // cmd strips the caret before child quote parsing; tokenizer words no longer match argv. - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe( - `${base} "--resume" "${SESSION_ID}"` - ) - } - ) - - it.each(['claude --resume (Get-Content id.txt)', 'claude --hook { npm -c run } --resume old'])( - 'fails open on bare powershell evaluation syntax: %s', - (base) => { - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - } - ) - it.each([ 'claude "-`r" foo --model x', 'claude "-`u{72}" oldid --resume x', @@ -207,14 +120,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it('fails open on a token-leading powershell backtick before whitespace', () => { - // PowerShell drops backtick+whitespace without a token; the extra tokenizer token would shift the locator. - const base = 'claude --resume ` \t"q"' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - it('fails open on the powershell stop-parsing token', () => { // After --%, appended quotes reach the child literally. const base = 'claude --% --resume stale --model sonnet' @@ -223,52 +128,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it('still strips when --% is quoted or on cmd, where it is an ordinary token', () => { - expect( - buildClaudeResumeLaunchCommand('claude "--%" --resume stale', RESUME, 'powershell') - ).toBe(`claude "--%" '--resume' '${SESSION_ID}'`) - expect(buildClaudeResumeLaunchCommand('claude --% --resume stale', RESUME, 'cmd')).toBe( - `claude --% "--resume" "${SESSION_ID}"` - ) - }) - - it('still strips with parens safely inside quotes on powershell', () => { - expect( - buildClaudeResumeLaunchCommand( - 'claude --allowedTools "Bash(git:*)" --resume old', - RESUME, - 'powershell' - ) - ).toBe(`claude --allowedTools "Bash(git:*)" '--resume' '${SESSION_ID}'`) - }) - - it.each([ - 'claude "--add-dir" "C:\\a\\" "--" "--out" "D:\\x\\"', - 'claude --add-dir "C:\\repo\\" --resume old' - ])('fails open when a cmd backslash run makes a quote literal: %s', (base) => { - // Odd backslash runs make quotes literal to CommandLineToArgvW, invalidating tokenizer boundaries. - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe( - `${base} "--resume" "${SESSION_ID}"` - ) - }) - - it.each([ - 'claude --add-dir "C:\\Users\\me\\repo" --resume old', - 'claude --add-dir "C:\\Program Files (x86)\\x" --resume old' - ])('still strips for ordinary quoted windows paths: %s', (base) => { - const command = buildClaudeResumeLaunchCommand(base, RESUME, 'cmd') - expect(command).not.toContain('--resume old') - expectSingleAuthoritativeResume(command, 'cmd') - }) - - it('fails open when a cmd caret escapes a real argument separator', () => { - // cmd strips ^ before the child splits the bare space; splicing a merged token would lose the second argument. - const base = 'claude --resume a^ b --model x' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe( - `${base} "--resume" "${SESSION_ID}"` - ) - }) - it.each([ ['claude --resume old \\', 'posix' as const], ['claude --resume old `', 'powershell' as const], @@ -279,88 +138,18 @@ describe('buildClaudeResumeLaunchCommand', () => { expect(buildClaudeResumeLaunchCommand(base, RESUME, shell)).toBe(`${base} ${quoted}`) }) - it('fails open on a windows escaped line continuation', () => { - const powershellBase = 'claude --resume stale `\n--resume hidden' - expect(buildClaudeResumeLaunchCommand(powershellBase, RESUME, 'powershell')).toBe( - `${powershellBase} '--resume' '${SESSION_ID}'` - ) - const cmdBase = 'claude --resume stale ^\n--resume hidden' - expect(buildClaudeResumeLaunchCommand(cmdBase, RESUME, 'cmd')).toBe( - `${cmdBase} "--resume" "${SESSION_ID}"` - ) - }) - - it('fails open on a posix line continuation hiding a selector', () => { - const base = 'claude --model x \\\n--resume old' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open on escape characters the windows shells keep literal', () => { - // cmd retains ^ in double quotes and PowerShell retains backticks in single quotes; neither forms a selector. - expect(buildClaudeResumeLaunchCommand('claude "-^-resume" old', RESUME, 'cmd')).toBe( - `claude "-^-resume" old "--resume" "${SESSION_ID}"` - ) - expect(buildClaudeResumeLaunchCommand("claude '-`-resume' old", RESUME, 'powershell')).toBe( - `claude '-\`-resume' old '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open on an unclosed expansion opened before claude', () => { - const base = '$(x; npx -- claude --resume stale)' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open when the executable token itself is unmodelable', () => { - // cmd has no single-quote syntax, so 'claude' is not the claude executable. - expect(buildClaudeResumeLaunchCommand("'claude' --resume old", RESUME, 'cmd')).toBe( - `'claude' --resume old "--resume" "${SESSION_ID}"` - ) - }) - - it.each(['cmd', 'powershell'] as const)( - 'fails open on a posix-style assignment prefix under %s', - (shell) => { - // Assignment prefixes are POSIX-only; Windows shells treat them as bogus executable names. - const base = "FOO='bar' claude --resume old" - const quoted = shell === 'cmd' ? `"--resume" "${SESSION_ID}"` : `'--resume' '${SESSION_ID}'` - expect(buildClaudeResumeLaunchCommand(base, RESUME, shell)).toBe(`${base} ${quoted}`) - } - ) - it('preserves an env-assignment or path prefix byte for byte', () => { expect( buildClaudeResumeLaunchCommand('FOO="$HOME/x" ~/bin/claude \'--resume\'', RESUME, 'posix') ).toBe(`FOO="$HOME/x" ~/bin/claude '--resume' '${SESSION_ID}'`) }) - it('recognizes Windows claude executable spellings', () => { - expect(buildClaudeResumeLaunchCommand('C:\\tools\\claude.CMD "--resume"', RESUME, 'cmd')).toBe( - `C:\\tools\\claude.CMD "--resume" "${SESSION_ID}"` - ) - }) - it("inserts the selector before claude's own -- terminator", () => { expect( buildClaudeResumeLaunchCommand('claude --resume stale -- positional', RESUME, 'posix') ).toBe(`claude '--resume' '${SESSION_ID}' -- positional`) }) - it('fails open when the base cannot be tokenized', () => { - expect(buildClaudeResumeLaunchCommand('claude "unterminated', RESUME, 'posix')).toBe( - `claude "unterminated '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open when no claude executable token exists', () => { - expect(buildClaudeResumeLaunchCommand('my-agent-wrapper --resume', RESUME, 'posix')).toBe( - `my-agent-wrapper --resume '--resume' '${SESSION_ID}'` - ) - }) - it.each([ 'claude -c && echo done', 'claude --resume stale; echo hi', @@ -375,29 +164,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it.each([ - 'claude --resume old "y"; echo hi', - "claude --resume old 'y'; echo hi", - 'claude --resume old --model "sonnet"&&echo hi', - 'claude --resume old "y"| tee /tmp/x', - 'claude --resume old \\"; echo hi' - ])('fails open when shell syntax hides behind partial quoting: %s', (base) => { - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open on escape-adjacent operators in windows shells', () => { - const powershellBase = 'claude --resume old `x; echo hi' - expect(buildClaudeResumeLaunchCommand(powershellBase, RESUME, 'powershell')).toBe( - `${powershellBase} '--resume' '${SESSION_ID}'` - ) - const cmdBase = 'claude --resume old ^x& echo hi' - expect(buildClaudeResumeLaunchCommand(cmdBase, RESUME, 'cmd')).toBe( - `${cmdBase} "--resume" "${SESSION_ID}"` - ) - }) - it.each([ 'claude --resume $(cat sid.txt)', 'claude --resume=$(cat sid.txt)', @@ -409,31 +175,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ) }) - it.each([ - 'claude --resume "`cat "a b"`"', - 'claude --model "$(pick "x -c y")"', - 'claude --model "$(f "x --resume y")"', - 'claude --resume "$(cat "$HOME/My Sessions/id")"' - ])('fails open on expansions nested inside double quotes: %s', (base) => { - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open on a powershell double-quoted subexpression', () => { - const base = 'claude --model "$(pick "x -c y")"' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - - it('fails open on a powershell subexpression locator', () => { - const base = 'claude --resume $(Get-Content sid.txt)' - expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe( - `${base} '--resume' '${SESSION_ID}'` - ) - }) - it('never walks the separator backoff into an escaped-space token', () => { expect( buildClaudeResumeLaunchCommand( @@ -444,13 +185,6 @@ describe('buildClaudeResumeLaunchCommand', () => { ).toBe(`claude --append-system-prompt Be\\ nice\\ '--resume' '${SESSION_ID}'`) }) - it('fails open when cmd operators hide in single quotes', () => { - // cmd.exe has no single-quote syntax, so '&' is a live command separator. - expect(buildClaudeResumeLaunchCommand("claude '&' --resume=old-session", RESUME, 'cmd')).toBe( - `claude '&' --resume=old-session "--resume" "${SESSION_ID}"` - ) - }) - it('fails open on cmd single-quoted literals that merely look like selectors', () => { // cmd passes single quotes through as junk positional bytes, not selectors/terminators. expect(buildClaudeResumeLaunchCommand("claude '--resume' old", RESUME, 'cmd')).toBe( @@ -460,36 +194,6 @@ describe('buildClaudeResumeLaunchCommand', () => { `claude '--' --resume old "--resume" "${SESSION_ID}"` ) }) - - it('still strips next to a caret-escaped literal ampersand on cmd', () => { - // ^& is an inactive, literal & in cmd, so the guard may keep working. - expect(buildClaudeResumeLaunchCommand('claude --resume old ^&', RESUME, 'cmd')).toBe( - `claude ^& "--resume" "${SESSION_ID}"` - ) - }) - - it('still strips when operators are safely quoted', () => { - expect( - buildClaudeResumeLaunchCommand( - "claude --append-system-prompt 'use && wisely' --resume stale", - RESUME, - 'posix' - ) - ).toBe(`claude --append-system-prompt 'use && wisely' '--resume' '${SESSION_ID}'`) - }) - - it('recognizes claude behind the PowerShell call operator', () => { - expect(buildClaudeResumeLaunchCommand("& claude '--resume'", RESUME, 'powershell')).toBe( - `& claude '--resume' '${SESSION_ID}'` - ) - expect( - buildClaudeResumeLaunchCommand( - "& 'C:\\Program Files\\claude\\claude.exe' --resume old", - RESUME, - 'powershell' - ) - ).toBe(`& 'C:\\Program Files\\claude\\claude.exe' '--resume' '${SESSION_ID}'`) - }) }) describe('buildAgentResumeStartupPlan claude selector guard', () => { @@ -520,40 +224,6 @@ describe('buildAgentResumeStartupPlan claude selector guard', () => { } ) - it('emits one identity-bearing resume when only default args carry a stale id', () => { - const restored = buildAgentResumeStartupPlan({ - agent: 'claude', - providerSession, - cmdOverrides: {}, - agentArgs: '--resume stale-session --model sonnet', - platform: 'linux' - }) - expect(restored?.launchCommand).toBe(`claude '--model' 'sonnet' '--resume' '${SESSION_ID}'`) - }) - - it('builds a resume command for a custom command the tokenizer rejects', () => { - const restored = buildAgentResumeStartupPlan({ - agent: 'claude', - providerSession, - cmdOverrides: {}, - agentCommand: 'claude --model $(cat ~/.claude-model) "unterminated', - platform: 'darwin' - }) - expect(restored).not.toBeNull() - expect(restored?.launchCommand.endsWith(`'--resume' '${SESSION_ID}'`)).toBe(true) - }) - - it('retains Gemini args while appending its resume identity', () => { - const restored = buildAgentResumeStartupPlan({ - agent: 'gemini', - providerSession, - cmdOverrides: {}, - agentArgs: '--resume', - platform: 'linux' - }) - expect(restored?.launchCommand).toBe(`gemini '--resume' '--resume' '${SESSION_ID}'`) - }) - it('persists the original base command unchanged', () => { const restored = buildAgentResumeStartupPlan({ agent: 'claude', diff --git a/src/shared/agent-row-conversation-name.test.ts b/src/shared/agent-row-conversation-name.test.ts index 3c32b6e64d6..427b3ebcd1e 100644 --- a/src/shared/agent-row-conversation-name.test.ts +++ b/src/shared/agent-row-conversation-name.test.ts @@ -9,31 +9,6 @@ function makeTab(overrides: Partial = {}): ConversationName } describe('getAgentRowConversationName', () => { - it('prefers the manual tab rename over every other source', () => { - const tab = makeTab({ - customTitle: 'Patient sync spike', - quickCommandLabel: 'Run tests', - generatedTitle: 'Fix intake flow', - title: '✳ Investigate replay bug' - }) - expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Patient sync spike') - }) - - it('falls back to the quick-command label before titles', () => { - const tab = makeTab({ quickCommandLabel: 'Run tests', title: '✳ Investigate replay bug' }) - expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Run tests') - }) - - it('keeps OpenCode semantic session titles whole', () => { - const tab = makeTab({ title: 'OC | build the release pipeline', generatedTitle: 'Older title' }) - expect(getAgentRowConversationName(tab, 'opencode', false)).toBe( - 'OC | build the release pipeline' - ) - expect(getAgentRowConversationName(tab, 'opencode', true)).toBe( - 'OC | build the release pipeline' - ) - }) - it("shows the title owned by the row's provider session", () => { const tab = makeTab({ aiVaultTitle: { agent: 'claude', sessionId: 's1', title: 'Fix the lease probe' }, @@ -50,28 +25,6 @@ describe('getAgentRowConversationName', () => { expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Fix intake flow') }) - it('lets a manual rename and a quick command still outrank the provider title', () => { - const vault = { agent: 'claude' as const, sessionId: 's1', title: 'Fix the lease probe' } - expect( - getAgentRowConversationName( - makeTab({ aiVaultTitle: vault, customTitle: 'Mine' }), - 'claude', - true, - undefined, - 's1' - ) - ).toBe('Mine') - expect( - getAgentRowConversationName( - makeTab({ aiVaultTitle: vault, quickCommandLabel: 'Run tests' }), - 'claude', - true, - undefined, - 's1' - ) - ).toBe('Run tests') - }) - it('keeps a cwd-shaped provider title that the live-title sanitizer would discard', () => { // `auth/login` is a real session name; as a scraped live title it would be read as a cwd. const tab = makeTab({ @@ -82,12 +35,6 @@ describe('getAgentRowConversationName', () => { expect(getAgentRowConversationName(makeTab({ title: 'auth/login' }), 'claude', true)).toBeNull() }) - it('uses the generated title only when generated titles are enabled', () => { - const tab = makeTab({ generatedTitle: 'Fix intake flow', title: '✳ Investigate replay bug' }) - expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Fix intake flow') - expect(getAgentRowConversationName(tab, 'claude', false)).toBe('Investigate replay bug') - }) - it('names a split pane from its own live title, not the tab title', () => { // Tab live title belongs to the focused pane; siblings need their own. const tab = makeTab({ title: '\u2733 Linear work log' }) @@ -100,134 +47,4 @@ describe('getAgentRowConversationName', () => { expect(getAgentRowConversationName(tab, 'claude', false, null)).toBeNull() expect(getAgentRowConversationName(tab, 'claude', false)).toBe('Linear work log') }) - - it('keeps tab-owned names above the pane title', () => { - // User-given names belong to the tab and remain stable across pane focus. - const custom = makeTab({ customTitle: 'Patient sync spike' }) - expect(getAgentRowConversationName(custom, 'claude', false, 'Redis cache strategy')).toBe( - 'Patient sync spike' - ) - const quick = makeTab({ quickCommandLabel: 'Run tests' }) - expect(getAgentRowConversationName(quick, 'claude', false, null)).toBe('Run tests') - const generated = makeTab({ generatedTitle: 'Fix intake flow' }) - expect(getAgentRowConversationName(generated, 'claude', true, null)).toBe('Fix intake flow') - }) - - it('strips leading status decoration from agent-set titles', () => { - expect( - getAgentRowConversationName(makeTab({ title: '✳ Fix patient intake flow' }), 'claude', false) - ).toBe('Fix patient intake flow') - expect( - getAgentRowConversationName(makeTab({ title: '⠋ Refactor replay guard' }), 'codex', false) - ).toBe('Refactor replay guard') - }) - - it('rejects spinner+cwd titles instead of surfacing paths as names', () => { - expect( - getAgentRowConversationName(makeTab({ title: '⠋ ~/orca/workspaces' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '/Users/dev/repo' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'C:\\repos\\orca' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'orca/workspaces' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName( - makeTab({ title: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\orca' }), - 'codex', - false - ) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'repos\\orca' }), 'codex', false) - ).toBeNull() - }) - - it('accepts multi-word titles that merely contain a slash', () => { - expect( - getAgentRowConversationName(makeTab({ title: 'Fix a/b toggle in settings' }), 'codex', false) - ).toBe('Fix a/b toggle in settings') - }) - - it('rejects synthetic status titles', () => { - expect( - getAgentRowConversationName(makeTab({ title: 'Codex ready' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'Codex - action required' }), 'codex', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'Cursor Agent' }), 'cursor', false) - ).toBeNull() - }) - - it('rejects identity-echo, management, and placeholder titles', () => { - expect(getAgentRowConversationName(makeTab({ title: 'Claude' }), 'claude', false)).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '✳ Claude Code' }), 'claude', false) - ).toBeNull() - expect( - getAgentRowConversationName( - makeTab({ title: 'Claude Code - action required' }), - 'claude', - false - ) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '✦ Gemini CLI' }), 'gemini', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '◇ Ready (orca)' }), 'gemini', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'claude agents' }), 'claude', false) - ).toBeNull() - expect(getAgentRowConversationName(makeTab({ title: 'Agent' }), 'claude', false)).toBeNull() - }) - - it("rejects jcode's live identity title so the row shows the prompt instead", () => { - // jcode's per-second codename/diff/duration title would hide the actual prompt/reply preview. - expect( - getAgentRowConversationName( - makeTab({ title: '🌐 jcode Puppy · +3 -0 · last ~23s' }), - 'jcode', - false - ) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '🐍 jcode Snake · work ~6s' }), 'jcode', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: '🐍 jcode/creek Snake' }), 'jcode', false) - ).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'Fix the greet helper' }), 'jcode', false) - ).toBe('Fix the greet helper') - }) - - it('rejects empty, glyph-only, and default terminal titles', () => { - expect( - getAgentRowConversationName( - makeTab({ title: 'Workbench', defaultTitle: 'Workbench' }), - 'claude', - false - ) - ).toBeNull() - expect(getAgentRowConversationName(makeTab(), 'claude', false)).toBeNull() - expect(getAgentRowConversationName(makeTab({ title: '✳' }), 'claude', false)).toBeNull() - expect( - getAgentRowConversationName(makeTab({ title: 'Terminal 1' }), 'claude', false) - ).toBeNull() - expect( - getAgentRowConversationName( - makeTab({ title: 'Terminal 2', defaultTitle: 'Terminal 2' }), - 'claude', - false - ) - ).toBeNull() - }) }) diff --git a/src/shared/agent-row-primary-text.test.ts b/src/shared/agent-row-primary-text.test.ts index 4a4bae4258b..9d5e30bcb21 100644 --- a/src/shared/agent-row-primary-text.test.ts +++ b/src/shared/agent-row-primary-text.test.ts @@ -1,40 +1,8 @@ import { describe, expect, it } from 'vitest' import { normalizeAgentStatusPayload } from './agent-status-types' -import { - getAgentRowGeneratedTitleText, - getAgentRowPrimaryText, - getOrcaDispatchTaskId, - isOrcaDispatchPrompt -} from './agent-row-primary-text' +import { getAgentRowPrimaryText } from './agent-row-primary-text' describe('getAgentRowPrimaryText', () => { - it('prefers orchestration display name over the raw hook prompt', () => { - expect( - getAgentRowPrimaryText({ - prompt: 'You are working inside Orca, a multi-agent IDE.', - orchestration: { - taskId: 'task-1', - dispatchId: 'ctx-1', - taskTitle: 'Checkout race', - displayName: 'Fix checkout race' - } - }) - ).toBe('Fix checkout race') - }) - - it('falls back to task title when display name is absent', () => { - expect( - getAgentRowPrimaryText({ - prompt: 'You are working inside Orca, a multi-agent IDE.', - orchestration: { - taskId: 'task-1', - dispatchId: 'ctx-1', - taskTitle: 'Checkout race' - } - }) - ).toBe('Checkout race') - }) - it('ignores sticky orchestration labels that belong to a different task id', () => { expect( getAgentRowPrimaryText({ @@ -53,57 +21,6 @@ Review dispatch prompts and make worker labels distinct`, ).toBe('Review dispatch prompts and make worker labels distinct') }) - it('uses the task block when orchestration metadata has not arrived yet', () => { - expect( - getAgentRowPrimaryText({ - prompt: `You are working inside Orca, a multi-agent IDE. You are a dispatched worker. -Your coordinator's terminal handle is: term_parent -Your task ID is: task_1 - -=== CLI COMMANDS === -orca orchestration send --to term_parent - -=== TASK === -Review dispatch prompts and make worker labels distinct - -Keep the raw preamble out of the sidebar.` - }) - ).toBe('Review dispatch prompts and make worker labels distinct') - }) - - it('falls back to the raw prompt outside orchestration workers', () => { - expect(getAgentRowPrimaryText({ prompt: ' Fix checkout race\n ' })).toBe('Fix checkout race') - }) - - it.each([ - // Pi 1.5.0 `pi @report.txt "summarize"`, folded and cut by status normalization before the closer. - [' long contents the preview cut off', 'report.txt'], - [' ', 'main.log'], - [' hi summarize this', 'summarize this'], - [' compare these', 'compare these'], - [' contents cut off', 'b.txt'], - [' { "tool": "x"', 'a.txt'], - // Real paste from the report, folded by status normalization. - ['{ "tool": "orca diagnostics", "takenAt": "2026-10-16T21:04:57.936Z"', ''], - ['[{"code": "ETIMEDOUT", "count": 12}]', ''], - ['[ "a", "b" ]', ''], - ['', ''], - [' ', ''] - ])('labels a prompt that opens with pasted data: %s', (prompt, label) => { - expect(getAgentRowPrimaryText({ prompt })).toBe(label) - }) - - it.each([ - '{foo} is undefined in the template', - '{ fix this block', - '[1, 2, 3] should come back sorted', - '[WIP] finish the login flow', - ' render this component', - ' tags are not closing' - ])('keeps a typed prompt that merely starts with punctuation: %s', (prompt) => { - expect(getAgentRowPrimaryText({ prompt })).toBe(prompt) - }) - // The normalizer must preserve a meaningful task preview within 200 chars despite multi-KB dispatch preambles. it('derives a task preview from a normalized 200-char dispatch prompt without labels', () => { const longCliNoise = Array.from( @@ -138,45 +55,6 @@ ${taskBody}` expect(preview).not.toContain('CLI COMMANDS') }) - it('prefers later orchestration labels over the extracted task preview', () => { - const normalized = normalizeAgentStatusPayload({ - state: 'working', - prompt: `You are working inside Orca, a multi-agent IDE. You are a dispatched worker. -Your task ID is: task_label_later - -=== CLI COMMANDS === -${'orca orchestration check\n'.repeat(30)} -=== TASK === -Implement the detailed worker instructions that should not stay as the final label` - }) - expect(normalized).not.toBeNull() - - expect(getAgentRowPrimaryText({ prompt: normalized!.prompt })).toContain( - 'Implement the detailed worker instructions' - ) - - expect( - getAgentRowPrimaryText({ - prompt: normalized!.prompt, - orchestration: { - taskId: 'task_label_later', - dispatchId: 'ctx-later', - taskTitle: 'Worker instructions', - displayName: 'Better worker label' - } - }) - ).toBe('Better worker label') - }) - - it('does not surface preamble boilerplate when the TASK marker is absent', () => { - expect( - getAgentRowPrimaryText({ - prompt: - 'You are working inside Orca, a multi-agent IDE. You are a dispatched worker. Your task ID is: task_no_body CLI noise only' - }) - ).toBe('') - }) - // Raw preambles need standalone markers so base-drift subjects mentioning === TASK === cannot win. it('ignores adversarial === TASK === text in raw multi-line base-drift subjects', () => { expect( @@ -195,87 +73,4 @@ Implement the detailed worker instructions that should not stay as the final lab }) ).toBe('Fix the actual dispatch fallback preview') }) - - it('still reads the task body from a normalized single-line compact prompt', () => { - expect( - getAgentRowPrimaryText({ - prompt: - 'You are working inside Orca, a multi-agent IDE. Your task ID is: task_inline === TASK === Compact body preview' - }) - ).toBe('Compact body preview') - }) -}) - -describe('getOrcaDispatchTaskId', () => { - it('extracts the id when the trailing newline was folded to a space', () => { - const normalized = normalizeAgentStatusPayload({ - state: 'working', - prompt: `You are working inside Orca, a multi-agent IDE. You are a dispatched worker. -Your task ID is: task_9f3ab2 - -=== TASK === -body` - }) - expect(getOrcaDispatchTaskId(normalized!.prompt)).toBe('task_9f3ab2') - }) - - it('extracts the id from a raw multi-line prompt', () => { - expect( - getOrcaDispatchTaskId( - `You are working inside Orca, a multi-agent IDE.\nYour task ID is: task_1\n\n=== TASK ===\nbody` - ) - ).toBe('task_1') - }) -}) - -describe('isOrcaDispatchPrompt / getAgentRowGeneratedTitleText', () => { - it('treats leading whitespace as still a dispatch preamble', () => { - expect( - isOrcaDispatchPrompt(' You are working inside Orca, a multi-agent IDE. Worker task') - ).toBe(true) - }) - - it('uses orchestration labels for generated titles only on matching dispatch prompts', () => { - expect( - getAgentRowGeneratedTitleText({ - prompt: `You are working inside Orca, a multi-agent IDE. You are a dispatched worker. -Your task ID is: task-1 - -=== TASK === -Checkout race body`, - orchestration: { - taskId: 'task-1', - dispatchId: 'ctx-1', - taskTitle: 'Checkout race', - displayName: 'Fix checkout race' - } - }) - ).toBe('Fix checkout race') - expect( - getAgentRowGeneratedTitleText({ - prompt: - 'You are working inside Orca, a multi-agent IDE. Your task ID is: task-2 === TASK === New worker body', - orchestration: { - taskId: 'task-1', - dispatchId: 'ctx-1', - taskTitle: 'Stale task', - displayName: 'Stale worker label' - } - }) - ).toBe('New worker body') - }) - - it('ignores sticky orchestration for non-dispatch generated titles', () => { - expect( - getAgentRowGeneratedTitleText({ - prompt: ' Refactor the auth middleware\n ', - orchestration: { - taskId: 'task-1', - dispatchId: 'ctx-1', - taskTitle: 'Stale task', - displayName: 'Stale worker label' - } - }) - ).toBe(' Refactor the auth middleware\n ') - }) }) diff --git a/src/shared/agent-scratch-worktrees.test.ts b/src/shared/agent-scratch-worktrees.test.ts index 5a1fd005bd5..e427a62c36e 100644 --- a/src/shared/agent-scratch-worktrees.test.ts +++ b/src/shared/agent-scratch-worktrees.test.ts @@ -1,29 +1,12 @@ import { describe, expect, it } from 'vitest' import { createAgentScratchWorktreePathMatcher, - isAgentScratchRepoRootPath, isAgentScratchWorktreePath } from './agent-scratch-worktrees' describe('isAgentScratchWorktreePath', () => { const repoPath = '/Users/dev/app' - it('matches Claude Code sub-agent worktrees', () => { - expect( - isAgentScratchWorktreePath( - repoPath, - '/Users/dev/app/.claude/worktrees/agent-a04ccaaa55ddadb91', - [] - ) - ).toBe(true) - }) - - it('matches gsd parallel-agent workspaces', () => { - expect( - isAgentScratchWorktreePath(repoPath, '/Users/dev/app/.gsd-workspaces/phase-1-subagent-2', []) - ).toBe(true) - }) - it('matches scratch worktrees created from a linked checkout', () => { const matchesAgentScratch = createAgentScratchWorktreePathMatcher( [repoPath, '/Users/dev/orca/workspaces/app/feature-x'], @@ -40,26 +23,6 @@ describe('isAgentScratchWorktreePath', () => { ) }) - it('matches Windows path separators and casing', () => { - expect( - isAgentScratchWorktreePath( - 'C:\\Users\\dev\\app', - 'c:\\USERS\\dev\\app\\.Claude\\Worktrees\\agent-a04ccaaa', - [] - ) - ).toBe(true) - }) - - it('matches WSL UNC paths', () => { - expect( - isAgentScratchWorktreePath( - '//wsl$/Ubuntu/home/dev/app', - '//wsl.localhost/Ubuntu/home/dev/app/.claude/worktrees/agent-a04ccaaa', - [] - ) - ).toBe(true) - }) - it('preserves case-sensitive POSIX and WSL tool segments', () => { expect( isAgentScratchWorktreePath(repoPath, '/Users/dev/app/.Claude/Worktrees/agent-a04ccaaa', []) @@ -87,12 +50,6 @@ describe('isAgentScratchWorktreePath', () => { expect(isAgentScratchWorktreePath(repoPath, '/Users/dev/app/.gsd-workspaces', [])).toBe(false) }) - it('does not match undotted claude directories', () => { - expect( - isAgentScratchWorktreePath(repoPath, '/Users/dev/app/claude/worktrees/agent-1', []) - ).toBe(false) - }) - it('does not inherit a scratch classification from the repo parent path', () => { expect( isAgentScratchWorktreePath( @@ -102,61 +59,4 @@ describe('isAgentScratchWorktreePath', () => { ) ).toBe(false) }) - - it('does not match user worktree conventions', () => { - expect(isAgentScratchWorktreePath(repoPath, '/Users/dev/app/.worktrees/feature-x', [])).toBe( - false - ) - expect( - isAgentScratchWorktreePath( - '/Users/dev/app', - '/Users/dev/.superset/worktrees/app/fix-notes', - [] - ) - ).toBe(false) - expect(isAgentScratchWorktreePath('/Users/dev/app', '/orca/workspaces/app/feature', [])).toBe( - false - ) - }) -}) - -describe('isAgentScratchRepoRootPath', () => { - it('matches codex scratch capsule repos', () => { - expect( - isAgentScratchRepoRootPath('/Users/dev/.codex-tmp/foragent-capsule-b1-repo-zP9Az6') - ).toBe(true) - expect(isAgentScratchRepoRootPath('/Users/dev/.codex-tmp/rc-fwd-qEXuEq')).toBe(true) - }) - - it('matches codex vendor imports and claude skills containers', () => { - expect(isAgentScratchRepoRootPath('/Users/dev/.codex/vendor_imports/skills')).toBe(true) - expect(isAgentScratchRepoRootPath('/Users/dev/.claude/skills/obsidian-second-brain')).toBe(true) - }) - - it('matches a repo registered at the scratch container itself', () => { - expect(isAgentScratchRepoRootPath('/Users/dev/.codex-tmp')).toBe(true) - expect(isAgentScratchRepoRootPath('/Users/dev/.codex/vendor_imports')).toBe(true) - }) - - it('matches scratch worktree containers used as repo roots', () => { - expect(isAgentScratchRepoRootPath('/Users/dev/app/.claude/worktrees/agent-a04ccaaa')).toBe(true) - expect(isAgentScratchRepoRootPath('/Users/dev/app/.gsd-workspaces/phase-1')).toBe(true) - }) - - it('matches Windows separators and casing', () => { - expect(isAgentScratchRepoRootPath('C:\\Users\\Dev\\.codex-tmp\\Capsule-X')).toBe(true) - expect(isAgentScratchRepoRootPath('C:\\Users\\Dev\\.Claude\\Skills\\foo')).toBe(true) - }) - - it('does not match ordinary user repos', () => { - expect(isAgentScratchRepoRootPath('/Users/dev/projects/app')).toBe(false) - expect(isAgentScratchRepoRootPath('/Users/dev/codex-tmp/app')).toBe(false) - expect(isAgentScratchRepoRootPath('/Users/dev/.codex/checkouts/app')).toBe(false) - expect(isAgentScratchRepoRootPath('/Users/dev/skills/.claude-app')).toBe(false) - }) - - it('does not match partial multi-segment markers', () => { - expect(isAgentScratchRepoRootPath('/Users/dev/.claude/config')).toBe(false) - expect(isAgentScratchRepoRootPath('/Users/dev/vendor_imports/app')).toBe(false) - }) }) diff --git a/src/shared/agent-session-handoff-context.test.ts b/src/shared/agent-session-handoff-context.test.ts index 7b85e5a3452..b7f0dca5233 100644 --- a/src/shared/agent-session-handoff-context.test.ts +++ b/src/shared/agent-session-handoff-context.test.ts @@ -107,39 +107,6 @@ it('preserves Unicode at both excerpt boundaries and across multiple blocks', () expect(context.indexOf('FAILED')).toBeLessThan(context.indexOf('Last detail')) }) -it('retains saved decisions, delivery states and image references while excluding reasoning and creation notices', () => { - const context = buildAgentSessionHandoffContext([ - { ...text('system', 'Private worker assignment'), agentStart: true }, - text('reasoning', 'Private reasoning'), - text('system', 'Prompt state: resolved\nSelected choice: Keep planning'), - { ...text('user', 'Do not deploy'), queued: true }, - { ...text('user', 'Unsent request'), unsent: true }, - message('user', [{ type: 'image-ref', path: '/work/reference.png' }]) - ]) - expect(context).toContain('Selected choice: Keep planning') - expect(context).toContain('[user queued] Do not deploy') - expect(context).toContain('[user unsent] Unsent request') - expect(context).toContain('[user] [image] /work/reference.png') - expect(context).not.toContain('Private') -}) - -it('pairs out-of-order tool results by call ID and labels standalone results without guessing a tool', () => { - const context = buildAgentSessionHandoffContext([ - message('assistant', [ - { type: 'tool-call', name: 'Read', input: 'a.ts', callId: 'a' }, - { type: 'tool-call', name: 'Search', input: 'pattern', callId: 'b' }, - { type: 'tool-result', output: 'search result', callId: 'b' }, - { type: 'tool-result', output: 'file contents', callId: 'a' }, - { type: 'tool-result', output: 'standalone result', callId: 'unknown' } - ]) - ]) - expect(context).toContain('[tool Read input preview] call=a a.ts') - expect(context).toContain('[tool result] call=a file contents') - expect(context).toContain('[tool Search input preview] call=b pattern') - expect(context).toContain('[tool result] call=b search result') - expect(context).toContain('[tool result] call=unknown standalone result') -}) - it.each([false, true])( 'prioritizes the latest out-of-order result under tool budget pressure (separate messages: %s)', (separateMessages) => { @@ -198,37 +165,6 @@ it('respects the total bound with oversized metadata and a nearly exhausted tool expect(buildAgentSessionHandoffContext([])).toBe('') }) -it('retains readable provider failures and warnings while excluding wordless frames', () => { - const frame = { - provider: 'codex', - kind: 'notification:hook/completed', - payload: { head: '{}', byteLength: 2, digest: 'abc', truncated: false } - } - const context = buildAgentSessionHandoffContext([ - text('user', 'Run the tests'), - message('system', [ - { - type: 'text', - text: 'Codex PreToolUse hook failed: permission denied', - tone: 'error', - providerFrame: frame - }, - { - type: 'text', - text: 'The configured sandbox is unavailable', - tone: 'warning', - providerFrame: frame - }, - { type: 'text', text: 'Readable provider notice', providerFrame: frame }, - { type: 'text', text: 'codex · notification:hook/completed', providerFrame: frame } - ]) - ]) - expect(context).toContain('permission denied') - expect(context).toContain('The configured sandbox is unavailable') - expect(context).toContain('Readable provider notice') - expect(context).not.toContain('codex · notification:hook/completed') -}) - it.each(['queued', 'unsent'] as const)( 'reserves delivered conversation ahead of oversized %s messages', (delivery) => { diff --git a/src/shared/agent-session/agent-session-approval-blocked-path.test.ts b/src/shared/agent-session/agent-session-approval-blocked-path.test.ts index 13bde2d66f8..42766b4ea89 100644 --- a/src/shared/agent-session/agent-session-approval-blocked-path.test.ts +++ b/src/shared/agent-session/agent-session-approval-blocked-path.test.ts @@ -5,19 +5,6 @@ import { } from './agent-session-approval-blocked-path' describe('approvalBlockedPathToShow', () => { - it('hides a path the tool input already shows, including JSON-escaped Windows paths', () => { - const blockedPath = 'C:\\qa\\demo\\notes.md' - expect( - approvalBlockedPathToShow({ - blockedPath, - detail: JSON.stringify({ file_path: blockedPath }, null, 2) - }) - ).toBeNull() - expect( - approvalBlockedPathToShow({ blockedPath: '/tmp/out.txt', detail: 'echo hi > /tmp/out.txt' }) - ).toBeNull() - }) - it('checks only the text the card draws: a plan replaces the detail', () => { expect( approvalBlockedPathToShow({ @@ -66,19 +53,4 @@ describe('approvalBlockedPathToShow', () => { }) ).toBe('C:\\qa\\demo') }) - - it('hides a path the text names before a separator, quote or space', () => { - expect( - approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: 'ls /srv/data/x' }) - ).toBeNull() - expect( - approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: "cd '/srv/data' && ls" }) - ).toBeNull() - expect( - approvalBlockedPathToShow({ - blockedPath: 'C:\\qa\\demo', - detail: JSON.stringify({ file_path: 'C:\\qa\\demo\\notes.md' }, null, 2) - }) - ).toBeNull() - }) }) diff --git a/src/shared/agent-session/agent-session-approval-subject.test.ts b/src/shared/agent-session/agent-session-approval-subject.test.ts deleted file mode 100644 index 9675ae5f46e..00000000000 --- a/src/shared/agent-session/agent-session-approval-subject.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { expect, it } from 'vitest' -import { pendingPromptsAllUnanswerableHere } from './agent-session-approval-subject' -import type { AgentJournalItemBody } from './journal/agent-session-journal-types' -import { isAdmissibleAgentJournalItemBody } from './journal/agent-session-journal-schemas' - -function pending(body: Record): { body: AgentJournalItemBody } { - const promptBody = { - ...body, - resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } - } - if (!isAdmissibleAgentJournalItemBody(promptBody)) { - throw new Error('Invalid pending prompt fixture') - } - return { body: promptBody } -} -const NEWER = pending({ - kind: 'approval', - title: 'Review', - detail: null, - subject: { kind: 'diff' }, - options: [{ id: 'allow', label: 'Approve' }] -}) -const PLAN = pending({ - kind: 'approval', - title: 'Plan', - detail: null, - subject: { kind: 'plan', text: 'do it' }, - options: [{ id: 'allow', label: 'Approve' }] -}) -const QUESTION = pending({ kind: 'question', question: 'Which?', options: [] }) - -it('holds only while every pending prompt is an approval this build cannot answer', () => { - expect(pendingPromptsAllUnanswerableHere([])).toBe(false) - expect(pendingPromptsAllUnanswerableHere([NEWER])).toBe(true) - expect(pendingPromptsAllUnanswerableHere([NEWER, NEWER])).toBe(true) - expect(pendingPromptsAllUnanswerableHere([NEWER, PLAN])).toBe(false) - expect(pendingPromptsAllUnanswerableHere([QUESTION, NEWER])).toBe(false) - const cancelled = JSON.parse( - JSON.stringify(NEWER).replace('"state":"pending"', '"state":"cancelled"') - ) - expect(pendingPromptsAllUnanswerableHere([cancelled, NEWER])).toBe(true) - expect(pendingPromptsAllUnanswerableHere([NEWER, cancelled])).toBe(true) - expect(pendingPromptsAllUnanswerableHere([cancelled, PLAN])).toBe(false) - expect(pendingPromptsAllUnanswerableHere([cancelled])).toBe(false) -}) diff --git a/src/shared/agent-session/agent-session-attachments.test.ts b/src/shared/agent-session/agent-session-attachments.test.ts index 2edb560b680..48f78e2c028 100644 --- a/src/shared/agent-session/agent-session-attachments.test.ts +++ b/src/shared/agent-session/agent-session-attachments.test.ts @@ -7,11 +7,6 @@ import { const bytes = (text: string): number => new TextEncoder().encode(text).byteLength describe('sanitizeAgentSessionAttachmentName', () => { - it('keeps only the last path segment', () => { - expect(sanitizeAgentSessionAttachmentName('../../etc/passwd')).toBe('passwd') - expect(sanitizeAgentSessionAttachmentName('C:\\Users\\me\\shot.png')).toBe('shot.png') - }) - it('caps a long name in UTF-8 bytes on whole characters, keeping its extension', () => { const name = sanitizeAgentSessionAttachmentName(`${'图'.repeat(120)}.pdf`) expect(bytes(name)).toBeLessThanOrEqual(AGENT_SESSION_ATTACHMENT_NAME_MAX_BYTES) @@ -27,10 +22,4 @@ describe('sanitizeAgentSessionAttachmentName', () => { expect(name).toBe('a'.repeat(AGENT_SESSION_ATTACHMENT_NAME_MAX_BYTES - 1)) expect(sanitizeAgentSessionAttachmentName('notes. .')).toBe('notes') }) - - it('renames what Windows reserves for devices', () => { - expect(sanitizeAgentSessionAttachmentName('CON')).toBe('_CON') - expect(sanitizeAgentSessionAttachmentName('nul.tar.gz')).toBe('_nul.tar.gz') - expect(sanitizeAgentSessionAttachmentName('console.log')).toBe('console.log') - }) }) diff --git a/src/shared/agent-session/agent-session-background-task-state-admission.test.ts b/src/shared/agent-session/agent-session-background-task-state-admission.test.ts deleted file mode 100644 index c5e044829db..00000000000 --- a/src/shared/agent-session/agent-session-background-task-state-admission.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { admitAgentSessionBackgroundTaskState } from './agent-session-background-task-state-admission' - -describe('a background-task frame a client admits', () => { - it('takes a frame whose only change is incomplete tracking, both ways', () => { - const listed = admitAgentSessionBackgroundTaskState({ state: 'monitoring', children: [] }) - const incomplete = admitAgentSessionBackgroundTaskState( - { state: 'monitoring', children: [], incomplete: true }, - listed - ) - expect(incomplete).not.toBe(listed) - expect(listed).toEqual({ state: 'monitoring', children: [] }) - expect(incomplete).toEqual({ state: 'monitoring', children: [], incomplete: true }) - const complete = admitAgentSessionBackgroundTaskState( - { state: 'monitoring', children: [] }, - incomplete - ) - expect(complete).not.toBe(incomplete) - expect(complete).toEqual({ state: 'monitoring', children: [] }) - expect( - admitAgentSessionBackgroundTaskState({ state: 'monitoring', children: [] }, complete) - ).toBe(complete) - }) -}) diff --git a/src/shared/agent-session/agent-session-context-clear.test.ts b/src/shared/agent-session/agent-session-context-clear.test.ts index b058df7c520..2d4a02c0e4e 100644 --- a/src/shared/agent-session/agent-session-context-clear.test.ts +++ b/src/shared/agent-session/agent-session-context-clear.test.ts @@ -179,13 +179,4 @@ describe('completed context clear projections', () => { expect(latestStructuredAgentContextFacts([newUsage, clear, usageRow])).toEqual(old) expect(selectStructuredAgentContextUsage([usageRow], undefined, clear.sequence)).toBeNull() }) - - it('chooses the newest clear independent of map order and scopes off-page rows', () => { - const again = { ...clear, itemId: 'clear-again', sequence: 8 } - expect(latestAgentSessionContextClearSequence([again, user, clear])).toBe(8) - expect(agentSessionCurrentContextRows([user], [oldSend], 5)).toEqual({ - items: [], - submissions: [] - }) - }) }) diff --git a/src/shared/agent-session/agent-session-conversation-name.test.ts b/src/shared/agent-session/agent-session-conversation-name.test.ts index 2b10caaf040..8666488e39f 100644 --- a/src/shared/agent-session/agent-session-conversation-name.test.ts +++ b/src/shared/agent-session/agent-session-conversation-name.test.ts @@ -1,55 +1,9 @@ import { describe, expect, it } from 'vitest' import { AGENT_SESSION_CONVERSATION_NAME_MAX_LENGTH, - isAgentSessionConversationName, normalizeAgentSessionConversationName } from './agent-session-conversation-name' -describe('normalizeAgentSessionConversationName', () => { - it('keeps a plain single-line name unchanged', () => { - expect(normalizeAgentSessionConversationName('Fix the flaky lease probe')).toBe( - 'Fix the flaky lease probe' - ) - }) - - it('flattens whitespace so a multi-line name cannot break the tab strip', () => { - expect(normalizeAgentSessionConversationName('Fix the\nlease\tprobe ')).toBe( - 'Fix the lease probe' - ) - }) - - it('rejects an empty or whitespace-only name rather than blanking the label', () => { - expect(normalizeAgentSessionConversationName('')).toBeNull() - expect(normalizeAgentSessionConversationName(' \n ')).toBeNull() - }) - - it('rejects anything that is not a string', () => { - expect(normalizeAgentSessionConversationName(undefined)).toBeNull() - expect(normalizeAgentSessionConversationName(null)).toBeNull() - expect(normalizeAgentSessionConversationName(42)).toBeNull() - expect(normalizeAgentSessionConversationName({ title: 'x' })).toBeNull() - }) - - it('bounds a pasted essay to the stored maximum', () => { - const normalized = normalizeAgentSessionConversationName(`First ${'x'.repeat(994)}`) - expect(normalized).toBe(`First ${'x'.repeat(194)}`) - expect(normalized).toHaveLength(AGENT_SESSION_CONVERSATION_NAME_MAX_LENGTH) - expect(isAgentSessionConversationName(normalized)).toBe(true) - }) -}) - -describe('isAgentSessionConversationName', () => { - it('accepts only bounded canonical names', () => { - expect(isAgentSessionConversationName('Fix the probe')).toBe(true) - expect(isAgentSessionConversationName('')).toBe(false) - expect(isAgentSessionConversationName('a'.repeat(201))).toBe(false) - expect(isAgentSessionConversationName(' Fix the probe ')).toBe(false) - expect(isAgentSessionConversationName('Fix\nthe probe')).toBe(false) - expect(isAgentSessionConversationName('Fix\u202Egnp.exe probe')).toBe(false) - expect(isAgentSessionConversationName(7)).toBe(false) - }) -}) - describe('normalizeAgentSessionConversationName hostile text', () => { it('strips controls and lone surrogates', () => { expect(normalizeAgentSessionConversationName('Fix\u0007the probe')).toBe('Fix the probe') @@ -66,12 +20,6 @@ describe('normalizeAgentSessionConversationName hostile text', () => { expect(normalized).toBe('a'.repeat(AGENT_SESSION_CONVERSATION_NAME_MAX_LENGTH - 1)) expect(normalized).not.toContain('\uFFFD') }) - - it('keeps a legitimate non-ASCII name intact', () => { - expect(normalizeAgentSessionConversationName('R\u00E9sum\u00E9 du fil \u2615')).toBe( - 'R\u00E9sum\u00E9 du fil \u2615' - ) - }) }) describe('normalizeAgentSessionConversationName joiners', () => { @@ -89,18 +37,6 @@ describe('normalizeAgentSessionConversationName joiners', () => { expect(normalizeAgentSessionConversationName(name)).toBe(name) }) - it.each([ - ['bidi override', 'Fix\u202Egnp.exe probe', 'Fix gnp.exe probe'], - ['isolate pair', 'Fix\u2066the\u2069 probe', 'Fix the probe'], - ['Arabic letter mark', 'Fix\u061Cthe probe', 'Fix the probe'], - ['soft hyphen', 'Fix\u00ADthe probe', 'Fix the probe'], - ['word joiner', 'Fix\u2060the probe', 'Fix the probe'], - ['zero-width space', 'Fix\u200Bthe probe', 'Fix the probe'], - ['byte order mark', 'Fix\uFEFFthe probe', 'Fix the probe'] - ])('still strips a %s', (_label, name, expected) => { - expect(normalizeAgentSessionConversationName(name)).toBe(expected) - }) - // These Cf runs escaped the old enumeration and produced nonempty labels that rendered invisibly. it.each([ ['invisible maths operators', '\u2061\u2062\u2063\u2064'], diff --git a/src/shared/agent-session/agent-session-message-source.test.ts b/src/shared/agent-session/agent-session-message-source.test.ts index c72837b242d..10aabd95672 100644 --- a/src/shared/agent-session/agent-session-message-source.test.ts +++ b/src/shared/agent-session/agent-session-message-source.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - agentMessageSendersShown, - readAgentMessageSource, - type AgentMessageSource -} from './agent-session-message-source' +import { readAgentMessageSource, type AgentMessageSource } from './agent-session-message-source' import { testOrcaSessionId } from '../orca-session-address-test-fixture' const SESSION = '4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37' @@ -34,11 +30,6 @@ const FROM: AgentMessageSource = { } describe("a message's sender, read through its one reader", () => { - it('reads absence as the person, and a valid sender back whole', () => { - expect(readAgentMessageSource(undefined)).toBeUndefined() - expect(readAgentMessageSource(structuredClone(FROM))).toEqual(FROM) - }) - it("reads a Dispatch's task back whole", () => { const task: AgentMessageSource = { ...FROM, @@ -87,23 +78,4 @@ describe("a message's sender, read through its one reader", () => { ]) expect(read?.senders[1]?.name?.startsWith('a b ')).toBe(true) }) - - it("reads a value that is not a sender at all as the person's message", () => { - for (const value of [null, 'nobody', 42, [], { senders: [] }]) { - expect(readAgentMessageSource(value)).toBeUndefined() - } - }) - - it('names at most a few senders and counts the rest', () => { - const many: AgentMessageSource = { - ...FROM, - senders: Array.from({ length: 5 }, (_, index) => ({ - party: { address: `term_${index}`, terminalHandle: `term_${index}`, orcaSessionId: null }, - name: `Agent ${index}` - })) - } - const { shown, more } = agentMessageSendersShown(many) - expect(shown.map((sender) => sender.name)).toEqual(['Agent 0', 'Agent 1', 'Agent 2']) - expect(more).toBe(2) - }) }) diff --git a/src/shared/agent-session/agent-session-provider-context.test.ts b/src/shared/agent-session/agent-session-provider-context.test.ts index b7597558087..8d2e2813081 100644 --- a/src/shared/agent-session/agent-session-provider-context.test.ts +++ b/src/shared/agent-session/agent-session-provider-context.test.ts @@ -38,16 +38,4 @@ describe('provider context after clear', () => { expect(activeProviderContext(record).handleChain).toBe(record.providerHandleChain) expect(isPersistedAgentSessionRecord(encodeAgentSessionRecord(record))).toBe(true) }) - - it('rejects a boundary beyond the record fence', () => { - const record = agentSessionRecordFixture() - expect( - isPersistedAgentSessionRecord( - encodeAgentSessionRecord({ - ...record, - providerContextBoundary: { ...boundary, afterFence: record.lease.runtimeFence + 1 } - }) - ) - ).toBe(false) - }) }) diff --git a/src/shared/agent-session/agent-session-question-answer.test.ts b/src/shared/agent-session/agent-session-question-answer.test.ts index 28b048bd654..f128fa370a7 100644 --- a/src/shared/agent-session/agent-session-question-answer.test.ts +++ b/src/shared/agent-session/agent-session-question-answer.test.ts @@ -58,21 +58,6 @@ describe('agent-session grouped question answers', () => { ).toEqual(unicodeAnswers) }) - it("keeps Claude's maximum choice group compact without losing selections", () => { - const maximumSelections = Array.from({ length: 4 }, (_, questionIndex) => ({ - questionId: `q${questionIndex + 1}`, - optionIds: Array.from( - { length: 4 }, - (_, optionIndex) => `q${questionIndex + 1}:choice-${optionIndex + 1}` - ) - })) - - const encoded = encodeAgentSessionQuestionAnswers(maximumSelections) - expect(encoded.length).toBeLessThanOrEqual(512) - expect(decodeAgentSessionQuestionAnswers(encoded)).toEqual(maximumSelections) - expect(decodeWithOriginalPercentDecoder(encoded)).toEqual(maximumSelections) - }) - it('validates each grouped answer against its question shape', () => { const questions = [ { diff --git a/src/shared/agent-session/agent-session-registered-agents.test.ts b/src/shared/agent-session/agent-session-registered-agents.test.ts index 9e9427c3a7c..8b194cfea1c 100644 --- a/src/shared/agent-session/agent-session-registered-agents.test.ts +++ b/src/shared/agent-session/agent-session-registered-agents.test.ts @@ -15,10 +15,6 @@ const CODEX = { } describe('decodeAgentSessionAgentsResult', () => { - it('reads every well-formed agent', () => { - expect(decodeAgentSessionAgentsResult({ agents: [CODEX] })).toEqual([CODEX]) - }) - it('drops a row it cannot read and keeps the rest', () => { expect( decodeAgentSessionAgentsResult({ @@ -61,15 +57,4 @@ describe('decodeAgentSessionAgentsResult', () => { } ]) }) - - it('keeps the first row of a repeated agent', () => { - const later = { ...CODEX, capabilities: { ...CODEX.capabilities, rewind: false } } - expect(decodeAgentSessionAgentsResult({ agents: [CODEX, later] })).toEqual([CODEX]) - }) - - it('answers null for a reply that is not an agent list', () => { - expect(decodeAgentSessionAgentsResult(null)).toBeNull() - expect(decodeAgentSessionAgentsResult({})).toBeNull() - expect(decodeAgentSessionAgentsResult({ agents: 'claude' })).toBeNull() - }) }) diff --git a/src/shared/agent-session/agent-session-thread-goal.test.ts b/src/shared/agent-session/agent-session-thread-goal.test.ts index 28772a4bb6d..cd8cb76a480 100644 --- a/src/shared/agent-session/agent-session-thread-goal.test.ts +++ b/src/shared/agent-session/agent-session-thread-goal.test.ts @@ -5,7 +5,6 @@ import type { AgentJournalThreadGoal } from './journal/agent-session-journal-types' import { - agentSessionThreadGoalElapsedSeconds, currentAgentSessionThreadGoal, currentAgentSessionThreadGoalSourceBySequence } from './agent-session-thread-goal' @@ -47,26 +46,6 @@ function goalRow(sequence: number, state: 'set' | 'cleared', value = goal()) { } describe('current thread goal', () => { - it('reports no answer when no row records a goal transition', () => { - expect( - currentAgentSessionThreadGoal([ - row(1, { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hi' }] }), - row(2, { kind: 'status', text: 'Context compacted' }) - ]) - ).toBeUndefined() - }) - - it('reads the last goal row of a rendered snapshot, past later rows of other kinds', () => { - const paused = goal({ status: 'paused' }) - expect( - currentAgentSessionThreadGoal([ - goalRow(1, 'set'), - goalRow(2, 'set', paused), - row(3, { kind: 'status', text: 'Context compacted' }) - ]) - ).toEqual(paused) - }) - it('takes the latest transition by sequence for items held unordered', () => { const paused = goal({ status: 'paused' }) const latest = goalRow(5, 'set', paused) @@ -81,10 +60,6 @@ describe('current thread goal', () => { expect(currentAgentSessionThreadGoalSourceBySequence([])?.goal).toBeUndefined() }) - it('answers null once the latest transition cleared the goal', () => { - expect(currentAgentSessionThreadGoal([goalRow(1, 'set'), goalRow(2, 'cleared')])).toBeNull() - }) - it('answers null for a row written before the typed snapshot existed', () => { const legacy = row(3, { kind: 'status', @@ -108,19 +83,3 @@ describe('current thread goal', () => { expect(currentAgentSessionThreadGoal([goalRow(1, 'set'), future])).toBeNull() }) }) - -describe('thread goal presentation facts', () => { - it('adds time only while an active goal has a turn running', () => { - const now = 1_000_000 + 7_500 - const running = { startedAt: null } - expect(agentSessionThreadGoalElapsedSeconds(goal(), now, null)).toBe(30) - expect(agentSessionThreadGoalElapsedSeconds(goal(), now, running)).toBe(37) - expect(agentSessionThreadGoalElapsedSeconds(goal({ status: 'paused' }), now, running)).toBe(30) - // A report older than the turn counts from the turn's start, not across the idle gap. - expect(agentSessionThreadGoalElapsedSeconds(goal(), now, { startedAt: now - 2_000 })).toBe(32) - // A provider clock ahead of this one never subtracts time. - expect( - agentSessionThreadGoalElapsedSeconds(goal({ updatedAt: now + 5_000 }), now, running) - ).toBe(30) - }) -}) diff --git a/src/shared/agent-session/failures/agent-session-failure-words.test.ts b/src/shared/agent-session/failures/agent-session-failure-words.test.ts index 5112915b830..60fdc46d786 100644 --- a/src/shared/agent-session/failures/agent-session-failure-words.test.ts +++ b/src/shared/agent-session/failures/agent-session-failure-words.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - readAgentSessionFailureFact, - type AgentSessionFailureFact, - type SubmissionRejectionKind -} from './agent-session-failure' +import './agent-session-failure' +import type { AgentSessionFailureFact, SubmissionRejectionKind } from './agent-session-failure' import { agentSessionFailureSentence, agentSessionFailureWords @@ -65,27 +62,3 @@ describe('the words written beside a failure fact', () => { expect(retrying({})).toBe('Codex hit a temporary problem and is retrying.') }) }) - -describe('an attachment problem as a reader meets it', () => { - it('keeps what it can place and drops what it cannot', () => { - expect( - readAgentSessionFailureFact({ - kind: 'attachmentInvalid', - attachment: { reason: 'tooMany', limit: 20 } - }) - ).toEqual({ kind: 'attachmentInvalid', attachment: { reason: 'tooMany', limit: 20 } }) - expect( - readAgentSessionFailureFact({ - kind: 'attachmentInvalid', - attachment: { reason: 'tooMany', limit: 'twenty' } - }) - ).toEqual({ kind: 'attachmentInvalid', attachment: { reason: 'tooMany' } }) - // A reason a newer host added reads as the generic attachment failure. - expect( - readAgentSessionFailureFact({ - kind: 'attachmentInvalid', - attachment: { reason: 'futureReason' } - }) - ).toEqual({ kind: 'attachmentInvalid' }) - }) -}) diff --git a/src/shared/agent-session/failures/agent-session-failure.test.ts b/src/shared/agent-session/failures/agent-session-failure.test.ts index 3cc5f3d6311..ac6b2f355e5 100644 --- a/src/shared/agent-session/failures/agent-session-failure.test.ts +++ b/src/shared/agent-session/failures/agent-session-failure.test.ts @@ -75,40 +75,6 @@ describe('reading a failure fact', () => { }) } }) - - it('keeps what a provider said it is retrying, and only that', () => { - expect( - readAgentSessionFailureFact({ - kind: 'providerRetrying', - retry: { error: 'rate_limit', status: 429, retryDelayMs: 600 } - }) - ).toEqual({ kind: 'providerRetrying', retry: { error: 'rate_limit', status: 429 } }) - expect( - readAgentSessionFailureFact({ kind: 'providerRetrying', retry: { error: '', status: 'x' } }) - ).toEqual({ kind: 'providerRetrying' }) - // The provider's own account of what failed survives a read, bounded like any detail. - expect( - readAgentSessionFailureFact({ - kind: 'providerRetrying', - retry: { cause: ` stream disconnected${' x'.repeat(400)}` } - })?.retry?.cause - ).toBe(`stream disconnected${' x'.repeat(400)}`.slice(0, 512).trim()) - expect( - readAgentSessionFailureFact({ kind: 'providerRetrying', retry: { cause: ' ' } }) - ).toEqual({ kind: 'providerRetrying' }) - }) - - it('drops what a newer host wrote that this build cannot place', () => { - expect(readAgentSessionFailureFact({ kind: 'futureKind' })).toBeUndefined() - expect(readAgentSessionFailureFact(undefined)).toBeUndefined() - expect( - readAgentSessionFailureFact({ - kind: 'restartFailed', - refusal: { code: 'agent_session_conflict', details: { reason: 'futureReason' } }, - detail: { text: 'x', audience: 'future' } - }) - ).toEqual({ kind: 'restartFailed', refusal: { code: 'agent_session_conflict' } }) - }) }) describe('reading all of a failure fact', () => { diff --git a/src/shared/agent-session/failures/agent-session-refusal-notice.test.ts b/src/shared/agent-session/failures/agent-session-refusal-notice.test.ts index a558342826f..a34751d46ad 100644 --- a/src/shared/agent-session/failures/agent-session-refusal-notice.test.ts +++ b/src/shared/agent-session/failures/agent-session-refusal-notice.test.ts @@ -4,32 +4,14 @@ import { agentSessionWriteNoticeEnglish, agentSessionWriteNoticeParts } from './agent-session-refusal-notice' -import { agentSessionFailureSentence } from './agent-session-failure-words' -import { readAgentSessionRefusalDetails } from './agent-session-refusal-details' +import './agent-session-failure-words' +import './agent-session-refusal-details' import { agentSessionRefusalFailure, parseAgentSessionWriteFailure } from './agent-session-write-failure' describe('agentSessionRefusalCauseParts', () => { - it.each(['managed', 'system'] as const)( - 'retains the %s account when saving a refused message', - (account) => { - const refusal = { - code: 'agent_session_operation_invalid', - details: { reason: 'notSignedIn', account } - } as const - const saved = agentSessionRefusalFailure(refusal) - expect(saved.details).toEqual(refusal.details) - const parts = agentSessionRefusalCauseParts(saved, { agentName: 'Claude' }) - expect(agentSessionWriteNoticeEnglish(parts)).toBe( - agentSessionFailureSentence({ kind: 'notSignedIn', account }, 'rejection', { - agentName: 'Claude' - }) - ) - } - ) - // A failed start the host refused for a running terminal says so; only a named chat is another chat. it('names the terminal that holds the conversation unless the refusal names a chat', () => { const held = agentSessionRefusalFailure({ @@ -102,53 +84,3 @@ describe('saved Arguments refusals', () => { expect(agentSessionRefusalCauseParts(refused)).toEqual([]) }) }) - -describe('an expired attachment refusal', () => { - const notice = (details: unknown): string => - agentSessionWriteNoticeEnglish( - agentSessionWriteNoticeParts( - agentSessionRefusalFailure({ - code: 'agent_session_operation_invalid', - details: readAgentSessionRefusalDetails('agent_session_operation_invalid', details) - }), - 'composer-send' - ) - ) - const expired = (...names: string[]) => ({ - reason: 'attachmentExpired', - expiredAttachments: names.map((name, index) => ({ uploadId: `upload-${index}`, name })) - }) - - it('names the file', () => { - expect(notice(expired('notes.pdf'))).toBe( - 'notes.pdf expired. Your message was not sent. Remove it and attach it again.' - ) - }) - - it('names several, then stops short of a long list', () => { - expect(notice(expired('a.pdf', 'b.pdf'))).toBe( - 'a.pdf, b.pdf expired. Your message was not sent. Remove them and attach them again.' - ) - expect(notice(expired('a.pdf', 'b.pdf', 'c.pdf', 'd.pdf'))).toBe( - 'a.pdf, b.pdf, c.pdf… expired. Your message was not sent. Remove them and attach them again.' - ) - }) - - it.each(['orca-paste-1-abc.png', 'orca-paste-1-abc.txt'])( - "keeps a paste's generated name out of the words: %s", - (name) => { - expect(notice(expired(name))).toBe( - 'This attachment expired. Your message was not sent. Remove it and attach it again.' - ) - } - ) - - it.each([ - ['an older host, which names none', { reason: 'attachmentExpired' }], - ['a list it cannot read', { reason: 'attachmentExpired', expiredAttachments: [{ name: 1 }] }] - ])('stays generic for %s', (_case, details) => { - expect(notice(details)).toBe( - 'This attachment expired. Your message was not sent. Remove it and attach it again.' - ) - }) -}) diff --git a/src/shared/agent-session/failures/agent-session-wire-refusals.test.ts b/src/shared/agent-session/failures/agent-session-wire-refusals.test.ts index 3e79a1a4af5..a31b32863ef 100644 --- a/src/shared/agent-session/failures/agent-session-wire-refusals.test.ts +++ b/src/shared/agent-session/failures/agent-session-wire-refusals.test.ts @@ -1,118 +1,6 @@ import { describe, expect, it } from 'vitest' import { readAgentSessionRefusalDetails } from './agent-session-refusal-details' -import { - agentSessionRefusalReference, - readAgentSessionRefusalReference, - refuse, - refuseUnclassified, - withAgentSessionRefusalFacts, - type AgentSessionWireRefusal -} from './agent-session-wire-refusals' -import type { AgentJournalResolution } from '../journal/agent-session-journal-types' - -const RESOLUTION: AgentJournalResolution = { - state: 'resolved', - selectedOptionId: 'allow', - resolvedBy: 'phone-1', - resolvedAt: 1_789_000_000_000 -} - -/** The refusal with its new field removed: exactly what a released client reads. */ -function asReleasedClientSeesIt(refusal: AgentSessionWireRefusal): unknown { - const { details: _details, ...rest } = JSON.parse(JSON.stringify(refusal)) - return rest -} - -describe('the loose fields released clients read', () => { - it('gain a verdict learned after the refusal, in details and at the top level', () => { - const stamped = withAgentSessionRefusalFacts( - refuse('agent_session_operation_invalid', { reason: 'providerStartFailed' }, 'm'), - { ownerVerdict: 'exited' } - ) - expect(stamped).toEqual({ - code: 'agent_session_operation_invalid', - message: 'm', - details: { reason: 'providerStartFailed', ownerVerdict: 'exited' }, - ownerVerdict: 'exited' - }) - // A refusal an older row replayed, with no reason, still reaches released clients with it. - expect( - withAgentSessionRefusalFacts(refuseUnclassified('agent_session_conflict', 'm'), { - ownerVerdict: 'unverifiable' - }) - ).toEqual({ - code: 'agent_session_conflict', - message: 'm', - details: { ownerVerdict: 'unverifiable' }, - ownerVerdict: 'unverifiable' - }) - }) - - // Each expected value is the literal the host built for the same situation before details. - it.each([ - [ - 'a moved prompt', - refuse( - 'agent_session_item_revision_stale', - { reason: 'promptMoved', currentRevision: 2, resolution: RESOLUTION }, - 'Item i has moved on.' - ), - { - code: 'agent_session_item_revision_stale', - message: 'Item i has moved on.', - currentRevision: 2, - resolution: RESOLUTION - } - ], - [ - 'a resolved prompt', - refuse( - 'agent_session_already_resolved', - { reason: 'promptAlreadyResolved', currentRevision: 2, resolution: RESOLUTION }, - 'Item i was already resolved.' - ), - { - code: 'agent_session_already_resolved', - message: 'Item i was already resolved.', - currentRevision: 2, - resolution: RESOLUTION - } - ], - [ - 'a stale fence', - refuse('agent_session_checkpoint_stale', { reason: 'fenceStale', currentFence: 4 }, 'm'), - { code: 'agent_session_checkpoint_stale', message: 'm', currentFence: 4 } - ], - [ - 'a refused rewind', - refuse( - 'agent_session_operation_invalid', - { reason: 'rewindRefused', rewindReason: 'busy' }, - 'agent_session_rewind:busy' - ), - { - code: 'agent_session_operation_invalid', - message: 'agent_session_rewind:busy', - rewindReason: 'busy' - } - ], - [ - 'a failed create', - withAgentSessionRefusalFacts( - refuse('agent_session_operation_invalid', { reason: 'providerStartFailed' }, 'exited 1'), - { ownerVerdict: 'exited' } - ), - { code: 'agent_session_operation_invalid', message: 'exited 1', ownerVerdict: 'exited' } - ], - [ - 'a refusal with no facts', - refuse('agent_session_conflict', { reason: 'claimConflicted' }, 'm'), - { code: 'agent_session_conflict', message: 'm' } - ] - ] as const)('read by a released client exactly as before for %s', (_label, refusal, before) => { - expect(asReleasedClientSeesIt(refusal)).toEqual(before) - }) -}) +import './agent-session-wire-refusals' describe('a stored refusal read back', () => { it('keeps what the code lists and drops the rest', () => { @@ -135,24 +23,4 @@ describe('a stored refusal read back', () => { undefined ) }) - - it('round-trips through its reference, and reads an older shape as naming nothing', () => { - const refusal = refuse( - 'agent_session_ownership_unknown', - { reason: 'noLiveOwner', ownerVerdict: 'live' }, - 'm' - ) - const reference = agentSessionRefusalReference(refusal) - expect(reference).toEqual({ - code: 'agent_session_ownership_unknown', - details: { reason: 'noLiveOwner', ownerVerdict: 'live' } - }) - expect(readAgentSessionRefusalReference(JSON.parse(JSON.stringify(reference)))).toEqual( - reference - ) - expect( - readAgentSessionRefusalReference({ code: 'agent_session_conflict', cause: 'claimConflicted' }) - ).toEqual({ code: 'agent_session_conflict' }) - expect(readAgentSessionRefusalReference({ code: 'agent_session_future' })).toBeUndefined() - }) }) diff --git a/src/shared/agent-session/failures/agent-session-write-failure.test.ts b/src/shared/agent-session/failures/agent-session-write-failure.test.ts index 1f970147e14..9476b30b803 100644 --- a/src/shared/agent-session/failures/agent-session-write-failure.test.ts +++ b/src/shared/agent-session/failures/agent-session-write-failure.test.ts @@ -1,54 +1,9 @@ import { describe, expect, it } from 'vitest' import { agentSessionRefusalFailure, - agentSessionThrownFailure, - parseAgentSessionWriteFailure, - readAgentSessionErrorRefusal + agentSessionThrownFailure } from './agent-session-write-failure' -const HOST_TEXT = 'Expected runtime fence 1; the session is at 3.' - -function saved(value: unknown): unknown { - return JSON.parse(JSON.stringify(value)) -} - -describe('parseAgentSessionWriteFailure', () => { - // The shape saved before refusals carried a reason; it loads as it was and reads as its code. - it('reads back an entry saved with the code alone, unchanged', () => { - expect( - parseAgentSessionWriteFailure(saved({ kind: 'refused', code: 'agent_session_conflict' })) - ).toEqual({ kind: 'refused', code: 'agent_session_conflict' }) - expect(parseAgentSessionWriteFailure({ kind: 'failed' })).toEqual({ kind: 'failed' }) - expect( - parseAgentSessionWriteFailure({ - kind: 'refused', - code: 'agent_session_conflict', - message: HOST_TEXT - }) - ).toEqual({ kind: 'refused', code: 'agent_session_conflict' }) - }) - - // A newer build's reason or verdict is dropped, never guessed; the code's words stand. - it.each([ - [{ reason: 'fromTheFuture' }, undefined], - [{ reason: 'fromTheFuture', ownerVerdict: 'exited' }, { ownerVerdict: 'exited' }], - [{ reason: 'ownerUnproven', ownerVerdict: 'gone' }, { reason: 'ownerUnproven' }], - [{ reason: 'chatStarting' }, undefined] - ])('drops what this build cannot place: %j', (details, kept) => { - expect( - parseAgentSessionWriteFailure({ - kind: 'refused', - code: 'agent_session_ownership_unknown', - details - }) - ).toEqual({ - kind: 'refused', - code: 'agent_session_ownership_unknown', - ...(kept ? { details: kept } : {}) - }) - }) -}) - describe('agentSessionRefusalFailure', () => { // What moves with the owner, the question's answer and the host's words are never kept. it('keeps only the facts that stay true after a reload', () => { @@ -80,14 +35,6 @@ describe('agentSessionRefusalFailure', () => { details: { reason: 'promptAlreadyResolved' } }) }) - - it('keeps a code from a newer host without reading its details', () => { - const refusal = JSON.parse('{"code":"agent_session_from_the_future","details":{"reason":"x"}}') - expect(agentSessionRefusalFailure(refusal)).toEqual({ - kind: 'refused', - code: 'agent_session_from_the_future' - }) - }) }) describe('a refusal a failed request carried in its error', () => { @@ -97,17 +44,6 @@ describe('a refusal a failed request carried in its error', () => { } const payload = { code: 'runtime_error', message: 'agent_session_journal_unreadable' } - it('is read from a thrown RPC error and from a stream payload alike', () => { - const expected = { - code: 'agent_session_journal_unreadable', - details: { reason: 'journalCorrupt' } - } - expect( - readAgentSessionErrorRefusal({ response: { error: { ...payload, data: { refusal } } } }) - ).toEqual(expected) - expect(readAgentSessionErrorRefusal(saved({ ...payload, data: { refusal } }))).toEqual(expected) - }) - it("words the refusal when there is one, else what the request's error code proves", () => { expect(agentSessionThrownFailure({ ...payload, data: { refusal } }, 'runtime_error')).toEqual({ kind: 'refused', @@ -120,17 +56,4 @@ describe('a refusal a failed request carried in its error', () => { code: 'structured_agent_session_unsupported' }) }) - - it("degrades a reason another build added to the code's own words", () => { - const newer = { - code: 'agent_session_journal_unreadable', - details: { reason: 'journalFromTheFuture' } - } - expect( - agentSessionThrownFailure({ ...payload, data: { refusal: newer } }, 'runtime_error') - ).toEqual({ - kind: 'refused', - code: 'agent_session_journal_unreadable' - }) - }) }) diff --git a/src/shared/agent-session/failures/structured-agent-session-rejection-words.test.ts b/src/shared/agent-session/failures/structured-agent-session-rejection-words.test.ts index 7ead34629f0..1c8d9f2dc7d 100644 --- a/src/shared/agent-session/failures/structured-agent-session-rejection-words.test.ts +++ b/src/shared/agent-session/failures/structured-agent-session-rejection-words.test.ts @@ -56,17 +56,4 @@ describe('the line under a recorded rejection', () => { it('says only that it was not sent when a loaded row already states the reason', () => { expect(line('Written by the host.', signedOut)).toBe('Your message was not sent.') }) - - it('words a fact no row states from the fact itself', () => { - expect(line('Written by the host.', { kind: 'writeFailed' })).toBe( - "Orca couldn't reach the agent. Your message was not sent." - ) - }) - - it("shows an older host's reason sentence, and never a legacy marker", () => { - expect(line('Claude messages support at most 20 images')).toBe( - 'Claude messages support at most 20 images' - ) - expect(line('not_delivered')).toBe('Your message was not sent.') - }) }) diff --git a/src/shared/agent-session/journal/agent-session-journal-body-admission.test.ts b/src/shared/agent-session/journal/agent-session-journal-body-admission.test.ts deleted file mode 100644 index 0a964375650..00000000000 --- a/src/shared/agent-session/journal/agent-session-journal-body-admission.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { readAgentJournalMessageBody } from './agent-session-journal-body-admission' - -describe("a submission's body", () => { - it('reads a message, a newer kind as unreadable, and another known kind as damage', () => { - expect(readAgentJournalMessageBody({ kind: 'message', role: 'user', blocks: [] })).toBe( - 'readable' - ) - expect(readAgentJournalMessageBody({ kind: 'voice-note', clip: 'x' })).toBe('unreadable') - expect(readAgentJournalMessageBody({ kind: ' ' })).toBe('malformed') - expect(readAgentJournalMessageBody({ kind: 'status', text: 'x' })).toBe('malformed') - expect( - readAgentJournalMessageBody({ kind: 'message', role: 'user', blocks: [{ type: 'text' }] }) - ).toBe('malformed') - }) -}) diff --git a/src/shared/agent-session/journal/agent-session-journal-producer.test.ts b/src/shared/agent-session/journal/agent-session-journal-producer.test.ts deleted file mode 100644 index 0ecda0bea52..00000000000 --- a/src/shared/agent-session/journal/agent-session-journal-producer.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { agentJournalLinkageFields, isRootAgentJournalItem } from './agent-session-journal-producer' - -describe('isRootAgentJournalItem', () => { - it('reads an id that failed to resolve as a subagent’s, not as root', () => { - // Presence, not truthiness: an unresolved child id must never attribute its content to the parent. - expect(isRootAgentJournalItem({ agentId: '' })).toBe(false) - }) -}) - -describe('agentJournalLinkageFields', () => { - it('omits absent members rather than writing them as undefined', () => { - // An own key set to undefined is still present; absence alone claims root. - expect(agentJournalLinkageFields({ agentId: 'task-1' })).toEqual({ agentId: 'task-1' }) - expect(Object.keys(agentJournalLinkageFields({ agentId: 'task-1' }))).toEqual(['agentId']) - expect(agentJournalLinkageFields(undefined)).toEqual({}) - }) -}) diff --git a/src/shared/agent-session/journal/agent-session-journal-submission-schema.spec.ts b/src/shared/agent-session/journal/agent-session-journal-submission-schema.spec.ts index d4eca299cf6..1e31ead3ddf 100644 --- a/src/shared/agent-session/journal/agent-session-journal-submission-schema.spec.ts +++ b/src/shared/agent-session/journal/agent-session-journal-submission-schema.spec.ts @@ -1,47 +1,10 @@ import { expect, it } from 'vitest' -import type { AgentJournalSubmission } from './agent-session-journal-types' + import { AgentJournalSubmissionSchema, isAdmissibleAgentJournalSubmission } from './agent-session-journal-submission-schema' -// Persisted and published: a reader that keeps the parsed value must not lose either fact. -it('keeps a kept send’s card id and its source through a parse', () => { - const kept: AgentJournalSubmission = { - clientMessageId: 'client-kept', - fence: 2, - payloadFingerprint: 'fingerprint-kept', - dispatchState: 'rejected', - providerItemId: null, - reason: 'Orca restarted before this message was sent.', - rejection: { kind: 'hostRestarted' }, - submittedAt: 1, - resolvedAt: 2, - recovered: true, - source: { kind: 'user' }, - keptAsQueuedMessageId: 'client-kept' - } - expect(isAdmissibleAgentJournalSubmission(kept)).toBe(true) - expect(AgentJournalSubmissionSchema.parse(kept)).toEqual(kept) - expect(AgentJournalSubmissionSchema.parse(JSON.parse(JSON.stringify(kept)))).toEqual(kept) -}) - -it('refuses an empty card id rather than reading it as kept', () => { - expect( - isAdmissibleAgentJournalSubmission({ - clientMessageId: 'client-kept', - fence: 2, - payloadFingerprint: 'fingerprint-kept', - dispatchState: 'rejected', - providerItemId: null, - reason: null, - submittedAt: 1, - resolvedAt: 2, - keptAsQueuedMessageId: '' - }) - ).toBe(false) -}) - it('admits newer fields without replacing or mutating the submission', () => { const value = { clientMessageId: 'client-1', diff --git a/src/shared/agent-session/lifecycle/agent-session-account-home.test.ts b/src/shared/agent-session/lifecycle/agent-session-account-home.test.ts deleted file mode 100644 index ad7cb121fb8..00000000000 --- a/src/shared/agent-session/lifecycle/agent-session-account-home.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { agentSessionAccountHomesEqual } from './agent-session-account-home' -import { isPersistedAgentSessionRecord } from './agent-session-record' -import { agentSessionRecordFixture } from './agent-session-record.test-fixture' -import { encodeAgentSessionRecord } from './agent-session-record-stored-form' - -describe('agent session account home', () => { - // Validate agent-specific env ownership at launch, when this binding becomes the child environment. - it('reads any well-formed variable, and sets aside one that is not a variable name', () => { - const record = encodeAgentSessionRecord(agentSessionRecordFixture()) - const withVariable = (variable: string) => - isPersistedAgentSessionRecord({ ...record, accountHome: { variable, path: '/tmp/x' } }) - expect(withVariable('CODEX_HOME')).toBe(true) - expect(withVariable('GROK_HOME')).toBe(true) - expect(withVariable('X'.repeat(128))).toBe(true) - for (const malformed of ['', 'A B', '1HOME', 'HOME=x', 'X'.repeat(129)]) { - expect(withVariable(malformed)).toBe(false) - } - }) - - it('validates tagged OpenCode locators, keeping ones a newer build added fields to', () => { - const record = encodeAgentSessionRecord(agentSessionRecordFixture()) - const managed = { - kind: 'opencode', - locator: { kind: 'managed', managedProfileId: '123e4567-e89b-42d3-a456-426614174000' } - } as const - const unmanaged = { kind: 'opencode', locator: { kind: 'unmanaged' } } as const - expect(isPersistedAgentSessionRecord({ ...record, accountHome: managed })).toBe(true) - expect(isPersistedAgentSessionRecord({ ...record, accountHome: unmanaged })).toBe(true) - expect(agentSessionAccountHomesEqual(managed, unmanaged)).toBe(false) - expect(agentSessionAccountHomesEqual(managed, { ...managed })).toBe(true) - expect( - agentSessionAccountHomesEqual(managed, { - ...managed, - locator: { ...managed.locator, managedProfileId: '223e4567-e89b-42d3-a456-426614174000' } - }) - ).toBe(false) - for (const managedProfileId of ['', 'not-a-profile-id']) { - expect( - isPersistedAgentSessionRecord({ - ...record, - accountHome: { ...managed, locator: { ...managed.locator, managedProfileId } } - }) - ).toBe(false) - } - expect(agentSessionAccountHomesEqual(unmanaged, { ...unmanaged })).toBe(true) - expect( - isPersistedAgentSessionRecord({ - ...record, - accountHome: { ...managed, path: '/fake/home' } - }) - ).toBe(true) - expect( - isPersistedAgentSessionRecord({ - ...record, - accountHome: { ...unmanaged, locator: { ...unmanaged.locator, channel: 'beta' } } - }) - ).toBe(true) - expect( - isPersistedAgentSessionRecord({ - ...record, - accountHome: { ...unmanaged, locator: { kind: 'detached' } } - }) - ).toBe(false) - }) -}) diff --git a/src/shared/agent-session/lifecycle/agent-session-lease-adjudication.test.ts b/src/shared/agent-session/lifecycle/agent-session-lease-adjudication.test.ts index 0c1a6df94d9..18b5b191d26 100644 --- a/src/shared/agent-session/lifecycle/agent-session-lease-adjudication.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-lease-adjudication.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest' import { adjudicateAgentSessionRestart, - agentSessionLeaseAdmitsWriter, evaluateAgentSessionAcquisition, - isProvenAliveProbe, - isProvenDeadProbe, type AgentSessionOwnerProbe } from './agent-session-lease-adjudication' import { normalizeLegacyHandoffLease } from './agent-session-legacy-handoff-lease' @@ -53,70 +50,7 @@ function acquire( }) } -describe('proof classification', () => { - it('treats a pid match with nothing PID-reuse-safe as no proof at all', () => { - // A bare pid match is exactly the case that mints a second writer after pid reuse. - expect(isProvenAliveProbe({ outcome: 'identity-matched', matchedOn: [] })).toBe(false) - expect(isProvenAliveProbe(MATCHED)).toBe(true) - expect(isProvenDeadProbe(INDETERMINATE)).toBe(false) - expect(isProvenAliveProbe(INDETERMINATE)).toBe(false) - }) - - it.each([ - ['exit-observed', { outcome: 'exit-observed' } as AgentSessionOwnerProbe], - ['pid-absent', { outcome: 'pid-absent' } as AgentSessionOwnerProbe], - [ - 'identity-mismatch', - { outcome: 'identity-mismatch', field: 'spawn-token' } as AgentSessionOwnerProbe - ] - ])('accepts %s as proof of death', (_name, probe) => { - expect(isProvenDeadProbe(probe)).toBe(true) - }) - - it('never counts a reservation probe or an indeterminate answer as death', () => { - expect(isProvenDeadProbe({ outcome: 'reservation-unused' })).toBe(false) - expect(isProvenDeadProbe(INDETERMINATE)).toBe(false) - }) -}) - describe('acquisition compare-and-swap', () => { - it('refuses a stale fence and grants at exactly fence + 1', () => { - const held = lease({ ownerProcess: null, claimStatus: 'released', reservedSpawnToken: null }) - expect( - evaluateAgentSessionAcquisition({ - lease: held, - expectedFence: held.runtimeFence - 1, - handoffOperationId: null, - probe: MATCHED - }) - ).toEqual({ - decision: 'refused', - code: 'agent_session_checkpoint_stale', - details: { reason: 'fenceStale' } - }) - expect(acquire(held, MATCHED)).toEqual({ decision: 'granted', nextFence: 8 }) - }) - - it('refuses a cached fence after the lease advances to a reservation', () => { - const before = lease({ ownerProcess: null, claimStatus: 'released', reservedSpawnToken: null }) - const winner = acquire(before, MATCHED) - expect(winner).toEqual({ decision: 'granted', nextFence: 8 }) - // The loser still holds the pre-swap fence, which is no longer current. - const after = lease({ ...before, runtimeFence: 8, claimStatus: 'reserved' }) - expect( - evaluateAgentSessionAcquisition({ - lease: after, - expectedFence: 7, - handoffOperationId: null, - probe: MATCHED - }) - ).toEqual({ - decision: 'refused', - code: 'agent_session_checkpoint_stale', - details: { reason: 'fenceStale' } - }) - }) - it('never grants a second owner on expiry alone', () => { // The recorded owner is long past its deadline; nothing here may consult that deadline. const expired = lease({ leaseDeadlineAt: 1, lastRenewedAt: 1 }) @@ -144,23 +78,6 @@ describe('acquisition compare-and-swap', () => { }) }) - it('keeps a conflicted claim conflicted regardless of proof', () => { - // Restart adjudication and recovery resolution are what retire it, once its owner is gone. - expect(acquire(lease({ claimStatus: 'conflicted' }), { outcome: 'exit-observed' })).toEqual({ - decision: 'refused', - code: 'agent_session_conflict', - details: { reason: 'claimConflicted' } - }) - }) - - it('refuses acquisition in the recovering stage', () => { - expect(acquire(lease({ handoffStage: 'recovering' }), { outcome: 'pid-absent' })).toEqual({ - decision: 'refused', - code: 'agent_session_ownership_unknown', - details: { reason: 'ownerUnproven' } - }) - }) - it('refuses a different acquisition operation and replays the matching one', () => { const mid = lease({ handoffStage: 'new-owner-proving', @@ -206,13 +123,6 @@ describe('restart reconciliation', () => { ).toMatchObject({ disposition: 'recovering', stage: 'recovering' }) }) - it('routes a surviving native owner to recovery instead of readopting a dead transport', () => { - // The runtime crash lost native stdio; stop the orphan and respawn at fence + 1 instead of readopting an undrivable child. - expect( - adjudicateAgentSessionRestart({ lease: lease(), probe: MATCHED, observedAt: 9_000 }) - ).toMatchObject({ disposition: 'recovering', stage: 'recovering' }) - }) - it('bumps the fence exactly once for a proven-dead owner and records the evidence', () => { const result = adjudicateAgentSessionRestart({ lease: lease(), @@ -244,16 +154,6 @@ describe('restart reconciliation', () => { ).toMatchObject({ evidence: { observedAt: 9_000, lastProvenAliveAt: 9_000 } }) }) - it('hands an unverifiable owner to recovery resolution instead of evicting it', () => { - expect( - adjudicateAgentSessionRestart({ - lease: lease({ leaseDeadlineAt: 1 }), - probe: INDETERMINATE, - observedAt: 9_000 - }) - ).toEqual({ disposition: 'recovering', stage: 'recovering', reason: 'no answer' }) - }) - it('re-adjudicates a claim an older record marked conflicted by the owner it names', () => { expect( adjudicateAgentSessionRestart({ @@ -271,27 +171,6 @@ describe('restart reconciliation', () => { ).toEqual({ disposition: 'evicted', nextFence: 8, evidence: null }) }) - it('frees a conflict whose named owner is proven gone', () => { - // Once this specific process is proven gone, retain no conflict that would permanently prevent reopening. - expect( - adjudicateAgentSessionRestart({ - lease: lease({ claimStatus: 'conflicted' }), - probe: { outcome: 'pid-absent' }, - observedAt: 9_000 - }) - ).toEqual({ - disposition: 'evicted', - nextFence: 8, - evidence: { - kind: 'pid-absent', - detail: 'recorded pid absent on host', - observedAt: 9_000, - ownerFence: 7, - lastProvenAliveAt: 500 - } - }) - }) - it('frees a lease that names neither an owner nor a reservation, without moving the fence', () => { // Evicted leases have neither owner nor token to probe; treating them as unproven reservations re-latches every released record. expect( @@ -336,13 +215,3 @@ describe('restart reconciliation', () => { } ) }) - -describe('writer admission', () => { - it('admits a writer only when reconciled, settled, live, and holding a process', () => { - expect(agentSessionLeaseAdmitsWriter(lease())).toBe(true) - expect(agentSessionLeaseAdmitsWriter(lease({ unreconciled: true }))).toBe(false) - expect(agentSessionLeaseAdmitsWriter(lease({ handoffStage: 'new-owner-proving' }))).toBe(false) - expect(agentSessionLeaseAdmitsWriter(lease({ claimStatus: 'reserved' }))).toBe(false) - expect(agentSessionLeaseAdmitsWriter(lease({ ownerProcess: null }))).toBe(false) - }) -}) diff --git a/src/shared/agent-session/lifecycle/agent-session-legacy-handoff-lease.test.ts b/src/shared/agent-session/lifecycle/agent-session-legacy-handoff-lease.test.ts deleted file mode 100644 index 5ffa1aa7c93..00000000000 --- a/src/shared/agent-session/lifecycle/agent-session-legacy-handoff-lease.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - leaseCarriesLegacyHandoffValues, - normalizeLegacyHandoffLease, - type PersistedAgentSessionLease -} from './agent-session-legacy-handoff-lease' -import type { AgentSessionClaimStatus } from './agent-session-record' -import { agentSessionLeaseFixture } from './agent-session-record.test-fixture' - -const CLAIMS: AgentSessionClaimStatus[] = ['reserved', 'live', 'conflicted', 'released'] - -function persisted(overrides: Partial): PersistedAgentSessionLease { - return { ...agentSessionLeaseFixture(), ...overrides } -} - -describe('normalizing a lease the removed terminal handoff wrote', () => { - it.each(['preparing', 'old-owner-stopped', 'manual-recovery'] as const)( - 'maps the %s stage to recovering and keeps its operation id', - (handoffStage) => { - const lease = persisted({ handoffStage, handoffOperationId: 'op-handoff' }) - expect(normalizeLegacyHandoffLease(lease)).toEqual({ - ...lease, - handoffStage: 'recovering' - }) - expect(leaseCarriesLegacyHandoffValues(lease)).toBe(true) - } - ) - - it.each(CLAIMS)( - 'turns a recorded terminal owner (%s) into a conflicted native claim', - (claim) => { - const lease = persisted({ runtimeKind: 'tui', claimStatus: claim }) - expect(normalizeLegacyHandoffLease(lease)).toEqual({ - ...lease, - runtimeKind: 'native', - claimStatus: 'conflicted' - }) - } - ) - - it.each(CLAIMS)('changes only the kind of a terminal lease naming no process (%s)', (claim) => { - const lease = persisted({ runtimeKind: 'tui', claimStatus: claim, ownerProcess: null }) - expect(normalizeLegacyHandoffLease(lease)).toEqual({ ...lease, runtimeKind: 'native' }) - expect(leaseCarriesLegacyHandoffValues(lease)).toBe(true) - }) - - it('maps a terminal owner mid return trip on both fields', () => { - const lease = persisted({ runtimeKind: 'tui', handoffStage: 'old-owner-stopped' }) - expect(normalizeLegacyHandoffLease(lease)).toMatchObject({ - runtimeKind: 'native', - handoffStage: 'recovering', - claimStatus: 'conflicted' - }) - }) -}) diff --git a/src/shared/agent-session/lifecycle/agent-session-mutation-envelope.test.ts b/src/shared/agent-session/lifecycle/agent-session-mutation-envelope.test.ts index 8d8c3dd8103..e18d9f1e129 100644 --- a/src/shared/agent-session/lifecycle/agent-session-mutation-envelope.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-mutation-envelope.test.ts @@ -55,52 +55,11 @@ describe('computeAgentSessionPayloadFingerprint', () => { expect(a).toBe(b) expect(a).toMatch(/^[0-9a-f]{64}$/) }) - - it('separates one payload from another and one method from another', () => { - const send = computeAgentSessionPayloadFingerprint({ - method: 'agentSession.send', - sessionId: 'session-1', - fields: { text: 'hi' } - }) - expect( - computeAgentSessionPayloadFingerprint({ - method: 'agentSession.send', - sessionId: 'session-1', - fields: { text: 'hi there' } - }) - ).not.toBe(send) - expect( - computeAgentSessionPayloadFingerprint({ - method: 'agentSession.cancel', - sessionId: 'session-1', - fields: { text: 'hi' } - }) - ).not.toBe(send) - }) }) describe('admitAgentSessionMutation', () => { const base = { envelope: envelope(), hostFingerprint: 'f'.repeat(64), lease: LEASE } - 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 outcome even while the lease is unreconciled', () => { const recorded = { ...row('f'.repeat(64)), @@ -125,62 +84,4 @@ describe('admitAgentSessionMutation', () => { expect(admission, String(expectedRuntimeFence)).toMatchObject({ decision: 'admit' }) } }) - - it('refuses a writer while the lease is unreconciled', () => { - const admission = admitAgentSessionMutation({ - ...base, - lease: { ...LEASE, unreconciled: true }, - ledger: ADMIT('f'.repeat(64)) - }) - expect(admission).toMatchObject({ - decision: 'refused', - refusal: { code: 'execution_owner_reconciling' } - }) - }) - - it('refuses a writer while the chat is still starting', () => { - const admission = admitAgentSessionMutation({ - ...base, - lease: { ...LEASE, handoffStage: 'new-owner-proving' }, - ledger: ADMIT('f'.repeat(64)) - }) - expect(admission).toMatchObject({ - decision: 'refused', - refusal: { code: 'agent_session_conflict', message: 'The chat is still starting.' } - }) - }) - - it('names recovery rather than a handoff when a restart left the owner unproven', () => { - const admission = admitAgentSessionMutation({ - ...base, - lease: { ...LEASE, handoffStage: 'recovering' }, - ledger: ADMIT('f'.repeat(64)) - }) - expect(admission).toMatchObject({ - decision: 'refused', - refusal: { - code: 'agent_session_conflict', - message: - "Orca has not yet confirmed that this chat's previous agent process stopped. Reopen the chat to check again." - } - }) - }) - - it('surfaces a ledger refusal verbatim', () => { - const admission = admitAgentSessionMutation({ - ...base, - ledger: { - decision: 'refused', - code: 'agent_session_operation_expired', - details: { reason: 'operationExpired' } - } - }) - expect(admission).toMatchObject({ - decision: 'refused', - refusal: { - code: 'agent_session_operation_expired', - details: { reason: 'operationExpired' } - } - }) - }) }) diff --git a/src/shared/agent-session/lifecycle/agent-session-operation-ledger.test.ts b/src/shared/agent-session/lifecycle/agent-session-operation-ledger.test.ts index cdc67061e51..ebf846476b5 100644 --- a/src/shared/agent-session/lifecycle/agent-session-operation-ledger.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-operation-ledger.test.ts @@ -49,13 +49,6 @@ function admit( } describe('operation admission', () => { - it('admits a fresh id once and replays the identical retry', () => { - const rows = new Map() - const row = admit(rows) - const replay = evaluate(rows) - expect(replay).toEqual({ decision: 'replay', row }) - }) - it('refuses the same id carrying different parameters', () => { const rows = new Map() admit(rows) @@ -150,11 +143,4 @@ describe('retention', () => { expect(pruneAgentSessionOperationRows(rows, lastAdmissibleAt).size).toBe(1) expect(evaluate(pruneAgentSessionOperationRows(rows, lastAdmissibleAt)).decision).toBe('replay') }) - - it('drops only rows past their own expiry', () => { - const rows = new Map() - const row = admit(rows) - expect(pruneAgentSessionOperationRows(rows, row.expiresAt).size).toBe(0) - expect(pruneAgentSessionOperationRows(rows, row.expiresAt - 1).size).toBe(1) - }) }) diff --git a/src/shared/agent-session/lifecycle/agent-session-provider-handle.test.ts b/src/shared/agent-session/lifecycle/agent-session-provider-handle.test.ts index d025618482e..95d538e0a36 100644 --- a/src/shared/agent-session/lifecycle/agent-session-provider-handle.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-provider-handle.test.ts @@ -10,8 +10,6 @@ import { } from './agent-session-provider-handle' import { agentSessionProviderHandleKey, - agentSessionProviderHandleRoot, - isAgentSessionProviderHandle, claudeProviderHandle, codexProviderHandle } from './agent-session-provider-handle-encoding' @@ -30,28 +28,6 @@ function link(overrides: Partial = {}) { } describe('handle identity', () => { - it('keys a Claude handle by session id AND leaf, so two branches are two handles', () => { - // Concurrent resumes branch one transcript silently; the session id alone cannot name a writer. - const branchA = agentSessionProviderHandleKey(CLAUDE) - const branchB = agentSessionProviderHandleKey(claudeProviderHandle('sess-1', 'leaf-2')) - expect(branchA).not.toEqual(branchB) - expect(agentSessionProviderHandleRoot(CLAUDE)).toEqual( - agentSessionProviderHandleRoot(claudeProviderHandle('sess-1', 'leaf-2')) - ) - }) - - it('distinguishes a null leaf from an empty-string leaf and rejects malformed handles', () => { - expect(isAgentSessionProviderHandle(claudeProviderHandle('sess-1', null))).toBe(true) - expect(isAgentSessionProviderHandle({ ...CLAUDE, resumeCursor: '' })).toBe(false) - expect(isAgentSessionProviderHandle({ ...CLAUDE, nativeId: '' })).toBe(false) - expect(isAgentSessionProviderHandle({ ...CLAUDE, nativeId: ' sess-1 ' })).toBe(false) - // A stored typed shape is not an in-memory handle: it must be decoded first. - expect( - isAgentSessionProviderHandle({ provider: 'claude', sessionId: 'x', leafUuid: null }) - ).toBe(false) - expect(isAgentSessionProviderHandle({ provider: 'gemini', sessionId: 'x' })).toBe(false) - }) - it('uses collision-free keys when Claude ids contain delimiters', () => { const left = claudeProviderHandle('a#b', 'c') const right = claudeProviderHandle('a', 'b#c') @@ -61,30 +37,6 @@ describe('handle identity', () => { }) describe('chain append', () => { - it('starts only from a created or adopted link', () => { - expect(appendAgentSessionProviderHandleLink([], link())).toEqual([link()]) - expect(appendAgentSessionProviderHandleLink([], link({ origin: 'adopted' }))).toHaveLength(1) - expect(() => appendAgentSessionProviderHandleLink([], link({ origin: 'resumed' }))).toThrow( - 'agent_session_provider_handle_invalid' - ) - }) - - it('refuses to record a fork as a resume', () => { - // --fork-session copies item IDs without proving continuity; never record it as a resume. - const chain = [link()] - expect(() => - appendAgentSessionProviderHandleLink( - chain, - link({ - linkId: 'link-2', - origin: 'resumed', - handle: claudeProviderHandle('sess-2', 'leaf-9'), - mintedAtFence: 2 - }) - ) - ).toThrow('agent_session_provider_handle_forked') - }) - it('records a fork only with a new root and the seed it came from', () => { const chain = [link()] const forked = link({ @@ -107,35 +59,6 @@ describe('chain append', () => { ).toThrow('agent_session_provider_handle_invalid') }) - it('rejects a link minted under an older fence', () => { - const chain = [link({ mintedAtFence: 5 })] - expect(() => - appendAgentSessionProviderHandleLink( - chain, - link({ - linkId: 'link-2', - origin: 'resumed', - handle: claudeProviderHandle('sess-1', 'leaf-2'), - mintedAtFence: 4 - }) - ) - ).toThrow('agent_session_provider_handle_stale_fence') - }) - - it('rejects a provider change mid-chain', () => { - expect(() => - appendAgentSessionProviderHandleLink( - [link()], - link({ - linkId: 'link-2', - origin: 'resumed', - handle: codexProviderHandle('thread-1'), - mintedAtFence: 2 - }) - ) - ).toThrow('agent_session_provider_handle_provider_mismatch') - }) - it('treats re-proving the same handle at the same fence as a retry, not a new link', () => { const chain = [link({ mintedAtFence: 3 })] const retried = appendAgentSessionProviderHandleLink( @@ -153,19 +76,6 @@ describe('chain append', () => { ).toHaveLength(2) }) - it('rejects reuse of a stable link id for a different proof', () => { - expect(() => - appendAgentSessionProviderHandleLink( - [link()], - link({ - origin: 'resumed', - handle: claudeProviderHandle('sess-1', 'leaf-2'), - mintedAtFence: 2 - }) - ) - ).toThrow('agent_session_provider_handle_invalid') - }) - it('refuses to grow past the cap rather than dropping fork provenance', () => { const chain: AgentSessionProviderHandleLink[] = [link()] for (let index = 1; index < MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS; index += 1) { @@ -195,38 +105,9 @@ describe('chain append', () => { `link-${MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS}` ) }) - - it('never mutates the chain it was given', () => { - const chain = [link()] - appendAgentSessionProviderHandleLink( - chain, - link({ - linkId: 'link-2', - origin: 'resumed', - handle: claudeProviderHandle('sess-1', 'leaf-2'), - mintedAtFence: 2 - }) - ) - expect(chain).toHaveLength(1) - }) }) describe('chain lookup and validation', () => { - it('rejects a persisted chain that is over the cap or holds a malformed link', () => { - expect(isAgentSessionProviderHandleChain([{ ...link(), mintedAtFence: -1 }])).toBe(false) - expect(isAgentSessionProviderHandleChain([{ ...link(), linkId: 'not a link id!' }])).toBe(false) - expect(isAgentSessionProviderHandleChain([{ ...link(), forkedFromKey: 'claude:seed' }])).toBe( - false - ) - expect( - isAgentSessionProviderHandleChain( - Array.from({ length: MAX_AGENT_SESSION_PROVIDER_HANDLE_LINKS + 1 }, (_value, index) => - link({ linkId: `link-${index}` }) - ) - ) - ).toBe(false) - }) - it('rejects persisted chains that bypass append invariants', () => { expect( isAgentSessionProviderHandleChain([ @@ -285,67 +166,6 @@ describe('adopted chain heads', () => { expect(chain.map((entry) => entry.origin)).toEqual(['adopted', 'resumed']) expect(agentSessionProviderHandleChainHead(chain)).toBe(resumed) }) - - it('elides a Claude re-proof of the identical adopted handle at the same fence', () => { - const chain = [adopted()] - const elided = appendAgentSessionProviderHandleLink( - chain, - link({ - linkId: 'claude-1-sess-1-empty-retry', - origin: 'resumed', - handle: claudeProviderHandle('sess-1', null), - mintedAtFence: 1 - }) - ) - - expect(elided).toEqual(chain) - }) - - it('appends a Codex resume only once the fence has moved', () => { - const codexAdopted = link({ - linkId: 'codex-1-thread-1', - origin: 'adopted', - handle: codexProviderHandle('thread-1') - }) - const reproved = link({ - linkId: 'codex-1-thread-1-retry', - origin: 'resumed', - handle: codexProviderHandle('thread-1'), - mintedAtFence: 1 - }) - - // Codex's thread id is the whole key, so a same-fence re-proof can only ever be a retry. - expect(appendAgentSessionProviderHandleLink([codexAdopted], reproved)).toEqual([codexAdopted]) - expect( - appendAgentSessionProviderHandleLink([codexAdopted], { ...reproved, mintedAtFence: 2 }) - ).toHaveLength(2) - }) - - it('refuses a second origin link on top of an adopted head', () => { - // Nothing re-origins a chain: a create landing here would erase where the conversation came from. - for (const origin of ['created', 'adopted'] as const) { - expect(() => - appendAgentSessionProviderHandleLink( - [adopted()], - link({ linkId: 'claude-2-sess-1-leaf-1', origin, handle: CLAUDE, mintedAtFence: 2 }) - ) - ).toThrow('agent_session_provider_handle_invalid') - } - }) - - it('refuses a resume that landed on another conversation entirely', () => { - expect(() => - appendAgentSessionProviderHandleLink( - [adopted()], - link({ - linkId: 'claude-1-sess-9-leaf-9', - origin: 'resumed', - handle: claudeProviderHandle('sess-9', 'leaf-9'), - mintedAtFence: 1 - }) - ) - ).toThrow('agent_session_provider_handle_forked') - }) }) describe('superseding a creation the provider never saved', () => { @@ -409,19 +229,6 @@ describe('superseding a creation the provider never saved', () => { ) ).toThrow('agent_session_provider_handle_stale_fence') }) - - it('carries supersession only on a creation, and never as a persisted second link', () => { - expect( - appendAgentSessionProviderHandleLink([], replacement({ origin: 'created' })) - ).toHaveLength(1) - expect(() => - appendAgentSessionProviderHandleLink( - [created], - replacement({ origin: 'resumed', handle: unsaved }) - ) - ).toThrow('agent_session_provider_handle_invalid') - expect(isAgentSessionProviderHandleChain([created, replacement()])).toBe(false) - }) }) describe('a transport shared code has never heard of', () => { @@ -544,39 +351,6 @@ describe('replacing a conversation the provider could not restore', () => { ).toThrow('agent_session_provider_handle_stale_fence') }) - it('is only ever a creation, and never opens a chain', () => { - expect(() => appendAgentSessionProviderHandleLink([], fresh())).toThrow( - 'agent_session_provider_handle_invalid' - ) - for (const origin of ['resumed', 'adopted', 'forked'] as const) { - expect(() => - appendAgentSessionProviderHandleLink( - [created, resumed], - fresh({ origin, forkedFromKey: lost.key }) - ) - ).toThrow('agent_session_provider_handle_invalid') - } - }) - - it('reads a reason a later build records, but no malformed one', () => { - const later = fresh({ replaces: { ...lost, reason: 'transport-retired' } }) - expect(appendAgentSessionProviderHandleLink([created, resumed], later)).toHaveLength(3) - for (const replaces of [ - { ...lost, reason: '' }, - { ...lost, reason: 'x'.repeat(65) }, - { ...lost, replacedAt: -1 }, - { key: lost.key, reason: lost.reason } - ]) { - expect(() => - appendAgentSessionProviderHandleLink( - [created, resumed], - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: deliberately malformed stored data the guard must refuse. - fresh({ replaces: replaces as AgentSessionProviderHandleLink['replaces'] }) - ) - ).toThrow('agent_session_provider_handle_invalid') - } - }) - it('stays lost when a fresh conversation the agent never saved is superseded in turn', () => { const chain = appendAgentSessionProviderHandleLink([created, resumed], fresh()) const superseding = link({ @@ -610,15 +384,6 @@ describe('replacing a conversation the provider could not restore', () => { ).toThrow('agent_session_provider_handle_invalid') }) - it('refuses a supersession key that names no creation in the chain', () => { - expect(() => - appendAgentSessionProviderHandleLink( - [created, resumed], - fresh({ supersedesKey: agentSessionProviderHandleKey(acp('s-9')) }) - ) - ).toThrow('agent_session_provider_handle_invalid') - }) - it('is refused in a Claude or Codex chain, whose rows older builds read', () => { for (const [first, lost, next] of [ [CLAUDE, CLAUDE, claudeProviderHandle('sess-2', null)], diff --git a/src/shared/agent-session/lifecycle/agent-session-record-stored-form.test.ts b/src/shared/agent-session/lifecycle/agent-session-record-stored-form.test.ts index 175a8ae441a..8785c52e653 100644 --- a/src/shared/agent-session/lifecycle/agent-session-record-stored-form.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-record-stored-form.test.ts @@ -2,15 +2,12 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { decodePersistedAgentSessionProviderHandleChain, encodePersistedAgentSessionProviderHandleChain, - isAgentSessionHandleProvider, type AgentSessionProviderHandle, type AgentSessionProviderHandleLink } from './agent-session-provider-handle' import { - agentSessionJournalProviderHandle, agentSessionProviderHandleKey, agentSessionProviderHandleRoot, - agentSessionWireProviderHandle, claudeProviderHandle, codexProviderHandle, decodePersistedAgentSessionProviderHandle, @@ -31,24 +28,6 @@ import { agentSessionRecordFixture } from './agent-session-record.test-fixture' -/** The pre-neutral-handle guard applied to every stored handle. */ -function olderBuildReadsHandle(value: unknown): boolean { - const isHandleField = (field: unknown): boolean => - typeof field === 'string' && field.length > 0 && field.length <= 512 && field === field.trim() - if (typeof value !== 'object' || value === null || !('provider' in value)) { - return false - } - if (value.provider === 'claude') { - return ( - 'sessionId' in value && - isHandleField(value.sessionId) && - 'leafUuid' in value && - (value.leafUuid === null || isHandleField(value.leafUuid)) - ) - } - return value.provider === 'codex' && 'threadId' in value && isHandleField(value.threadId) -} - /** A Claude record row exactly as builds before this change wrote it: a fork with its seed key. */ const STORED_CLAUDE_ROW = JSON.stringify({ schemaVersion: 2, @@ -102,18 +81,6 @@ const STORED_CODEX_ROW = JSON.stringify({ }) describe('a record row older builds wrote', () => { - it.each([undefined, 0, 6, 7, 9, Number.MAX_SAFE_INTEGER])( - 'keeps a valid legacy next-fence floor %s readable', - (minimumNextFence) => { - const record = agentSessionRecordFixture(agentSessionLeaseFixture({ minimumNextFence })) - const stored = JSON.parse(JSON.stringify(encodeAgentSessionRecord(record))) - expect(isPersistedAgentSessionRecord(stored)).toBe(true) - expect(decodePersistedAgentSessionRecord(stored).record.lease.minimumNextFence).toBe( - minimumNextFence - ) - } - ) - it.each([ ['Claude', STORED_CLAUDE_ROW], ['Codex', STORED_CODEX_ROW] @@ -129,30 +96,6 @@ describe('a record row older builds wrote', () => { expect(JSON.stringify(encodeAgentSessionRecord(record))).toBe(row) }) - it.each([undefined, 25, 0, -1, '25', null, Number.NaN, Number.POSITIVE_INFINITY])( - 'normalizes a saved sleep value %s without hiding the conversation', - (sleptAt) => { - const stored = { ...encodeAgentSessionRecord(agentSessionRecordFixture()), sleptAt } - expect(isPersistedAgentSessionRecord(stored)).toBe(true) - const decoded = decodePersistedAgentSessionRecord(stored) - expectTypeOf(decoded.record).toMatchTypeOf() - expect(decoded.record.sleptAt).toBe(sleptAt === 25 ? 25 : undefined) - expect(decoded.normalized).toBe(sleptAt !== undefined && sleptAt !== 25) - } - ) - - it('hands shared code only the neutral handle', () => { - const stored: unknown = JSON.parse(STORED_CLAUDE_ROW) - if (!isPersistedAgentSessionRecord(stored)) { - throw new Error('fixture row must be readable') - } - const { record } = decodePersistedAgentSessionRecord(stored) - expect(record.providerHandleChain.map((link) => link.handle)).toEqual([ - { transport: 'claude-sdk', agent: 'claude', nativeId: 'sess-1', resumeCursor: 'leaf-1' }, - { transport: 'claude-sdk', agent: 'claude', nativeId: 'sess-2' } - ]) - }) - it('still reports a lease only the removed terminal handoff wrote', () => { const record = agentSessionRecordFixture() const stored = encodeAgentSessionRecord(record) @@ -188,28 +131,6 @@ describe('a record row older builds wrote', () => { }) describe('what this build writes', () => { - it('stores Claude and Codex handles in the typed form older builds read', () => { - const claude = claudeProviderHandle('sess-1', 'leaf-1') - const codex = codexProviderHandle('thread-1') - expect(encodePersistedAgentSessionProviderHandle(claude)).toEqual({ - provider: 'claude', - sessionId: 'sess-1', - leafUuid: 'leaf-1' - }) - expect(encodePersistedAgentSessionProviderHandle(claudeProviderHandle('sess-1', null))).toEqual( - { provider: 'claude', sessionId: 'sess-1', leafUuid: null } - ) - expect(encodePersistedAgentSessionProviderHandle(codex)).toEqual({ - provider: 'codex', - threadId: 'thread-1' - }) - for (const handle of [claude, codex]) { - const stored = encodePersistedAgentSessionProviderHandle(handle) - expect(olderBuildReadsHandle(stored)).toBe(true) - expect(decodePersistedAgentSessionProviderHandle(stored)).toEqual(handle) - } - }) - it('keeps the persisted key and root strings older builds derived', () => { // Fork seeds, superseded creations and resume offers store these; a new spelling strands them. const claude = claudeProviderHandle('sess-1', 'leaf-1') @@ -222,27 +143,6 @@ describe('what this build writes', () => { expect(agentSessionProviderHandleRoot(codexProviderHandle('t'))).toBe('codex:"t"') }) - it('round-trips another transport in the neutral form, which an older build refuses', () => { - const grok = { - transport: 'acp', - agent: 'grok', - nativeId: 'acp-session-1', - resumeCursor: '{"cwd":"/repo"}' - } - const stored = encodePersistedAgentSessionProviderHandle(grok) - expect(stored).toEqual(grok) - expect(decodePersistedAgentSessionProviderHandle(JSON.parse(JSON.stringify(stored)))).toEqual( - grok - ) - // Unknown providers must never impersonate Codex on an older build. - expect(olderBuildReadsHandle(stored)).toBe(false) - expect(isAgentSessionHandleProvider(grok.agent)).toBe(false) - expect(agentSessionWireProviderHandle(grok)).toBeNull() - // Root, not the resume cursor: the cursor is resume state, not identity. - expect(agentSessionProviderHandleRoot(grok)).toBe('acp/grok:"acp-session-1"') - expect(agentSessionProviderHandleKey(grok)).toBe(agentSessionProviderHandleRoot(grok)) - }) - it('refuses a stored handle that is not the one form its transport writes', () => { for (const value of [ // Claude or Codex in the neutral form: no build writes it. @@ -318,34 +218,6 @@ describe('what this build writes', () => { }) }) -describe('the wire and journal forms', () => { - it('records the journal rows exactly as before, and another transport as opaque', () => { - expect( - agentSessionJournalProviderHandle({ - agent: 'claude', - providerHandle: claudeProviderHandle('sess-1', 'leaf-1') - }) - ).toEqual({ kind: 'claude', sessionId: 'sess-1', leafUuid: 'leaf-1' }) - expect( - agentSessionJournalProviderHandle({ - agent: 'codex', - providerHandle: codexProviderHandle('t') - }) - ).toEqual({ kind: 'codex', threadId: 't' }) - expect(agentSessionJournalProviderHandle({ agent: 'codex', providerHandle: null })).toEqual({ - kind: 'opaque', - agent: 'codex', - value: 'pending' - }) - expect( - agentSessionJournalProviderHandle({ - agent: 'grok', - providerHandle: { transport: 'acp', agent: 'grok', nativeId: 's' } - }) - ).toEqual({ kind: 'opaque', agent: 'grok', value: 's' }) - }) -}) - describe('a chat whose saved conversation could not be restored', () => { const acp = (nativeId: string): AgentSessionProviderHandle => ({ transport: 'acp', diff --git a/src/shared/agent-session/lifecycle/agent-session-resume-cursor.test.ts b/src/shared/agent-session/lifecycle/agent-session-resume-cursor.test.ts deleted file mode 100644 index a6156891699..00000000000 --- a/src/shared/agent-session/lifecycle/agent-session-resume-cursor.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { extractAgentProviderSession, isResumableTuiAgent } from './agent-session-resume' -import { getAgentResumeArgv } from '../../agent-resume-argv' -import { buildAgentResumeStartupPlan } from '../../tui-agent-resume-startup' -import { resolveStartupShell, tokenizeStartupCommand } from '../../tui-agent-startup-shell' - -describe('Cursor conversation continuity', () => { - it('captures only the hook conversation identity', () => { - expect(isResumableTuiAgent('cursor')).toBe(true) - expect( - extractAgentProviderSession('cursor', { - conversation_id: 'conversation-742', - session_id: 'tool-session' - }) - ).toEqual({ key: 'conversation_id', id: 'conversation-742' }) - expect(extractAgentProviderSession('cursor', { session_id: 'tool-session' })).toBeNull() - expect(extractAgentProviderSession('cursor', { conversation_id: '--continue' })).toBeNull() - expect( - extractAgentProviderSession('cursor', { conversation_id: 'session\ncommand' }) - ).toBeNull() - }) - - it('resumes the exact conversation instead of the latest one', () => { - expect( - getAgentResumeArgv('cursor', { key: 'conversation_id', id: 'conversation-742' }) - ).toEqual(['cursor-agent', '--resume', 'conversation-742']) - expect(getAgentResumeArgv('cursor', { key: 'session_id', id: 'tool-session' })).toBeNull() - }) - - it.each(['linux', 'darwin', 'win32'] as const)( - 'preserves the configured executable and launch recipe on %s', - (platform) => { - const plan = buildAgentResumeStartupPlan({ - agent: 'cursor', - providerSession: { key: 'conversation_id', id: 'conversation-742' }, - cmdOverrides: { cursor: 'custom-cursor' }, - platform, - agentArgs: '--mode ask' - }) - expect(plan).not.toBeNull() - if (!plan) { - throw new Error('Missing Cursor resume plan') - } - expect( - tokenizeStartupCommand(plan.launchCommand, resolveStartupShell(platform)) - ).toMatchObject({ - ok: true, - tokens: ['custom-cursor', '--mode', 'ask', '--resume', 'conversation-742'] - }) - } - ) -}) diff --git a/src/shared/agent-session/lifecycle/agent-session-resume.test.ts b/src/shared/agent-session/lifecycle/agent-session-resume.test.ts deleted file mode 100644 index cd387e3c7ca..00000000000 --- a/src/shared/agent-session/lifecycle/agent-session-resume.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - agentProviderSessionsEqual, - extractAgentProviderSession, - normalizeAgentProviderSession -} from './agent-session-resume' -import { getAgentResumeArgv } from '../../agent-resume-argv' - -describe('agent session resume metadata', () => { - it.each([ - ['claude', { session_id: 'claude-session' }, { key: 'session_id', id: 'claude-session' }], - ['codex', { session_id: 'codex-session' }, { key: 'session_id', id: 'codex-session' }], - ['gemini', { session_id: 'gemini-session' }, { key: 'session_id', id: 'gemini-session' }], - [ - 'antigravity', - { conversationId: 'agy-conversation' }, - { key: 'conversation_id', id: 'agy-conversation' } - ], - ['opencode', { sessionID: 'opencode-session' }, { key: 'session_id', id: 'opencode-session' }], - [ - 'pi', - { session_id: 'pi-session', session_file: '/tmp/pi-session.jsonl' }, - { key: 'session_id', id: 'pi-session', transcriptPath: '/tmp/pi-session.jsonl' } - ], - ['mimo-code', { sessionID: 'mimo-session' }, { key: 'session_id', id: 'mimo-session' }], - ['droid', { session_id: 'droid-session' }, { key: 'session_id', id: 'droid-session' }], - ['grok', { sessionId: 'grok-session' }, { key: 'session_id', id: 'grok-session' }], - ['devin', { session_id: 'devin-session' }, { key: 'session_id', id: 'devin-session' }], - ['omp', { session_id: 'omp-session' }, { key: 'session_id', id: 'omp-session' }], - [ - 'prime-agent', - { session_id: 'prime-session', session_file: '/tmp/prime-session.jsonl' }, - { key: 'session_id', id: 'prime-session', transcriptPath: '/tmp/prime-session.jsonl' } - ], - [ - 'copilot', - { session_id: '940237d9-c712-48e8-bca1-fd75fc4a8d4b' }, - { key: 'session_id', id: '940237d9-c712-48e8-bca1-fd75fc4a8d4b' } - ], - ['copilot', { sessionId: 'copilot-camel' }, { key: 'session_id', id: 'copilot-camel' }], - [ - 'kimi', - { session_id: 'session_431324d7-2165-42f0-9ecd-9f93437b3201' }, - { key: 'session_id', id: 'session_431324d7-2165-42f0-9ecd-9f93437b3201' } - ], - ['jcode', { session_id: 'session_jc_1' }, { key: 'session_id', id: 'session_jc_1' }], - ['jcode', { sessionId: 'session_jc_2' }, { key: 'session_id', id: 'session_jc_2' }], - [ - 'hermes', - { session_id: '20261020_221305_5e3a1c' }, - { key: 'session_id', id: '20261020_221305_5e3a1c' } - ] - ] as const)('extracts %s provider session ids', (source, payload, expected) => { - expect(extractAgentProviderSession(source, payload)).toEqual(expected) - }) - - it.each([ - ['claude', { key: 'session_id', id: 's1' }, ['claude', '--resume', 's1']], - [ - 'claude', - { key: 'session_id', id: 's1', effort: 'max' }, - ['claude', '--effort', 'max', '--resume', 's1'] - ], - ['codebuddy', { key: 'session_id', id: 's1' }, ['codebuddy', '--resume', 's1']], - ['codex', { key: 'session_id', id: 's1' }, ['codex', 'resume', 's1']], - ['gemini', { key: 'session_id', id: 's1' }, ['gemini', '--resume', 's1']], - ['antigravity', { key: 'conversation_id', id: 's1' }, ['agy', '--conversation', 's1']], - ['opencode', { key: 'session_id', id: 's1' }, ['opencode', '--session', 's1']], - [ - 'pi', - { key: 'session_id', id: 's1', transcriptPath: '/tmp/pi-session.jsonl' }, - ['pi', '--session', '/tmp/pi-session.jsonl'] - ], - ['mimo-code', { key: 'session_id', id: 's1' }, ['mimo', '--session', 's1']], - ['droid', { key: 'session_id', id: 's1' }, ['droid', '--resume', 's1']], - ['grok', { key: 'session_id', id: 's1' }, ['grok', '--resume', 's1']], - ['devin', { key: 'session_id', id: 'abc12345' }, ['devin', '--resume', 'abc12345']], - ['omp', { key: 'session_id', id: 's1' }, ['omp', '--resume', 's1']], - [ - 'prime-agent', - { key: 'session_id', id: 's1', transcriptPath: '/tmp/prime-session.jsonl' }, - ['prime-agent', '--resume', '/tmp/prime-session.jsonl'] - ], - ['copilot', { key: 'session_id', id: 's1' }, ['copilot', '--resume=s1']], - [ - 'kimi', - { key: 'session_id', id: 'session_431324d7' }, - ['kimi', '--session', 'session_431324d7'] - ], - ['jcode', { key: 'session_id', id: 'session_jc_1' }, ['jcode', '--resume', 'session_jc_1']], - ['hermes', { key: 'session_id', id: 's1' }, ['hermes', '--resume', 's1']], - ['dsh', { key: 'session_id', id: 's1' }, ['dsh-tui', '--backend=dsh', '--resume', 's1']] - ] as const)('builds %s resume argv', (agent, providerSession, expected) => { - expect(getAgentResumeArgv(agent, providerSession)).toEqual(expected) - }) - - it('rejects unsupported sources and unsafe ids', () => { - expect(extractAgentProviderSession('cursor', { session_id: 'cursor-session' })).toBeNull() - expect(normalizeAgentProviderSession({ key: 'session_id', id: 'bad\nid' })).toBeNull() - expect(normalizeAgentProviderSession({ key: 'session_id', id: '--last' })).toBeNull() - expect(extractAgentProviderSession('codex', { session_id: '--last' })).toBeNull() - expect(normalizeAgentProviderSession({ key: 'session_id', id: 'ok' })).toEqual({ - key: 'session_id', - id: 'ok' - }) - }) - - it('does not capture ephemeral Pi sessions without a session file', () => { - expect(extractAgentProviderSession('pi', { session_id: 'pi-session' })).toBeNull() - expect( - extractAgentProviderSession('pi', { session_id: 'pi-session', session_file: '' }) - ).toBeNull() - expect(extractAgentProviderSession('pi', { session_file: '/tmp/pi-session.jsonl' })).toBeNull() - expect(getAgentResumeArgv('pi', { key: 'session_id', id: 'pi-session' })).toBeNull() - }) - - it('compares the actual provider resume locator for each agent', () => { - const first = { key: 'session_id' as const, id: 'session-1', transcriptPath: '/tmp/first' } - const second = { key: 'session_id' as const, id: 'session-1', transcriptPath: '/tmp/second' } - - expect(agentProviderSessionsEqual('pi', first, second)).toBe(false) - expect(agentProviderSessionsEqual('prime-agent', first, second)).toBe(false) - expect(agentProviderSessionsEqual('claude', first, second)).toBe(true) - }) - - it('captures the hook transcript_path for native-chat agents (claude/codex)', () => { - expect( - extractAgentProviderSession('claude', { - session_id: 'cs', - transcript_path: '/home/u/.claude/projects/slug/real.jsonl' - }) - ).toEqual({ - key: 'session_id', - id: 'cs', - transcriptPath: '/home/u/.claude/projects/slug/real.jsonl' - }) - expect( - extractAgentProviderSession('codex', { session_id: 'xs', transcriptPath: '/x/r.jsonl' }) - ).toEqual({ key: 'session_id', id: 'xs', transcriptPath: '/x/r.jsonl' }) - }) - - it('does not attach transcript_path for non-native-chat agents', () => { - expect( - extractAgentProviderSession('gemini', { session_id: 'gs', transcript_path: '/x/r.jsonl' }) - ).toEqual({ key: 'session_id', id: 'gs' }) - }) - - it('round-trips transcriptPath through normalizeAgentProviderSession', () => { - expect( - normalizeAgentProviderSession({ key: 'session_id', id: 'ok', transcriptPath: '/x/r.jsonl' }) - ).toEqual({ key: 'session_id', id: 'ok', transcriptPath: '/x/r.jsonl' }) - expect( - normalizeAgentProviderSession({ - key: 'session_id', - id: 'ok', - transcriptPath: '/tmp/bad\npath.jsonl' - }) - ).toEqual({ key: 'session_id', id: 'ok' }) - }) - - it('round-trips a Claude effort level and drops values unsafe for argv', () => { - expect(normalizeAgentProviderSession({ key: 'session_id', id: 'ok', effort: 'xhigh' })).toEqual( - { key: 'session_id', id: 'ok', effort: 'xhigh' } - ) - for (const effort of ['--model', 'high; rm -rf /', 'HIGH', '', 3]) { - expect(normalizeAgentProviderSession({ key: 'session_id', id: 'ok', effort })).toEqual({ - key: 'session_id', - id: 'ok' - }) - } - }) -}) - -describe('OMP recorded resume locators', () => { - it.each([ - { - explicit: '/explicit/session.jsonl', - recorded: '/hook/session.jsonl', - target: '/explicit/session.jsonl' - }, - { explicit: undefined, recorded: ' /hook/session.jsonl ', target: '/hook/session.jsonl' }, - { explicit: ' ', recorded: '/hook/session.jsonl', target: '/hook/session.jsonl' }, - { explicit: undefined, recorded: ' ', target: 'session-id' }, - { explicit: undefined, recorded: undefined, target: 'session-id' } - ])('selects explicit then recorded path then UUID %j', ({ explicit, recorded, target }) => { - expect( - getAgentResumeArgv( - 'omp', - { key: 'session_id', id: 'session-id', transcriptPath: recorded }, - explicit - ) - ).toEqual(['omp', '--resume', target]) - }) - it('retains UUID equality when hook path metadata arrives later', () => { - expect( - agentProviderSessionsEqual( - 'omp', - { key: 'session_id', id: 'session-id' }, - { key: 'session_id', id: 'session-id', transcriptPath: '/hook/session.jsonl' } - ) - ).toBe(true) - }) -}) diff --git a/src/shared/agent-session/lifecycle/agent-session-stored-agent.test.ts b/src/shared/agent-session/lifecycle/agent-session-stored-agent.test.ts index b5c8b49da43..726b9fc2070 100644 --- a/src/shared/agent-session/lifecycle/agent-session-stored-agent.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-stored-agent.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest' import { isPersistedAgentSessionRecord, type AgentSessionRecord } from './agent-session-record' import { agentSessionRecordFixture } from './agent-session-record.test-fixture' -import { - decodePersistedAgentSessionRecord, - encodeAgentSessionRecord -} from './agent-session-record-stored-form' +import { encodeAgentSessionRecord } from './agent-session-record-stored-form' function grokRecord(transport = 'acp'): AgentSessionRecord { const record = agentSessionRecordFixture() @@ -20,35 +17,6 @@ function grokRecord(transport = 'acp'): AgentSessionRecord { } describe('provider-independent stored records', () => { - it('reads and decodes a well-formed record', () => { - const stored = encodeAgentSessionRecord(grokRecord()) - expect(isPersistedAgentSessionRecord(stored)).toBe(true) - if (!isPersistedAgentSessionRecord(stored)) { - return - } - expect(decodePersistedAgentSessionRecord(stored).record).toEqual(grokRecord()) - }) - - it.each(['', '1grok', 'gro k', 'grok/', 'g'.repeat(65)])( - 'quarantines a malformed provider id %j even without a handle', - (provider) => { - const record = grokRecord() - expect( - isPersistedAgentSessionRecord({ - ...encodeAgentSessionRecord(record), - provider, - providerHandleChain: [], - lease: { ...record.lease, claimStatus: 'released' } - }) - ).toBe(false) - } - ) - - // Check transport drivability at start (structured-agent-session-drivability.test.ts), so definition changes never hide stored chats. - it("reads a record whose handles are in a transport other than the agent's current one", () => { - expect(isPersistedAgentSessionRecord(encodeAgentSessionRecord(grokRecord('other')))).toBe(true) - }) - it('sets aside a record whose handles name another agent, or mix transports', () => { const record = grokRecord() const [link] = record.providerHandleChain @@ -75,38 +43,4 @@ describe('provider-independent stored records', () => { ).toBe(false) } }) - - it('keeps reading Claude and Codex records exactly as before', () => { - const claude = agentSessionRecordFixture() - const codex = { - ...claude, - provider: 'codex', - accountHome: { variable: 'CODEX_HOME', path: '/home/user/.codex' }, - providerHandleChain: claude.providerHandleChain.map((link) => ({ - ...link, - handle: { transport: 'codex-app-server', agent: 'codex', nativeId: 'codex-thread-1' } - })) - } - const storedClaude = encodeAgentSessionRecord(claude) - const storedCodex = encodeAgentSessionRecord(codex) - expect(isPersistedAgentSessionRecord(storedClaude)).toBe(true) - expect(isPersistedAgentSessionRecord(storedCodex)).toBe(true) - expect(JSON.stringify(storedClaude.providerHandleChain[0]?.handle)).toBe( - '{"provider":"claude","sessionId":"provider-session-alpha-1","leafUuid":null}' - ) - expect(JSON.stringify(storedCodex.providerHandleChain[0]?.handle)).toBe( - '{"provider":"codex","threadId":"codex-thread-1"}' - ) - expect(decodePersistedAgentSessionRecord(storedClaude).record).toEqual(claude) - expect(decodePersistedAgentSessionRecord(storedCodex).record).toEqual(codex) - }) - - it('refuses a Claude record whose handles another agent owns', () => { - const record = agentSessionRecordFixture() - expect( - isPersistedAgentSessionRecord( - encodeAgentSessionRecord({ ...record, provider: 'codex', accountHome: record.accountHome }) - ) - ).toBe(false) - }) }) diff --git a/src/shared/agent-session/lifecycle/agent-session-turn-record.test.ts b/src/shared/agent-session/lifecycle/agent-session-turn-record.test.ts index 9c56644e297..f54c37f3756 100644 --- a/src/shared/agent-session/lifecycle/agent-session-turn-record.test.ts +++ b/src/shared/agent-session/lifecycle/agent-session-turn-record.test.ts @@ -1,13 +1,8 @@ import { describe, expect, it } from 'vitest' -import { - AGENT_SESSION_JOURNAL_SCHEMA_VERSION, - journalRowSchemaVersion, - type AgentJournalTurnLifecycle -} from '../journal/agent-session-journal-types' +import '../journal/agent-session-journal-types' +import type { AgentJournalTurnLifecycle } from '../journal/agent-session-journal-types' import { agentJournalTurnBody, - isRunningAgentJournalTurn, - legacyAgentJournalTurnStatusBody, readAgentJournalTurn, readAgentJournalTurnOutcome } from './agent-session-turn-record' @@ -21,51 +16,7 @@ const turn: AgentJournalTurnLifecycle = { durationMs: 7_172 } -describe('readAgentJournalTurn', () => { - it('reads the typed item and the legacy status form alike', () => { - expect(readAgentJournalTurn(agentJournalTurnBody(turn))).toEqual(turn) - expect(readAgentJournalTurn({ kind: 'status', text: 'x', turnLifecycle: turn })).toEqual(turn) - }) - - it('reads nothing off other bodies', () => { - expect(readAgentJournalTurn({ kind: 'status', text: 'Conversation compacted.' })).toBeNull() - expect( - readAgentJournalTurn({ - kind: 'message', - role: 'user', - blocks: [{ type: 'text', text: 'hi' }] - }) - ).toBeNull() - expect(readAgentJournalTurn(undefined)).toBeNull() - }) - - it('answers the running check for either form', () => { - expect(isRunningAgentJournalTurn(agentJournalTurnBody({ ...turn, state: 'running' }))).toBe( - true - ) - expect(isRunningAgentJournalTurn({ kind: 'status', text: 'x', turnLifecycle: turn })).toBe( - false - ) - }) -}) - describe('agentJournalTurnBody', () => { - it('preserves every lifecycle field', () => { - const complete: Required = { - turnId: 't1', - state: 'completed', - outcome: 'success', - userItemId: 'codex:thread:t1:0', - startedAt: 1_000, - requestedAt: 900, - completedAt: 8_200, - durationMs: 7_172, - contextUsage: { window: { tokens: 100, capturedAt: 8_200 } }, - providerTurnId: 'provider-t1' - } - expect(agentJournalTurnBody(complete)).toEqual({ kind: 'turn', ...complete }) - }) - it('normalizes known fields and preserves extensions without changing the saved lifecycle', () => { const saved = { ...turn, @@ -87,44 +38,9 @@ describe('agentJournalTurnBody', () => { }) expect(saved).toEqual(before) }) - - it.each([0, -1, Number.NaN, Number.POSITIVE_INFINITY])( - 'omits an invalid optional time %s without inventing an end', - (completedAt) => { - expect(agentJournalTurnBody({ turnId: 't', state: 'interrupted', completedAt })).toEqual({ - kind: 'turn', - turnId: 't', - state: 'interrupted' - }) - } - ) - - it('omits invalid fields newly typed on a legacy lifecycle', () => { - const saved = { - ...turn, - providerTurnId: '', - contextUsage: { window: { tokens: -1, capturedAt: 1 } } - } - expect(agentJournalTurnBody(saved)).toEqual({ kind: 'turn', ...turn }) - }) }) describe('readAgentJournalTurnOutcome', () => { - it.each(['success', 'failure', 'cancellation'] as const)( - 'reads a %s verdict off either journal shape', - (outcome) => { - const withOutcome = { ...turn, outcome } - expect( - readAgentJournalTurnOutcome(readAgentJournalTurn(agentJournalTurnBody(withOutcome))) - ).toBe(outcome) - expect( - readAgentJournalTurnOutcome( - readAgentJournalTurn({ kind: 'status', text: 'x', turnLifecycle: withOutcome }) - ) - ).toBe(outcome) - } - ) - it('never reads an old host completed row as success', () => { // Pre-outcome hosts marked API-error turns completed; absent verdict must remain unknown or historical failures become successes. expect(readAgentJournalTurnOutcome(turn)).toBeNull() @@ -135,71 +51,4 @@ describe('readAgentJournalTurnOutcome', () => { ) ).toBeNull() }) - - it('reads a verdict from a later vocabulary as unknown rather than an arm', () => { - // Persisted verdicts are open strings; the reader, not schema typing, checks known arms. - expect(readAgentJournalTurnOutcome({ ...turn, outcome: 'partially-refused' })).toBeNull() - }) - - it('answers unknown for a body that carries no turn at all', () => { - expect(readAgentJournalTurnOutcome(null)).toBeNull() - expect(readAgentJournalTurnOutcome(undefined)).toBeNull() - expect( - readAgentJournalTurnOutcome(readAgentJournalTurn({ kind: 'status', text: 'compacted' })) - ).toBeNull() - }) -}) - -describe('legacyAgentJournalTurnStatusBody', () => { - it('names the agent from the lifecycle identity and never calls an unobserved end completed', () => { - expect(legacyAgentJournalTurnStatusBody(turn, 'legacy:claude:s:turn-lifecycle%3At1')).toEqual({ - kind: 'status', - text: 'Claude turn completed', - turnLifecycle: turn - }) - expect( - legacyAgentJournalTurnStatusBody( - { turnId: 't2', state: 'unverifiable', startedAt: 1 }, - 'legacy:codex:s:turn-lifecycle%3At2' - ).text - ).toBe('Codex turn outcome unverifiable') - }) - - it('names any other agent by its own name, never as Codex', () => { - expect(legacyAgentJournalTurnStatusBody(turn, 'orca:command-turn:t1', 'gemini').text).toBe( - 'Gemini turn completed' - ) - expect(legacyAgentJournalTurnStatusBody(turn, 'legacy:gemini:s:turn-lifecycle%3At1').text).toBe( - 'Gemini turn completed' - ) - // Nothing names the agent: say so rather than guess one. - expect(legacyAgentJournalTurnStatusBody(turn, 'orca:command-turn:t1').text).toBe( - 'Agent turn completed' - ) - expect(legacyAgentJournalTurnStatusBody(turn, 'orca:command-turn:t1', 'codex').text).toBe( - 'Codex turn completed' - ) - }) - - it('carries the verdict to a client that predates the turn item', () => { - // Keep legacy completed copy while carrying the verdict under turnLifecycle, so clients can learn outcomes without a host change. - const failed = { ...turn, outcome: 'failure' as const } - expect(legacyAgentJournalTurnStatusBody(failed, 'legacy:claude:s:turn-lifecycle%3At1')).toEqual( - { - kind: 'status', - text: 'Claude turn completed', - turnLifecycle: failed - } - ) - }) -}) - -describe('journalRowSchemaVersion', () => { - it('stamps only rows that carry a turn item with the current version', () => { - // The newest this build reads is the queue row's. - expect(AGENT_SESSION_JOURNAL_SCHEMA_VERSION).toBe(4) - expect(journalRowSchemaVersion([agentJournalTurnBody(turn)])).toBe(3) - expect(journalRowSchemaVersion([{ kind: 'message' }, { kind: 'status' }])).toBe(2) - expect(journalRowSchemaVersion([])).toBe(2) - }) }) diff --git a/src/shared/agent-session/lifecycle/structured-agent-session-message-projection-stopped-before-start.test.ts b/src/shared/agent-session/lifecycle/structured-agent-session-message-projection-stopped-before-start.test.ts index 45b0ed7c042..8960ce92d91 100644 --- a/src/shared/agent-session/lifecycle/structured-agent-session-message-projection-stopped-before-start.test.ts +++ b/src/shared/agent-session/lifecycle/structured-agent-session-message-projection-stopped-before-start.test.ts @@ -9,14 +9,8 @@ import type { AgentJournalSubmission } from '../journal/agent-session-journal-types' import type { AgentJournalTurnScope } from '../journal/agent-session-journal-turn-scope' -import { - NATIVE_CHAT_STOPPED_BEFORE_START_PRESENTATION, - NATIVE_CHAT_STOPPED_BEFORE_START_TEXT -} from '../../native-chat/native-chat-stopped-before-start' -import { - DISPATCH_REJECTED_CANCELLED, - DISPATCH_REJECTED_WRITE_FAILED -} from '../requests/structured-agent-session-dispatch-rejection' +import '../../native-chat/native-chat-stopped-before-start' +import { DISPATCH_REJECTED_CANCELLED } from '../requests/structured-agent-session-dispatch-rejection' import { projectStructuredAgentSessionMessages } from '../view/structured-agent-session-message-projection' import { projectNativeChatTranscript, @@ -116,37 +110,6 @@ function takenBackOn( } describe('a send a Stop took back before the agent started it', () => { - it('stays where it was sent, with one row after it', () => { - const items = [sent('warm-up', 'warm up'), sent('never-ran', 'look around')] - - expect(rows(items, [submission('warm-up'), stopped('never-ran')])).toEqual([ - user('warm-up'), - user('never-ran'), - stopRow('never-ran') - ]) - const [, , row] = projectStructuredAgentSessionMessages(items, [], [stopped('never-ran')]) - expect(row?.blocks).toEqual([ - { - type: 'text', - text: NATIVE_CHAT_STOPPED_BEFORE_START_TEXT, - presentation: NATIVE_CHAT_STOPPED_BEFORE_START_PRESENTATION - } - ]) - expect(row?.journalPosition).toEqual( - projectStructuredAgentSessionMessages(items, [], [stopped('never-ran')])[1]?.journalPosition - ) - }) - - it('reads the same from the typed fact as from the older marker', () => { - const items = [sent('never-ran', 'look around')] - const typed = stopped('never-ran', { - reason: 'This message was withdrawn before the agent started it.', - rejection: { kind: 'cancelled' } - }) - - expect(rows(items, [typed])).toEqual(rows(items, [stopped('never-ran')])) - }) - it('shares one row with the sends taken back right before it', () => { const items = [sent('first', 'one'), sent('second', 'two')] @@ -157,21 +120,6 @@ describe('a send a Stop took back before the agent started it', () => { ]) }) - it('stays in the turn that opened for it, whose end already says it was stopped', () => { - const items = [ - sent('opened', 'look around'), - entry('turn-1', { - kind: 'turn', - turnId: 'turn-1', - state: 'interrupted', - outcome: 'cancellation', - userItemId: agentJournalSubmissionKey('opened') - }) - ] - - expect(rows(items, [stopped('opened')])).toEqual([user('opened')]) - }) - // Codex reports the turn open before it echoes the send, so the record names the provider's key. it('stays in the turn that opened for it before the provider echoed it', () => { const items = [ @@ -320,38 +268,6 @@ describe('a send a Stop took back before the agent started it', () => { ]) }) - it('is drawn after the turn it waited behind when it was never handed over', () => { - const items = [ - sent('first', 'first prompt'), - entry('t1', { - kind: 'turn', - turnId: 't1', - state: 'interrupted', - outcome: 'cancellation', - userItemId: agentJournalSubmissionKey('first') - }), - said('before', 'working on it', 't1'), - sent('queued', 'queued while t1 ran'), - said('after', 'more t1 output', 't1'), - entry( - 'note', - { kind: 'status', text: 'Cancellation requested.' }, - { kind: 'turn', turnItemId: 't1' } - ) - ] - - expect( - rows(items, [submission('first'), stopped('queued', { handoverRecorded: true })]) - ).toEqual([ - user('first'), - { id: 'before', role: 'assistant' }, - { id: 'after', role: 'assistant' }, - { id: 'note', role: 'system' }, - user('queued'), - stopRow('queued') - ]) - }) - // The first send opens the turn; stopped follow-ups stay below that turn’s rows. it("is drawn after a turn that opened while it waited, with that turn's rows above it", () => { const items = [ @@ -391,37 +307,6 @@ describe('a send a Stop took back before the agent started it', () => { ]) }) - // One Stop takes back two /compact-queued sends: only the first had a handover. - it('keeps two sends it took back in the order they were sent', () => { - const compactTurn = 'compact-turn' - const items = [ - sent('compact', '/compact'), - entry(compactTurn, { - kind: 'turn', - turnId: compactTurn, - state: 'completed', - userItemId: agentJournalSubmissionKey('compact') - }), - sent('queued-second', 'queued second'), - said('compacted', 'Context compacted', compactTurn), - sent('queued-first', 'queued first') - ] - - expect( - rows(items, [ - submission('compact', { submittedAt: 1 }), - stopped('queued-first', { submittedAt: 2, handoverRecorded: true, handedOverAt: 5 }), - stopped('queued-second', { submittedAt: 3, handoverRecorded: true }) - ]) - ).toEqual([ - user('compact'), - { id: 'compacted', role: 'assistant' }, - user('queued-first'), - user('queued-second'), - stopRow('queued-second') - ]) - }) - // The host's clock stepped back between the two accepts; the journal's order still holds. it('keeps two sends in journal order where the host publishes it, whatever the clock says', () => { const compactTurn = 'compact-turn' @@ -589,54 +474,6 @@ describe('a send a Stop took back before the agent started it', () => { } ) - // Without an uncertain send, the earlier exchange’s opener is the old host’s latest ordering evidence. - it.each(HOSTS)( - 'stays below a finished exchange whose opener was the last thing sent before it, on %s', - (_host, published) => { - const compactTurn = 'compact-turn' - const items = [ - sent('compact', '/compact'), - entry(compactTurn, { - kind: 'turn', - turnId: compactTurn, - state: 'completed', - userItemId: agentJournalSubmissionKey('compact') - }), - sent('queued-b', 'queued B'), - said('compacted', 'Context compacted', compactTurn), - sent('queued-zero', 'queued zero'), - entry('turn-2', { - kind: 'turn', - turnId: 'turn-2', - state: 'completed', - userItemId: agentJournalSubmissionKey('queued-zero') - }), - said('answer', 'answer to zero', 'turn-2') - ] - - const host = takenBackOn(published, items, 'queued-b', sequence + 1) - - expect( - rows(host.items, [ - submission('compact', { submittedAt: 1 }), - submission('queued-zero', { submittedAt: 2, handoverRecorded: true, handedOverAt: 5 }), - stopped('queued-b', { - submittedAt: 4, - handoverRecorded: true, - ...host.sent - }) - ]) - ).toEqual([ - user('compact'), - { id: 'compacted', role: 'assistant' }, - user('queued-zero'), - { id: 'answer', role: 'assistant' }, - user('queued-b'), - stopRow('queued-b') - ]) - } - ) - // Tied accepts cannot order the exchanges; only the moved take-back row proves which finished before Stop. it('stays below a finished exchange when accept times tie, by the row the host moved', () => { const compactTurn = 'compact-turn' @@ -676,53 +513,4 @@ describe('a send a Stop took back before the agent started it', () => { stopRow('queued-b') ]) }) - - // The first taken back by one Stop, a later send by another: each stays where it was sent. - it('keeps a send a later Stop took back below one an earlier Stop took', () => { - const compactTurn = 'compact-turn' - const items = [ - sent('compact', '/compact'), - entry(compactTurn, { - kind: 'turn', - turnId: compactTurn, - state: 'completed', - userItemId: agentJournalSubmissionKey('compact') - }), - sent('queued', 'queued during compact'), - said('compacted', 'Context compacted', compactTurn), - sent('later', 'sent after the first Stop') - ] - - expect( - rows(items, [ - submission('compact', { submittedAt: 1 }), - stopped('queued', { submittedAt: 2, handoverRecorded: true }), - stopped('later', { submittedAt: 9, handoverRecorded: true, handedOverAt: 10 }) - ]) - ).toEqual([ - user('compact'), - { id: 'compacted', role: 'assistant' }, - user('queued'), - user('later'), - stopRow('later') - ]) - }) - - it('leaves a queued card to hold its own text', () => { - const items = [sent('card-send', 'from a card')] - - expect(rows(items, [stopped('card-send', { queuedMessageId: 'card-1' })])).toEqual([]) - }) - - it('draws any other refused send as not sent, with no stop row', () => { - const shown = projectStructuredAgentSessionMessages( - [sent('refused', 'hello')], - [], - [submission('refused', { dispatchState: 'rejected', reason: DISPATCH_REJECTED_WRITE_FAILED })] - ) - - expect(shown.map(({ id, unsent }) => ({ id, unsent }))).toEqual([ - { id: agentJournalSubmissionKey('refused'), unsent: true } - ]) - }) }) diff --git a/src/shared/agent-session/lifecycle/structured-agent-session-mutation.spec.ts b/src/shared/agent-session/lifecycle/structured-agent-session-mutation.spec.ts index ea92d8f0fd6..60569ebb4f9 100644 --- a/src/shared/agent-session/lifecycle/structured-agent-session-mutation.spec.ts +++ b/src/shared/agent-session/lifecycle/structured-agent-session-mutation.spec.ts @@ -3,22 +3,6 @@ import { structuredAgentSessionPayloadFingerprint } from './structured-agent-ses import { computeAgentSessionPayloadFingerprint } from './agent-session-mutation-envelope' describe('structured agent session client mutations', () => { - it('canonicalizes payload fields before hashing', () => { - const first = structuredAgentSessionPayloadFingerprint({ - method: 'agentSession.send', - sessionId: 'session-1', - fields: { body: { role: 'user', kind: 'message' }, omitted: undefined } - }) - const second = structuredAgentSessionPayloadFingerprint({ - method: 'agentSession.send', - sessionId: 'session-1', - fields: { body: { kind: 'message', role: 'user' } } - }) - - expect(first).toBe(second) - expect(first).toMatch(/^[a-f0-9]{64}$/) - }) - it('matches host code-unit ordering for mixed-case and non-ASCII keys', () => { const input = { method: 'agentSession.send', diff --git a/src/shared/agent-session/options/agent-session-option-catalog-grok.test.ts b/src/shared/agent-session/options/agent-session-option-catalog-grok.test.ts index 67c46cac9c3..8602e1b7765 100644 --- a/src/shared/agent-session/options/agent-session-option-catalog-grok.test.ts +++ b/src/shared/agent-session/options/agent-session-option-catalog-grok.test.ts @@ -1,112 +1,11 @@ import { describe, expect, it } from 'vitest' import { mergeDiscoveredAuthoritativeModels } from './agent-session-option-catalog' -import type { CatalogModel, CatalogOption } from './agent-session-option-catalog-types' +import type { CatalogModel } from './agent-session-option-catalog-types' import { GROK_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-grok' import { resolveAgentSessionOptionLaunch } from './agent-session-option-launch' -import { parseBuiltSessionOptionCommand } from '../../native-chat/input/native-chat-session-option-commands' - -function grokEffortOption(modelId = 'grok-4.7'): CatalogOption { - const model = GROK_SESSION_OPTION_CATALOG.models.find((candidate) => candidate.id === modelId)! - return model.options.find((option) => option.id === 'effort')! -} - -describe('grok launch args', () => { - it('emits the model flag first, then effort', () => { - expect(resolveAgentSessionOptionLaunch('grok', { model: 'grok-4.5', effort: 'low' })).toEqual({ - args: ['-m', 'grok-4.5', '--reasoning-effort', 'low'], - appliedValues: { model: 'grok-4.5', effort: 'low' } - }) - }) - - it('falls back to the seeded effort default when none is stored', () => { - // `high`, not the menu's ceiling: xhigh is opt-in, never a silent escalation. - expect(resolveAgentSessionOptionLaunch('grok', { model: 'grok-4.6' }).args).toEqual([ - '-m', - 'grok-4.6', - '--reasoning-effort', - 'high' - ]) - expect(resolveAgentSessionOptionLaunch('grok', { model: 'grok-4.5' }).args).toEqual([ - '-m', - 'grok-4.5', - '--reasoning-effort', - 'high' - ]) - }) - - it('emits only -m for a persisted model the seed does not carry', () => { - // A remembered model remains the launch selection even when discovery no longer lists it. - expect(resolveAgentSessionOptionLaunch('grok', { model: 'grok-build' })).toEqual({ - args: ['-m', 'grok-build'], - appliedValues: { model: 'grok-build' } - }) - }) - - it('carries a picked effort onto a discovered model the seed never listed', () => { - // Static-seed resolution once dropped --reasoning-effort for discovered IDs absent from the seed. - expect(resolveAgentSessionOptionLaunch('grok', { model: 'grok-build', effort: 'low' })).toEqual( - { - args: ['-m', 'grok-build', '--reasoning-effort', 'low'], - appliedValues: { model: 'grok-build', effort: 'low' } - } - ) - }) - - it('drops an effort value the menu does not offer on an unseeded model', () => { - expect( - resolveAgentSessionOptionLaunch('grok', { model: 'grok-build', effort: 'none' }) - ).toEqual({ args: ['-m', 'grok-build'], appliedValues: { model: 'grok-build' } }) - }) - - it('honors a user effort flag over the picker on an unseeded model too', () => { - expect( - resolveAgentSessionOptionLaunch('grok', { model: 'grok-build', effort: 'low' }, [ - '--reasoning-effort=high' - ]).appliedValues - ).toEqual({ model: 'grok-build' }) - }) - - it('resolves no picker args when no model was ever picked', () => { - expect(resolveAgentSessionOptionLaunch('grok', undefined)).toEqual({ - args: [], - appliedValues: {} - }) - }) -}) +import '../../native-chat/input/native-chat-session-option-commands' describe('grok agent-arg overrides', () => { - const removeModel = GROK_SESSION_OPTION_CATALOG.modelApply.removeAgentArgs! - const removeEffort = grokEffortOption().apply.removeAgentArgs! - - it('strips a user-supplied model flag in every spelling', () => { - for (const tokens of [ - ['-m', 'grok-build'], - ['-mgrok-build'], - ['--model', 'grok-build'], - ['--model=grok-build'] - ]) { - expect(removeModel(tokens)).toEqual([]) - } - }) - - it('does not fire on a different flag or a positional that contains -m', () => { - for (const tokens of [ - ['--model-context', '8000'], - ['summarize-my-diff'], - ['--reasoning-effort', 'low'], - [] - ]) { - expect(removeModel(tokens)).toEqual(tokens) - } - }) - - it('strips both effort spellings', () => { - expect(removeEffort(['--effort', 'low', '--reasoning-effort=high', '--keep'])).toEqual([ - '--keep' - ]) - expect(removeEffort(['--effortless'])).toEqual(['--effortless']) - }) - it('drops only the overridden key from the launch record and argv', () => { expect( resolveAgentSessionOptionLaunch('grok', { model: 'grok-4.5', effort: 'high' }, [ @@ -127,37 +26,6 @@ describe('grok agent-arg overrides', () => { ]).appliedValues ).toEqual({}) }) - - it('keeps the record intact when a lookalike flag is present', () => { - expect( - resolveAgentSessionOptionLaunch('grok', { model: 'grok-4.5', effort: 'low' }, [ - '--model-context', - '8000' - ]).appliedValues - ).toEqual({ model: 'grok-4.5', effort: 'low' }) - }) -}) - -describe('grok mid-session commands', () => { - it('round-trips the model command through the built-command parser', () => { - const midSession = GROK_SESSION_OPTION_CATALOG.modelApply.midSession! - if (midSession.kind !== 'command') { - throw new Error('grok model changes must be a typed command, not an agent picker') - } - expect(midSession.build('grok-4.5')).toBe('/model grok-4.5') - expect(parseBuiltSessionOptionCommand(midSession.build, '/model grok-4.5')).toBe('grok-4.5') - expect(parseBuiltSessionOptionCommand(midSession.build, '/effort low')).toBeNull() - }) - - it('round-trips the effort command', () => { - const midSession = grokEffortOption().apply.midSession! - if (midSession.kind !== 'command') { - throw new Error('grok effort changes must be a typed command') - } - expect(midSession.build('low')).toBe('/effort low') - expect(parseBuiltSessionOptionCommand(midSession.build, '/effort low')).toBe('low') - expect(parseBuiltSessionOptionCommand(midSession.build, '/effort ')).toBeNull() - }) }) describe('mergeDiscoveredAuthoritativeModels', () => { @@ -169,25 +37,6 @@ describe('mergeDiscoveredAuthoritativeModels', () => { return effort?.kind.type === 'select' ? effort.kind.choices.map((choice) => choice.value) : [] } - it('takes the default flag from the probe and drops the seed’s stale one', () => { - // Follow the account's default so the picker matches an unflagged launch. - const merged = mergeDiscoveredAuthoritativeModels(seed, [ - { id: 'grok-4.5', label: 'Grok 4.5', options: [] }, - { id: 'grok-5', label: 'Grok 5', isDefault: true, options: [] } - ]) - expect(merged.map(({ id, isDefault }) => [id, isDefault])).toEqual([ - ['grok-4.5', undefined], - ['grok-5', true] - ]) - }) - - it('names no default when the probe marks none, rather than reviving the seed’s', () => { - const merged = mergeDiscoveredAuthoritativeModels(seed, discovered('grok-4.5')) - expect(merged[0].isDefault).toBeUndefined() - // The seed's menu still survives — only the default claim is discovery's to make. - expect(merged[0].options.map(({ id }) => id)).toEqual(['effort']) - }) - it('gives a matched seed row its own menu, not the default row’s', () => { const multiSeed: CatalogModel[] = [ { id: 'grok-4.6', label: 'Grok 4.6', isDefault: true, options: seed[0].options }, @@ -205,29 +54,4 @@ describe('mergeDiscoveredAuthoritativeModels', () => { expect(mergedEffortValues(merged[0])).toEqual(['low', 'medium', 'high']) expect(mergedEffortValues(merged[1])).toEqual(['low', 'medium', 'high', 'xhigh']) }) - - it('takes the inherited options from the default seed row, not the first one', () => { - const multiSeed: CatalogModel[] = [ - { id: 'legacy', label: 'Legacy', options: [] }, - { id: 'grok-4.6', label: 'Grok 4.6', isDefault: true, options: seed[0].options } - ] - const merged = mergeDiscoveredAuthoritativeModels(multiSeed, discovered('grok-build')) - expect(merged[0].options.map(({ id }) => id)).toEqual(['effort']) - }) - - it('lends no options when the seed is empty', () => { - expect(mergeDiscoveredAuthoritativeModels([], discovered('grok-build'))).toEqual([ - { id: 'grok-build', label: 'grok-build', options: [] } - ]) - }) - - it('preserves discovery order rather than seed order', () => { - const merged = mergeDiscoveredAuthoritativeModels(seed, discovered('grok-build', 'grok-4.5')) - expect(merged.map(({ id }) => id)).toEqual(['grok-build', 'grok-4.5']) - }) - - it('publishes an empty list for an empty discovery, so callers must gate on it', () => { - // Only enrichment's non-empty guard keeps a failed probe from blanking the picker. - expect(mergeDiscoveredAuthoritativeModels(seed, [])).toEqual([]) - }) }) diff --git a/src/shared/agent-session/options/agent-session-option-catalog-omp.test.ts b/src/shared/agent-session/options/agent-session-option-catalog-omp.test.ts index 6b682a0ecda..1cf96e96785 100644 --- a/src/shared/agent-session/options/agent-session-option-catalog-omp.test.ts +++ b/src/shared/agent-session/options/agent-session-option-catalog-omp.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { OMP_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-omp' -import { resolveAgentSessionOptionLaunch } from './agent-session-option-launch' -import { parseBuiltSessionOptionCommand } from '../../native-chat/input/native-chat-session-option-commands' +import './agent-session-option-catalog-omp' +import './agent-session-option-launch' +import '../../native-chat/input/native-chat-session-option-commands' import { parseOmpModelList } from '../../omp-model-list-probe' const LISTING = JSON.stringify({ @@ -38,33 +38,6 @@ describe('omp model list probe', () => { ]) }) - it('keeps the context window each row states', () => { - // Row shape as `omp models --json` prints it (OMP 17.0.5). - const listing = JSON.stringify({ - models: [ - { - provider: 'openai-codex', - id: 'gpt-5.5', - selector: 'openai-codex/gpt-5.5', - name: 'GPT-5.5', - contextWindow: 272000, - maxTokens: 128000 - }, - { provider: 'openai-codex', id: 'gpt-5.4', name: 'GPT-5.4', contextWindow: 1000000 }, - { provider: 'zai', id: 'glm', name: 'GLM', contextWindow: 0 }, - { provider: 'zai', id: 'glm-air', name: 'GLM Air', contextWindow: '128000' } - ] - }) - expect( - parseOmpModelList(listing).map(({ id, contextWindowTokens }) => ({ id, contextWindowTokens })) - ).toEqual([ - { id: 'openai-codex/gpt-5.5', contextWindowTokens: 272000 }, - { id: 'openai-codex/gpt-5.4', contextWindowTokens: 1000000 }, - { id: 'zai/glm', contextWindowTokens: undefined }, - { id: 'zai/glm-air', contextWindowTokens: undefined } - ]) - }) - it('tolerates an update notice printed ahead of the JSON', () => { const noisy = `Package updates are available. Run omp update\n${LISTING}\n` expect(parseOmpModelList(noisy).map(({ id }) => id)).toEqual([ @@ -94,46 +67,4 @@ describe('omp model list probe', () => { expect(parseOmpModelList(value)).toEqual([]) } }) - - it('returns nothing for output that is not a listing', () => { - expect(parseOmpModelList('')).toEqual([]) - expect(parseOmpModelList('omp: command not found')).toEqual([]) - expect(parseOmpModelList('{"models": "nope"}')).toEqual([]) - expect(parseOmpModelList('[1, 2]')).toEqual([]) - }) -}) - -describe('omp session option catalog', () => { - it('launches a picked model with --model ', () => { - expect(resolveAgentSessionOptionLaunch('omp', { model: 'deepseek/deepseek-v4-pro' })).toEqual({ - args: ['--model', 'deepseek/deepseek-v4-pro'], - appliedValues: { model: 'deepseek/deepseek-v4-pro' } - }) - expect(resolveAgentSessionOptionLaunch('omp', {})).toEqual({ args: [], appliedValues: {} }) - }) - - it('yields to a user --model in the launch args, in either spelling', () => { - const remove = OMP_SESSION_OPTION_CATALOG.modelApply.removeAgentArgs! - expect(remove(['--model', 'opus', '--model=openai/gpt-5.5', '--no-extensions'])).toEqual([ - '--no-extensions' - ]) - // `--models` scopes Ctrl+P cycling; it does not pick a model. omp has no `-m`. - expect(remove(['--models=anthropic/*', '-m', 'opus'])).toEqual([ - '--models=anthropic/*', - '-m', - 'opus' - ]) - }) - - it('builds and parses /orca-model with the exact selector', () => { - const midSession = OMP_SESSION_OPTION_CATALOG.modelApply.midSession - expect(midSession).toMatchObject({ kind: 'command' }) - if (midSession?.kind !== 'command') { - throw new Error('expected a command apply') - } - const command = midSession.build('minimax-cn/MiniMax-M3') - expect(command).toBe('/orca-model minimax-cn/MiniMax-M3') - expect(parseBuiltSessionOptionCommand(midSession.build, command)).toBe('minimax-cn/MiniMax-M3') - expect(parseBuiltSessionOptionCommand(midSession.build, '/orca-model ')).toBeNull() - }) }) diff --git a/src/shared/agent-session/options/agent-session-option-catalog-opencode.test.ts b/src/shared/agent-session/options/agent-session-option-catalog-opencode.test.ts index a39cb38a6da..348cf51af53 100644 --- a/src/shared/agent-session/options/agent-session-option-catalog-opencode.test.ts +++ b/src/shared/agent-session/options/agent-session-option-catalog-opencode.test.ts @@ -5,28 +5,6 @@ import { } from './agent-session-option-launch' describe('OpenCode launch options', () => { - it('maps an opaque model id to the OpenCode --model flag', () => { - expect( - resolveAgentSessionOptionLaunch('opencode', { - model: 'zai-coding-plan/glm-5.3-flash' - }) - ).toEqual({ - args: ['--model', 'zai-coding-plan/glm-5.3-flash'], - appliedValues: { model: 'zai-coding-plan/glm-5.3-flash' } - }) - }) - - it('removes a configured model flag before applying a per-launch model', () => { - expect( - removeOverriddenAgentSessionArgs('opencode', { model: 'zai-coding-plan/glm-5.3-flash' }, [ - '--model', - 'opencode/global-default', - '--log-level', - 'DEBUG' - ]) - ).toEqual(['--log-level', 'DEBUG']) - }) - it('keeps arguments after the terminator while removing every earlier model choice', () => { expect( removeOverriddenAgentSessionArgs('opencode', { model: 'private-proof/model-b' }, [ @@ -41,15 +19,6 @@ describe('OpenCode launch options', () => { ).toEqual(['--log-level', 'DEBUG', '--', '--model', 'trailing']) }) - it('does not send or record the selected model when free-form arguments override it', () => { - expect( - resolveAgentSessionOptionLaunch('opencode', { model: 'private-proof/model-b' }, [ - '-m', - 'private-proof/model-a' - ]) - ).toEqual({ args: [], appliedValues: {} }) - }) - it('records the selected model when model-like arguments follow the terminator', () => { expect( resolveAgentSessionOptionLaunch('opencode', { model: 'private-proof/model-b' }, [ diff --git a/src/shared/agent-session/options/agent-session-option-catalog-removers.test.ts b/src/shared/agent-session/options/agent-session-option-catalog-removers.test.ts index a9e880ce855..b6b9644cd68 100644 --- a/src/shared/agent-session/options/agent-session-option-catalog-removers.test.ts +++ b/src/shared/agent-session/options/agent-session-option-catalog-removers.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest' import { getAgentSessionOptionCatalog } from './agent-session-option-catalog' -import { agentArgTerminatorIndex, removeAgentArgOption } from './agent-session-option-agent-args' -import { - removeOverriddenAgentSessionArgs, - resolveAgentSessionOptionLaunch -} from './agent-session-option-launch' +import { agentArgTerminatorIndex } from './agent-session-option-agent-args' +import { resolveAgentSessionOptionLaunch } from './agent-session-option-launch' const removeCodexModel = getAgentSessionOptionCatalog('codex')!.modelApply.removeAgentArgs! @@ -31,36 +28,9 @@ describe('catalog removers', () => { ] expect(removeCodexModel(tokens)).toEqual(tokens) }) - - it('leaves everything after a terminator', () => { - const tokens = ['--', '-c', 'model=o3'] - expect(removeCodexModel(tokens)).toEqual(tokens) - }) - - it('lets a chat pick replace a configured gemini model on server launches', () => { - expect( - removeOverriddenAgentSessionArgs('gemini', { model: 'gemini-2.5-pro' }, [ - '-m', - 'a', - '--model=b', - '-mc', - '--yolo' - ]) - ).toEqual(['--yolo']) - }) }) describe('removeAgentArgOption', () => { - it('consumes an empty value', () => { - expect(removeAgentArgOption('claude', ['--model', '', '--yolo'], ['--model'])).toEqual([ - '--yolo' - ]) - }) - - it('keeps a following flag', () => { - expect(removeAgentArgOption('claude', ['--model', '--yolo'], ['--model'])).toEqual(['--yolo']) - }) - it('requires an exact terminator outside option values', () => { expect( agentArgTerminatorIndex('claude', [ diff --git a/src/shared/agent-session/options/agent-session-option-catalog.test.ts b/src/shared/agent-session/options/agent-session-option-catalog.test.ts index 935c25e2abe..16c810f5910 100644 --- a/src/shared/agent-session/options/agent-session-option-catalog.test.ts +++ b/src/shared/agent-session/options/agent-session-option-catalog.test.ts @@ -1,29 +1,12 @@ import { describe, expect, it } from 'vitest' -import { getAgentSessionOptionCatalog, mergeCatalogModels } from './agent-session-option-catalog' -import { resolveAgentSessionOptionLaunch } from './agent-session-option-launch' +import { getAgentSessionOptionCatalog } from './agent-session-option-catalog' +import './agent-session-option-launch' import { resolveNativeChatSessionOptionDefaults, updateNativeChatSessionOptionDefaults } from '../../native-chat/input/native-chat-session-option-defaults' describe('agent session option catalog', () => { - it('merges discovered labels while preserving cataloged option shapes', () => { - const seed = getAgentSessionOptionCatalog('cursor')!.models - const merged = mergeCatalogModels(seed, [ - { id: 'gpt-5.3-codex', label: 'GPT 5.3 (live)', options: [] }, - { id: 'new-account-model', label: 'new-account-model', options: [] } - ]) - expect(merged.find((model) => model.id === 'gpt-5.3-codex')).toMatchObject({ - label: 'GPT 5.3 (live)', - options: expect.arrayContaining([expect.objectContaining({ id: 'effort' })]) - }) - expect(merged.at(-1)).toEqual({ - id: 'new-account-model', - label: 'new-account-model', - options: [] - }) - }) - it('parses Claude list_models discovery into catalog models with options', () => { const stdout = JSON.stringify({ type: 'control_response', @@ -77,64 +60,6 @@ describe('agent session option catalog', () => { expect(parsed[2].options).toEqual([]) }) - it('returns no discovered models for unsupported or malformed Claude output', () => { - const parse = getAgentSessionOptionCatalog('claude')!.listModels!.parse - const unsupported = - '{"type":"control_response","response":{"subtype":"error","request_id":"x","error":"Unsupported control request subtype: list_models"}}' - expect(parse(unsupported)).toEqual([]) - expect(parse('')).toEqual([]) - expect(parse('garbage')).toEqual([]) - }) - - it('merges discovered Claude variants after the seed and overlays matched labels', () => { - const catalog = getAgentSessionOptionCatalog('claude')! - const merged = mergeCatalogModels(catalog.models, [ - { id: 'opus[1m]', label: 'Opus (1M context)', options: [] }, - { id: 'sonnet', label: 'Sonnet', description: 'Sonnet 5 · Efficient', options: [] } - ]) - expect(merged.map(({ id }) => id)).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]']) - const sonnet = merged.find((model) => model.id === 'sonnet')! - expect(sonnet.description).toBe('Sonnet 5 · Efficient') - expect(sonnet.isDefault).toBe(true) - expect(sonnet.options.map(({ id }) => id)).toEqual(['effort']) - }) - - it('parses Cursor model discovery without treating headings as models', () => { - const parsed = getAgentSessionOptionCatalog('cursor')!.listModels!.parse( - 'Available models:\n- auto (default)\n- gpt-5.3-codex\nmodels\n' - ) - expect(parsed.map(({ id }) => id)).toEqual(['auto', 'gpt-5.3-codex']) - }) - - it('composes Cursor effort and fast mode into the supported slug form', () => { - const resolved = resolveAgentSessionOptionLaunch('cursor', { - model: 'gpt-5.3-codex', - effort: 'high', - fastMode: true - }) - expect(resolved.args).toEqual(['--model', 'gpt-5.3-codex-high-fast']) - expect(resolved.appliedValues).toEqual({ - model: 'gpt-5.3-codex', - effort: 'high', - fastMode: true - }) - }) - - it('passes unknown models through and applies effort only to cataloged models', () => { - expect( - resolveAgentSessionOptionLaunch('claude', { - model: 'claude-future', - effort: 'future-effort' - }) - ).toEqual({ args: ['--model', 'claude-future'], appliedValues: { model: 'claude-future' } }) - expect( - resolveAgentSessionOptionLaunch('claude', { model: 'opus', effort: 'future-effort' }) - ).toMatchObject({ - args: ['--model', 'opus', '--effort', 'future-effort'], - appliedValues: { model: 'opus', effort: 'future-effort' } - }) - }) - it('resolves only stored values without leaking values across models', () => { let persisted = updateNativeChatSessionOptionDefaults({ persisted: undefined, @@ -163,51 +88,4 @@ describe('agent session option catalog', () => { }) expect(persisted.claude?.valuesByModel?.opus).toEqual({ effort: 'xhigh' }) }) - - it('spawns vanilla when the user has not explicitly selected a model', () => { - // Fresh launches must preserve CLI defaults instead of forcing catalog model/effort (#9085). - expect(resolveNativeChatSessionOptionDefaults(undefined, 'claude')).toBeUndefined() - expect(resolveNativeChatSessionOptionDefaults({}, 'claude')).toBeUndefined() - expect(resolveNativeChatSessionOptionDefaults({}, 'future-agent')).toBeUndefined() - }) - - it('resolves an explicitly selected model and only its stored options', () => { - let persisted = updateNativeChatSessionOptionDefaults({ - persisted: undefined, - agent: 'claude', - modelId: 'opus', - optionId: 'model', - value: 'opus' - }) - expect(resolveNativeChatSessionOptionDefaults(persisted, 'claude')).toEqual({ - model: 'opus' - }) - persisted = updateNativeChatSessionOptionDefaults({ - persisted, - agent: 'claude', - modelId: 'opus', - optionId: 'effort', - value: 'xhigh' - }) - expect(resolveNativeChatSessionOptionDefaults(persisted, 'claude')).toEqual({ - model: 'opus', - effort: 'xhigh' - }) - }) - - it('keeps catalog option defaults after the user explicitly selects a model', () => { - const persisted = updateNativeChatSessionOptionDefaults({ - persisted: undefined, - agent: 'claude', - modelId: 'sonnet', - optionId: 'model', - value: 'sonnet' - }) - const defaults = resolveNativeChatSessionOptionDefaults(persisted, 'claude') - - expect(resolveAgentSessionOptionLaunch('claude', defaults)).toEqual({ - args: ['--model', 'sonnet', '--effort', 'high'], - appliedValues: { model: 'sonnet', effort: 'high' } - }) - }) }) diff --git a/src/shared/agent-session/options/structured-agent-session-model-catalog-application.test.ts b/src/shared/agent-session/options/structured-agent-session-model-catalog-application.test.ts index 61298b3c897..ecbe413c610 100644 --- a/src/shared/agent-session/options/structured-agent-session-model-catalog-application.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-model-catalog-application.test.ts @@ -63,21 +63,6 @@ describe('structured option state from the host model catalog', () => { } ) - it('upgrades the seed with host models as a provisional, uncommitted default', () => { - const state = applyStructuredAgentSessionModelCatalog( - createStructuredAgentSessionOptionState('codex', SEED), - SEED, - HOST_CATALOG, - LAUNCH - ) - expect(state.catalogSource).toBe('host') - const snapshot = structuredAgentSessionOptionSnapshot(state) - const model = snapshot.find((descriptor) => descriptor.id === 'model')! - expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBe('gpt-hosted') - // Provisional provenance: a listing default, never a reported value. - expect(model.valueSource).toBe('default') - }) - it('lists host models but names no value for a session it did not launch', () => { // A reopened session may run a model picked in it, not the listing's default. const state = applyStructuredAgentSessionModelCatalog( @@ -112,36 +97,6 @@ describe('structured option state from the host model catalog', () => { expect(currentModel(false)).toBeUndefined() }) - it('names the default effort the listing states, and none it does not', () => { - const effortOf = (catalog: AgentSessionModelCatalogResult) => { - const state = applyStructuredAgentSessionModelCatalog( - createStructuredAgentSessionOptionState('codex', SEED), - SEED, - catalog, - LAUNCH - ) - return structuredAgentSessionOptionSnapshot(state).find((d) => d.id === 'effort')! - } - const stated = effortOf(HOST_CATALOG) - expect(stated.kind.type === 'select' ? stated.kind.currentValue : null).toBe('high') - expect(stated.valueSource).toBe('default') - const { defaultEffort: _stated, ...unstated } = HOST_CATALOG.models[0]! - const silent = effortOf({ ...HOST_CATALOG, models: [unstated] }) - expect(silent.kind.type === 'select' ? silent.kind.currentValue : null).toBeUndefined() - expect(silent.valueSource).toBe('unknown') - }) - - it('settles on the built-in list on an unknown host answer, and keeps a host list', () => { - const seeded = createStructuredAgentSessionOptionState('codex', SEED) - for (const answer of [{ origin: 'unknown' as const }]) { - const settled = applyStructuredAgentSessionModelCatalog(seeded, SEED, answer, LAUNCH) - expect(settled.catalogSource).toBe('builtin') - expect(settled.catalog).toBe(seeded.catalog) - const hosted = applyStructuredAgentSessionModelCatalog(seeded, SEED, HOST_CATALOG, LAUNCH) - expect(applyStructuredAgentSessionModelCatalog(hosted, SEED, answer, LAUNCH)).toBe(hosted) - } - }) - it('keeps known empty inventory distinct from an unavailable observation', () => { const seeded = createStructuredAgentSessionOptionState('codex', SEED) const hosted = applyStructuredAgentSessionModelCatalog(seeded, SEED, HOST_CATALOG, LAUNCH) diff --git a/src/shared/agent-session/options/structured-agent-session-option-codec.spec.ts b/src/shared/agent-session/options/structured-agent-session-option-codec.spec.ts deleted file mode 100644 index beb35cb8b72..00000000000 --- a/src/shared/agent-session/options/structured-agent-session-option-codec.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - decodeStructuredAgentSessionOptionValue, - encodeStructuredAgentSessionOptionValue -} from './structured-agent-session-option-codec' - -describe('structured agent session option codec', () => { - it('encodes and decodes explicit Fast mode booleans', () => { - expect(encodeStructuredAgentSessionOptionValue('fastMode', true)).toBe('true') - expect(encodeStructuredAgentSessionOptionValue('fastMode', false)).toBe('false') - expect(decodeStructuredAgentSessionOptionValue('fastMode', 'true')).toBe(true) - expect(decodeStructuredAgentSessionOptionValue('fastMode', 'false')).toBe(false) - }) - - it('rejects string booleans on the UI side and non-canonical strings on the wire side', () => { - for (const value of ['true', 'false']) { - expect(encodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull() - } - for (const value of ['TRUE', '1', 'False', ' true', 'false ', '', '0']) { - expect(decodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull() - } - }) - - it('preserves string options in both directions and rejects boolean values', () => { - for (const [id, value] of [ - ['model', 'gpt-example'], - ['effort', 'high'], - ['serviceTier', 'flex'] - ] as const) { - expect(encodeStructuredAgentSessionOptionValue(id, value)).toBe(value) - expect(decodeStructuredAgentSessionOptionValue(id, value)).toBe(value) - expect(encodeStructuredAgentSessionOptionValue(id, true)).toBeNull() - expect(encodeStructuredAgentSessionOptionValue(id, false)).toBeNull() - } - }) -}) diff --git a/src/shared/agent-session/options/structured-agent-session-option-pick-book.test.ts b/src/shared/agent-session/options/structured-agent-session-option-pick-book.test.ts deleted file mode 100644 index 7d46809677d..00000000000 --- a/src/shared/agent-session/options/structured-agent-session-option-pick-book.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - beginStructuredOptionPick, - createStructuredOptionPickBook, - settleStructuredOptionPick, - structuredOptionPickCommit -} from './structured-agent-session-option-pick-book' - -describe('structured option pick commits', () => { - it('commits only its own option when another option was picked since', () => { - const book = createStructuredOptionPickBook() - const model = beginStructuredOptionPick(book, 'model', 'opus', false) - beginStructuredOptionPick(book, 'effort', 'low', false) - expect(structuredOptionPickCommit(book, model, { model: 'opus', effort: 'high' })).toEqual({ - model: 'opus' - }) - }) - - it('commits nothing once the same option was picked again', () => { - const book = createStructuredOptionPickBook() - const first = beginStructuredOptionPick(book, 'model', 'opus', false) - beginStructuredOptionPick(book, 'model', 'sonnet', false) - expect(structuredOptionPickCommit(book, first, undefined)).toBeNull() - settleStructuredOptionPick(book, first) - expect(book.pending).toEqual({ model: 'sonnet' }) - }) -}) diff --git a/src/shared/agent-session/options/structured-agent-session-option-picks.test.ts b/src/shared/agent-session/options/structured-agent-session-option-picks.test.ts index 7b37d92e197..1b0bbebd6a6 100644 --- a/src/shared/agent-session/options/structured-agent-session-option-picks.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-option-picks.test.ts @@ -108,14 +108,6 @@ describe('structuredAgentSessionOptionPicks', () => { }) }) - it('writes nothing before the provider catalog lands', () => { - expect( - structuredAgentSessionOptionPicks(createStructuredAgentSessionOptionState('codex'), { - effort: 'high' - }) - ).toEqual([]) - }) - it('remembers explicit Fast on and off as booleans and re-encodes the launch seed', () => { const state = liveState({ model: 'account-model', fastMode: true }) const picks = structuredAgentSessionOptionPicks(state, { fastMode: 'false' }) @@ -126,21 +118,3 @@ describe('structuredAgentSessionOptionPicks', () => { }) }) }) - -describe('applyNativeChatSessionOptionPicks', () => { - it('leaves every other agent untouched', () => { - const persisted = applyNativeChatSessionOptionPicks({ - persisted: { claude: { model: 'opus', valuesByModel: { opus: { effort: 'high' } } } }, - agent: 'codex', - picks: [{ modelId: 'gpt-fast', optionId: 'effort', value: 'low' }] - }) - expect(resolveStructuredLaunchSeedOptions(persisted, 'claude')).toEqual({ - model: 'opus', - effort: 'high' - }) - expect(resolveStructuredLaunchSeedOptions(persisted, 'codex')).toEqual({ - model: 'gpt-fast', - effort: 'low' - }) - }) -}) diff --git a/src/shared/agent-session/options/structured-agent-session-option-view.test.ts b/src/shared/agent-session/options/structured-agent-session-option-view.test.ts index 3e1756cecb8..3910ce862e3 100644 --- a/src/shared/agent-session/options/structured-agent-session-option-view.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-option-view.test.ts @@ -90,16 +90,6 @@ describe('a Claude model its CLI no longer lists', () => { } }) - it('shows the literal seed when a built-in id has no host-listed label', () => { - const seed = { model: 'sonnet' } - const view = structuredAgentSessionOptionView(withHostList(), seed, {}) - const model = structuredAgentSessionOptionSnapshot(view).find((entry) => entry.id === 'model') - expect(model?.kind).toMatchObject({ - currentValue: 'sonnet', - choices: expect.arrayContaining([{ value: 'sonnet', label: 'Sonnet', description: 'sonnet' }]) - }) - }) - it('shows an unlisted seed literally and carries a current effort without an offered menu', () => { const view = structuredAgentSessionOptionView(withHostList(), remembered, {}) expect(carried(withHostList(), remembered)).toEqual(remembered) @@ -275,12 +265,6 @@ describe('what a send carries', () => { } ) - it('carries the default it shows when nothing names a model', () => { - const named = { ...SONNET, isDefault: true } - const listing = { ...LISTED, models: [OPUS, named], listingNamesConfiguredModel: true } - expect(carried(withHostList(fresh(), listing))).toEqual({ model: 'sonnet' }) - }) - it('carries held picks without committing them to the chat', () => { const listed = withHostList(fresh(), { ...LISTED, models: [OPUS, SONNET] }) const originalRecord = structuredClone(listed.record) @@ -299,16 +283,6 @@ describe('what a send carries', () => { model: 'claude-retired' }) }) - - it('carries nothing before a model shows, or for another agent', () => { - expect(carried(fresh())).toBeUndefined() - const codex = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex', CLAUDE_SESSION_OPTION_CATALOG), - CLAUDE_SESSION_OPTION_CATALOG, - { models: [OPUS], current: { model: 'opus[1m]' } } - ) - expect(carried(codex)).toBeUndefined() - }) }) describe("a chat's model and effort, apart from the defaults filling them in", () => { diff --git a/src/shared/agent-session/options/structured-agent-session-options-read-owner.test.ts b/src/shared/agent-session/options/structured-agent-session-options-read-owner.test.ts index b69ce2f6914..c5ab28edeca 100644 --- a/src/shared/agent-session/options/structured-agent-session-options-read-owner.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-options-read-owner.test.ts @@ -291,30 +291,6 @@ it('clears account-switch evidence only on a current owner read failure and reta close() }) -it('retries a failed options read for a restarted process without an unrelated event', async () => { - vi.useFakeTimers() - const { reader, context, reads } = fixture() - let close = reader.attach(context) - try { - await tick() - reads[0].resolve(answer) - await tick() - close() - close = reader.attach({ ...context, reset: [2], fence: 2 }) - await tick() - reads[1].reject(new Error('host timeout')) - await tick() - await vi.advanceTimersByTimeAsync(1_000) - expect(reads).toHaveLength(3) - reads[2].resolve(answer) - await tick() - expect(reader.getSnapshot().support?.fence).toBe(2) - } finally { - close() - vi.useRealTimers() - } -}) - it('bounds failed reads, wakes on invalidation, and cancels a detached lease retry', async () => { vi.useFakeTimers() const { reader, context, reads, controller } = fixture() diff --git a/src/shared/agent-session/options/structured-agent-session-options.test.ts b/src/shared/agent-session/options/structured-agent-session-options.test.ts index 825f865232d..7f1752cc0b1 100644 --- a/src/shared/agent-session/options/structured-agent-session-options.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-options.test.ts @@ -5,8 +5,8 @@ import { CODEX_SESSION_OPTION_CATALOG, CLAUDE_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-claude-codex' -import { buildNativeChatSessionOptionSnapshot } from '../../native-chat/input/native-chat-session-option-snapshot' -import { createNativeChatSessionOptionRecord } from '../../native-chat/input/native-chat-session-option-state' +import '../../native-chat/input/native-chat-session-option-snapshot' +import '../../native-chat/input/native-chat-session-option-state' import { applyStructuredAgentSessionModelCatalog, applyStructuredAgentSessionOptions, @@ -15,10 +15,7 @@ import { settleStructuredAgentSessionBuiltinCatalog, type StructuredAgentSessionOptionState } from './structured-agent-session-options' -import { - canSetStructuredAgentSessionOption, - structuredAgentSessionOptionSnapshot -} from './structured-agent-session-option-snapshot' +import { structuredAgentSessionOptionSnapshot } from './structured-agent-session-option-snapshot' import { structuredAgentSessionOptionView, structuredAgentSessionMessageSelection @@ -124,81 +121,6 @@ describe('structured agent session options', () => { }) }) - it('projects native Codex selects while bridge Codex keeps its agent picker', () => { - const state = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex'), - CODEX_SESSION_OPTION_CATALOG, - { - models: [ - { - id: 'account-model', - label: 'Account Model', - isDefault: true, - defaultEffort: 'medium', - efforts: [ - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' } - ] - } - ], - current: { model: 'account-model', effort: 'medium' } - } - ) - - const structured = structuredAgentSessionOptionSnapshot(state) - expect(structured.map((descriptor) => descriptor.id)).toEqual(['model', 'effort']) - expect(structured[0]).toMatchObject({ - settable: true, - kind: { type: 'select', currentValue: 'account-model' } - }) - expect(structured[0]).not.toHaveProperty('action') - expect(structured[1]).toMatchObject({ - settable: true, - kind: { type: 'select', currentValue: 'medium' } - }) - - const bridgeRecord = createNativeChatSessionOptionRecord('codex') - bridgeRecord.model = { value: 'gpt-5.6-sol', source: 'reported' } - const bridge = buildNativeChatSessionOptionSnapshot({ - catalog: CODEX_SESSION_OPTION_CATALOG, - models: CODEX_SESSION_OPTION_CATALOG.models, - record: bridgeRecord, - mode: 'live', - modelLabel: 'Model', - liveTransport: 'catalog' - }) - // Same catalog, same `dispatched` vocabulary — only the transport separates them. - expect(structured.every((descriptor) => descriptor.transport === 'agent-session')).toBe(true) - expect(bridge.every((descriptor) => descriptor.transport === 'catalog')).toBe(true) - expect(bridge[0]).toMatchObject({ action: { type: 'agent-picker' } }) - expect(bridge.find((descriptor) => descriptor.id === 'effort')).toMatchObject({ - action: { type: 'agent-picker' } - }) - }) - - it('uses provider-scoped models and retains the current unknown id', () => { - const state = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex'), - CODEX_SESSION_OPTION_CATALOG, - { - models: [ - { - id: 'account-model', - label: 'Account Model', - isDefault: false, - efforts: [] - } - ], - current: { model: 'persisted-unknown' } - } - ) - const model = structuredAgentSessionOptionSnapshot(state)[0] - expect( - model.kind.type === 'select' ? model.kind.choices.map((choice) => choice.value) : [] - ).toEqual(['account-model', 'persisted-unknown']) - expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBe('persisted-unknown') - }) - it('projects Fast mode only from positive session and model capability', () => { const supported = applyStructuredAgentSessionOptions( createStructuredAgentSessionOptionState('codex'), @@ -242,77 +164,6 @@ describe('structured agent session options', () => { expect(absent.record.values.fastMode).toBeUndefined() }) - it('offers one speed choice in place of the Fast toggle where the model lists speeds', () => { - const state = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex'), - CODEX_SESSION_OPTION_CATALOG, - { - models: [ - { - id: 'gpt-6.1-sol', - label: 'GPT-6.1 Sol', - isDefault: true, - efforts: [], - supportsFastMode: true, - serviceTiers: [ - { value: 'priority', label: 'Fast' }, - { value: 'ultrafast', label: 'Ultrafast' } - ] - } - ], - fastModeSupport: { supported: true }, - current: { model: 'gpt-6.1-sol', serviceTier: 'ultrafast', confirmed: ['serviceTier'] } - } - ) - const snapshot = structuredAgentSessionOptionSnapshot(state) - expect(snapshot.map(({ id }) => id)).toEqual(['model', 'serviceTier']) - expect(snapshot).toContainEqual( - expect.objectContaining({ - id: 'serviceTier', - kind: { - type: 'select', - currentValue: 'ultrafast', - choices: [ - { value: 'default', label: 'Standard' }, - { value: 'priority', label: 'Fast' }, - { value: 'ultrafast', label: 'Ultrafast' } - ] - }, - valueSource: 'reported', - settable: true - }) - ) - }) - - it('renders Fast off but marked unreported when support is known and no value is', () => { - const state = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex'), - CODEX_SESSION_OPTION_CATALOG, - { - models: [ - { - id: 'account-model', - label: 'Account Model', - isDefault: true, - efforts: [], - supportsFastMode: true - } - ], - fastModeSupport: { supported: true }, - current: { model: 'account-model' } - } - ) - - // A binary switch resolves false, but unknown provenance prevents claiming it is the running default in this live-only lane. - expect(structuredAgentSessionOptionSnapshot(state)).toContainEqual( - expect.objectContaining({ - id: 'fastMode', - kind: { type: 'boolean', currentValue: false }, - valueSource: 'unknown' - }) - ) - }) - it('shows the launch seed until the record names a model, and held picks over both', () => { const cold = createStructuredAgentSessionOptionState('codex', CODEX_SESSION_OPTION_CATALOG) const seeded = applyStructuredAgentSessionModelCatalog( @@ -364,50 +215,6 @@ describe('structured agent session options', () => { expect(viewModel(live, seed, { model: 'gpt-5.5' })).toBe('gpt-5.5') expect(live.record.values.model?.value).toBe('gpt-5.6-luna') }) - - it('keeps a saved Fast with no model known, Standard turning it off', () => { - const sol = { - id: 'gpt-6.1-sol', - label: 'GPT-6.1 Sol', - isDefault: false, - efforts: [], - serviceTiers: [{ value: 'priority', label: 'Fast' }] - } - // A resumed chat's at-rest answer, whose listing names no default. - const fast = applyStructuredAgentSessionOptions( - createStructuredAgentSessionOptionState('codex'), - CODEX_SESSION_OPTION_CATALOG, - { - models: [sol], - fastModeSupport: { supported: true }, - current: { serviceTier: 'priority', defaulted: [] } - } - ) - expect( - structuredAgentSessionOptionSnapshot(fast).find(({ id }) => id === 'serviceTier') - ).toMatchObject({ settable: true, kind: { currentValue: 'priority' } }) - const standard = commitStructuredAgentSessionOption(fast, 'serviceTier', 'default') - // Inventory does not authorize a literal pick. - expect(canSetStructuredAgentSessionOption(standard, 'serviceTier', 'priority')).toBe(true) - }) -}) - -it('separates typed literal readiness from an empty or refreshing catalog', () => { - const state = createStructuredAgentSessionOptionState('codex') - const snapshot = structuredAgentSessionOptionSnapshot(state) - expect(canSetStructuredAgentSessionOption(state, 'model', 'unlisted', snapshot, false)).toBe( - false - ) - expect(canSetStructuredAgentSessionOption(state, 'model', 'unlisted', snapshot, true)).toBe(true) - expect( - canSetStructuredAgentSessionOption( - { ...state, listingInProgress: true }, - 'effort', - 'high', - snapshot, - true - ) - ).toBe(true) }) it.each([false, true])( diff --git a/src/shared/agent-session/options/structured-agent-session-permission-option.test.ts b/src/shared/agent-session/options/structured-agent-session-permission-option.test.ts index 83d78154ef8..e4732f69f8d 100644 --- a/src/shared/agent-session/options/structured-agent-session-permission-option.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-permission-option.test.ts @@ -1,16 +1,15 @@ import { describe, expect, it } from 'vitest' import type { AgentSessionOptionsResult } from './agent-session-options-result' -import { sortNativeChatSessionOptions } from '../../native-chat/input/native-chat-session-option-snapshot' +import '../../native-chat/input/native-chat-session-option-snapshot' import { canSetStructuredAgentSessionOption, structuredAgentSessionOptionSnapshot } from './structured-agent-session-option-snapshot' import { applyStructuredAgentSessionOptions, - createStructuredAgentSessionOptionState, - structuredAgentSessionOptionPicks + createStructuredAgentSessionOptionState } from './structured-agent-session-options' -import { structuredAgentSessionOptionView } from './structured-agent-session-option-view' +import './structured-agent-session-option-view' import { structuredAgentSessionSeedCatalog } from './structured-agent-session-seed-catalog' const permission = (state: Parameters[0]) => @@ -68,20 +67,6 @@ describe('the permission mode picker', () => { ).toMatchObject({ settable: true, kind: { currentValue: 'full-access' } }) }) - it.each(['claude', 'codex', 'grok', 'opencode', 'opencode2', 'omp', 'antigravity'])( - 'keeps a saved Ask choice editable for %s despite stale or missing reports', - (agent) => { - const state = live(agent, { permissionMode: 'ask' }) - for (const reportedPermissionMode of [undefined, null, 'full-access', 'retired-mode']) { - expect(permission({ ...state, reportedPermissionMode })).toMatchObject({ - settable: true, - valueSource: 'applied', - kind: { currentValue: 'ask' } - }) - } - } - ) - it('admits supported Own delegation through the same descriptor/readiness predicate', () => { const state = { ...live('codex', { permissionMode: 'ask' }), permissionOwnSupported: true } expect(canSetStructuredAgentSessionOption(state, 'permissionMode', 'own')).toBe(true) @@ -114,41 +99,4 @@ describe('the permission mode picker', () => { ) ).toBe(true) }) - - it('shows the seeded mode before the agent starts, beside the quiet model pill', () => { - const state = createStructuredAgentSessionOptionState( - 'codex', - structuredAgentSessionSeedCatalog('codex') - ) - const view = structuredAgentSessionOptionView(state, { permissionMode: 'full-access' }, {}) - expect(structuredAgentSessionOptionSnapshot(view).map((descriptor) => descriptor.id)).toEqual([ - 'model', - 'permissionMode' - ]) - expect(permission(view)).toMatchObject({ - settable: true, - kind: { currentValue: 'full-access' } - }) - }) - - it("shows a held pick over the host's value, and the host's over the seed", () => { - const state = live('codex', { permissionMode: 'auto' }) - const seed = { permissionMode: 'ask' } - expect(permission(structuredAgentSessionOptionView(state, seed, {}))).toMatchObject({ - kind: { currentValue: 'auto' } - }) - expect( - permission(structuredAgentSessionOptionView(state, seed, { permissionMode: 'full-access' })) - ).toMatchObject({ kind: { currentValue: 'full-access' } }) - }) - - it('stays out of the per-model option pill and out of the saved selection', () => { - const state = live('codex', { permissionMode: 'ask' }) - expect( - sortNativeChatSessionOptions(structuredAgentSessionOptionSnapshot(state)).map( - (descriptor) => descriptor.id - ) - ).not.toContain('permissionMode') - expect(structuredAgentSessionOptionPicks(state, { permissionMode: 'full-access' })).toEqual([]) - }) }) diff --git a/src/shared/agent-session/requests/structured-agent-session-dispatch-rejection.test.ts b/src/shared/agent-session/requests/structured-agent-session-dispatch-rejection.test.ts index 36b102e19a9..40c85d9e8b7 100644 --- a/src/shared/agent-session/requests/structured-agent-session-dispatch-rejection.test.ts +++ b/src/shared/agent-session/requests/structured-agent-session-dispatch-rejection.test.ts @@ -88,12 +88,6 @@ describe('classifyDispatchRejection', () => { }) describe('isWriteFailureSubmission', () => { - it('holds for a legacy row in any state that carries the marker', () => { - expect( - isWriteFailureSubmission({ reason: 'provider_write_failed: closed before enqueue' }) - ).toBe(true) - }) - it('holds for the bare marker a new host writes, as released clients already read it', () => { const written = agentSessionFailureWords(agentSessionFailureFact('writeFailed'), { surface: 'rejection' @@ -101,11 +95,4 @@ describe('isWriteFailureSubmission', () => { expect(written.reason).toBe('provider_write_failed') expect(isWriteFailureSubmission({ reason: written.reason })).toBe(true) }) - - it('holds for a typed write failure and for nothing else', () => { - expect(isWriteFailureSubmission({ reason: 'x', rejection: { kind: 'writeFailed' } })).toBe(true) - expect(isWriteFailureSubmission({ reason: DISPATCH_REJECTED_QUEUE_FULL })).toBe(false) - expect(isWriteFailureSubmission({ reason: 'x', rejection: { kind: 'queueFull' } })).toBe(false) - expect(isWriteFailureSubmission({ reason: null })).toBe(false) - }) }) diff --git a/src/shared/agent-session/requests/structured-agent-session-first-prompt.spec.ts b/src/shared/agent-session/requests/structured-agent-session-first-prompt.spec.ts index 4789fca795e..e695ae90b20 100644 --- a/src/shared/agent-session/requests/structured-agent-session-first-prompt.spec.ts +++ b/src/shared/agent-session/requests/structured-agent-session-first-prompt.spec.ts @@ -47,27 +47,6 @@ describe('first structured chat prompt', () => { ).toBe('first user prompt') }) - it('bounds a multi-block prompt before joining it', () => { - const prompt = message('', { - body: { - kind: 'message', - role: 'user', - blocks: [ - { type: 'text', text: 'x'.repeat(STRUCTURED_CHAT_NAME_PROMPT_LIMIT - 2) }, - { type: 'text', text: 'yz'.repeat(100_000) } - ] - } - }) - const result = firstStructuredAgentSessionPrompt([prompt]) - expect(result).toHaveLength(STRUCTURED_CHAT_NAME_PROMPT_LIMIT) - expect(result.endsWith('\ny')).toBe(true) - }) - - it('clips a single prompt block without splitting a Unicode character', () => { - const prefix = 'A'.repeat(STRUCTURED_CHAT_NAME_PROMPT_LIMIT - 1) - expect(firstStructuredAgentSessionPrompt([message(`${prefix}𠮷`)])).toBe(prefix) - }) - it('clips a later prompt block without splitting a Unicode character', () => { const prefix = 'A'.repeat(STRUCTURED_CHAT_NAME_PROMPT_LIMIT - 2) const prompt = message('', { @@ -117,9 +96,4 @@ describe('first structured chat prompt', () => { ).toBe('修复登录') expect(firstStructuredAgentSessionPrompt([message('?!')])).toBe('') }) - - it('retains a URL-only first request', () => { - const url = 'https://example.test/issues/123' - expect(firstStructuredAgentSessionPrompt([message(url), message('Later')])).toBe(url) - }) }) diff --git a/src/shared/agent-session/requests/structured-agent-session-opening-send.test.ts b/src/shared/agent-session/requests/structured-agent-session-opening-send.test.ts deleted file mode 100644 index a5d18ee1d3d..00000000000 --- a/src/shared/agent-session/requests/structured-agent-session-opening-send.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { agentJournalSubmissionKey } from '../journal/agent-session-journal-item-key' -import { - structuredAgentSessionSendOpeningTurn, - type StructuredAgentSessionOpeningSendItem -} from './structured-agent-session-opening-send' - -const send = { - clientMessageId: 'send-1', - dispatchState: 'pending', - handedOverAt: 10, - fence: 1 -} as const -const handover: StructuredAgentSessionOpeningSendItem = { - itemId: agentJournalSubmissionKey('send-1'), - sequence: 5, - body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'first' }] }, - turnScope: { kind: 'thread' } -} -const turnRecord = (sequence: number): StructuredAgentSessionOpeningSendItem => ({ - itemId: `turn-${sequence}`, - sequence, - body: { kind: 'turn', turnId: 'turn-1', state: 'running' } -}) - -function opening(items: StructuredAgentSessionOpeningSendItem[]): boolean { - return structuredAgentSessionSendOpeningTurn([send], (visit) => items.forEach(visit), 1) -} - -describe('a send still opening its turn', () => { - it('holds the next message until a turn record follows its handover', () => { - expect(opening([handover])).toBe(true) - expect(opening([handover, turnRecord(6)])).toBe(false) - }) - - // A Stop the provider took before the turn opened withdraws the send: nothing is opening. - it('ends once the send is settled, with no turn record', () => { - const withdrawn = { ...send, dispatchState: 'rejected' } as const - expect(structuredAgentSessionSendOpeningTurn([withdrawn], (visit) => visit(handover), 1)).toBe( - false - ) - }) -}) diff --git a/src/shared/agent-session/requests/structured-agent-session-projection-sender.spec.ts b/src/shared/agent-session/requests/structured-agent-session-projection-sender.spec.ts index e0188e961b3..46d21fc77ea 100644 --- a/src/shared/agent-session/requests/structured-agent-session-projection-sender.spec.ts +++ b/src/shared/agent-session/requests/structured-agent-session-projection-sender.spec.ts @@ -34,10 +34,6 @@ function message(role: 'user' | 'assistant', from?: unknown): AgentJournalRender } describe("a message's sender, projected for every client", () => { - it('carries the sender of a user-role message another agent sent', () => { - expect(projectStructuredItemToNativeChat(message('user', FROM))?.from).toEqual(FROM) - }) - it("leaves the person's message, and a value no reader can place, without one", () => { expect(projectStructuredItemToNativeChat(message('user'))).not.toHaveProperty('from') expect(projectStructuredItemToNativeChat(message('user', 'nobody'))).not.toHaveProperty('from') diff --git a/src/shared/agent-session/requests/structured-agent-session-send-evidence.test.ts b/src/shared/agent-session/requests/structured-agent-session-send-evidence.test.ts index 63445cb4678..76baa839927 100644 --- a/src/shared/agent-session/requests/structured-agent-session-send-evidence.test.ts +++ b/src/shared/agent-session/requests/structured-agent-session-send-evidence.test.ts @@ -47,21 +47,6 @@ function thrown(rpcCode: string | undefined): StructuredAgentSessionSendAnswer { } describe('structuredAgentSessionSendEvidence', () => { - it('reads a row the host returns, in any state, as its own from then on', () => { - for (const dispatchState of ['pending', 'accepted', 'rejected', 'unknown'] as const) { - expect(structuredAgentSessionSendEvidence(withRow({ dispatchState })).kind).toBe('recorded') - } - // A restart lost its outcome, or a queued card's hand-off: a record, never a probe. - const rows: Partial[] = [ - { dispatchState: 'unknown', recovered: true, reason: 'host_restarted' }, - { dispatchState: 'rejected', clientMessageId: 'other', queuedMessageId: 'm' }, - { dispatchState: 'rejected', keptAsQueuedMessageId: 'm' } - ] - for (const row of rows) { - expect(structuredAgentSessionSendEvidence(withRow(row)).kind).toBe('recorded') - } - }) - it('reads a made-up row for an id the host journal lost as unconfirmed, not recorded', () => { expect( structuredAgentSessionSendEvidence( @@ -104,18 +89,6 @@ describe('structuredAgentSessionSendEvidence', () => { ).toBe('uncertain') }) - // Pins the full turned-away list (round-2 S5): every other code, client-minted ones included, is uncertain. - it('treats a call the host turned away before running it as never written', () => { - for (const rpcCode of [ - 'method_not_found', - 'method_not_supported', - 'invalid_argument', - 'unauthorized' - ]) { - expect(structuredAgentSessionSendEvidence(thrown(rpcCode)).kind).toBe('not-recorded') - } - }) - it('reads an unknown outcome or a lost result as unconfirmed, and an unconfirmed rewind as not sent', () => { for (const reason of ['outcomeUnknown', 'resultLost']) { expect( @@ -128,17 +101,4 @@ describe('structuredAgentSessionSendEvidence', () => { ).kind ).toBe('not-recorded') }) - - // The one request is the only one that carried the id, so a refusal of it proves nothing ran. - it('reads any other refusal as never written', () => { - for (const answer of [ - refused('agent_session_ownership_unknown', 'sessionNotAttached'), - refused('agent_session_conflict'), - refused('agent_session_operation_capacity'), - refused('agent_session_journal_unreadable'), - refused('agent_session_operation_expired', 'operationExpired') - ]) { - expect(structuredAgentSessionSendEvidence(answer).kind).toBe('not-recorded') - } - }) }) diff --git a/src/shared/agent-session/requests/structured-agent-session-send-mutation.spec.ts b/src/shared/agent-session/requests/structured-agent-session-send-mutation.spec.ts index 24569ee5dd4..aad841870f5 100644 --- a/src/shared/agent-session/requests/structured-agent-session-send-mutation.spec.ts +++ b/src/shared/agent-session/requests/structured-agent-session-send-mutation.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import type { AgentJournalMessageItem } from '../journal/agent-session-journal-types' import type { AgentMessageSource } from '../agent-session-message-source' -import { AgentJournalSubmissionSchema } from '../journal/agent-session-journal-submission-schema' +import '../journal/agent-session-journal-submission-schema' import { SendParams } from '../../rpc-contract/structured-agent-session-params' import { structuredAgentSessionPayloadFingerprint } from '../lifecycle/structured-agent-session-mutation' import { @@ -27,24 +27,6 @@ const FROM: AgentMessageSource = { } describe('send fingerprints never cover the sender', () => { - it('reads a saved submission’s choices, ignoring keys an older build stored', () => { - const submission = { - clientMessageId: 'message-1', - options: { model: 'model-a', unknownChoice: 'kept-by-an-older-build' }, - fence: 1, - payloadFingerprint: 'a'.repeat(64), - dispatchState: 'accepted', - providerItemId: null, - reason: null, - submittedAt: 1, - resolvedAt: 2 - } - expect(AgentJournalSubmissionSchema.parse(submission).options).toEqual({ model: 'model-a' }) - expect( - AgentJournalSubmissionSchema.omit({ options: true }).parse(submission) - ).not.toHaveProperty('options') - }) - it('keeps the digest every stored submission and draft already holds', () => { // Pinned bytes: a stored fingerprint that changed would stop matching its provider echo. expect(agentSessionSendBodyFingerprint('session-1', BODY)).toBe( diff --git a/src/shared/source-control-plan-contracts.test.ts b/src/shared/source-control-plan-contracts.test.ts index d69656416de..f6205e7542e 100644 --- a/src/shared/source-control-plan-contracts.test.ts +++ b/src/shared/source-control-plan-contracts.test.ts @@ -4,4 +4,3 @@ import './commit-message-omp.spec' import './antigravity-source-control.spec' import './conversation-name-action.spec' import './source-control-ai-action-variables.spec' -import './agent-model-probe-spec.spec' diff --git a/src/shared/structured-session-message-contracts.test.ts b/src/shared/structured-session-message-contracts.test.ts index 39dd9884151..c2e4cc6a27c 100644 --- a/src/shared/structured-session-message-contracts.test.ts +++ b/src/shared/structured-session-message-contracts.test.ts @@ -6,5 +6,4 @@ import './agent-session/requests/structured-agent-session-first-prompt.spec' import './agent-session/journal/agent-session-journal-submission-schema.spec' import './agent-session/requests/structured-agent-session-projection-sender.spec' import './agent-session/lifecycle/structured-agent-session-mutation.spec' -import './agent-session/options/structured-agent-session-option-codec.spec' import './agent-session/structured-agent-session-first-frame-picker.spec'