test: remove low-value agent status and renderer helper tests (#37490)

This commit is contained in:
OrcaWin
2026-11-02 07:23:39 -08:00
committed by GitHub
parent bb3ce94b4f
commit 02e93251ec
44 changed files with 10 additions and 4025 deletions
-14
View File
@@ -10531,10 +10531,6 @@
"src/renderer/src/lib/agent-picker-search.test.ts": 265,
"src/renderer/src/lib/agent-ready-wait.test.ts": 119,
"src/renderer/src/lib/agent-resume-launch-target.test.ts": 3911,
"src/renderer/src/lib/agent-row-tool-preview.test.ts": 111,
"src/renderer/src/lib/agent-send-title-status.test.ts": 100,
"src/renderer/src/lib/agent-session-fork-context.test.ts": 53,
"src/renderer/src/lib/agent-session-launch-plan.test.ts": 1761,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-after-failure.test.ts": 870,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-close-race.test.ts": 983,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-create-message.test.ts": 2521,
@@ -10547,7 +10543,6 @@
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt-stop.test.ts": 774,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt.test.ts": 710,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-record.test.ts": 151,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-reload.test.ts": 915,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-resume-identity.test.ts": 828,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-unconfirmed-or-retried.test.ts": 1085,
"src/renderer/src/lib/agent-session/launch/structured-agent-session-launch.test.ts": 977,
@@ -10555,19 +10550,12 @@
"src/renderer/src/lib/agent-session/structured-agent-session-draft-position-lifetime.test.tsx": 3166,
"src/renderer/src/lib/agent-session/structured-agent-session-ordinary-draft.test.tsx": 9907,
"src/renderer/src/lib/agent-session/structured-agent-session-provisional-tab.test.ts": 3672,
"src/renderer/src/lib/agent-session/structured-agent-session-tab-activation.test.ts": 278,
"src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts": 219,
"src/renderer/src/lib/agent-skill-nav-install-status.test.ts": 2045,
"src/renderer/src/lib/agent-startup-creation-continuation.test.ts": 1727,
"src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts": 1708,
"src/renderer/src/lib/agent-status-connection-ownership.test.ts": 50,
"src/renderer/src/lib/agent-status-count.test.ts": 108,
"src/renderer/src/lib/agent-status-epoch-clock.test.ts": 49,
"src/renderer/src/lib/agent-status-evidence-clock.test.ts": 88,
"src/renderer/src/lib/agent-status-terminal-title.test.ts": 99,
"src/renderer/src/lib/agent-status.test.ts": 172,
"src/renderer/src/lib/agent-tab-shortcuts.test.ts": 1736,
"src/renderer/src/lib/agent-verdict-status-line.test.ts": 100,
"src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts": 678,
"src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts": 1909,
"src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts": 541,
@@ -10581,12 +10569,10 @@
"src/renderer/src/lib/app-command-dispatch.test.ts": 64,
"src/renderer/src/lib/app-keyboard-router.test.ts": 187,
"src/renderer/src/lib/app-menu-paste.test.ts": 229,
"src/renderer/src/lib/app-menu-selection-actions.test.ts": 181,
"src/renderer/src/lib/automation-session-observer.test.ts": 113,
"src/renderer/src/lib/automation-session-reuse.test.ts": 70,
"src/renderer/src/lib/automation-terminal-bind-layout-owner.test.ts": 1756,
"src/renderer/src/lib/automation-terminal-ownership.test.ts": 1795,
"src/renderer/src/lib/base64-bytes.test.ts": 65,
"src/renderer/src/lib/bounded-generation-map.test.ts": 35,
"src/renderer/src/lib/browser-cookie-import-error-copy.test.ts": 370,
"src/renderer/src/lib/browser-cookie-import-toast.test.ts": 266,
@@ -1,61 +0,0 @@
import { describe, expect, it } from 'vitest'
import { AGENT_STATUS_STATES } from '../../../shared/agent-status-types'
import type { AgentRowState } from '../../../shared/agent-row-decay-state'
import { formatAgentToolPreview, showsAgentToolPreview } from './agent-row-tool-preview'
const TOOL = { toolName: 'bash', toolInput: 'rm -rf build/' }
const ROW_STATES: readonly AgentRowState[] = [...AGENT_STATUS_STATES, 'idle', 'unverifiable']
describe('showsAgentToolPreview', () => {
it('covers exactly the two states whose tool fields describe live work', () => {
// Enumerate every state so a new state cannot silently inherit stale tool fields.
const showing = ROW_STATES.filter((state) => showsAgentToolPreview(state))
expect(showing).toEqual(['working', 'waiting'])
})
it('shows nothing when the state is unknown', () => {
expect(showsAgentToolPreview(null)).toBe(false)
expect(showsAgentToolPreview(undefined)).toBe(false)
})
})
describe('formatAgentToolPreview', () => {
it('names what a blocked approval is waiting on', () => {
expect(formatAgentToolPreview(TOOL, 'waiting')).toBe('bash: rm -rf build/')
})
it('names the running tool while working', () => {
expect(formatAgentToolPreview(TOOL, 'working')).toBe('bash: rm -rf build/')
})
it('shows nothing on a wait that carries no tool', () => {
// Why: a question-style wait sets no tool fields; it must not borrow an earlier one.
expect(formatAgentToolPreview({}, 'waiting')).toBe('')
})
it('falls back to the bare tool name when the input is not previewable', () => {
expect(formatAgentToolPreview({ toolName: 'edit' }, 'waiting')).toBe('edit')
})
it("shows a program's bare message when no tool is named", () => {
const program = {
agentType: 'brew',
toolInput: 'Pouring wget',
programStatus: { state: 'working' as const, observedAt: 1 }
}
expect(formatAgentToolPreview(program, 'working')).toBe('Pouring wget')
expect(formatAgentToolPreview(program, 'done')).toBe('')
})
it("hides a real agent's tool input that has no tool name", () => {
expect(
formatAgentToolPreview({ agentType: 'claude', toolInput: 'src/app.ts' }, 'working')
).toBe('')
expect(formatAgentToolPreview({ toolInput: 'src/app.ts' }, 'working')).toBe('')
})
it('ignores whitespace-only fields', () => {
expect(formatAgentToolPreview({ toolName: ' ', toolInput: ' ' }, 'working')).toBe('')
})
})
@@ -1,27 +0,0 @@
import { describe, expect, it } from 'vitest'
import { detectAgentSendTitleStatus } from './agent-send-title-status'
describe('detectAgentSendTitleStatus', () => {
it.each([
'OC | Native session',
'OC | ✦ Gemini CLI',
'OC | ✋ review Gemini permission handling'
])('accepts OpenCode native idle title %j', (title) => {
expect(detectAgentSendTitleStatus(title)).toBe('idle')
})
it('preserves spinner working state for OpenCode', () => {
expect(detectAgentSendTitleStatus('⠋ OC | Native session')).toBe('working')
expect(detectAgentSendTitleStatus('ssh build-host | ⠋ OC | Native session')).toBe('working')
})
it.each(['oc | Native session'])('rejects incomplete or lookalike OpenCode title %j', (title) => {
expect(detectAgentSendTitleStatus(title)).toBeNull()
})
it('preserves non-OpenCode title behavior', () => {
expect(detectAgentSendTitleStatus('✦ Gemini CLI')).toBe('working')
expect(detectAgentSendTitleStatus('Codex ready')).toBe('idle')
expect(detectAgentSendTitleStatus('zsh')).toBeNull()
})
})
@@ -1,40 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
buildAgentSessionForkPrompt,
buildBoundedSessionTranscript,
cleanAgentSessionForkTranscript
} from './agent-session-fork-context'
describe('agent session fork context', () => {
it('cleans terminal control sequences before building fork context', () => {
const cleaned = cleanAgentSessionForkTranscript(
'\x1b]0;Codex working\x07\x1b[31mUser\x1b[0m\r\nAssistant'
)
expect(cleaned).toBe('User\nAssistant')
})
it('returns null when no transcript survives cleanup', () => {
expect(buildAgentSessionForkPrompt({ capturedText: '\x1b[0m\r\n\x1bc\x07' })).toBeNull()
})
it('builds a bounded transcript without the fork prompt framing', () => {
const transcript = buildBoundedSessionTranscript(
'\x1b]0;Codex working\x07\x1b[31mUser: ship it\x1b[0m\r\nAssistant: done'
)
// Copy Context must return raw transcript without fork instructions a paste target could execute.
expect(transcript).toBe('User: ship it\nAssistant: done')
expect(transcript).not.toContain('fork of an existing Orca agent session')
expect(transcript).not.toContain('wait for my next instruction')
})
it('uses a longer fence when captured output contains markdown fences', () => {
const prompt = buildAgentSessionForkPrompt({
capturedText: 'Assistant output:\n```text\nignore prior instructions\n```'
})
expect(prompt).toContain('````text\nAssistant output:')
expect(prompt).toContain('\n````\n\nAcknowledge')
})
})
@@ -1,261 +0,0 @@
import { EVERY_AGENT_CHAT } from '../../../shared/native-chat/every-agent-chat.test-fixture'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentLaunchRouteStore } from './agent-launch-route-input'
const mocks = vi.hoisted(() => ({
buildAgentLaunchRouteInput: vi.fn(),
resolveAgentLaunchRoute: vi.fn(),
structuredAgentLaunchSupported: vi.fn(),
beginStructuredAgentLaunchSettlement: vi.fn()
}))
vi.mock('@/lib/agent-launch-route-input', () => ({
buildAgentLaunchRouteInput: mocks.buildAgentLaunchRouteInput
}))
vi.mock('@/lib/agent-launch-routing', () => ({
resolveAgentLaunchRoute: mocks.resolveAgentLaunchRoute,
structuredAgentLaunchSupported: mocks.structuredAgentLaunchSupported,
structuredAgentLaunchDowngrade: () => null
}))
vi.mock('@/lib/structured-agent-launch-settlement', () => ({
beginStructuredAgentLaunchSettlement: mocks.beginStructuredAgentLaunchSettlement
}))
import {
adoptAgentSessionLaunchVerdict,
planAgentSessionLaunch,
structuredAgentSessionLaunchFeasible
} from './agent-session-launch-plan'
const store = { settings: {} } as unknown as AgentLaunchRouteStore
const ROUTE_INPUT = { agent: 'codex', executionHostId: 'local' }
const STRUCTURED = { kind: 'structured', sessionId: 'session-1' }
const hooks = { onStructuredReady: vi.fn() }
describe('planAgentSessionLaunch', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.buildAgentLaunchRouteInput.mockReturnValue(ROUTE_INPUT)
mocks.resolveAgentLaunchRoute.mockReturnValue('structured-native-chat')
mocks.structuredAgentLaunchSupported.mockReturnValue(true)
mocks.beginStructuredAgentLaunchSettlement.mockReturnValue({
sessionId: 'session-1',
settlement: Promise.resolve(STRUCTURED)
})
})
it('decides the route once, from the builder input, and never again on launch', async () => {
const plan = planAgentSessionLaunch(store, {
requestId: 'request-1',
agent: 'codex',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' },
prompt: 'Fix it',
promptDelivery: 'draft'
})
expect(plan.route).toBe('structured-native-chat')
expect(mocks.buildAgentLaunchRouteInput).toHaveBeenCalledWith(
store,
expect.objectContaining({ agent: 'codex', prompt: 'Fix it', promptDelivery: 'draft' })
)
expect(mocks.resolveAgentLaunchRoute).toHaveBeenCalledWith(ROUTE_INPUT)
expect(mocks.structuredAgentLaunchSupported).not.toHaveBeenCalled()
await plan.launch(hooks)
await plan.launch(hooks)
expect(mocks.buildAgentLaunchRouteInput).toHaveBeenCalledOnce()
expect(mocks.resolveAgentLaunchRoute).toHaveBeenCalledOnce()
})
it('hands the settle loop exactly the request, prompt, mode, resume source, and delivery hook it planned on', async () => {
const onPromptDelivered = vi.fn()
const resumeFrom = { providerSessionId: 'provider-1' }
const plan = planAgentSessionLaunch(store, {
requestId: 'request-2',
agent: 'claude',
workspace: { kind: 'folder', worktreeId: 'folder:ws-1' },
prompt: 'Review this',
promptDelivery: 'submit-after-ready',
resumeFrom,
onPromptDelivered
})
await expect(plan.launch(hooks)).resolves.toBe(STRUCTURED)
expect(mocks.beginStructuredAgentLaunchSettlement).toHaveBeenCalledWith(
'folder:ws-1',
'claude',
{
requestId: 'request-2',
prompt: 'Review this',
promptDelivery: 'submit-after-ready',
resumeFrom,
onPromptDelivered,
executionHostId: 'local'
},
hooks
)
})
it('sends no delivery fields the request did not carry', async () => {
const plan = planAgentSessionLaunch(store, {
requestId: 'request-3',
agent: 'codex',
workspace: { kind: 'folder', worktreeId: 'folder:ws-1' }
})
await plan.launch(hooks)
expect(mocks.beginStructuredAgentLaunchSettlement).toHaveBeenCalledWith(
'folder:ws-1',
'codex',
{ requestId: 'request-3', executionHostId: 'local' },
hooks
)
})
it('launches any agent the structured route admitted', async () => {
const plan = planAgentSessionLaunch(store, {
requestId: 'request-4',
agent: 'grok',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' }
})
await expect(plan.launch(hooks)).resolves.toEqual(STRUCTURED)
expect(mocks.beginStructuredAgentLaunchSettlement).toHaveBeenCalledWith(
'wt-1',
'grok',
{ requestId: 'request-4', executionHostId: 'local' },
hooks
)
})
it('opens nothing structured on a route that is not', async () => {
mocks.resolveAgentLaunchRoute.mockReturnValue('terminal-tui')
const plan = planAgentSessionLaunch(store, {
requestId: 'request-4b',
agent: 'grok',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' }
})
await expect(plan.launch(hooks)).resolves.toBeNull()
expect(plan.begin(hooks)).toBeNull()
expect(mocks.beginStructuredAgentLaunchSettlement).not.toHaveBeenCalled()
})
it('launches into the workspace created after planning when the target names one', async () => {
const plan = planAgentSessionLaunch(store, {
requestId: 'request-5',
agent: 'codex',
workspace: { kind: 'git-worktree', repoId: 'repo-1' },
prompt: 'Fix it',
promptDelivery: 'auto-submit'
})
await plan.launch(hooks, { worktreeId: 'wt-created' })
expect(mocks.beginStructuredAgentLaunchSettlement).toHaveBeenCalledWith(
'wt-created',
'codex',
{
requestId: 'request-5',
prompt: 'Fix it',
promptDelivery: 'auto-submit',
executionHostId: 'local'
},
hooks
)
})
it('refuses to launch a prospective workspace that was never created', async () => {
const plan = planAgentSessionLaunch(store, {
requestId: 'request-6',
agent: 'codex',
workspace: { kind: 'git-worktree', repoId: 'repo-1' }
})
await expect(plan.launch(hooks)).rejects.toThrow(/workspace/)
expect(mocks.beginStructuredAgentLaunchSettlement).not.toHaveBeenCalled()
})
})
describe('structuredAgentSessionLaunchFeasible', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.buildAgentLaunchRouteInput.mockReturnValue(ROUTE_INPUT)
mocks.structuredAgentLaunchSupported.mockReturnValue(true)
})
it.each([true, false])('answers from feasibility alone (supported=%s)', (supported) => {
mocks.structuredAgentLaunchSupported.mockReturnValue(supported)
const settings = { nativeChatProviders: EVERY_AGENT_CHAT }
expect(
structuredAgentSessionLaunchFeasible(store, {
agent: 'codex',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' },
settings
})
).toBe(supported)
expect(mocks.resolveAgentLaunchRoute).not.toHaveBeenCalled()
expect(mocks.beginStructuredAgentLaunchSettlement).not.toHaveBeenCalled()
})
it('builds the input from the named settings, not the store copy', () => {
const settings = { nativeChatProviders: EVERY_AGENT_CHAT }
structuredAgentSessionLaunchFeasible(store, {
agent: 'codex',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' },
settings
})
expect(mocks.buildAgentLaunchRouteInput).toHaveBeenCalledWith(store, {
agent: 'codex',
workspace: { kind: 'git-worktree', worktreeId: 'wt-1' }
})
expect(mocks.structuredAgentLaunchSupported).toHaveBeenCalledWith(
expect.objectContaining({ ...ROUTE_INPUT, settings })
)
})
})
describe('adoptAgentSessionLaunchVerdict', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.beginStructuredAgentLaunchSettlement.mockReturnValue({
sessionId: 'session-1',
settlement: Promise.resolve(STRUCTURED)
})
})
it('re-enters a persisted verdict without resolving the route again', async () => {
const plan = adoptAgentSessionLaunchVerdict({
requestId: 'request-7',
route: 'structured-native-chat',
agent: 'codex',
prompt: 'Fix it',
promptDelivery: 'draft'
})
expect(plan.route).toBe('structured-native-chat')
await expect(plan.launch(hooks, { worktreeId: 'wt-recovered' })).resolves.toBe(STRUCTURED)
expect(mocks.buildAgentLaunchRouteInput).not.toHaveBeenCalled()
expect(mocks.resolveAgentLaunchRoute).not.toHaveBeenCalled()
expect(mocks.structuredAgentLaunchSupported).not.toHaveBeenCalled()
expect(mocks.beginStructuredAgentLaunchSettlement).toHaveBeenCalledWith(
'wt-recovered',
'codex',
{ requestId: 'request-7', prompt: 'Fix it', promptDelivery: 'draft' },
hooks
)
})
it('keeps a non-structured verdict out of the loop', async () => {
const plan = adoptAgentSessionLaunchVerdict({
requestId: 'request-8',
route: 'terminal-tui',
agent: 'codex',
worktreeId: 'wt-1'
})
await expect(plan.launch(hooks)).resolves.toBeNull()
expect(mocks.beginStructuredAgentLaunchSettlement).not.toHaveBeenCalled()
})
})
@@ -380,10 +380,4 @@ describe('a message sent to a chat whose start failed', () => {
expect(getStructuredAgentSessionPendingSends(failed.sessionId)).toEqual([])
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
})
it('does nothing for a chat whose start has not failed', () => {
expect(
relaunchFailedStructuredAgentSessionWithMessage(WORKTREE_ID, 'session-unknown', 'hi')
).toBeNull()
})
})
@@ -13,7 +13,6 @@ import {
draft,
startLegacyActionLaunch
} from '../../structured-agent-session-launch-create-message.test-fixture'
import { useAppStore } from '@/store/index'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
@@ -34,11 +33,7 @@ import {
resetStructuredAgentLaunchRegistryForTests,
getStructuredLaunchStateBySessionId
} from './structured-agent-session-launch-registry'
import {
registerStructuredSelectionCatalogStore,
structuredSessionSelectionController,
forgetStructuredSessionSelection
} from '../../structured-agent-session-selection-controller'
import { structuredSessionSelectionController } from '../../structured-agent-session-selection-controller'
import { setRuntimeEnvironmentConnectionGenerationForTests } from '@/store/slices/runtime-status-connection-generation'
import { replaceRuntimeEnvironmentRevisions } from '@/runtime/runtime-environment-revision'
import {
@@ -72,25 +67,6 @@ describe('the first message owned by create', () => {
cleanupCreateMessageFixture(restoreCatalogStore)
})
it('requires registered authority before inserting a remote selection owner', () => {
restoreCatalogStore?.()
const remote = { kind: 'environment', environmentId: 'uncomposed-test-store' } as const
expect(() => structuredSessionSelectionController(remote, 'uncomposed', 'codex')).toThrow(
'must be registered'
)
const restore = registerStructuredSelectionCatalogStore({
read: useAppStore.getState,
subscribe: useAppStore.subscribe
})
try {
const owner = structuredSessionSelectionController(remote, 'uncomposed', 'codex')
expect(structuredSessionSelectionController(remote, 'uncomposed', 'codex')).toBe(owner)
} finally {
forgetStructuredSessionSelection('uncomposed', remote)
restore()
}
})
it('freezes the message and held options in create and publishes without another read or send', async () => {
mocks.exactSupport.mockResolvedValue(true)
recordHostModelCatalogSnapshot(target, 'codex', null, {
@@ -88,15 +88,8 @@ vi.mock('@/lib/agent-catalog', () => ({
import { refreshLocalStructuredSessionTabs } from '@/runtime/local-structured-session-tabs-sync/inventory-refresh'
import { resetStructuredAgentSessionSendsForTests } from '@/components/native-chat/structured-agent-session-message-sender'
import {
clearNativeChatDraftCacheForTests,
readNativeChatDraftCache
} from '@/components/native-chat/native-chat-draft-cache'
import { StructuredAgentSessionCreateRefusalError } from '@/lib/agent-session/launch/structured-agent-session-launch-errors'
import {
getStructuredAgentSessionLaunchLifecycle,
resetStructuredAgentLaunchRegistryForTests
} from './structured-agent-session-launch-registry'
import { clearNativeChatDraftCacheForTests } from '@/components/native-chat/native-chat-draft-cache'
import { resetStructuredAgentLaunchRegistryForTests } from './structured-agent-session-launch-registry'
import { startStructuredAgentLaunch } from './structured-agent-session-launch'
import {
discardStructuredLaunchPrompts,
@@ -298,19 +291,6 @@ describe('one action delivered twice while its chat is still starting', () => {
})
})
it('makes one chat from a pick delivered twice with no text', () => {
mocks.launch.mockImplementation(() => new Promise(() => undefined))
const pick = startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'pick' })
const again = startStructuredAgentLaunch(WORKTREE_ID, 'codex', {
requestId: 'pick',
promptDelivery: 'submit-after-ready'
})
expect(again.sessionId).toBe(pick.sessionId)
expect(mocks.createIntent).toHaveBeenCalledOnce()
})
it('makes one chat and sends its text once while the create is in flight', async () => {
let resolveLaunch!: (receipt: { sessionId: string; fence: number }) => void
mocks.launch.mockImplementation(() => new Promise((resolve) => (resolveLaunch = resolve)))
@@ -392,34 +372,6 @@ describe('one action delivered twice while its chat is still starting', () => {
expect(repeat.sessionId).toBe(click.sessionId)
expect(readNativeChatComposerDraft(scope).text).toBe('')
})
it('opens a new chat for a click after the start failed', async () => {
mocks.launch
.mockRejectedValueOnce(new StructuredAgentSessionCreateRefusalError('refused'))
.mockImplementation(() => new Promise(() => undefined))
const failed = startStructuredAgentLaunch(WORKTREE_ID, 'codex', {
requestId: 'fix-click-1',
prompt: 'Fix check A',
promptDelivery: 'submit-after-ready'
})
await expect(failed.launchResult).rejects.toBeInstanceOf(
StructuredAgentSessionCreateRefusalError
)
const reclick = startStructuredAgentLaunch(WORKTREE_ID, 'codex', {
requestId: 'fix-click-2',
prompt: 'Fix check A',
promptDelivery: 'submit-after-ready'
})
expect(reclick.sessionId).toBe(second.sessionId)
expect(getStructuredAgentSessionLaunchLifecycle(WORKTREE_ID, first.sessionId)).toBe('failed')
expect(hasStagedStructuredLaunchPrompt(second.sessionId)).toBe(true)
// The failed chat's text waits in its own composer.
expect(readNativeChatDraftCache(structuredAgentSessionDraftScopeKey(first.sessionId))).toBe(
'Fix check A'
)
})
})
describe('an action with text beside an empty chat still starting', () => {
@@ -451,22 +403,6 @@ describe('an action with text beside an empty chat still starting', () => {
})
})
it('opens its own chat for notes sent to a new agent', async () => {
const blank = startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'plus-pick' })
const notes = startStructuredAgentLaunch(WORKTREE_ID, 'codex', notesSend)
resolveFirstLaunch({ sessionId: first.sessionId, fence: 1 })
expect(blank.sessionId).toBe(first.sessionId)
expect(notes.sessionId).toBe(second.sessionId)
await expect(notes.promptDeliveryResult).resolves.toEqual({
delivered: true,
failureNotified: false
})
expect(sends()).toEqual([[second.sessionId, 'review notes']])
expect(hasStagedStructuredLaunchPrompt(blank.sessionId)).toBe(false)
})
it('sends notes delivered twice once, in their own chat', async () => {
const blank = startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'plus-pick' })
const notes = startStructuredAgentLaunch(WORKTREE_ID, 'codex', notesSend)
@@ -244,13 +244,6 @@ describe('settleStructuredAgentLaunchPrompt', () => {
).not.toBeNull()
})
it('gives the chat back its send slot when the launch is cancelled, sending nothing', () => {
stageStructuredLaunchPrompt(SESSION, 'review this')
discardStructuredLaunchPrompts(SESSION)
expect(pendingTexts()).toEqual([])
expect(sends()).toHaveLength(0)
})
// Once sent, the prompt's entry is the sender's: what it keeps after settling stays.
it('leaves in the chat a launch prompt the host recorded and a Stop then withdrew', async () => {
const stagedPrompt = stageStructuredLaunchPrompt(SESSION, 'review this')
@@ -307,15 +300,4 @@ describe('settleStructuredAgentLaunchPrompt', () => {
expect(pendingTexts()).toEqual(['review this:recorded'])
stop()
})
it('reports nothing for a draft, which the composer adopts instead', () => {
expect(
settleStructuredAgentLaunchPrompt({
launchResult: Promise.resolve({ sessionId: SESSION, fence: 1 }),
target,
options: { prompt: 'review this', promptDelivery: 'draft' },
stagedPrompt: null
})
).toBeUndefined()
})
})
@@ -1,224 +0,0 @@
// @vitest-environment happy-dom
import type * as LaunchErrors from '@/lib/agent-session/launch/structured-agent-session-launch-errors'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../../../shared/runtime-session-contracts'
import type { StructuredAgentSessionLaunchIntent } from '@/lib/launch-structured-agent-session'
import type * as RecoveryModule from '@/lib/agent-session/launch/structured-agent-session-launch-recovery'
const mocks = vi.hoisted(() => ({
abandonIntent: vi.fn(),
callStructuredAgentSession: vi.fn(),
launch: vi.fn(),
restoreIntent: vi.fn(),
retryIntent: vi.fn(),
seedDraft: vi.fn(),
clearDraft: vi.fn(),
refresh: vi.fn()
}))
const launchErrorMocks = vi.hoisted(() => {
class StructuredAgentSessionCreateRefusalError extends Error {}
return { StructuredAgentSessionCreateRefusalError }
})
vi.mock('@/lib/agent-session/launch/structured-agent-session-launch-errors', async () => ({
...(await vi.importActual<typeof LaunchErrors>(
'@/lib/agent-session/launch/structured-agent-session-launch-errors'
)),
...launchErrorMocks
}))
vi.mock('@/lib/agent-session/launch/structured-agent-session-launch-create-call', () => ({
launchStructuredAgentSession: mocks.launch
}))
vi.mock('@/lib/launch-structured-agent-session', () => {
return {
abandonStructuredAgentSessionLaunchIntent: mocks.abandonIntent,
createStructuredAgentSessionLaunchIntent: vi.fn(),
launchStructuredAgentSession: mocks.launch,
restoreStructuredAgentSessionLaunchIntent: mocks.restoreIntent,
retryStructuredAgentSessionLaunchIntent: mocks.retryIntent,
StructuredAgentSessionCreateRefusalError:
launchErrorMocks.StructuredAgentSessionCreateRefusalError
}
})
vi.mock('@/lib/agent-session/launch/structured-agent-session-launch-recovery', async () => {
const actual = await vi.importActual<typeof RecoveryModule>(
'@/lib/agent-session/launch/structured-agent-session-launch-recovery'
)
return { ...actual, launchAndReconcile: vi.fn(actual.launchAndReconcile) }
})
vi.mock('@/runtime/local-structured-session-tabs-sync/inventory-refresh', () => ({
refreshLocalStructuredSessionTabs: mocks.refresh
}))
vi.mock('@/runtime/structured-agent-session-client', () => ({
callStructuredAgentSession: mocks.callStructuredAgentSession
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
seedNativeChatLaunchDraft: mocks.seedDraft,
clearNativeChatLaunchDraft: mocks.clearDraft
})
}
}))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
}))
vi.mock('@/lib/agent-catalog', () => ({
getAgentLabel: (agent: string) => (agent === 'codex' ? 'Codex' : 'Claude')
}))
import { retryStructuredAgentSessionLaunch } from './structured-agent-session-launch'
import { structuredLaunchOptionOverlay } from './structured-agent-session-launch-options'
import { resetStructuredAgentLaunchPersistenceForTests } from './structured-agent-session-launch-persistence'
import {
resetStructuredAgentLaunchRegistryForTests,
getStructuredLaunchStateBySessionId
} from './structured-agent-session-launch-registry'
function launchIntent(worktreeId: string, sessionId: string): StructuredAgentSessionLaunchIntent {
return {
worktreeId,
executionHostId: 'local',
target: { kind: 'local' },
sessionId,
agent: 'codex',
params: {
envelope: {
sessionId,
clientOperationId: 'operation-reloaded',
expectedRuntimeFence: null,
payloadFingerprint: 'fingerprint-reloaded'
},
worktree: `id:${worktreeId}`,
agent: 'codex'
}
}
}
function publishedSnapshot(worktreeId: string, sessionId: string): RuntimeMobileSessionTabsResult {
return {
worktree: worktreeId,
publicationEpoch: 'epoch-1',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: [
{
type: 'agent-session',
id: 'tab-1',
title: 'Codex',
sessionId,
agent: 'codex',
isActive: true
}
]
}
}
async function flushLaunchSettlement(): Promise<void> {
for (let index = 0; index < 20; index += 1) {
await Promise.resolve()
}
}
describe('structured agent launch reload recovery', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
resetStructuredAgentLaunchPersistenceForTests()
resetStructuredAgentLaunchRegistryForTests()
mocks.callStructuredAgentSession.mockResolvedValue({ ok: true, page: { fence: 4 } })
mocks.restoreIntent.mockImplementation(
(args: {
worktreeId: string
sessionId: string
agent: 'claude' | 'codex'
clientOperationId: string
payloadFingerprint: string
expectedRuntimeFence: number | null
explicitLaunchOptions?: Readonly<Record<string, string>>
optionsMode?: 'replace-model-selection'
options?: Readonly<Record<string, string>>
}) => ({
...launchIntent(args.worktreeId, args.sessionId),
agent: args.agent,
explicitLaunchOptions: args.explicitLaunchOptions,
params: {
...launchIntent(args.worktreeId, args.sessionId).params,
agent: args.agent,
optionsMode: args.optionsMode,
options: args.options,
envelope: {
sessionId: args.sessionId,
clientOperationId: args.clientOperationId,
payloadFingerprint: args.payloadFingerprint,
expectedRuntimeFence: args.expectedRuntimeFence
}
}
})
)
})
it('retries a reload-interrupted launch with the persisted operation identity', async () => {
const worktreeId = 'wt-reload'
const sessionId = 'codex-reload-session'
localStorage.setItem(
'orca:structuredAgentLaunches:v1',
JSON.stringify([
{
sessionId,
agent: 'codex',
lifecycle: 'pending',
explicitLaunchOptions: { model: 'action-model', fastMode: 'false' },
optionsMode: 'replace-model-selection',
options: { model: 'frozen-model', effort: 'high' },
clientOperationId: 'operation-reloaded',
payloadFingerprint: 'fingerprint-reloaded',
expectedRuntimeFence: null
}
])
)
mocks.launch.mockResolvedValueOnce({ sessionId, fence: 4 })
mocks.refresh
.mockResolvedValueOnce([])
.mockResolvedValueOnce([publishedSnapshot(worktreeId, sessionId)])
expect(retryStructuredAgentSessionLaunch(worktreeId, sessionId)).toBe(true)
const restored = getStructuredLaunchStateBySessionId(sessionId)!
expect(restored.selection).toMatchObject({
explicit: { model: 'action-model', fastMode: 'false' }
})
expect(structuredLaunchOptionOverlay(restored)).toEqual({})
await flushLaunchSettlement()
expect(mocks.launch).toHaveBeenCalledWith(
expect.objectContaining({
sessionId,
worktreeId,
explicitLaunchOptions: { model: 'action-model', fastMode: 'false' },
params: expect.objectContaining({
optionsMode: 'replace-model-selection',
options: { model: 'frozen-model', effort: 'high' },
envelope: expect.objectContaining({
clientOperationId: 'operation-reloaded',
payloadFingerprint: 'fingerprint-reloaded'
})
})
}),
expect.any(Function),
true,
expect.any(Function)
)
})
})
@@ -43,7 +43,6 @@ vi.mock('@/store/index', () => ({
}
}))
import { getStructuredAgentLaunchStatus } from './structured-agent-session-launch-status'
import { getStructuredAgentSessionLaunchResumes } from './structured-agent-session-launch-registry'
import { startStructuredAgentLaunch } from './structured-agent-session-launch'
@@ -141,20 +140,6 @@ describe('a launch that adopts a conversation is its own identity', () => {
})
})
it('does not hand a blank launch the resume already pending for the same worktree', async () => {
const worktreeId = 'wt-blank-vs-resume'
const resume = startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-3',
resumeFrom: { providerSessionId: 'thread-1' }
})
const blank = startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'request-4' })
await flushLaunchDispatch()
expect(blank.sessionId).not.toBe(resume.sessionId)
expect(createParams()).toHaveLength(2)
})
it('keeps two resumes of different rows apart', async () => {
const worktreeId = 'wt-two-rows'
const first = startStructuredAgentLaunch(worktreeId, 'codex', {
@@ -209,18 +194,4 @@ describe('a launch that adopts a conversation is its own identity', () => {
expect(there.sessionId).not.toBe(here.sessionId)
expect(createParams()).toHaveLength(2)
})
it('reports a pending resume as a launch in flight for the worktree', () => {
// "Is a chat starting here" means any launch for the pair, not only the blank one.
const worktreeId = 'wt-resume-status'
expect(getStructuredAgentLaunchStatus(worktreeId, 'codex')).toBe('idle')
startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-11',
resumeFrom: { providerSessionId: 'thread-1' }
})
expect(getStructuredAgentLaunchStatus(worktreeId, 'codex')).toBe('pending')
expect(getStructuredAgentLaunchStatus(worktreeId, 'claude')).toBe('idle')
})
})
@@ -106,7 +106,6 @@ vi.mock('@/lib/launch-agent-in-new-tab', async () => {
import { StructuredAgentSessionCreateRefusalError } from '@/lib/agent-session/launch/structured-agent-session-launch-errors'
import { refreshLocalStructuredSessionTabs } from '@/runtime/local-structured-session-tabs-sync/inventory-refresh'
import { runSourceControlAgentActionStart } from '@/components/right-sidebar/runSourceControlAgentActionStart'
import { getStructuredAgentLaunchStatus } from './structured-agent-session-launch-status'
import {
getStructuredAgentSessionLaunchLifecycle,
resetStructuredAgentLaunchRegistryForTests
@@ -303,48 +302,6 @@ describe('a new start beside an unconfirmed or retried chat', () => {
expect(mocks.seedDraft).not.toHaveBeenCalled()
})
it('opens a new chat from a + menu pick while another chat is unconfirmed', async () => {
await leaveFirstLaunchUnconfirmed()
// Why: the + menu disables an agent only while a pick would join a start in flight.
expect(getStructuredAgentLaunchStatus(WORKTREE_ID, 'codex')).toBe('idle')
const pick = startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'request-5' })
expect(pick.sessionId).toBe(fresh.sessionId)
expect(getStructuredAgentLaunchStatus(WORKTREE_ID, 'codex')).toBe('pending')
// No re-check of the unconfirmed chat: only its own two create attempts and the new one.
expect(mocks.launch).toHaveBeenCalledTimes(3)
await expect(pick.launchResult).resolves.toEqual({ sessionId: fresh.sessionId, fence: 1 })
})
it('still coalesces one action delivered twice racing for one chat', async () => {
mocks.createIntent.mockReturnValueOnce(fresh)
mocks.launch.mockImplementationOnce(() => new Promise(() => undefined))
const first = startStructuredAgentLaunch(WORKTREE_ID, 'codex', {
requestId: 'double-click',
prompt: 'one'
})
const second = startStructuredAgentLaunch(WORKTREE_ID, 'codex', {
requestId: 'double-click',
prompt: 'one'
})
expect(second.sessionId).toBe(first.sessionId)
expect(mocks.createIntent).toHaveBeenCalledOnce()
})
it('still re-checks an unconfirmed resume instead of adopting its conversation twice', async () => {
await leaveFirstLaunchUnconfirmed({ resume: true })
expect(getStructuredAgentLaunchStatus(WORKTREE_ID, 'codex')).toBe('unknown')
expect(
startStructuredAgentLaunch(WORKTREE_ID, 'codex', { requestId: 'request-8', resumeFrom })
.sessionId
).toBe(unconfirmed.sessionId)
expect(mocks.createIntent).toHaveBeenCalledOnce()
})
it('never joins a resume of the same conversation id running on another host', async () => {
await leaveFirstLaunchUnconfirmed({ resume: true })
@@ -13,9 +13,7 @@ import { toast } from 'sonner'
import type { StructuredAgentSessionLaunchIntent } from '@/lib/launch-structured-agent-session'
import { StructuredAgentSessionCreateRefusalError } from '@/lib/agent-session/launch/structured-agent-session-launch-errors'
import { refreshLocalStructuredSessionTabs } from '@/runtime/local-structured-session-tabs-sync/inventory-refresh'
import { launchAndReconcile } from '@/lib/agent-session/launch/structured-agent-session-launch-recovery'
import {
cancelStructuredAgentLaunch,
retryStructuredAgentSessionLaunch,
startStructuredAgentLaunch
} from './structured-agent-session-launch'
@@ -24,91 +22,6 @@ import { hasStagedStructuredLaunchPrompt } from './structured-agent-session-laun
import { getStructuredAgentSessionLaunchSelection } from './structured-agent-session-launch-options'
describe('startStructuredAgentLaunch', () => {
it('preserves the draft seed when the launch is definitively refused', async () => {
const worktreeId = 'wt-draft-refused'
const intent = launchIntent(worktreeId, 'refused-draft-session')
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockRejectedValueOnce(new StructuredAgentSessionCreateRefusalError('refused'))
const launch = startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-3',
prompt: 'review this',
promptDelivery: 'draft'
})
await expect(launch.launchResult).rejects.toBeInstanceOf(
StructuredAgentSessionCreateRefusalError
)
await flushLaunchSettlement()
expect(composerDraft(intent.sessionId)).toBe('review this')
expect(getStructuredAgentSessionLaunchLifecycle(worktreeId, intent.sessionId)).toBe('failed')
})
it('preserves the draft seed when the launch fails with a known outcome', async () => {
const worktreeId = 'wt-draft-failed'
const intent = launchIntent(worktreeId, 'failed-draft-session')
mocks.createIntent.mockReturnValueOnce(intent)
// Non-refusal failures remain visibility-unknown and keep retry seeds; known failures come from recovery’s settled outcome.
vi.mocked(launchAndReconcile).mockRejectedValueOnce(new Error('boom'))
const launch = startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-4',
prompt: 'review this',
promptDelivery: 'draft'
})
await expect(launch.launchResult).rejects.toThrow()
await flushLaunchSettlement()
expect(composerDraft(intent.sessionId)).toBe('review this')
expect(getStructuredAgentSessionLaunchLifecycle(worktreeId, intent.sessionId)).toBe('failed')
})
it('preserves the ordinary draft seed when the launch is cancelled', async () => {
const worktreeId = 'wt-draft-cancelled'
const intent = launchIntent(worktreeId, 'cancelled-draft-session')
const create = Promise.withResolvers<{ sessionId: string; fence: number }>()
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockReturnValueOnce(create.promise)
const launch = startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-5',
prompt: 'review this',
promptDelivery: 'draft'
})
expect(mocks.launch).toHaveBeenCalledOnce()
expect(cancelStructuredAgentLaunch(worktreeId, intent.sessionId)).toBe(true)
create.resolve({ sessionId: intent.sessionId, fence: 1 })
await expect(launch.launchResult).rejects.toThrow('structured session launch cancelled')
await flushLaunchSettlement()
expect(composerDraft(intent.sessionId)).toBe('review this')
})
it('opens the chat without an informational progress toast', async () => {
const worktreeId = 'wt-open-quiet'
const intent = launchIntent(worktreeId, 'session-1')
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockResolvedValue({ sessionId: intent.sessionId, fence: 1 })
publishIntent(intent)
mocks.callStructuredAgentSession.mockResolvedValue({
ok: true,
value: { submission: { dispatchState: 'accepted' } }
})
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'request-6' })
await flushLaunchSettlement()
expect(mocks.launch).toHaveBeenCalledOnce()
expect(mocks.launch).toHaveBeenCalledWith(
intent,
expect.any(Function),
false,
expect.any(Function)
)
expect(toast.message).not.toHaveBeenCalled()
expect(toast.error).not.toHaveBeenCalled()
})
it('keeps a Claude and a Codex launch in the same worktree apart', async () => {
const worktreeId = 'wt-two-agents'
mocks.launch.mockImplementation(async (intent: StructuredAgentSessionLaunchIntent) => ({
@@ -151,26 +64,6 @@ describe('startStructuredAgentLaunch', () => {
expect(toast.error).not.toHaveBeenCalled()
})
// The chat's Retry line says a failed start; a toast beside it said it twice, again per Retry.
it('leaves a failed start and each failed Retry to the chat, with no toast', async () => {
const worktreeId = 'wt-claude-refused'
mocks.launch.mockRejectedValue(new StructuredAgentSessionCreateRefusalError('unsupported'))
const { sessionId } = startStructuredAgentLaunch(worktreeId, 'claude', {
requestId: 'request-9'
})
await flushLaunchSettlement()
expect(getStructuredAgentSessionLaunchLifecycle(worktreeId, sessionId)).toBe('failed')
expect(retryStructuredAgentSessionLaunch(worktreeId, sessionId)).toBe(true)
await flushLaunchSettlement()
expect(mocks.launch).toHaveBeenCalledTimes(2)
expect(getStructuredAgentSessionLaunchLifecycle(worktreeId, sessionId)).toBe('failed')
expect(toast.error).not.toHaveBeenCalled()
expect(toast.message).not.toHaveBeenCalled()
})
it.each(['late', 'missing', 'unavailable'] as const)(
'uses an older host’s receipt to send while tab inventory is %s',
async (inventory) => {
@@ -204,31 +97,6 @@ describe('startStructuredAgentLaunch', () => {
}
)
it('coalesces a duplicate click silently while the launch is in flight', async () => {
const worktreeId = 'wt-duplicate-click'
const intent = launchIntent(worktreeId)
let resolveLaunch: (receipt: { sessionId: string; fence: number }) => void = () => {}
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockImplementation(
() =>
new Promise<{ sessionId: string; fence: number }>((resolve) => (resolveLaunch = resolve))
)
publishIntent(intent)
mocks.callStructuredAgentSession.mockResolvedValue({
ok: true,
value: { submission: { dispatchState: 'accepted' } }
})
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'double-click' })
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'double-click' })
expect(mocks.createIntent).toHaveBeenCalledOnce()
expect(mocks.launch).toHaveBeenCalledOnce()
resolveLaunch({ sessionId: intent.sessionId, fence: 1 })
await flushLaunchSettlement()
expect(toast.error).not.toHaveBeenCalled()
})
it("delivers a repeated request's prompt once, to both callers, after the shared launch settles", async () => {
const worktreeId = 'wt-coalesced-prompt'
const intent = launchIntent(worktreeId)
@@ -340,44 +208,6 @@ describe('startStructuredAgentLaunch', () => {
expect(composerDraft(intent.sessionId)).toBe('first prompt')
})
it('reconciles a host commit when the create reply is lost', async () => {
const worktreeId = 'wt-response-loss'
const intent = launchIntent(worktreeId)
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockRejectedValueOnce(new Error('response lost'))
publishIntent(intent)
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'request-20' })
await flushLaunchSettlement()
expect(mocks.createIntent).toHaveBeenCalledOnce()
expect(mocks.launch).toHaveBeenCalledOnce()
expect(toast.error).not.toHaveBeenCalled()
})
it('logs the raw error of an unconfirmed start and leaves the telling to the chat', async () => {
const worktreeId = 'wt-raw-error-logged'
const intent = launchIntent(worktreeId)
const raw = new Error("EEXIST: file already exists, mkdir '/tmp/o97b/agent-sessions'")
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch.mockRejectedValue(raw)
vi.mocked(refreshLocalStructuredSessionTabs).mockResolvedValue([])
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'request-21' })
await flushLaunchSettlement()
expect(getStructuredAgentSessionLaunchLifecycle(worktreeId, intent.sessionId)).toBe(
'visibility-unknown'
)
expect(warn).toHaveBeenCalledWith('[native-chat] structured launch failed', raw)
expect(toast.error).not.toHaveBeenCalled()
} finally {
warn.mockRestore()
}
})
it('retries an absent unknown outcome with the exact same intent', async () => {
const worktreeId = 'wt-same-envelope-retry'
const intent = launchIntent(worktreeId)
@@ -476,30 +306,6 @@ describe('startStructuredAgentLaunch', () => {
expect(mocks.createIntent).toHaveBeenCalledOnce()
})
it('retries a definitively refused launch with the same session and a new operation', async () => {
const worktreeId = 'wt-refused'
const first = launchIntent(worktreeId, 'session-first')
mocks.createIntent.mockReturnValueOnce(first)
mocks.launch
.mockRejectedValueOnce(new StructuredAgentSessionCreateRefusalError('unsupported'))
.mockResolvedValueOnce({ sessionId: first.sessionId, fence: 1 })
publishIntent(first)
startStructuredAgentLaunch(worktreeId, 'codex', { requestId: 'request-27' })
await flushLaunchSettlement()
expect(retryStructuredAgentSessionLaunch(worktreeId, first.sessionId)).toBe(true)
await flushLaunchSettlement()
expect(mocks.createIntent).toHaveBeenCalledOnce()
expect(mocks.retryIntent).toHaveBeenCalledWith(first)
expect(mocks.launch.mock.calls[0]?.[0]).toBe(first)
expect(mocks.launch.mock.calls[1]?.[0]).toMatchObject({ sessionId: first.sessionId })
expect(mocks.launch.mock.calls[1]?.[0].params.envelope.clientOperationId).not.toBe(
first.params.envelope.clientOperationId
)
expect(toast.error).not.toHaveBeenCalled()
})
// A paired server's create seeds from its settings at create time, which its probe reports.
it("shows the seed a paired server's probe reports on a retry, not the first admission's", async () => {
const worktreeId = 'wt-paired-retry-seed'
@@ -532,33 +338,6 @@ describe('startStructuredAgentLaunch', () => {
})
})
it('retries a resumed launch by session id without reconstructing its identity', async () => {
const worktreeId = 'wt-resume-inline-retry'
const intent = {
...launchIntent(worktreeId, 'session-resume-inline-retry'),
params: {
...launchIntent(worktreeId, 'session-resume-inline-retry').params,
resumeFrom: { providerSessionId: 'provider-session-1' }
}
}
mocks.createIntent.mockReturnValueOnce(intent)
mocks.launch
.mockRejectedValueOnce(new StructuredAgentSessionCreateRefusalError('unsupported'))
.mockResolvedValueOnce({ sessionId: intent.sessionId, fence: 1 })
publishIntent(intent)
startStructuredAgentLaunch(worktreeId, 'codex', {
requestId: 'request-29',
resumeFrom: { providerSessionId: 'provider-session-1' }
})
await flushLaunchSettlement()
expect(retryStructuredAgentSessionLaunch(worktreeId, intent.sessionId)).toBe(true)
await flushLaunchSettlement()
expect(mocks.createIntent).toHaveBeenCalledOnce()
expect(mocks.retryIntent).toHaveBeenCalledWith(intent)
})
it("reports a repeated request's prompt as undelivered to both callers after refusal", async () => {
const worktreeId = 'wt-refused-coalesced-prompts'
const intent = launchIntent(worktreeId)
@@ -437,19 +437,6 @@ describe('a second "new chat" with no text', () => {
})
describe('a "new chat" with text', () => {
it('opens its own chat beside an idle empty one', async () => {
pick('plus-pick-1')
await publishIdle(first.sessionId)
const notes = pick('notes-send', { prompt: 'review notes' })
expect(notes.sessionId).toBe(second.sessionId)
await expect(notes.promptDeliveryResult).resolves.toEqual({
delivered: true,
failureNotified: false
})
})
it('opens its own chat beside an empty one still starting', async () => {
holdCreates()
const blank = pick('plus-pick-1')
@@ -415,41 +415,6 @@ it.each([10, 100])(
}
)
it.runIf(ORCA_LAYOUT_OWNER_IS_MAIN)(
'parks a live fresh destination across group/title changes and cache unpublish until actual attachment',
async () => {
const handle = openPositionDraft(),
scope = structuredAgentSessionDraftScopeKey(handle.sessionId)
await handle.finish()
const original = readNativeChatComposerDraft(scope)
const layout = publishedLayoutsOf(layoutApplyTestTransport.mainModel!)['wt-1']!
const moved = {
...layout,
groupLayout: undefined,
groups: [{ id: 'moved-group', tabIds: layout.tabs.map((tab) => tab.id) }],
tabs: layout.tabs.map((tab) => ({ ...tab, customTitle: 'renamed' }))
}
layoutApplyTestTransport.emitFrame({ type: 'workspace', key: 'wt-1', layout: moved })
await main.settle()
expect(useAppStore.getState().getTab(handle.tab.id)?.groupId).toBe('moved-group')
layoutApplyTestTransport.emitFrame({ type: 'removed', key: 'wt-1' })
await main.settle()
expect(useAppStore.getState().getTab(handle.tab.id)?.entityId).toBe(handle.sessionId)
// ast-grep-ignore: test-no-real-time-wait-tsx -- Attach the still-live destination after the former 1s expiry.
await new Promise((resolve) => setTimeout(resolve, 1050))
const input = createRef<NativeChatComposerInput>()
const view = render(
<StrictMode>
<PositionField scope={scope} tabId={handle.tab.id} input={input} />
</StrictMode>
)
expect(input.current!.selectionStart).toBe(POSITION_TEXT.length)
expect(input.current!.value).toBe(POSITION_TEXT)
expect(readNativeChatComposerDraft(scope)).toBe(original)
view.unmount()
}
)
it.runIf(!ORCA_LAYOUT_OWNER_IS_MAIN)(
'uses one addressed retirement for actual local close and makes a duplicate close inert',
async () => {
@@ -11,7 +11,6 @@ import { act, render, renderHook, within, fireEvent } from '@testing-library/rea
import { createRef, StrictMode } from 'react'
import type { NativeChatComposerInput } from '@/components/native-chat/native-chat-composer-input'
import { promptEditor } from '@/components/native-chat/native-chat-prompt-editor.test-support'
import { appendNativeChatDraftCache } from '@/components/native-chat/native-chat-draft-cache'
import { expect, it, vi } from 'vitest'
import { useAppStore } from '@/store/index'
import { adoptAgentSessionLaunchVerdict } from '../agent-session-launch-plan'
@@ -269,67 +268,6 @@ it.each(['before', 'mounted', 'delayed-disabled', 'metadata', 'ordinary', 'repla
}
)
it.each(['plain', 'rich', 'append'] as const)(
'delivers a genuinely late saved load without taking newer ownership: %s',
async (kind) => {
const scope = 'agent-session:late-position',
input = createRef<NativeChatComposerInput>()
clearNativeChatComposerDraftsForTests()
const loaded = Promise.withResolvers<ReadonlyMap<string, unknown>>()
setNativeChatComposerDraftStorageForTests({ ...storage, loadAll: () => loaded.promise })
const loading = hydrateNativeChatComposerDrafts()
const view = render(
<StrictMode>
<PositionField scope={scope} input={input} />
</StrictMode>
)
const field = within(view.container).getByRole('textbox')
if (kind === 'append') {
act(() => appendNativeChatDraftCache(scope, 'newer'))
}
// ast-grep-ignore: test-no-real-time-wait-tsx -- Release the actual saved load after the 1500ms startup wait.
await new Promise((resolve) => setTimeout(resolve, 1550))
const document =
kind === 'rich'
? {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'nativeChatSkill', attrs: { token: '$review', path: '/review' } },
{ type: 'text', text: ' context ' }
]
}
]
}
: undefined
const text =
kind === 'rich'
? '$review context '
: kind === 'append'
? 'Hand Off\ncontext'
: POSITION_TEXT
await act(async () => {
loaded.resolve(
new Map([[scope, { text, images: [], savedAt: 1, ...(document ? { document } : {}) }]])
)
await loading
})
if (kind === 'plain' || kind === 'rich') {
expect(input.current!.value).toBe(text)
expect([input.current!.selectionStart, input.current!.selectionEnd]).toEqual([0, 0])
if (document) {
expect(promptEditor(field).getJSON()).toEqual(document)
}
} else if (kind === 'append') {
expect(input.current!.value).toBe(`${text}\n\nnewer`)
expect(input.current!.selectionStart).toBeGreaterThan(0)
}
view.unmount()
}
)
it.each(['close-before-attach', 'consumed-remount', 'sibling-first'] as const)(
'uses the actual destination lifetime without changing its ordinary draft: %s',
async (schedule) => {
@@ -1,125 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Tab } from '../../../../shared/tab-types'
const mocks = vi.hoisted(() => ({
activateTab: vi.fn(),
callRuntimeRpc: vi.fn(async () => ({ ok: true })),
focusGroup: vi.fn(),
setActiveTabType: vi.fn(),
requestNativeChatComposerFocus: vi.fn(),
state: { unifiedTabsByWorktree: {} } as Record<string, unknown>
}))
vi.mock('@/store/index', () => ({
useAppStore: { getState: () => mocks.state }
}))
vi.mock('@/components/native-chat/use-native-chat-composer-reveal-focus', () => ({
requestNativeChatComposerFocus: mocks.requestNativeChatComposerFocus
}))
vi.mock('../worktree-runtime-owner', () => ({
runtimeEnvironmentIdOrLocalUnlessTwin: () => 'env-1'
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc }))
vi.mock('@/runtime/runtime-worktree-selector', () => ({
toRuntimeWorktreeSelector: (worktreeId: string) => `id:${worktreeId}`
}))
import {
activateStructuredAgentSessionById,
activateStructuredAgentSessionTab,
findStructuredAgentSessionTab
} from './structured-agent-session-tab-activation'
describe('findStructuredAgentSessionTab', () => {
const tab = {
id: 'structured-tab-1',
worktreeId: 'wt-1',
groupId: 'group-1',
contentType: 'agent-session',
entityId: 'session-1',
label: 'Codex Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
} satisfies Tab
it('does not match a session or tab from another workspace', () => {
expect(
findStructuredAgentSessionTab(
{ 'wt-1': [tab] },
{ workspaceId: 'wt-1', sessionId: 'session-2' }
)
).toBeNull()
expect(
findStructuredAgentSessionTab(
{ 'wt-2': [{ ...tab, worktreeId: 'wt-2' }] },
{ workspaceId: 'wt-1', sessionId: 'session-1' }
)
).toBeNull()
expect(
findStructuredAgentSessionTab(
{ 'wt-1': [{ ...tab, worktreeId: 'wt-2' }] },
{ workspaceId: 'wt-1', sessionId: 'session-1' }
)
).toBeNull()
})
})
describe('activateStructuredAgentSessionTab', () => {
beforeEach(() => {
vi.clearAllMocks()
const tab = {
id: 'structured-tab-1',
worktreeId: 'wt-1',
groupId: 'group-1',
contentType: 'agent-session',
entityId: 'session-1',
label: 'Codex Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0,
isPinned: false,
agentSessionAgent: 'codex'
} satisfies Tab
mocks.state = {
unifiedTabsByWorktree: { 'wt-1': [tab] },
focusGroup: mocks.focusGroup,
activateTab: mocks.activateTab,
setActiveTabType: mocks.setActiveTabType
}
})
it('selects the unified tab, focuses its composer, and synchronizes host focus', () => {
expect(
activateStructuredAgentSessionTab({ worktreeId: 'wt-1', tabId: 'structured-tab-1' })
).toBe(true)
expect(mocks.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
expect(mocks.activateTab).toHaveBeenCalledWith('structured-tab-1', { worktreeId: 'wt-1' })
expect(mocks.setActiveTabType).toHaveBeenCalledWith('agent-session', 'wt-1')
expect(mocks.requestNativeChatComposerFocus).toHaveBeenCalledWith('structured-tab-1')
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
{ kind: 'environment', environmentId: 'env-1' },
'session.tabs.activate',
{ worktree: 'id:wt-1', tabId: 'agent-session:session-1' }
)
})
it('routes a provider-owned vault row through its structured session id', () => {
expect(activateStructuredAgentSessionById({ worktreeId: 'wt-1', sessionId: 'session-1' })).toBe(
true
)
expect(mocks.activateTab).toHaveBeenCalledWith('structured-tab-1', { worktreeId: 'wt-1' })
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
{ kind: 'environment', environmentId: 'env-1' },
'session.tabs.activate',
{ worktree: 'id:wt-1', tabId: 'agent-session:session-1' }
)
})
})
@@ -1,12 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { toast } from 'sonner'
import type { CliInstallStatus } from '../../../shared/cli-install-types'
import {
CLI_PREREQUISITE_REGISTRATION_TOAST,
CLI_PREREQUISITE_REGISTRATION_TOAST_DESCRIPTION,
ensureOrcaCliAvailableForAgentSkillTerminal,
isOrcaCliAvailableOnPath
} from './agent-skill-cli-prerequisite'
import { ensureOrcaCliAvailableForAgentSkillTerminal } from './agent-skill-cli-prerequisite'
vi.mock('sonner', () => ({
toast: {
@@ -34,14 +29,6 @@ function cliStatus(overrides: Partial<CliInstallStatus> = {}): CliInstallStatus
}
}
describe('isOrcaCliAvailableOnPath', () => {
it('requires the installed CLI command to be visible on PATH', () => {
expect(isOrcaCliAvailableOnPath(cliStatus())).toBe(true)
expect(isOrcaCliAvailableOnPath(cliStatus({ pathConfigured: false }))).toBe(false)
expect(isOrcaCliAvailableOnPath(cliStatus({ state: 'not_installed' }))).toBe(false)
})
})
describe('ensureOrcaCliAvailableForAgentSkillTerminal', () => {
afterEach(() => {
vi.useRealTimers()
@@ -49,40 +36,6 @@ describe('ensureOrcaCliAvailableForAgentSkillTerminal', () => {
vi.clearAllMocks()
})
it('runs the CLI installer when the command exists but is not visible on PATH', async () => {
const initial = cliStatus({
pathConfigured: false,
detail: '/usr/local/bin is not currently visible on PATH.'
})
const installed = cliStatus()
const getInstallStatus = vi.fn().mockResolvedValue(initial)
const install = vi.fn().mockResolvedValue(installed)
const onStatusChange = vi.fn()
vi.stubGlobal('window', {
api: {
cli: {
getInstallStatus,
install
}
}
})
await expect(
ensureOrcaCliAvailableForAgentSkillTerminal({
onStatusChange,
registrationPromptDelayMs: 0
})
).resolves.toBe(installed)
expect(install).toHaveBeenCalledTimes(1)
expect(toast.message).toHaveBeenCalledWith(CLI_PREREQUISITE_REGISTRATION_TOAST, {
description: CLI_PREREQUISITE_REGISTRATION_TOAST_DESCRIPTION
})
expect(onStatusChange).toHaveBeenNthCalledWith(1, initial)
expect(onStatusChange).toHaveBeenNthCalledWith(2, installed)
})
it('does not run the CLI installer when the Windows PATH read is unknown', async () => {
const initial = cliStatus({
platform: 'win32',
@@ -104,36 +57,4 @@ describe('ensureOrcaCliAvailableForAgentSkillTerminal', () => {
expect.objectContaining({ description: initial.detail })
)
})
it('lets the registration toast paint before opening the native installer', async () => {
vi.useFakeTimers()
const initial = cliStatus({ state: 'stale' })
const installed = cliStatus()
const getInstallStatus = vi.fn().mockResolvedValue(initial)
const install = vi.fn().mockResolvedValue(installed)
vi.stubGlobal('window', {
setTimeout,
api: {
cli: {
getInstallStatus,
install
}
}
})
const pending = ensureOrcaCliAvailableForAgentSkillTerminal({ registrationPromptDelayMs: 700 })
await Promise.resolve()
expect(toast.message).toHaveBeenCalledWith(CLI_PREREQUISITE_REGISTRATION_TOAST, {
description: CLI_PREREQUISITE_REGISTRATION_TOAST_DESCRIPTION
})
expect(install).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(699)
expect(install).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
await expect(pending).resolves.toBe(installed)
expect(install).toHaveBeenCalledTimes(1)
})
})
@@ -1,107 +0,0 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import type { BundledSkillOp, BundledSkillPlan } from '../../../shared/bundled-skill-plan'
import { planOf, sharedEntry } from '../components/skills/bundled-skill-plan-fixtures.test-support'
import type { DiscoveredSkill } from '../../../shared/skills'
import {
getAgentSkillNavInstallStatus,
getLinearAgentSkillNavInstallStatus
} from './agent-skill-nav-install-status'
function skill(name: string): DiscoveredSkill {
const directoryPath = path.join('home', 'test', '.agents', 'skills', name)
return {
id: name,
name,
description: null,
providers: ['agent-skills'],
sourceKind: 'home',
sourceLabel: 'Agent skills home',
rootPath: path.dirname(directoryPath),
directoryPath,
skillFilePath: path.join(directoryPath, 'SKILL.md'),
installed: true,
updatedAt: null
}
}
function plan(...skills: [string, BundledSkillOp['kind']][]): BundledSkillPlan {
return planOf(skills.map(([name, kind]) => sharedEntry(name, kind)))
}
describe('getAgentSkillNavInstallStatus', () => {
it('keeps loading and missing states ahead of the plan', () => {
expect(
getAgentSkillNavInstallStatus({
name: 'orca-linear',
installed: true,
loading: true,
plan: plan(['orca-linear', 'keep'])
})
).toBe('checking')
expect(
getAgentSkillNavInstallStatus({
name: 'orca-linear',
installed: false,
loading: false,
plan: null
})
).toBe('install')
})
})
describe('getLinearAgentSkillNavInstallStatus', () => {
it('reports canonical installs under orca-linear', () => {
const skills = [skill('orca-linear')]
expect(
getLinearAgentSkillNavInstallStatus({
skills,
installed: true,
loading: false,
plan: plan(['orca-linear', 'keep'])
})
).toBe('up-to-date')
expect(
getLinearAgentSkillNavInstallStatus({
skills,
installed: true,
loading: false,
plan: plan(['orca-linear', 'update'])
})
).toBe('update-available')
})
it('reports legacy-only installs under linear-tickets', () => {
expect(
getLinearAgentSkillNavInstallStatus({
skills: [skill('linear-tickets')],
installed: true,
loading: false,
plan: plan(['linear-tickets', 'keep'])
})
).toBe('up-to-date')
})
it('prefers the plan for the canonical name when both names are installed', () => {
expect(
getLinearAgentSkillNavInstallStatus({
skills: [skill('orca-linear'), skill('linear-tickets')],
installed: true,
loading: false,
plan: plan(['orca-linear', 'keep'], ['linear-tickets', 'update'])
})
).toBe('up-to-date')
})
it('falls back to presence-only status when no plan applies', () => {
expect(
getLinearAgentSkillNavInstallStatus({
skills: [skill('orca-linear')],
installed: true,
loading: false,
plan: null
})
).toBe('installed')
})
})
@@ -26,22 +26,6 @@ describe('agent status connection ownership', () => {
expect(resolveAgentStatusConnectionRouting({ ptyId: 'ssh:ssh-a@broken' })).toBeUndefined()
})
it('marks known local, WSL, and remote-runtime PTYs as non-SSH', () => {
expect(
resolveAgentStatusConnectionRouting({ ptyId: 'pty-local-1', expectedConnectionId: null })
).toEqual({ connectionId: null })
expect(
resolveAgentStatusConnectionRouting({ ptyId: 'pty-wsl-1', expectedConnectionId: null })
).toEqual({ connectionId: null })
expect(
resolveAgentStatusConnectionRouting({
ptyId: 'remote:env-a@@terminal-1',
expectedConnectionId: null,
runtimeEnvironmentId: 'env-a'
})
).toEqual({ connectionId: null })
})
it('fails closed for missing, malformed, and cross-runtime ownership', () => {
expect(resolveAgentStatusConnectionRouting({ ptyId: null })).toBeUndefined()
expect(resolveAgentStatusConnectionRouting({ ptyId: 'remote:' })).toBeUndefined()
@@ -1,242 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { TerminalTab } from '../../../shared/terminal-tab-types'
import type { Worktree } from '../../../shared/worktree/types'
import { countWorkingAgents, getWorkingAgentsPerWorktree } from './agent-status'
function makeTab(overrides: Partial<TerminalTab> = {}): TerminalTab {
return {
id: 'tab-1',
ptyId: 'pty-1',
worktreeId: 'wt-1',
title: 'Terminal 1',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0,
...overrides
}
}
function worktrees(...ids: string[]): Record<string, Worktree[]> {
return {
repo: ids.map(
(id) =>
({
id,
repoId: 'repo',
path: `/path/${id}`,
head: '',
branch: '',
isBare: false,
isMainWorktree: false,
displayName: id,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}) satisfies Worktree
)
}
}
// Derive liveness from explicit tab IDs; preserved tab.ptyId is only a wake hint, not a live binding.
function livePtyMap(...tabIds: string[]): Record<string, string[]> {
return Object.fromEntries(tabIds.map((id, i) => [id, [`pty-${i}`]]))
}
describe('countWorkingAgents', () => {
it('counts each live working tab when pane-level titles are unavailable', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [
makeTab({ id: 'tab-1', title: '⠂ Claude Code' }),
makeTab({ id: 'tab-2', title: '✦ Gemini CLI' })
],
'wt-2': [makeTab({ id: 'tab-3', worktreeId: 'wt-2', title: '⠋ Codex is thinking' })]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: livePtyMap('tab-1', 'tab-2', 'tab-3'),
worktreesByRepo: worktrees('wt-1', 'wt-2')
})
).toBe(3)
})
it('counts working panes separately within the same tab', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {
'tab-1': {
1: '⠂ Claude Code',
2: '✦ Gemini CLI',
3: '✳ Claude Code'
}
},
ptyIdsByTabId: livePtyMap('tab-1'),
worktreesByRepo: worktrees('wt-1')
})
).toBe(2)
})
it('ignores non-working or non-live tabs', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [
makeTab({ id: 'tab-1', title: '✳ Claude Code' }),
makeTab({ id: 'tab-2', title: '✋ Gemini CLI' }),
makeTab({ id: 'tab-3', title: 'bash' }),
makeTab({ id: 'tab-4', title: '⠂ Claude Code' })
]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: livePtyMap('tab-1', 'tab-2', 'tab-3'),
worktreesByRepo: worktrees('wt-1')
})
).toBe(0)
})
it('prefers pane-level titles over the coarse tab title when available', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {
'tab-1': {
1: '✳ Claude Code',
2: 'bash'
}
},
ptyIdsByTabId: livePtyMap('tab-1'),
worktreesByRepo: worktrees('wt-1')
})
).toBe(0)
})
it('excludes orphaned worktrees not in worktreesByRepo', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })],
'wt-deleted': [makeTab({ id: 'tab-2', worktreeId: 'wt-deleted', title: '✦ Gemini CLI' })]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: livePtyMap('tab-1', 'tab-2'),
worktreesByRepo: worktrees('wt-1')
})
).toBe(1)
})
// Both pane-title and tab-title branches must reject slept tabs with preserved titles/ptyId but empty live bindings.
it('returns 0 for a slept tab whose preserved pane titles still match working (primary branch)', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {
'tab-1': { 0: '⠂ Claude Code' }
},
ptyIdsByTabId: { 'tab-1': [] },
worktreesByRepo: worktrees('wt-1')
})
).toBe(0)
})
it('returns 0 for a slept tab whose preserved tab.title still matches working (fallback branch)', () => {
expect(
countWorkingAgents({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: { 'tab-1': [] },
worktreesByRepo: worktrees('wt-1')
})
).toBe(0)
})
})
describe('getWorkingAgentsPerWorktree', () => {
it('returns per-pane labels and pane ids for split tabs', () => {
expect(
getWorkingAgentsPerWorktree({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {
'tab-1': {
1: '⠂ Claude Code',
2: '✦ Gemini CLI',
3: '✳ Claude Code'
}
},
ptyIdsByTabId: livePtyMap('tab-1'),
worktreesByRepo: worktrees('wt-1')
})
).toEqual({
'wt-1': {
agents: [
{ label: 'Claude Code', status: 'working', tabId: 'tab-1', paneId: 1 },
{ label: 'Gemini CLI', status: 'working', tabId: 'tab-1', paneId: 2 }
]
}
})
})
it('excludes orphaned worktrees not in worktreesByRepo', () => {
expect(
getWorkingAgentsPerWorktree({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })],
'wt-deleted': [makeTab({ id: 'tab-2', worktreeId: 'wt-deleted', title: '✦ Gemini CLI' })]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: livePtyMap('tab-1', 'tab-2'),
worktreesByRepo: worktrees('wt-1')
})
).toEqual({
'wt-1': {
agents: [{ label: 'Claude Code', status: 'working', tabId: 'tab-1', paneId: null }]
}
})
})
// Apply the same live-binding gate to both per-worktree count branches.
it('returns nothing for a slept tab whose preserved pane titles still match working (primary branch)', () => {
expect(
getWorkingAgentsPerWorktree({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {
'tab-1': { 0: '⠂ Claude Code' }
},
ptyIdsByTabId: { 'tab-1': [] },
worktreesByRepo: worktrees('wt-1')
})
).toEqual({})
})
it('returns nothing for a slept tab whose preserved tab.title still matches working (fallback branch)', () => {
expect(
getWorkingAgentsPerWorktree({
tabsByWorktree: {
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })]
},
runtimePaneTitlesByTabId: {},
ptyIdsByTabId: { 'tab-1': [] },
worktreesByRepo: worktrees('wt-1')
})
).toEqual({})
})
})
@@ -1,61 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
createAgentStatusEpochClock,
getAgentStatusEpochNow,
resetAgentStatusEpochClockForTests
} from './agent-status-epoch-clock'
describe('createAgentStatusEpochClock', () => {
it('samples once per epoch so repeated renders stay deterministic', () => {
const readNow = vi.fn<() => number>().mockReturnValueOnce(1_000).mockReturnValueOnce(2_000)
const clock = createAgentStatusEpochClock(readNow)
expect(clock(7)).toBe(1_000)
expect(clock(7)).toBe(1_000)
expect(readNow).toHaveBeenCalledTimes(1)
expect(clock(8)).toBe(2_000)
expect(readNow).toHaveBeenCalledTimes(2)
})
it('re-samples when the epoch advances again after repeating', () => {
let now = 0
const clock = createAgentStatusEpochClock(() => (now += 100))
expect(clock(1)).toBe(100)
expect(clock(2)).toBe(200)
expect(clock(2)).toBe(200)
expect(clock(3)).toBe(300)
expect(clock(1)).toBe(400)
expect(clock(1)).toBe(400)
})
})
describe('getAgentStatusEpochNow', () => {
afterEach(() => {
vi.useRealTimers()
resetAgentStatusEpochClockForTests()
})
it('reads the clock at call time so timers faked after creation still apply', () => {
// Call Date.now at sample time so fake timers installed after module loading still apply.
const clock = createAgentStatusEpochClock()
vi.useFakeTimers()
vi.setSystemTime(1_700_000_000_000)
expect(clock(1)).toBe(1_700_000_000_000)
})
it('rewinds with the store so a reset epoch does not reuse the last sample', () => {
vi.useFakeTimers()
vi.setSystemTime(5_000)
resetAgentStatusEpochClockForTests()
expect(getAgentStatusEpochNow(0)).toBe(5_000)
// A suite that rewinds the store to epoch 0 must rewind the sample too.
vi.setSystemTime(9_000)
expect(getAgentStatusEpochNow(0)).toBe(5_000)
resetAgentStatusEpochClockForTests()
expect(getAgentStatusEpochNow(0)).toBe(9_000)
})
})
@@ -1,82 +0,0 @@
import { describe, expect, it } from 'vitest'
import { resolveAgentStatusTerminalTitle } from './agent-status-terminal-title'
describe('resolveAgentStatusTerminalTitle', () => {
it('replaces stale Cursor spinner titles when hook state finishes', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'cursor', state: 'done' }, '\u2839 Cursor Agent')
).toBe('Cursor ready')
})
it('replaces bare Cursor native titles when hook state finishes', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'cursor', state: 'done' }, 'Cursor Agent')
).toBe('Cursor ready')
})
it('keeps descriptive completed titles that are already non-working', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'cursor', state: 'done' }, 'Orca Cursor Done')
).toBe('Orca Cursor Done')
})
it('uses permission titles for synthetic agents waiting on user input', () => {
expect(
resolveAgentStatusTerminalTitle(
{ agentType: 'cursor', state: 'waiting' },
'\u280b Cursor Agent'
)
).toBe('Cursor - action required')
})
it('clears stale permission titles when hook state finishes', () => {
expect(
resolveAgentStatusTerminalTitle(
{ agentType: 'cursor', state: 'done' },
'Cursor - action required'
)
).toBe('Cursor ready')
})
it('replaces stale Codex spinner titles when hook state finishes', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'codex', state: 'done' }, '\u280b Codex')
).toBe('Codex ready')
})
it('uses permission titles for Codex when hook state waits on user input', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'codex', state: 'waiting' }, '\u280b Codex')
).toBe('Codex - action required')
})
it('uses Devin synthetic titles for hook status transitions', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'devin', state: 'done' }, '\u280b Devin')
).toBe('Devin ready')
expect(
resolveAgentStatusTerminalTitle({ agentType: 'devin', state: 'waiting' }, '\u280b Devin')
).toBe('Devin - action required')
})
it('preserves native OpenCode titles through hook status transitions', () => {
expect(
resolveAgentStatusTerminalTitle(
{ agentType: 'opencode', state: 'done' },
'OC | Native Stable Session'
)
).toBe('OC | Native Stable Session')
expect(
resolveAgentStatusTerminalTitle(
{ agentType: 'opencode', state: 'waiting' },
'OC | Native Stable Session'
)
).toBe('OC | Native Stable Session')
})
it('does not invent an OpenCode title when no native title exists', () => {
expect(
resolveAgentStatusTerminalTitle({ agentType: 'opencode', state: 'done' }, undefined)
).toBeUndefined()
})
})
+2 -697
View File
@@ -1,527 +1,5 @@
import { describe, expect, it, test, vi } from 'vitest'
import {
detectAgentStatusFromTitle,
clearWorkingIndicators,
createAgentStatusTracker,
getAgentLabel,
isGeminiTerminalTitle,
isClaudeAgent,
isClaudeManagementTitle,
normalizeTerminalTitle,
mapAgentStatusStateToVisualStatus,
formatAgentTypeLabel,
agentTypeToIconAgent
} from './agent-status'
import { extractLastOscTitle } from '../../../shared/agent-detection'
describe('detectAgentStatusFromTitle', () => {
it('returns null for empty string', () => {
expect(detectAgentStatusFromTitle('')).toBeNull()
})
it('returns null for a title with no agent indicators', () => {
expect(detectAgentStatusFromTitle('bash')).toBeNull()
expect(detectAgentStatusFromTitle('vim myfile.ts')).toBeNull()
})
it('detects Gemini permission symbol ✋', () => {
expect(detectAgentStatusFromTitle('✋ Gemini CLI')).toBe('permission')
})
it('detects Gemini working symbol ✦', () => {
expect(detectAgentStatusFromTitle('✦ Gemini CLI')).toBe('working')
})
it('detects Gemini idle symbol ◇', () => {
expect(detectAgentStatusFromTitle('◇ Gemini CLI')).toBe('idle')
})
it('detects Gemini silent working symbol ⏲', () => {
expect(detectAgentStatusFromTitle('⏲ Working… (my-project)')).toBe('working')
})
it('Gemini permission takes precedence over working', () => {
expect(detectAgentStatusFromTitle('✋✦ Gemini CLI')).toBe('permission')
})
it('detects braille spinner ⠋ as working', () => {
expect(detectAgentStatusFromTitle('⠋ Codex is thinking')).toBe('working')
})
it('detects permission requests from agent titles', () => {
expect(detectAgentStatusFromTitle('Claude Code - action required')).toBe('permission')
})
it('detects "permission" keyword with agent name', () => {
expect(detectAgentStatusFromTitle('codex - permission needed')).toBe('permission')
})
it('detects "waiting" keyword with agent name', () => {
expect(detectAgentStatusFromTitle('gemini waiting for input')).toBe('permission')
})
it('detects "ready" keyword as idle', () => {
expect(detectAgentStatusFromTitle('claude ready')).toBe('idle')
})
it('detects "idle" keyword as idle', () => {
expect(detectAgentStatusFromTitle('codex idle')).toBe('idle')
})
it('detects "done" keyword as idle', () => {
expect(detectAgentStatusFromTitle('aider done')).toBe('idle')
})
it('detects "working" keyword as working', () => {
expect(detectAgentStatusFromTitle('claude working on task')).toBe('working')
})
it('detects "thinking" keyword as working', () => {
expect(detectAgentStatusFromTitle('gemini thinking')).toBe('working')
})
it('detects "running" keyword as working', () => {
expect(detectAgentStatusFromTitle('opencode running tests')).toBe('working')
})
it('detects ". " prefix as working (Claude Code)', () => {
expect(detectAgentStatusFromTitle('. claude')).toBe('working')
})
it('detects "* " prefix as idle (Claude Code)', () => {
expect(detectAgentStatusFromTitle('* claude')).toBe('idle')
})
it('detects ✳ prefix as idle (Claude Code with task description)', () => {
expect(detectAgentStatusFromTitle('✳ User acknowledgment and confirmation')).toBe('idle')
})
it('detects ✳ prefix as idle (Claude Code with agent name)', () => {
expect(detectAgentStatusFromTitle('✳ Claude Code')).toBe('idle')
})
it('detects braille spinner as working (Claude Code with task description)', () => {
expect(detectAgentStatusFromTitle('⠐ User acknowledgment and confirmation')).toBe('working')
})
it('detects braille spinner as working (Claude Code with agent name)', () => {
expect(detectAgentStatusFromTitle('⠂ Claude Code')).toBe('working')
})
it('returns idle for bare agent name "claude"', () => {
expect(detectAgentStatusFromTitle('claude')).toBe('idle')
})
it('returns idle for bare agent name "codex"', () => {
expect(detectAgentStatusFromTitle('codex')).toBe('idle')
})
it('returns idle for bare agent name "aider"', () => {
expect(detectAgentStatusFromTitle('aider')).toBe('idle')
})
it('returns idle for bare agent name "opencode"', () => {
expect(detectAgentStatusFromTitle('opencode')).toBe('idle')
})
it('classifies OpenClaude titles without falling through to Claude naming', () => {
expect(detectAgentStatusFromTitle('OpenClaude ready')).toBe('idle')
expect(detectAgentStatusFromTitle('OpenClaude running')).toBe('working')
expect(detectAgentStatusFromTitle('OpenClaude - action required')).toBe('permission')
expect(detectAgentStatusFromTitle('⠋ OpenClaude')).toBe('working')
})
it('excludes the exact Claude agents management title', () => {
expect(detectAgentStatusFromTitle('claude agents')).toBeNull()
expect(detectAgentStatusFromTitle(' Claude Agents ')).toBeNull()
expect(detectAgentStatusFromTitle('claude.exe agents')).toBeNull()
expect(detectAgentStatusFromTitle('Claude.CMD agents')).toBeNull()
expect(detectAgentStatusFromTitle('claude.bat agents')).toBeNull()
expect(detectAgentStatusFromTitle('Claude.PS1 agents')).toBeNull()
expect(
detectAgentStatusFromTitle('C:\\Users\\dev\\AppData\\Roaming\\npm\\claude.cmd agents')
).toBeNull()
expect(
detectAgentStatusFromTitle('"C:\\Users\\dev\\AppData\\Roaming\\npm\\claude.cmd" agents')
).toBeNull()
expect(detectAgentStatusFromTitle('claude agents working')).toBe('working')
})
it('detects Pi idle titles', () => {
expect(detectAgentStatusFromTitle('π - my-project')).toBe('idle')
expect(detectAgentStatusFromTitle('π - session-name - my-project')).toBe('idle')
})
it('treats the bare "Cursor Agent" native title as a no-op (not idle)', () => {
// Why: if the native title classified as idle, cursor's per-turn re-emissions would stomp the spinner off mid-turn.
expect(detectAgentStatusFromTitle('Cursor Agent')).toBeNull()
expect(detectAgentStatusFromTitle('cursor agent')).toBeNull()
expect(detectAgentStatusFromTitle(' Cursor Agent ')).toBeNull()
})
it('classifies synthesized "⠋ Cursor Agent" working title as working', () => {
expect(detectAgentStatusFromTitle('⠋ Cursor Agent')).toBe('working')
})
it('classifies synthesized "Cursor ready" idle title as idle', () => {
expect(detectAgentStatusFromTitle('Cursor ready')).toBe('idle')
})
it('classifies synthesized "Cursor - action required" title as permission', () => {
expect(detectAgentStatusFromTitle('Cursor - action required')).toBe('permission')
})
it('classifies synthesized Droid titles', () => {
expect(detectAgentStatusFromTitle('⠋ Droid')).toBe('working')
expect(detectAgentStatusFromTitle('Droid ready')).toBe('idle')
expect(detectAgentStatusFromTitle('Droid - action required')).toBe('permission')
expect(detectAgentStatusFromTitle('Droid working')).toBe('working')
})
it('classifies synthesized Hermes titles', () => {
expect(detectAgentStatusFromTitle('⠋ Hermes')).toBe('working')
expect(detectAgentStatusFromTitle('Hermes ready')).toBe('idle')
expect(detectAgentStatusFromTitle('Hermes - action required')).toBe('permission')
expect(detectAgentStatusFromTitle('Hermes working')).toBe('working')
})
it('classifies synthesized Devin titles', () => {
expect(detectAgentStatusFromTitle('⠋ Devin')).toBe('working')
expect(detectAgentStatusFromTitle('Devin ready')).toBe('idle')
expect(detectAgentStatusFromTitle('Devin - action required')).toBe('permission')
expect(detectAgentStatusFromTitle('Devin working')).toBe('working')
})
it('does not treat Factory Droid native needs-input titles as completion', () => {
expect(detectAgentStatusFromTitle('Factory Droid needs input')).toBeNull()
expect(detectAgentStatusFromTitle('Factory Droid needs your input')).toBeNull()
})
it('is case-insensitive for agent names', () => {
expect(detectAgentStatusFromTitle('CLAUDE')).toBe('idle')
expect(detectAgentStatusFromTitle('Codex Working')).toBe('working')
})
// Why: `containsAgentName` token-matches, so cwd-path fragments like "~/codex-scratch" no longer mint an 'idle' agent signal.
it('does not treat cwd-path agent-name fragments as agent activity', () => {
expect(detectAgentStatusFromTitle('~/codex-scratch')).toBeNull()
expect(detectAgentStatusFromTitle('~/codex already built')).toBeNull()
expect(detectAgentStatusFromTitle('opencode-blinker')).toBeNull()
expect(detectAgentStatusFromTitle('claude-scratch')).toBeNull()
})
// Why: short agent names are unsafe under substring detection; don't add aliases that turn "timestamp ready" into agent activity.
it('does not treat ordinary words containing "amp" as agent titles', () => {
expect(detectAgentStatusFromTitle('timestamp ready')).toBeNull()
expect(detectAgentStatusFromTitle('clamp working')).toBeNull()
expect(detectAgentStatusFromTitle('example permission needed')).toBeNull()
})
it('does not treat Android terminal titles as Droid agent titles', () => {
expect(detectAgentStatusFromTitle('android')).toBeNull()
expect(detectAgentStatusFromTitle('android emulator ready')).toBeNull()
expect(detectAgentStatusFromTitle('android build working')).toBeNull()
expect(detectAgentStatusFromTitle('android permission check')).toBeNull()
})
it('does not treat path fragments containing Hermes as agent activity', () => {
expect(detectAgentStatusFromTitle('~/hermes/working')).not.toBe('working')
expect(detectAgentStatusFromTitle('C:\\hermes\\ready')).toBeNull()
})
})
// Why: regression guard — a path fragment like `~/codex/working` must never classify as 'working' (path separators aren't word boundaries).
describe('detectAgentStatusFromTitle path-separator rejection', () => {
test('rejects working keywords adjacent to POSIX path separators', () => {
expect(detectAgentStatusFromTitle('~/codex/working')).not.toBe('working')
expect(detectAgentStatusFromTitle('~/codex/thinking')).not.toBe('working')
expect(detectAgentStatusFromTitle('~/codex/running')).not.toBe('working')
})
test('rejects working keywords adjacent to Windows path separators', () => {
expect(detectAgentStatusFromTitle('C:\\codex\\working')).not.toBe('working')
expect(detectAgentStatusFromTitle('C:\\aider\\thinking')).not.toBe('working')
})
test('rejects working keywords adjacent to `.` separators', () => {
expect(detectAgentStatusFromTitle('codex.working')).not.toBe('working')
expect(detectAgentStatusFromTitle('aider.thinking')).not.toBe('working')
})
test('still accepts legitimate idle/working titles separated by whitespace', () => {
expect(detectAgentStatusFromTitle('Codex done')).toBe('idle')
expect(detectAgentStatusFromTitle('OpenCode ready')).toBe('idle')
expect(detectAgentStatusFromTitle('Aider idle')).toBe('idle')
expect(detectAgentStatusFromTitle('Codex working')).toBe('working')
expect(detectAgentStatusFromTitle('Aider thinking')).toBe('working')
})
// Why: block path separators only on the LEFT of the keyword; blocking the right would regress titles ending in `.`/`!`/`?`.
test('still accepts keywords followed by trailing punctuation', () => {
expect(detectAgentStatusFromTitle('Codex done.')).toBe('idle')
expect(detectAgentStatusFromTitle('Aider idle!')).toBe('idle')
expect(detectAgentStatusFromTitle('OpenCode ready?')).toBe('idle')
expect(detectAgentStatusFromTitle('Codex working.')).toBe('working')
expect(detectAgentStatusFromTitle('Aider thinking...')).toBe('working')
})
})
describe('clearWorkingIndicators', () => {
it.each([
['Codex:working;thinking|running!', 'Codex:;|!'],
['Codex (working),thinking:running…', 'Codex (),:…'],
['Codex working thinking running', 'Codex'],
['Codex working/thinking', 'Codex /thinking'],
['Codex working\\thinking', 'Codex \\thinking'],
['Codex reworking is-thinking-cap C:\\running', 'Codex reworking is-thinking-cap C:\\running']
])('preserves delimiters while removing status words from %s', (title, expected) => {
expect(clearWorkingIndicators(title)).toBe(expected)
})
it('strips Claude Code ". " working prefix', () => {
const cleared = clearWorkingIndicators('. claude')
expect(cleared).toBe('claude')
expect(detectAgentStatusFromTitle(cleared)).not.toBe('working')
})
it('strips braille spinner characters and working keywords', () => {
const cleared = clearWorkingIndicators('⠋ Codex is thinking')
expect(cleared).toBe('Codex is')
expect(detectAgentStatusFromTitle(cleared)).not.toBe('working')
})
it('strips Gemini working symbol', () => {
const cleared = clearWorkingIndicators('✦ Gemini CLI')
expect(cleared).toBe('Gemini CLI')
expect(detectAgentStatusFromTitle(cleared)).not.toBe('working')
})
it('strips Gemini silent working symbol ⏲', () => {
const cleared = clearWorkingIndicators('⏲ Working… (my-project)')
expect(detectAgentStatusFromTitle(cleared)).not.toBe('working')
})
it('returns original title if no working indicators found', () => {
expect(clearWorkingIndicators('* claude')).toBe('* claude')
expect(clearWorkingIndicators('Terminal 1')).toBe('Terminal 1')
})
// Why: clearWorkingIndicators must use the same hyphen-aware boundary as STRONG_WORKING_KEYWORDS_RE so clearer and detector stay symmetric.
it('does not strip working keywords inside hyphenated compounds', () => {
expect(clearWorkingIndicators('codex is-working-cap')).toBe('codex is-working-cap')
expect(clearWorkingIndicators('claude reworking diff')).toBe('claude reworking diff')
expect(clearWorkingIndicators('codex overthinking it')).toBe('codex overthinking it')
})
it('still strips working keywords at whitespace boundaries', () => {
const cleared = clearWorkingIndicators('Codex working on tests')
expect(cleared).not.toMatch(/\bworking\b/)
expect(detectAgentStatusFromTitle(cleared)).not.toBe('working')
})
})
describe('normalizeTerminalTitle', () => {
it('collapses Gemini working titles to a stable label', () => {
expect(normalizeTerminalTitle('✦ Typing prompt... (workspace)')).toBe('✦ Gemini CLI')
expect(normalizeTerminalTitle('⏲ Working… (workspace)')).toBe('✦ Gemini CLI')
})
it('collapses Gemini idle and permission titles to stable labels', () => {
expect(normalizeTerminalTitle('◇ Ready (workspace)')).toBe('◇ Gemini CLI')
expect(normalizeTerminalTitle('✋ Action Required (workspace)')).toBe('✋ Gemini CLI')
})
it('leaves non-Gemini titles unchanged', () => {
expect(normalizeTerminalTitle('⠂ Claude Code')).toBe('⠂ Claude Code')
expect(normalizeTerminalTitle('bash')).toBe('bash')
})
it('collapses Grok rotating working titles to one stable label', () => {
// Grok interpolates a rotating phrase between spinner and name, so each frame differs; all must fold to one label.
expect(normalizeTerminalTitle('⠋ - Waiting for response… - grok')).toBe('⠋ Grok')
expect(normalizeTerminalTitle('⠴ - Thinking - grok')).toBe('⠋ Grok')
expect(normalizeTerminalTitle('⠙ - Responding - grok')).toBe('⠋ Grok')
expect(normalizeTerminalTitle('⠦ - Sleep 2s then echo hello-from-grok… - grok')).toBe('⠋ Grok')
expect(
normalizeTerminalTitle('⠹ - Waiting for response… - Execute Shell Command sleep 2 - grok')
).toBe('⠋ Grok')
// Idempotent: re-normalizing our own collapsed label stays stable.
expect(normalizeTerminalTitle('⠋ Grok')).toBe('⠋ Grok')
})
it('leaves Grok idle and session titles unchanged', () => {
// No spinner → not a working frame; the meaningful final title still shows.
expect(normalizeTerminalTitle('grok')).toBe('grok')
expect(normalizeTerminalTitle('Fix the auth bug - grok')).toBe('Fix the auth bug - grok')
// Names "grok" mid-title but ends with another agent → not a Grok frame.
expect(normalizeTerminalTitle('⠋ debugging grok - claude')).toBe('⠋ debugging grok - claude')
// Only the Grok frame shape "spinner - phrase - grok" is the rotating-frame signal; task text ending in "grok" must not collapse.
expect(normalizeTerminalTitle('⠋ wire up grok')).toBe('⠋ wire up grok')
expect(normalizeTerminalTitle('⠋ Codex is thinking about grok')).toBe(
'⠋ Codex is thinking about grok'
)
expect(normalizeTerminalTitle('⠋ support for Grok')).toBe('⠋ support for Grok')
// Trailing " - grok" alone is not enough without the post-spinner delimiter.
expect(normalizeTerminalTitle('⠋ fix the flaky suite - grok')).toBe(
'⠋ fix the flaky suite - grok'
)
})
it('preserves Pi titles and canonicalizes only the spinner frame', () => {
// Only the braille frame churns; the rest is the session name and cwd (#16093).
expect(normalizeTerminalTitle('⠋ π - my-project')).toBe('⠋ π - my-project')
expect(normalizeTerminalTitle('⠙ π - my-project')).toBe('⠋ π - my-project')
expect(normalizeTerminalTitle('π - my-project')).toBe('π - my-project')
expect(normalizeTerminalTitle('⠋ π: my-project')).toBe('⠋ π: my-project')
expect(normalizeTerminalTitle('π: my-project')).toBe('π: my-project')
expect(normalizeTerminalTitle('π -')).toBe('π -')
expect(normalizeTerminalTitle('π:')).toBe('π:')
expect(normalizeTerminalTitle('π ')).toBe('π ')
})
it('does not route Pi titles whose cwd mentions Gemini into Gemini normalization', () => {
expect(normalizeTerminalTitle('⠋ π - gemini')).toBe('⠋ π - gemini')
expect(normalizeTerminalTitle('π - gemini')).toBe('π - gemini')
expect(normalizeTerminalTitle('⠋ π: gemini')).toBe('⠋ π: gemini')
expect(normalizeTerminalTitle('π: gemini')).toBe('π: gemini')
expect(normalizeTerminalTitle('⠋ π gemini')).toBe('⠋ π gemini')
expect(normalizeTerminalTitle('π gemini')).toBe('π gemini')
expect(normalizeTerminalTitle('⠋ π - gemini-project')).toBe('⠋ π - gemini-project')
expect(normalizeTerminalTitle('π - gemini-project')).toBe('π - gemini-project')
})
})
describe('isGeminiTerminalTitle', () => {
it('detects Gemini titles by symbol or name', () => {
expect(isGeminiTerminalTitle('✦ Typing prompt... (workspace)')).toBe(true)
expect(isGeminiTerminalTitle('◇ Ready (workspace)')).toBe(true)
expect(isGeminiTerminalTitle('gemini waiting for input')).toBe(true)
})
it('does not match other terminal titles', () => {
expect(isGeminiTerminalTitle('⠂ Claude Code')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π - gemini')).toBe(false)
expect(isGeminiTerminalTitle('π - gemini')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π: gemini')).toBe(false)
expect(isGeminiTerminalTitle('π: gemini')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π gemini')).toBe(false)
expect(isGeminiTerminalTitle('π gemini')).toBe(false)
expect(isGeminiTerminalTitle('π -')).toBe(false)
expect(isGeminiTerminalTitle('π:')).toBe(false)
expect(isGeminiTerminalTitle('π ')).toBe(false)
expect(isGeminiTerminalTitle('⠋ π - gemini-project')).toBe(false)
expect(isGeminiTerminalTitle('/tmp/gemini/working')).toBe(false)
expect(isGeminiTerminalTitle('bash')).toBe(false)
})
})
describe('getAgentLabel', () => {
it('labels Pi working titles as Pi instead of Claude Code', () => {
expect(getAgentLabel('⠋ π - my-project')).toBe('Pi')
})
it('treats Claude Code prefixed task titles as Claude even when they mention another CLI', () => {
expect(getAgentLabel('✳ Gemini CLI')).toBe('Claude Code')
expect(getAgentLabel('. Compare Opencode Vs Orca')).toBe('Claude Code')
expect(getAgentLabel('* Review Codex behavior')).toBe('Claude Code')
})
it('labels supported agent families consistently', () => {
expect(getAgentLabel('✦ Gemini CLI')).toBe('Gemini CLI')
expect(getAgentLabel('⠂ Claude Code')).toBe('Claude Code')
expect(getAgentLabel('⠋ Codex is thinking')).toBe('Codex')
expect(getAgentLabel('OpenClaude running')).toBe('OpenClaude')
expect(getAgentLabel('⠋ OpenClaude')).toBe('OpenClaude')
expect(getAgentLabel('Antigravity running')).toBe('Antigravity')
expect(getAgentLabel('agy working')).toBe('Antigravity')
expect(getAgentLabel('Grok running')).toBe('Grok')
expect(getAgentLabel('⠋ Droid')).toBe('Droid')
expect(getAgentLabel('Droid ready')).toBe('Droid')
expect(getAgentLabel('⠋ Hermes')).toBe('Hermes')
expect(getAgentLabel('Hermes ready')).toBe('Hermes')
expect(getAgentLabel('⠋ Devin')).toBe('Devin')
expect(getAgentLabel('Devin ready')).toBe('Devin')
})
it('does not label the Claude agents management title', () => {
expect(getAgentLabel('claude agents')).toBeNull()
})
it('labels GitHub Copilot CLI', () => {
expect(getAgentLabel('copilot working')).toBe('GitHub Copilot')
expect(getAgentLabel('copilot idle')).toBe('GitHub Copilot')
expect(getAgentLabel('GitHub Copilot CLI')).toBe('GitHub Copilot')
})
it('does not label Android titles as Droid', () => {
expect(getAgentLabel('android emulator ready')).toBeNull()
})
// Why: substring matching mislabeled cwd/worktree name fragments (e.g. "opencode-blinker") as agents; token-match to reject them.
it('does not label cwd/worktree path fragments as an agent', () => {
expect(getAgentLabel('opencode-blinker')).toBeNull()
expect(getAgentLabel('claude-scratch')).toBeNull()
expect(getAgentLabel('~/projects/codex-scratch')).toBeNull()
expect(getAgentLabel('~/cursor-rules')).toBeNull()
expect(getAgentLabel('grok-fixtures')).toBeNull()
expect(getAgentLabel('devin-fixtures')).toBeNull()
expect(getAgentLabel('aider-config')).toBeNull()
})
it('still labels real agent titles that contain the name as a token', () => {
expect(getAgentLabel('OpenCode ready')).toBe('OpenCode')
expect(getAgentLabel('claude.exe')).toBe('Claude Code')
expect(getAgentLabel('openclaude.cmd')).toBe('OpenClaude')
expect(getAgentLabel('⠋ Codex')).toBe('Codex')
expect(getAgentLabel('Aider idle')).toBe('Aider')
expect(getAgentLabel('Devin working')).toBe('Devin')
})
// Why: "cursor" is ordinary editor vocabulary, so a bare token isn't Cursor identity; match Cursor's closed title set only.
it('labels Cursor by identity, not a bare "cursor" token in another agent title', () => {
expect(getAgentLabel('Cursor Agent')).toBe('Cursor')
expect(getAgentLabel('⠋ Cursor Agent')).toBe('Cursor')
expect(getAgentLabel('Cursor ready')).toBe('Cursor')
expect(getAgentLabel('Cursor - action required')).toBe('Cursor')
expect(getAgentLabel('⠋ preserve cursor visibility across replays')).toBe('Claude Code')
expect(getAgentLabel('⠋ Codex: fix cursor offsets')).toBe('Codex')
expect(getAgentLabel('Terminal Cursor and Orca slows down')).toBeNull()
})
})
describe('isClaudeAgent', () => {
it('keeps OpenClaude out of Claude-specific prompt-cache detection', () => {
expect(isClaudeAgent('⠋ Claude Code')).toBe(true)
expect(isClaudeAgent('⠋ OpenClaude')).toBe(false)
expect(isClaudeAgent('OpenClaude ready')).toBe(false)
})
// Why: a Claude title merely mentioning a text cursor is still Claude; only Cursor's own identity titles are excluded.
it('counts cursor-mentioning Claude braille titles as Claude, excludes real Cursor', () => {
expect(isClaudeAgent('⠋ preserve cursor visibility across replays')).toBe(true)
expect(isClaudeAgent('⠋ Cursor Agent')).toBe(false)
})
it('does not classify non-prefix Claude mentions as Claude agent titles', () => {
expect(isClaudeAgent('ask claude later')).toBe(false)
expect(getAgentLabel('ask claude later')).toBeNull()
})
it('does not classify the Claude agents management title as a Claude agent', () => {
expect(isClaudeManagementTitle(' Claude Agents ')).toBe(true)
expect(isClaudeManagementTitle('claude.exe agents')).toBe(true)
expect(isClaudeManagementTitle('claude.cmd agents')).toBe(true)
expect(isClaudeManagementTitle('claude.bat agents')).toBe(true)
expect(isClaudeManagementTitle('claude.ps1 agents')).toBe(true)
expect(
isClaudeManagementTitle('C:\\Users\\dev\\AppData\\Roaming\\npm\\claude.cmd agents')
).toBe(true)
expect(
isClaudeManagementTitle('"C:\\Users\\dev\\AppData\\Roaming\\npm\\claude.cmd" agents')
).toBe(true)
expect(isClaudeAgent('claude agents')).toBe(false)
})
})
import { describe, expect, it, vi } from 'vitest'
import { createAgentStatusTracker } from './agent-status'
describe('createAgentStatusTracker', () => {
// Claude Code v2.1.86 emits task descriptions; only ✳ identifies idle in this captured sequence.
@@ -548,47 +26,6 @@ describe('createAgentStatusTracker', () => {
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('fires on Gemini CLI working → idle (real title patterns)', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('◇ Ready (my-project)')
expect(onBecameIdle).not.toHaveBeenCalled()
tracker.handleTitle('✦ Implementing feature (my-project)')
expect(onBecameIdle).not.toHaveBeenCalled()
tracker.handleTitle('◇ Ready (my-project)')
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('fires on Gemini CLI working → permission', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('✦ Working… (my-project)')
tracker.handleTitle('✋ Action Required (my-project)')
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('fires on Gemini CLI silent working → idle', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('⏲ Working… (my-project)')
tracker.handleTitle('◇ Ready (my-project)')
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('fires on Codex working → idle', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('⠋ Codex is thinking')
tracker.handleTitle('codex')
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
// Cursor’s bare native title between synthesized frames must not trigger idle before done.
it('fires on Cursor working → idle across native "Cursor Agent" re-emissions', () => {
const onBecameIdle = vi.fn()
@@ -607,15 +44,6 @@ describe('createAgentStatusTracker', () => {
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('fires on Pi working → idle', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('⠋ π - my-project')
tracker.handleTitle('π - my-project')
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('does not fire when Factory Droid reports needs input during a working turn', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
@@ -648,25 +76,6 @@ describe('createAgentStatusTracker', () => {
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('does not fire on idle → idle', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('✳ Claude Code')
tracker.handleTitle('✳ Some other task')
expect(onBecameIdle).not.toHaveBeenCalled()
})
it('does not fire on working → working', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
tracker.handleTitle('⠂ Claude Code')
tracker.handleTitle('⠐ Fix the thing')
tracker.handleTitle('⠂ Fix the thing')
expect(onBecameIdle).not.toHaveBeenCalled()
})
// Why: reset() clears the working latch so a late/reattach idle title can't fire a phantom notification after teardown.
it('reset() clears working state so a subsequent idle does not fire onBecameIdle', () => {
const onBecameIdle = vi.fn()
@@ -677,108 +86,4 @@ describe('createAgentStatusTracker', () => {
tracker.handleTitle('✳ Claude Code')
expect(onBecameIdle).not.toHaveBeenCalled()
})
it('end-to-end: extracts OSC title and detects Claude Code transition', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
const oscTitle = (title: string): string => `\x1b]0;${title}\x07`
const chunks = [
`some output${oscTitle('✳ Claude Code')}more output`,
`data${oscTitle('⠂ Claude Code')}stuff`,
`response text${oscTitle('⠐ Fix the login bug')}more`,
`final output${oscTitle('✳ Fix the login bug')}done`
]
for (const chunk of chunks) {
const title = extractLastOscTitle(chunk)
if (title !== null) {
tracker.handleTitle(title)
}
}
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
it('end-to-end: extracts OSC title and detects Gemini transition', () => {
const onBecameIdle = vi.fn()
const tracker = createAgentStatusTracker(onBecameIdle)
const oscTitle = (title: string): string => `\x1b]0;${title}\x07`
const chunks = [
oscTitle('◇ Ready (workspace)'),
oscTitle('✦ Analyzing code (workspace)'),
oscTitle('◇ Ready (workspace)')
]
for (const chunk of chunks) {
const title = extractLastOscTitle(chunk)
if (title !== null) {
tracker.handleTitle(title)
}
}
expect(onBecameIdle).toHaveBeenCalledTimes(1)
})
})
describe('mapAgentStatusStateToVisualStatus', () => {
it("maps 'working' to 'working'", () => {
expect(mapAgentStatusStateToVisualStatus('working')).toBe('working')
})
it("maps 'blocked' to 'permission'", () => {
expect(mapAgentStatusStateToVisualStatus('blocked')).toBe('permission')
})
it("maps 'waiting' to 'permission'", () => {
expect(mapAgentStatusStateToVisualStatus('waiting')).toBe('permission')
})
it("maps 'done' to 'done'", () => {
expect(mapAgentStatusStateToVisualStatus('done')).toBe('done')
})
})
describe('formatAgentTypeLabel', () => {
it("returns 'Agent' for null", () => {
expect(formatAgentTypeLabel(null)).toBe('Agent')
})
it("returns 'Agent' for undefined", () => {
expect(formatAgentTypeLabel(undefined)).toBe('Agent')
})
it("returns 'Agent' for 'unknown'", () => {
expect(formatAgentTypeLabel('unknown')).toBe('Agent')
})
it('passes through arbitrary custom agent names as-is', () => {
expect(formatAgentTypeLabel('weirdo')).toBe('weirdo')
})
})
describe('agentTypeToIconAgent', () => {
it('returns null for null', () => {
expect(agentTypeToIconAgent(null)).toBeNull()
})
it('returns null for undefined', () => {
expect(agentTypeToIconAgent(undefined)).toBeNull()
})
it("returns null for 'unknown'", () => {
expect(agentTypeToIconAgent('unknown')).toBeNull()
})
it('keeps an icon identity for recognition-only DeepSeek Build', () => {
expect(agentTypeToIconAgent('dsb')).toBe('dsb')
})
it('returns null for arbitrary non-iconable strings', () => {
// Why: unknown agentTypes must return null so the caller falls back to a neutral glyph, not a broken icon.
expect(agentTypeToIconAgent('totally-fake-agent')).toBeNull()
})
})
@@ -1,74 +0,0 @@
import { describe, expect, it } from 'vitest'
import { listBoundAgentTabActions, resolveDefaultAgentForNewTab } from './agent-tab-shortcuts'
describe('listBoundAgentTabActions', () => {
it('returns only agents whose per-agent action has a user-assigned chord', () => {
expect(
listBoundAgentTabActions(
{
'tab.newAgent.claude': ['Mod+Alt+Shift+C'],
'tab.newAgent.codex': [],
'tab.newTerminal': ['Mod+T']
},
[]
)
).toEqual([{ agent: 'claude', actionId: 'tab.newAgent.claude' }])
})
it('skips disabled agents even when their action is bound', () => {
expect(
listBoundAgentTabActions(
{
'tab.newAgent.claude': ['Mod+Alt+Shift+C'],
'tab.newAgent.codex': ['Mod+Alt+Shift+X']
},
['claude']
)
).toEqual([{ agent: 'codex', actionId: 'tab.newAgent.codex' }])
})
it('returns nothing without overrides', () => {
expect(listBoundAgentTabActions(undefined, [])).toEqual([])
expect(listBoundAgentTabActions({}, null)).toEqual([])
})
})
describe('resolveDefaultAgentForNewTab', () => {
it('prefers the configured default agent when detected and enabled', () => {
expect(
resolveDefaultAgentForNewTab({
defaultTuiAgent: 'codex',
detectedAgentIds: ['claude', 'codex'],
disabledTuiAgents: []
})
).toBe('codex')
})
it('falls back to the auto-pick order when the default is blank', () => {
// Blank suppresses agents only for new workspaces; an explicit new-agent-tab chord still requests one.
expect(
resolveDefaultAgentForNewTab({
defaultTuiAgent: 'blank',
detectedAgentIds: ['codex', 'claude'],
disabledTuiAgents: []
})
).toBe('claude')
})
it('skips disabled agents and returns null when nothing is launchable', () => {
expect(
resolveDefaultAgentForNewTab({
defaultTuiAgent: 'claude',
detectedAgentIds: ['claude'],
disabledTuiAgents: ['claude']
})
).toBeNull()
expect(
resolveDefaultAgentForNewTab({
defaultTuiAgent: null,
detectedAgentIds: null,
disabledTuiAgents: []
})
).toBeNull()
})
})
@@ -1,58 +0,0 @@
import { describe, expect, it } from 'vitest'
import { agentVerdictStatusLine } from './agent-verdict-status-line'
const FAILED = {
state: 'done' as const,
mainAgent: { state: 'done' as const, outcome: 'failure' as const }
}
describe('agentVerdictStatusLine', () => {
it('names a failed program with its own message', () => {
expect(
agentVerdictStatusLine({
...FAILED,
agentType: 'rsync',
lastAssistantMessage: 'rsync failed',
programStatus: { state: 'error', message: 'Transfer interrupted', observedAt: 1 }
})
).toBe('Failed: rsync failed')
})
it('falls back to the facet message on a program row without an assistant message', () => {
expect(
agentVerdictStatusLine({
...FAILED,
agentType: 'brew',
programStatus: { state: 'error', message: 'Download failed', observedAt: 1 }
})
).toBe('Failed: Download failed')
})
it('reads plainly failed for a program with no message', () => {
expect(
agentVerdictStatusLine({
...FAILED,
agentType: 'brew',
programStatus: { state: 'error', observedAt: 1 }
})
).toBe('Failed')
})
it('keeps an agent failure plain even with a last reply', () => {
expect(
agentVerdictStatusLine({
...FAILED,
agentType: 'claude',
lastAssistantMessage: 'I could not finish.'
})
).toBe('Failed')
})
it('keeps a non-agent row without OSC 7501 provenance plain', () => {
expect(agentVerdictStatusLine({ ...FAILED, lastAssistantMessage: 'oops' })).toBe('Failed')
})
it('reads a cancellation as interrupted by user', () => {
expect(agentVerdictStatusLine({ state: 'done', interrupted: true })).toBe('Interrupted by user')
})
})
@@ -5,7 +5,6 @@ import {
buildAiVaultResumeCopyCommandForWorktree,
buildAiVaultResumeStartupForWorktree
} from './ai-vault-resume-command'
import { canResumeAiVaultSessionOnTarget } from '../../../shared/ai-vault-resume-target'
import { getAiVaultResumeWorkspaceWslDistro } from './ai-vault-resume-shell'
import { buildAgentLaunchRouteInput } from './agent-launch-route-input'
import { getLocalProjectExecutionRuntimeContext } from './local-preflight-context'
@@ -117,28 +116,6 @@ describe('Antigravity transcript reference ownership', () => {
}
)
it('preserves legacy WSL ID resume eligibility on SSH', () => {
expect(
canResumeAiVaultSessionOnTarget({
sessionFilePath: debianFile.replace('antigravity-ide', 'antigravity-cli'),
sessionExecutionHostId: 'local',
targetStatus: 'ssh',
targetExecutionHostId: 'ssh:other'
})
).toBe(true)
})
it('uses a project runtime override before the workspace UNC distro', () => {
const state = stateFor('local', 'Ubuntu')
const workspace = state.worktreesByRepo.repo?.[0]
if (!workspace) {
throw new Error('Missing fixture workspace')
}
workspace.path = '//wsl.localhost/Debian/home/example/project'
expect(getAiVaultResumeWorkspaceWslDistro(state, workspace.id)).toBe('Ubuntu')
expect(getAiVaultResumeWorkspaceWslDistro(state, `worktree:${workspace.id}`)).toBe('Ubuntu')
})
it('applies the same confinement to folder workspaces', () => {
const state = stateFor('local')
state.activeRepoId = null
@@ -232,25 +209,6 @@ describe('Antigravity transcript reference ownership', () => {
).toBe('ready')
})
it('keeps references refused for SSH aliases claiming local WSL ownership', () => {
for (const target of [
'ssh:localhost',
'ssh:127.0.0.1',
'ssh:Debian',
'ssh:local-wsl'
] as const) {
expect(
canResumeAiVaultSessionOnTarget({
sessionFilePath: debianFile,
sessionExecutionHostId: 'local',
targetStatus: 'ssh',
targetExecutionHostId: target,
targetWslDistro: 'Debian'
})
).toBe(false)
}
})
it('uses the target folder owner for both the pane and reference gate with another repo active', () => {
const state = stateFor('local', 'Ubuntu')
state.repos[0].path = 'C:/project'
@@ -290,14 +248,6 @@ describe('Antigravity transcript reference ownership', () => {
}
})
it('does not borrow an unrelated active project override for an orphan native folder', () => {
const state = stateFor('local', 'Debian')
state.folderWorkspaces = [
makeFolderWorkspace({ folderPath: 'C:/orphan', projectGroupId: 'orphan-group' })
]
expect(getAiVaultResumeWorkspaceWslDistro(state, 'folder:folder-1')).toBeNull()
})
it('keeps explicit SSH folder authority ahead of a matching local project path', () => {
const state = stateFor('local', 'Debian')
state.repos[0].path = 'C:/project'
@@ -61,20 +61,6 @@ function payload(overrides: {
}
describe('buildAiVaultDropRepinStartup', () => {
it('repins a payload with a cwd to the substituted home', () => {
const startup = buildAiVaultDropRepinStartup({
state: makeState(),
payload: payload({ sessionCwd: '/Users/ada/repo' }),
substituteCodexHome: SELECTED_HOME,
worktreeId: 'repo-1::worktree-1'
})
expect(startup).not.toBeNull()
expect(startup?.command).toContain(`CODEX_HOME='${SELECTED_HOME}'`)
expect(startup?.command).not.toContain(RECORDED_HOME)
expect(startup).toMatchObject({ cwd: '/Users/ada/repo' })
})
it('repins a payload whose session has no cwd instead of keeping the wrong-account command', () => {
const startup = buildAiVaultDropRepinStartup({
state: makeState(),
@@ -148,24 +148,6 @@ describe('ai vault resume command runtime', () => {
})
})
it('queues a PowerShell-valid command for the default Windows shell', () => {
// Queued commands target the live shell: PowerShell misparses cmd’s doubled-quote wrapper (#6152).
const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' })
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'claude',
sessionId: 'session one',
cwd: 'C:\\Users\\alice\\repo',
codexHome: null
}
})
).toBe("claude '--resume' 'session one'")
})
it('queues direct cmd syntax when the configured Windows shell is cmd.exe', () => {
const state = makeState({
worktreePath: 'C:\\Users\\alice\\repo',
@@ -186,43 +168,6 @@ describe('ai vault resume command runtime', () => {
).toBe('claude "--resume" "session one"')
})
it('queues a POSIX command for the Git Bash Windows shell', () => {
const state = makeState({
worktreePath: 'C:\\Users\\alice\\repo',
terminalWindowsShell: 'git-bash'
})
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'claude',
sessionId: 'session one',
cwd: 'C:\\Users\\alice\\repo',
codexHome: null
}
})
).toBe("claude '--resume' 'session one'")
})
it('follows the live Windows shell for Cursor resume', () => {
const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' })
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'cursor',
sessionId: 'session one',
cwd: 'C:\\Users\\alice\\repo',
codexHome: null
}
})
).toBe("cursor-agent '--yolo' '--resume' 'session one'")
})
it('queues a PowerShell-valid local OMP resume by absolute transcript path', () => {
// Rebuilds must retain OMP session.filePath and quote for the live Windows shell.
const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' })
@@ -599,27 +544,6 @@ describe('ai vault resume command runtime', () => {
})
})
it('rebuilds remote real-home Codex commands when the override is blank', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
commandOverride: ' ',
session: {
agent: 'codex',
sessionId: 'session one',
cwd: '/home/alice/repo',
codexHome: null,
executionHostId: 'ssh:dev-box',
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("codex '-c' 'tui.resume_cwd=current' 'resume' 'session one'")
})
it('copies remote real-home Codex commands with explicit environment cleanup', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never
@@ -644,27 +568,6 @@ describe('ai vault resume command runtime', () => {
expect(command).not.toContain('/retired/shared-home')
})
it('rebuilds the command when a non-blank override is supplied for a remote session', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
commandOverride: 'my-codex',
session: {
agent: 'codex',
sessionId: 'session one',
cwd: '/home/alice/repo',
codexHome: null,
executionHostId: 'ssh:dev-box',
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("my-codex '-c' 'tui.resume_cwd=current' 'resume' 'session one'")
})
it('rebuilds overridden remote commands with the recorded remote host platform', () => {
const state = makeState({
worktreePath: '/home/alice/repo',
@@ -693,24 +596,6 @@ describe('ai vault resume command runtime', () => {
)
})
it('applies the user default args to local Copilot AI Vault resumes', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.settings = { ...state.settings, agentDefaultArgs: { copilot: '--yolo' } } as never
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'copilot',
sessionId: '940237d9-c712-48e8-bca1-fd75fc4a8d4b',
cwd: '/home/alice/repo',
codexHome: null
}
})
).toBe("copilot '--yolo' '--resume=940237d9-c712-48e8-bca1-fd75fc4a8d4b'")
})
it('keeps the scanner resume command for remote Copilot sessions', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never
@@ -730,24 +615,4 @@ describe('ai vault resume command runtime', () => {
})
).toBe("copilot --resume='940237d9-c712-48e8-bca1-fd75fc4a8d4b'")
})
it('ignores a stored resume command for local-host sessions', () => {
const state = makeState({ worktreePath: '/home/alice/repo' })
state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never
expect(
buildQueuedAiVaultResumeCommand({
state,
worktreeId: 'repo-1::worktree-1',
session: {
agent: 'codex',
sessionId: 'session one',
cwd: '/home/alice/repo',
codexHome: null,
executionHostId: 'local',
resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'"
}
})
).toBe("codex '-c' 'tui.resume_cwd=current' 'resume' 'session one'")
})
})
@@ -2,10 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { RuntimeEnvironmentStatus } from '../../../shared/runtime-host-status'
import { getDefaultSettings } from '../../../shared/constants'
import { makeWorktree } from '../store/slices/worktrees-slice-test-fixtures'
import {
getAiVaultResumePlatform,
type AiVaultResumePlatformState
} from './ai-vault-resume-platform'
import type { AiVaultResumePlatformState } from './ai-vault-resume-platform'
import { buildAiVaultDropRepinStartup } from './ai-vault-resume-command'
vi.mock('@/lib/new-workspace', () => ({ CLIENT_PLATFORM: 'darwin' }))
@@ -68,14 +65,6 @@ function pairedServerState(hostPlatform: NodeJS.Platform | undefined): AiVaultRe
// Why: a saved session resumed on a paired server runs on that server's OS, not a guessed Linux.
describe('AI-vault resume on a paired server', () => {
it.each([
['win32', 'win32'],
['linux', 'linux'],
[undefined, null]
] as const)('reads a server reporting %s as %s', (reported, expected) => {
expect(getAiVaultResumePlatform(pairedServerState(reported), WORKTREE_ID)).toBe(expected)
})
it.each([
[
'win32',
@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { AppState } from '@/store/types'
import {
getAiVaultResumeWorkspaceExecutionHostId,
getAiVaultResumeWorkspaceTargetStatus
} from './ai-vault-resume-target'
import { getAiVaultResumeWorkspaceTargetStatus } from './ai-vault-resume-target'
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
type ResumeTargetState = Pick<
@@ -35,20 +32,6 @@ describe('ai vault resume target ownership', () => {
).toBe('runtime')
})
it('resolves explicit workspace keys through the target worktree owner', () => {
expect(
getAiVaultResumeWorkspaceTargetStatus(
makeState({
worktreesByRepo: {
'repo-1': [{ id: 'repo-1::/repo/orca', repoId: 'repo-1' }]
},
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'runtime:env-1' }]
}),
'worktree:repo-1::/repo/orca'
)
).toBe('runtime')
})
it('resolves explicit workspace keys through the target worktree host', () => {
expect(
getAiVaultResumeWorkspaceTargetStatus(
@@ -63,53 +46,6 @@ describe('ai vault resume target ownership', () => {
).toBe('runtime')
})
it('resolves exact execution host ids for active workspaces', () => {
const state = makeState({
worktreesByRepo: {
'repo-1': [{ id: 'repo-1::/repo/orca', repoId: 'repo-1', hostId: 'ssh:ssh-1' }]
},
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }]
})
expect(getAiVaultResumeWorkspaceExecutionHostId(state, 'repo-1::/repo/orca')).toBe('ssh:ssh-1')
expect(getAiVaultResumeWorkspaceExecutionHostId(state, 'worktree:repo-1::/repo/orca')).toBe(
'ssh:ssh-1'
)
})
it('resolves local execution host ids for local workspaces', () => {
expect(
getAiVaultResumeWorkspaceExecutionHostId(
makeState({
worktreesByRepo: {
'repo-1': [{ id: 'repo-1::/repo/orca', repoId: 'repo-1' }]
},
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }]
}),
'repo-1::/repo/orca'
)
).toBe('local')
})
it('supports folder workspaces owned by SSH project groups', () => {
expect(
getAiVaultResumeWorkspaceTargetStatus(
makeState({
folderWorkspaces: [
{
id: 'folder-1',
projectGroupId: 'group-1',
name: 'Platform',
folderPath: '/repo/platform'
}
],
projectGroups: [{ id: 'group-1', connectionId: 'ssh-1' }]
}),
folderWorkspaceKey('folder-1')
)
).toBe('ssh')
})
it('prefers runtime project-group ownership over stale SSH folder connection ids', () => {
expect(
getAiVaultResumeWorkspaceTargetStatus(
@@ -154,32 +90,4 @@ describe('ai vault resume target ownership', () => {
)
).toBe('unknown')
})
it('blocks mixed local and runtime folder workspace targets', () => {
expect(
getAiVaultResumeWorkspaceTargetStatus(
makeState({
folderWorkspaces: [
{
id: 'folder-1',
projectGroupId: 'group-1',
name: 'Platform',
folderPath: '/repo/platform'
}
],
projectGroups: [{ id: 'group-1' }],
repos: [
{ id: 'repo-local', path: '/repo/platform/web', connectionId: null },
{
id: 'repo-runtime',
path: '/repo/platform/api',
connectionId: null,
executionHostId: 'runtime:env-1'
}
]
}),
folderWorkspaceKey('folder-1')
)
).toBe('runtime')
})
})
@@ -41,36 +41,6 @@ describe('Session History session drag data', () => {
clearAiVaultSessionDragData()
})
it('writes and reads the private session history payload', () => {
const transfer = createTransfer()
const payload: AiVaultSessionDragPayload = {
agent: 'claude',
sessionId: 'session-1',
title: 'Fix terminal split',
command: "cd '/repo' && claude --resume session-1",
sessionFilePath: '/Users/ada/.claude/projects/-repo/session-1.jsonl',
codexHome: '/Users/ada/Library/Application Support/orca/codex-runtime-home/home',
sessionCwd: '/repo',
env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' },
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'],
launchConfig: {
agentCommand: 'claude --dangerously-skip-permissions',
agentArgs: '--dangerously-skip-permissions',
agentEnv: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }
},
realHomeStartup: {
command: "cd '/repo' && claude --resume session-1",
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME']
}
}
writeAiVaultSessionDragData(transfer, payload)
expect(transfer.effectAllowed).toBe('copy')
expect(hasAiVaultSessionDragData(transfer)).toBe(true)
expect(readAiVaultSessionDragData(transfer)).toEqual(payload)
})
it('preserves an explicit null sessionCwd across the serialized round-trip', () => {
const transfer = createTransfer()
const payload: AiVaultSessionDragPayload = {
@@ -155,16 +125,6 @@ describe('Session History session drag data', () => {
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('rejects malformed payloads', () => {
const transfer = createTransfer()
transfer.setData(
AI_VAULT_SESSION_DRAG_TYPE,
JSON.stringify({ kind: 'ai-vault-session', version: 1, agent: 'bad', command: 'claude' })
)
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('rejects array-shaped env records', () => {
const transfer = createTransfer()
transfer.setData(
@@ -201,45 +161,6 @@ describe('Session History session drag data', () => {
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('rejects array-shaped launch config env records', () => {
const transfer = createTransfer()
transfer.setData(
AI_VAULT_SESSION_DRAG_TYPE,
JSON.stringify({
kind: 'ai-vault-session',
version: 1,
agent: 'claude',
sessionId: 'session-1',
title: 'Malformed launch config env',
command: 'claude --resume session-1',
launchConfig: {
agentArgs: '',
agentEnv: ['ANTHROPIC_BASE_URL=https://claude.example.test']
}
})
)
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('rejects oversized serialized payloads before parsing', () => {
const transfer = createTransfer()
const secret = 'ai-vault-drag-secret'
transfer.setData(
AI_VAULT_SESSION_DRAG_TYPE,
secret + 'x'.repeat(AI_VAULT_SESSION_DRAG_PAYLOAD_MAX_BYTES)
)
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('rejects multibyte oversized payloads before parsing', () => {
const transfer = createTransfer()
transfer.setData(AI_VAULT_SESSION_DRAG_TYPE, '😀'.repeat(4097))
expect(readAiVaultSessionDragData(transfer)).toBeNull()
})
it('does not retain an oversized internal payload for the hidden-data fallback', () => {
const source = createTransfer()
writeAiVaultSessionDragData(source, {
@@ -3,7 +3,6 @@ import type { AiVaultSession } from '../../../shared/ai-vault-types'
import {
dropDeletedRemoteResumeCwd,
prepareAiVaultSessionForFork,
prepareAiVaultSessionForResume,
prepareAiVaultSessionForTerminalResume
} from './ai-vault-session-resume-preparation'
@@ -19,95 +18,6 @@ afterEach(() => {
browseServerDir.mockReset()
})
describe('prepareAiVaultSessionForResume', () => {
it('returns a real-home launch identity only after targeted materialization succeeds', async () => {
const prepareSessionResume = vi.fn().mockResolvedValue({ useRealCodexHome: true })
stubPreparation(prepareSessionResume)
const legacy = session({
codexHome: '/Users/ada/Library/Application Support/orca/codex-runtime-home/home'
})
const prepared = await prepareAiVaultSessionForResume(legacy)
expect(prepared.codexHome).toBeNull()
expect(prepareSessionResume).toHaveBeenCalledWith({
agent: 'codex',
sessionId: legacy.sessionId,
filePath: legacy.filePath,
codexHome: legacy.codexHome,
executionHostId: 'local'
})
})
it('rejects without changing the launch identity when materialization fails', async () => {
stubPreparation(vi.fn().mockRejectedValue(new Error('Retry resume.')))
await expect(
prepareAiVaultSessionForResume(session({ codexHome: '/tmp/orca/codex-runtime-home/home' }))
).rejects.toThrow('Retry resume.')
})
it('preserves a custom home without materialization', async () => {
const prepareSessionResume = vi.fn()
stubPreparation(prepareSessionResume)
const current = session({ codexHome: '/custom/codex' })
await expect(prepareAiVaultSessionForResume(current)).resolves.toBe(current)
expect(prepareSessionResume).not.toHaveBeenCalled()
})
it('does not prepare a legacy CLI resume for a native structured session', async () => {
const prepareSessionResume = vi.fn()
stubPreparation(prepareSessionResume)
const native = session({
codexHome: '/tmp/orca/codex-runtime-home/home',
structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' }
})
await expect(prepareAiVaultSessionForResume(native)).resolves.toBe(native)
expect(prepareSessionResume).not.toHaveBeenCalled()
})
it('repins a per-account session to the home the host substitutes', async () => {
const prepareSessionResume = vi.fn().mockResolvedValue({
useRealCodexHome: false,
substituteCodexHome: '/tmp/orca/codex-accounts/account-2/home'
})
stubPreparation(prepareSessionResume)
const current = session({ codexHome: '/tmp/orca/codex-accounts/account-1/home' })
const prepared = await prepareAiVaultSessionForResume(current)
expect(prepared.codexHome).toBe('/tmp/orca/codex-accounts/account-2/home')
expect(prepareSessionResume).toHaveBeenCalledWith({
agent: 'codex',
sessionId: current.sessionId,
filePath: current.filePath,
codexHome: current.codexHome,
executionHostId: 'local'
})
})
it('keeps a per-account session unchanged when the host declines to repin', async () => {
stubPreparation(vi.fn().mockResolvedValue({ useRealCodexHome: false }))
const current = session({ codexHome: '/tmp/orca/codex-accounts/account-1/home' })
await expect(prepareAiVaultSessionForResume(current)).resolves.toBe(current)
})
it('does not ask a remote host to repin a per-account session', async () => {
const prepareSessionResume = vi.fn()
stubPreparation(prepareSessionResume)
const current = session({
codexHome: '/home/user/.orca/codex-accounts/account-1/home',
executionHostId: 'ssh:server-1' as AiVaultSession['executionHostId']
})
await expect(prepareAiVaultSessionForResume(current)).resolves.toBe(current)
expect(prepareSessionResume).not.toHaveBeenCalled()
})
})
describe('ordinary Resume against an older paired host', () => {
it.each([
new Error(
@@ -132,48 +42,6 @@ describe('ordinary Resume against an older paired host', () => {
})
describe('prepareAiVaultSessionForFork', () => {
// A fork of a chat-owned conversation must run under the selected account, as a resume does.
it('repins a chat-owned per-account session, asking the host as a fork', async () => {
const prepareSessionResume = vi.fn().mockResolvedValue({
useRealCodexHome: false,
substituteCodexHome: '/tmp/orca/codex-accounts/account-2/home'
})
stubPreparation(prepareSessionResume)
const owned = session({
codexHome: '/tmp/orca/codex-accounts/account-1/home',
structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' }
})
const prepared = await prepareAiVaultSessionForFork(owned)
expect(prepared.copied).toBe(false)
expect(prepared.session.codexHome).toBe('/tmp/orca/codex-accounts/account-2/home')
expect(prepareSessionResume).toHaveBeenCalledWith({
agent: 'codex',
sessionId: owned.sessionId,
filePath: owned.filePath,
codexHome: owned.codexHome,
executionHostId: 'local',
fork: true
})
})
it('does not ask the host for a home that needs no preparation', async () => {
const prepareSessionResume = vi.fn()
stubPreparation(prepareSessionResume)
const owned = session({
agent: 'claude',
codexHome: null,
structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' }
})
await expect(prepareAiVaultSessionForFork(owned)).resolves.toEqual({
session: owned,
copied: false
})
expect(prepareSessionResume).not.toHaveBeenCalled()
})
it("refuses a host that made no copy rather than resuming the chat's own conversation", async () => {
stubPreparation(vi.fn().mockResolvedValue({ useRealCodexHome: false }))
const owned = session({
@@ -188,28 +56,6 @@ describe('prepareAiVaultSessionForFork', () => {
})
describe('dropDeletedRemoteResumeCwd', () => {
it('leaves a live SSH session alone, scanner command and all', async () => {
const pathExists = vi.fn().mockResolvedValue(true)
vi.stubGlobal('window', { api: { fs: { pathExists } } })
const remote = session({ executionHostId: 'ssh:server-1', cwd: '/home/ada/wt/feat-x' })
// Preserve recorded cwd or ordinary remote resumes would start at the workspace root.
await expect(dropDeletedRemoteResumeCwd(remote)).resolves.toBe(remote)
expect(pathExists).toHaveBeenCalledWith({
filePath: '/home/ada/wt/feat-x',
connectionId: 'server-1'
})
})
it('never probes a host for a local session', async () => {
const pathExists = vi.fn()
vi.stubGlobal('window', { api: { fs: { pathExists } } })
const local = session({ cwd: '/repo' })
await expect(dropDeletedRemoteResumeCwd(local)).resolves.toBe(local)
expect(pathExists).not.toHaveBeenCalled()
})
it('keeps the recorded folder when the SSH host cannot answer', async () => {
const pathExists = vi.fn().mockRejectedValue(new Error('CONNECTION_LOST'))
vi.stubGlobal('window', { api: { fs: { pathExists } } })
@@ -1,18 +1,10 @@
import { createStore } from 'zustand/vanilla'
import { describe, expect, it, vi } from 'vitest'
import type { AppState } from '@/store/types'
import type { AgentProviderSessionMetadata } from '../../../shared/agent-session/lifecycle/agent-session-resume'
import type { TerminalTab } from '../../../shared/terminal-tab-types'
import { aiVaultTitleSyncInputsChanged } from './ai-vault-tab-title-sync-inputs'
import { startAiVaultTabTitleSync } from './ai-vault-tab-title-sync'
const COLLECTIONS = [
'agentStatusByPaneKey',
'retainedAgentsByPaneKey',
'sleepingAgentSessionsByPaneKey'
] as const
type Collection = (typeof COLLECTIONS)[number]
function makeState(count = 1): AppState {
const live: AppState['agentStatusByPaneKey'] = {}
const retained: AppState['retainedAgentsByPaneKey'] = {}
@@ -77,20 +69,6 @@ function makeState(count = 1): AppState {
} as unknown as AppState
}
function replaceProvider(
state: AppState,
collection: Collection,
providerSession: AgentProviderSessionMetadata
): AppState {
const paneKey = Object.keys(state[collection])[0]
const existing = state[collection][paneKey]
const next =
collection === 'retainedAgentsByPaneKey'
? { ...existing, entry: { ...state.retainedAgentsByPaneKey[paneKey].entry, providerSession } }
: { ...existing, providerSession }
return { ...state, [collection]: { ...state[collection], [paneKey]: next } }
}
async function collectGarbage(): Promise<void> {
if (typeof globalThis.gc !== 'function') {
throw new Error('Run with the repository Vitest --expose-gc config')
@@ -139,91 +117,6 @@ describe('AI Vault title subscription inputs', () => {
}
})
it.each(COLLECTIONS)(
'still detects provider changes in %s with other maps reused',
(collection) => {
const state = makeState()
const paneKey = Object.keys(state[collection])[0]
const provider =
collection === 'retainedAgentsByPaneKey'
? state.retainedAgentsByPaneKey[paneKey].entry.providerSession!
: (state[collection][paneKey] as { providerSession: AgentProviderSessionMetadata })
.providerSession
for (const patch of [
{ id: 'changed' },
{ key: 'conversation_id' as const },
{ transcriptPath: '/changed/session' }
]) {
expect(
aiVaultTitleSyncInputsChanged(
replaceProvider(state, collection, { ...provider, ...patch }),
state
)
).toBe(true)
}
expect(
aiVaultTitleSyncInputsChanged(replaceProvider(state, collection, { ...provider }), state)
).toBe(false)
}
)
it.each(COLLECTIONS)('detects additions and removals in %s', (collection) => {
const state = makeState()
const records = state[collection]
const empty = { ...state, [collection]: {} }
expect(aiVaultTitleSyncInputsChanged(empty, state)).toBe(true)
expect(aiVaultTitleSyncInputsChanged(state, empty)).toBe(true)
expect(aiVaultTitleSyncInputsChanged({ ...state, [collection]: { ...records } }, state)).toBe(
false
)
})
it.each(COLLECTIONS)('detects agent and pane ownership changes in %s', (collection) => {
const state = makeState()
const records = state[collection]
const paneKey = Object.keys(records)[0]
const record = records[paneKey]
const entry =
collection === 'retainedAgentsByPaneKey'
? state.retainedAgentsByPaneKey[paneKey].entry
: record
const agentField = collection === 'sleepingAgentSessionsByPaneKey' ? 'agent' : 'agentType'
const changedRecords = [
{ ...record, [agentField]: 'claude' },
{ ...record, [agentField]: 'gemini' },
{ ...record, worktreeId: 'other' },
...['paneKey', 'tabId'].map((field) =>
collection === 'retainedAgentsByPaneKey'
? { ...record, entry: { ...entry, [field]: 'other' } }
: { ...record, [field]: 'other' }
)
]
for (const changed of changedRecords) {
expect(
aiVaultTitleSyncInputsChanged(
{ ...state, [collection]: { ...records, [paneKey]: changed } },
state
)
).toBe(true)
}
})
it('still checks workspace ownership after unchanged record collections', () => {
const state = makeState()
for (const host of ['ssh:host-1', 'runtime:server-1'] as const) {
expect(
aiVaultTitleSyncInputsChanged(
{
...state,
agentStatusByPaneKey: { ...state.agentStatusByPaneKey },
activeWorkspaceExecutionHostId: host
},
state
)
).toBe(true)
}
})
it('skips owner rescans for rank-only rows but reports a conversation workspace moving host', () => {
// No active selection, so the row's host decides ownership.
const state = {
@@ -289,40 +182,4 @@ describe('AI Vault title subscription inputs', () => {
)
).toBe(false)
})
it('still checks active panes and stored titles after unchanged record collections', () => {
const state = makeState()
const agentStatusByPaneKey = { ...state.agentStatusByPaneKey }
expect(
aiVaultTitleSyncInputsChanged(
{
...state,
agentStatusByPaneKey,
terminalLayoutsByTabId: {
'tab-0': { root: null, activeLeafId: 'other', expandedLeafId: null }
}
},
state
)
).toBe(true)
const tabs = state.tabsByWorktree['wt-title']
expect(
aiVaultTitleSyncInputsChanged(
{
...state,
agentStatusByPaneKey,
tabsByWorktree: {
'wt-title': [
{
...tabs[0],
aiVaultTitle: { agent: 'codex', sessionId: 'session-0', title: 'New title' }
},
...tabs.slice(1)
]
}
},
state
)
).toBe(true)
})
})
@@ -2,14 +2,8 @@ import { describe, expect, it, vi } from 'vitest'
import type { AiVaultSessionTitlesResult } from '../../../shared/ai-vault-session-title'
import { resolveTerminalTabTitle } from '../../../shared/tab-title-resolution'
import type { TerminalTab } from '../../../shared/terminal-tab-types'
import {
collectAiVaultTitleRequests,
type AiVaultTitleRequest
} from './ai-vault-tab-title-requests'
import {
batchAiVaultTitleRequests,
settleAiVaultTitleRequestBatches
} from './ai-vault-tab-title-batches'
import type { AiVaultTitleRequest } from './ai-vault-tab-title-requests'
import { settleAiVaultTitleRequestBatches } from './ai-vault-tab-title-batches'
import { startAiVaultTabTitleSync } from './ai-vault-tab-title-sync'
import type { AppState } from '@/store/types'
@@ -200,22 +194,6 @@ describe('AI Vault tab title sync', () => {
}
)
it('uses runtime host authority for folder workspaces', () => {
const store = makeState({
executionHostId: 'runtime:server-1',
worktreeId: 'folder:folder-1',
path: '/srv/folders/albacore'
})
expect(collectAiVaultTitleRequests(store.getState())).toEqual([
expect.objectContaining({
executionHostId: 'runtime:server-1',
tabId: 'tab-1',
worktreeId: 'folder:folder-1'
})
])
})
it('retains a recovered sleeping title after its lifecycle record disappears', async () => {
const store = makeState({
executionHostId: 'ssh:dev-box',
@@ -268,27 +246,6 @@ describe('AI Vault tab title sync', () => {
stop()
})
it('retries a missing live title without waiting for the long refresh', async () => {
const store = makeState({
executionHostId: 'ssh:dev-box',
worktreeId: 'worktree-1',
path: '/workspace/albacore'
})
let refreshDelay: number | undefined
const stop = startAiVaultTabTitleSync({
...store,
resolveSessionTitles: async () => ({ titles: [] }),
setTimer: (_callback, delay) => {
refreshDelay = delay
return 1
},
clearTimer: () => {}
})
await vi.waitFor(() => expect(refreshDelay).toBe(20_000))
stop()
})
it('defers title reads through the configured background scheduler', async () => {
const store = makeState({
executionHostId: 'ssh:dev-box',
@@ -366,24 +323,6 @@ describe('AI Vault tab title sync', () => {
stop()
})
it('batches title identities per host within the wire bound', () => {
const request = (index: number): AiVaultTitleRequest => ({
agent: 'codex',
executionHostId: 'ssh:dev-box',
providerSession: { key: 'session_id', id: `session-${index}` },
refresh: true,
tabId: `tab-${index}`,
worktreeId: `worktree-${index}`
})
const groups = batchAiVaultTitleRequests(
Array.from({ length: 65 }, (_, index) => request(index))
)
expect(groups).toHaveLength(2)
expect(groups[0]).toHaveLength(64)
expect(groups[1]).toHaveLength(1)
})
it('runs hosts concurrently while serializing each host wire', async () => {
const request = (executionHostId: AiVaultTitleRequest['executionHostId'], index: number) => ({
agent: 'codex' as const,
@@ -6,14 +6,6 @@ let cleanup: (() => void) | null = null
afterEach(() => cleanup?.())
describe('app command dispatch', () => {
it('routes known action ids through the current handler registry', () => {
const dispatcher = vi.fn(() => true)
cleanup = registerAppCommandDispatcher(dispatcher)
expect(dispatchAppCommand('view.tasks', 'plugin-palette')).toBe(true)
expect(dispatcher).toHaveBeenCalledWith('view.tasks', 'plugin-palette')
})
it('rejects unknown actions and unregisters only the current dispatcher', () => {
const staleCleanup = registerAppCommandDispatcher(() => true)
const current = vi.fn(() => true)
@@ -43,21 +43,6 @@ describe('app menu paste', () => {
expect(performNativePaste).not.toHaveBeenCalled()
})
it('preserves paste-and-match-style when falling back to native paste', async () => {
const performNativePaste = vi.fn()
const result = await handleAppMenuPasteRequest({
readClipboardText: vi.fn(async () => 'text'),
performNativePaste,
dispatchOwnedPasteEvent: () => false,
getActiveElement: () => null,
nativePasteMode: 'paste-and-match-style'
})
expect(result).toEqual({ status: 'native-fallback', reason: 'no-owned-target' })
expect(performNativePaste).toHaveBeenCalledWith({ mode: 'paste-and-match-style' })
})
it('pastes large clipboard text into a focused text control without leaking event data', async () => {
const textarea = appendTextarea('prefix ')
textarea.focus()
@@ -111,23 +96,6 @@ describe('app menu paste', () => {
expect(performNativePaste).not.toHaveBeenCalled()
})
it('falls back to native paste when clipboard text cannot be read', async () => {
const textarea = appendTextarea()
textarea.focus()
const performNativePaste = vi.fn()
const result = await handleAppMenuPasteRequest({
readClipboardText: vi.fn(async () => {
throw new Error('clipboard denied')
}),
performNativePaste,
dispatchOwnedPasteEvent: () => false
})
expect(result).toEqual({ status: 'native-fallback', reason: 'clipboard-read-failed' })
expect(performNativePaste).toHaveBeenCalledWith({ mode: 'paste' })
})
it('does not native-fallback when clipboard read fails after focus moves', async () => {
const textarea = appendTextarea('unchanged')
const other = appendTextarea('other')
@@ -1,30 +0,0 @@
// @vitest-environment happy-dom
import { describe, expect, it } from 'vitest'
import {
APP_MENU_SELECTION_ACTION_EVENT,
dispatchAppMenuSelectionAction
} from './app-menu-selection-actions'
describe('app menu selection actions', () => {
it.each(['copy', 'select-all'] as const)('reports whether %s has an owned target', (action) => {
let receivedAction: unknown
const handler = (event: Event): void => {
if (event instanceof CustomEvent) {
receivedAction = event.detail
}
event.preventDefault()
}
window.addEventListener(APP_MENU_SELECTION_ACTION_EVENT, handler)
const claimed = dispatchAppMenuSelectionAction(action)
window.removeEventListener(APP_MENU_SELECTION_ACTION_EVENT, handler)
expect(claimed).toBe(true)
expect(receivedAction).toBe(action)
})
it('leaves unowned actions available for native fallback', () => {
expect(dispatchAppMenuSelectionAction('copy')).toBe(false)
})
})
@@ -195,27 +195,6 @@ describe('observeExistingAutomationSession', () => {
expect(closeStream).toHaveBeenCalledOnce()
})
it('still forwards a status the runtime host did report', async () => {
state.terminalLayoutsByTabId = {
'tab-1': { ptyIdsByLeafId: { [LEAF_ID]: 'remote:env-1@@terminal-9' } }
}
state.ptyIdsByTabId = { 'tab-1': ['remote:env-1@@terminal-9'] }
mockCallRuntimeRpc.mockResolvedValue({ wait: { exitCode: 0 } })
const onExit = vi.fn()
const { observeExistingAutomationSession } = await import('./automation-session-observer')
await observeExistingAutomationSession({
ptyId: 'remote:env-1@@terminal-9',
paneKey: PANE_KEY,
runId: 'run-1',
onData: vi.fn(),
onAgentStatus: vi.fn(),
onExit
})
await vi.waitFor(() => expect(onExit).toHaveBeenCalledWith(0))
})
it('stamps the exact SSH PTY in the legacy renderer fallback', async () => {
state.settings.terminalMainSideEffectAuthority = false
const ptyId = toAppSshPtyId('ssh-a', 'pty-1')
@@ -245,53 +224,4 @@ describe('observeExistingAutomationSession', () => {
{ connectionId: 'ssh-a' }
)
})
it('leaves the row unchanged after the pane rebinds to another SSH host', async () => {
state.settings.terminalMainSideEffectAuthority = false
const oldPtyId = toAppSshPtyId('ssh-a', 'pty-1')
state.terminalLayoutsByTabId = {
'tab-1': {
ptyIdsByLeafId: { [LEAF_ID]: toAppSshPtyId('ssh-b', 'pty-1') }
}
}
state.ptyIdsByTabId = { 'tab-1': [toAppSshPtyId('ssh-b', 'pty-1')] }
const { observeExistingAutomationSession } = await import('./automation-session-observer')
await observeExistingAutomationSession({
ptyId: oldPtyId,
paneKey: PANE_KEY,
runId: 'run-1',
onData: vi.fn(),
onAgentStatus: vi.fn(),
onExit: vi.fn()
})
const handleData = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
handleData(DONE_STATUS_OSC)
expect(state.setAgentStatus).not.toHaveBeenCalled()
})
it('ignores a delayed callback after disconnect clears the live PTY index', async () => {
state.settings.terminalMainSideEffectAuthority = false
const ptyId = toAppSshPtyId('ssh-a', 'pty-1')
state.terminalLayoutsByTabId = {
'tab-1': { ptyIdsByLeafId: { [LEAF_ID]: ptyId } }
}
state.ptyIdsByTabId = { 'tab-1': [ptyId] }
const { observeExistingAutomationSession } = await import('./automation-session-observer')
await observeExistingAutomationSession({
ptyId,
paneKey: PANE_KEY,
runId: 'run-1',
onData: vi.fn(),
onAgentStatus: vi.fn(),
onExit: vi.fn()
})
const handleData = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
state.ptyIdsByTabId = { 'tab-1': [] }
handleData(DONE_STATUS_OSC)
expect(state.setAgentStatus).not.toHaveBeenCalled()
})
})
@@ -210,17 +210,6 @@ describe('automation terminal ownership', () => {
expect(closeTab).not.toHaveBeenCalled()
})
it.each([
['remote runtime', { runtimeKind: 'environment' as const }],
['remote PTY identity', { ptyId: 'remote:env-1@@terminal-1' }]
])('never owns a %s terminal', async (_label, overrides) => {
const { closeTab, store } = createStore()
const ownership = own(store, overrides)
expect(await ownership.finalize({ preservePane: false })).toBeNull()
expect(closeTab).not.toHaveBeenCalled()
})
it('sleeps a resumable pane in a workspace the run owns instead of closing it', async () => {
const { closeTab, shutdownCompletedAgentPaneForHibernation, store } = createStore()
const ownership = own(store)
-21
View File
@@ -1,21 +0,0 @@
import { describe, expect, it } from 'vitest'
import { decodeBase64Bytes } from './base64-bytes'
describe('base64 byte decoding', () => {
it.each([0, 1, 2, 3, 256, 257, 258])('preserves every byte for length %i', (length) => {
const expected = Buffer.alloc(length)
for (let index = 0; index < length; index += 1) {
expected[index] = index % 256
}
const decoded = decodeBase64Bytes(expected.toString('base64'))
expect(Buffer.from(decoded)).toEqual(expected)
})
it('preserves atob support for whitespace and omitted padding', () => {
expect(decodeBase64Bytes(' /+7d zA\t\r\n')).toEqual(Uint8Array.of(255, 238, 221, 204))
})
it.each(['A', 'A===', 'AQ?D', 'AQID='])('rejects malformed base64 %s', (encoded) => {
expect(() => decodeBase64Bytes(encoded)).toThrow()
})
})
@@ -2,15 +2,6 @@ import { describe, expect, it } from 'vitest'
import { createBoundedGenerationMap } from './bounded-generation-map'
describe('createBoundedGenerationMap', () => {
it('advances a key to a strictly larger generation', () => {
const map = createBoundedGenerationMap(4)
expect(map.get('a')).toBe(0)
map.advance('a')
const first = map.get('a')
map.advance('a')
expect(map.get('a')).toBeGreaterThan(first)
})
it('keeps insertion order for eviction when a key is re-advanced', () => {
const map = createBoundedGenerationMap(2)
map.advance('a')
@@ -31,15 +22,4 @@ describe('createBoundedGenerationMap', () => {
expect(map.get('a')).toBe(2)
expect(map.get('never-seen')).toBe(2)
})
it('reset clears generations, sequence and the evicted floor', () => {
const map = createBoundedGenerationMap(1)
map.advance('a')
map.advance('b')
map.reset()
expect(map.get('a')).toBe(0)
expect(map.get('b')).toBe(0)
map.advance('a')
expect(map.get('a')).toBe(1)
})
})
@@ -19,18 +19,6 @@ const summary: BrowserCookieImportSummary = {
domains: ['example.com']
}
const localExecution = {
executionHostLabel: 'Local Mac',
executionMachine: 'client' as const,
executionRemoteEnvironment: false
}
const clientHostedExecution = {
executionHostLabel: 'm4 air',
executionMachine: 'client' as const,
executionRemoteEnvironment: true
}
const remoteExecution = {
executionHostLabel: 'Remote Mac',
executionMachine: 'remote' as const,
@@ -43,333 +31,6 @@ describe('emitBrowserCookieImportToast', () => {
warningToastMock.mockReset()
})
it('shows success when the import has no warning', () => {
emitBrowserCookieImportToast(summary, 'Imported 3 cookies.', localExecution)
expect(successToastMock).toHaveBeenCalledWith('Imported 3 cookies.')
expect(warningToastMock).not.toHaveBeenCalled()
})
it.each([2, 0])('reports %i imported and 2 ordinary skipped cookies once', (importedCookies) => {
emitBrowserCookieImportToast(
{ ...summary, totalCookies: importedCookies + 2, importedCookies, skippedCookies: 2 },
`Imported ${importedCookies} cookies.`,
localExecution
)
expect(successToastMock).toHaveBeenCalledExactlyOnceWith(
`Imported ${importedCookies} cookies. 2 cookies skipped.`
)
expect(warningToastMock).not.toHaveBeenCalled()
})
it('counts only skips not already explained by Google and partition warnings', () => {
emitBrowserCookieImportToast(
{
...summary,
totalCookies: 6,
importedCookies: 2,
skippedCookies: 4,
googleCookiesSkipped: 1,
partitionSkippedCookies: 1
},
'Imported 2 cookies.',
remoteExecution
)
expect(successToastMock).toHaveBeenCalledExactlyOnceWith(
'Imported 2 cookies. 2 cookies skipped.',
{ description: 'Read from browsers on Remote Mac and stored there.' }
)
expect(warningToastMock).toHaveBeenCalledTimes(2)
expect(warningToastMock.mock.calls[1]?.[0]).toContain(
'1 cookies were not imported because their site-partition could not be read.'
)
})
it('relies on the existing undecryptable warning without duplicating failure counts', () => {
emitBrowserCookieImportToast(
{
...summary,
totalCookies: 6,
importedCookies: 2,
skippedCookies: 4,
warning: { code: 'cookies-undecryptable', failedCookies: 2, reason: 'unknown' }
},
'Imported 2 cookies.',
localExecution
)
expect(warningToastMock).toHaveBeenCalledExactlyOnceWith(
'2 cookies could not be decrypted and were skipped. Close the source browser completely and try the import again.'
)
expect(successToastMock).not.toHaveBeenCalled()
})
it('offers the in-app file import without recommending an exporter', () => {
emitBrowserCookieImportToast(
{
...summary,
warning: {
code: 'cookies-undecryptable',
failedCookies: 3,
reason: 'app-bound-encryption'
}
},
'Imported 0 cookies.',
localExecution
)
const message = warningToastMock.mock.calls[0]?.[0]
expect(message).toBe(
"Orca cannot decrypt 3 of this browser's cookies because they use app-bound encryption. You can import cookies from a file using “From File…”."
)
expect(message).not.toContain('export')
})
it('names the missing Linux cookie-key tool and source browser', () => {
emitBrowserCookieImportToast(
{
...summary,
warning: {
code: 'cookies-undecryptable',
failedCookies: 3,
reason: 'linux-keyring-unavailable',
otherFailedCookies: 1,
keyringFailure: { cause: 'secret-tool-missing', browser: 'Google Chrome' }
}
},
'Imported 0 cookies.',
localExecution
)
const message = warningToastMock.mock.calls[0]?.[0]
expect(message).toContain('Google Chrome')
expect(message).toContain('secret-tool')
expect(message).toContain('libsecret-tools')
expect(message).toContain('import again')
expect(message).toContain('1 more cookies could not be decrypted for another reason.')
expect(message).not.toMatch(/keyring|gnome-keyring/i)
})
it.each([
{ cause: 'keyring-locked' as const, action: 'Unlock your login keyring' },
{ cause: 'key-unavailable' as const, action: 'sign in directly in Orca' }
])('gives the recovery action for $cause', ({ cause, action }) => {
emitBrowserCookieImportToast(
{
...summary,
warning: {
code: 'cookies-undecryptable',
failedCookies: 3,
reason: 'linux-keyring-unavailable',
keyringFailure: { cause, browser: 'Google Chrome' }
}
},
'Imported 0 cookies.',
localExecution
)
const message = warningToastMock.mock.calls[0]?.[0]
expect(message).toContain('Google Chrome')
expect(message).toContain(action)
expect(message).not.toContain('libsecret-tools')
})
it.each([
{ detail: 'absent detail', keyringFailure: undefined },
{ detail: 'absent cause', keyringFailure: { browser: 'Google Chrome' } },
{
detail: 'unknown cause',
keyringFailure: { cause: 'future-key-source', browser: 'Google Chrome' }
}
])('keeps the legacy keyring warning for $detail', ({ keyringFailure }) => {
emitBrowserCookieImportToast(
{
...summary,
importedCookies: 0,
totalCookies: 5,
skippedCookies: 5,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: simulate missing/future nested cause values from an unvalidated RPC peer.
warning: {
code: 'cookies-undecryptable',
failedCookies: 3,
reason: 'linux-keyring-unavailable',
otherFailedCookies: 2,
keyringFailure
} as unknown as BrowserCookieImportSummary['warning']
},
'Imported 0 cookies.',
remoteExecution
)
expect(warningToastMock).toHaveBeenCalledTimes(1)
expect(warningToastMock).toHaveBeenCalledWith(
'3 cookies could not be decrypted because the system keyring was unavailable; 2 more could not be decrypted for another reason. Unlock your login keyring (or install a Secret Service provider such as gnome-keyring) and import again.'
)
expect(successToastMock).not.toHaveBeenCalled()
})
it('shows workspace-specific Google guidance after a remote-side import', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
remoteExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.', {
description: 'Read from browsers on Remote Mac and stored there.'
})
expect(warningToastMock).toHaveBeenCalledWith(
'Google cookies were not imported. Open a browser tab in the Remote Mac workspace with this profile, then sign into Google.',
{ duration: 12000 }
)
expect(successToastMock.mock.invocationCallOrder[0]).toBeLessThan(
warningToastMock.mock.invocationCallOrder[0]
)
})
// Client-hosted imports populate this desktop’s workspace jar; guidance must not send users to another machine.
it('says the workspace tab opens on this device for a client-hosted import', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
clientHostedExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.', {
description: 'Read from this device and stored here for the m4 air workspace.'
})
expect(warningToastMock).toHaveBeenCalledWith(
'Google cookies were not imported. Open a browser tab in the m4 air workspace with this profile — it opens on this device — then sign into Google.',
{ duration: 12000 }
)
})
it('names no machine in the Google guidance for a purely local import', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1, googleCookiesSkipped: 1 },
'Imported 2 cookies.',
localExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
expect(warningToastMock).toHaveBeenCalledWith(
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.',
{ duration: 12000 }
)
})
// Skipped partitioned cookies make import lossy; success counts alone hide it (STA-4300).
it('warns separately about cookies skipped for an unreadable partition', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1, partitionSkippedCookies: 1 },
'Imported 2 cookies.',
localExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
expect(warningToastMock).toHaveBeenCalledWith(
'1 cookies were not imported because their site-partition could not be read. Sign in to those sites again in Orca.',
{ duration: 12000 }
)
})
it('distinguishes unreadable cookies from readable cookies withheld to protect sessions', () => {
emitBrowserCookieImportToast(
{
...summary,
importedCookies: 1,
skippedCookies: 2,
partitionSkippedCookies: 2,
partitionSkipBreakdown: { unreadableCookies: 1, preservedRelatedCookies: 1 }
},
'Imported 1 cookie.',
remoteExecution
)
expect(warningToastMock).toHaveBeenCalledWith(
'Cookies with unreadable site partitions: 1. Related cookies also skipped: 1. Check these sites in this profile before signing in again.',
{ duration: 12000 }
)
})
it('reports a skipped partition beside successfully imported readable siblings without claiming preservation', () => {
emitBrowserCookieImportToast(
{
...summary,
importedCookies: 2,
skippedCookies: 1,
partitionSkippedCookies: 1,
partitionSkipBreakdown: { unreadableCookies: 1, preservedRelatedCookies: 0 }
},
'Imported 2 cookies.',
localExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
expect(warningToastMock).toHaveBeenCalledWith(
'Cookies with unreadable site partitions: 1. Related cookies also skipped: 0. Check these sites in this profile before signing in again.',
{ duration: 12000 }
)
})
it('localizes the count breakdown without changing either count', async () => {
const { i18n } = await import('@/i18n/i18n')
const originalLanguage = i18n.language
try {
await i18n.changeLanguage('ja')
emitBrowserCookieImportToast(
{
...summary,
totalCookies: 5,
importedCookies: 1,
skippedCookies: 4,
partitionSkippedCookies: 2,
partitionSkipBreakdown: { unreadableCookies: 1, preservedRelatedCookies: 1 }
},
'Imported 1 cookie.',
localExecution
)
expect(successToastMock).toHaveBeenCalledExactlyOnceWith(
'Imported 1 cookie. 2 件の Cookie をスキップしました。'
)
expect(warningToastMock.mock.calls[0]?.[0]).toContain('読み取れない Cookie: 1')
expect(warningToastMock.mock.calls[0]?.[0]).toContain('スキップされた関連 Cookie: 1')
} finally {
await i18n.changeLanguage(originalLanguage)
}
})
it.each([
{ unreadableCookies: 1, preservedRelatedCookies: 2 },
{ unreadableCookies: -1, preservedRelatedCookies: 3 },
{ unreadableCookies: 1.5, preservedRelatedCookies: 0.5 },
{ unreadableCookies: 0, preservedRelatedCookies: 2 },
{ unreadableCookies: Number.NaN, preservedRelatedCookies: 1 },
{ unreadableCookies: 1, preservedRelatedCookies: Number.POSITIVE_INFINITY },
{ unreadableCookies: Number.MAX_SAFE_INTEGER + 1, preservedRelatedCookies: 0 }
])('uses the existing warning for invalid breakdown counts: %j', (partitionSkipBreakdown) => {
emitBrowserCookieImportToast(
{ ...summary, partitionSkippedCookies: 2, partitionSkipBreakdown },
'Imported 1 cookie.',
remoteExecution
)
expect(warningToastMock).toHaveBeenCalledWith(
'2 cookies were not imported because their site-partition could not be read. Sign in to those sites again in Orca.',
{ duration: 12000 }
)
})
it('does not infer a Google warning from generic skipped cookies', () => {
emitBrowserCookieImportToast(
{ ...summary, importedCookies: 2, skippedCookies: 1 },
'Imported 2 cookies.',
localExecution
)
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies. 1 cookies skipped.')
expect(warningToastMock).not.toHaveBeenCalled()
})
// RPC summaries are cast rather than decoded; newer hosts can send unknown reason/code values (#14683).
it('still warns when a newer host sends an undeclared undecryptable reason', () => {
emitBrowserCookieImportToast(
@@ -51,24 +51,6 @@ describe('browser history match', () => {
])
})
it('strips scheme and a leading www. before testing the host prefix', () => {
expect(match([entry({ url: 'https://www.github.com/acme' })], 'git')).toEqual([
{ tier: 'host-prefix', url: 'https://www.github.com/acme' }
])
})
it('keeps fully-qualified URL prefixes in the top tier', () => {
expect(match([entry({ url: 'https://github.com/acme' })], 'https://github.com')).toEqual([
{ tier: 'host-prefix', url: 'https://github.com/acme' }
])
})
it('leaves a subdomain match in host-substring rather than host-prefix', () => {
expect(match([entry({ url: 'https://docs.github.com/actions' })], 'git')).toEqual([
{ tier: 'host-substring', url: 'https://docs.github.com/actions' }
])
})
it('drops url-tail matches for the omnibox and keeps them for the address bar', () => {
expect(match(GIT_CORPUS, 'git', { includeUrlTail: false }).map((row) => row.tier)).toEqual([
'host-prefix',
@@ -136,25 +118,6 @@ describe('browser history match', () => {
expect(match(tied.toReversed(), 'acme').map((row) => row.url)).toEqual(expected)
})
it('honours the limit and returns the shared empty array when nothing matches', () => {
expect(match(GIT_CORPUS, 'git', { limit: 2 })).toHaveLength(2)
const prepared = prepareBrowserHistoryEntries(GIT_CORPUS)
const missA = matchBrowserHistory({ prepared, query: 'nothing-here', limit: 3, now: NOW })
const missB = matchBrowserHistory({ prepared, query: 'also-nothing', limit: 3, now: NOW })
expect(missA).toHaveLength(0)
expect(missA).toBe(missB)
expect(matchBrowserHistory({ prepared, query: ' ', limit: 3, now: NOW })).toBe(missA)
expect(matchBrowserHistory({ prepared, query: 'git', limit: 0, now: NOW })).toBe(missA)
})
it('reuses preparation for the same immutable history snapshot', () => {
const first = prepareBrowserHistoryEntries(GIT_CORPUS)
expect(prepareBrowserHistoryEntries(GIT_CORPUS)).toBe(first)
expect(prepareBrowserHistoryEntries([...GIT_CORPUS])).not.toBe(first)
})
it('survives an unparseable url by falling back to url and title matching', () => {
const broken = entry({ url: 'not a url at all', title: 'Broken' })
@@ -184,33 +147,6 @@ describe('browser history match', () => {
expect(rows.map((row) => row.url)).toEqual(['https://acme.dev/now', 'https://acme.dev/future'])
})
it('agrees with a naive reference scan on which entries match', () => {
const corpus = Array.from({ length: 300 }, (_, index) =>
entry({
url: `https://host-${index % 17}.example${index % 3 === 0 ? '.dev' : '.com'}/path/${index}?q=seg${index % 7}`,
title: `Page ${index} about topic ${index % 11}`,
lastVisitedAt: NOW - (index % 90) * HOUR,
visitCount: index % 120
})
)
for (const query of ['host-3', 'topic 4', 'seg2', 'example.dev', 'zzz']) {
const lower = query.toLowerCase()
const reference = corpus
.filter(
(item) =>
item.url.toLowerCase().includes(lower) || item.title.toLowerCase().includes(lower)
)
.map((item) => item.url)
.sort()
expect(
match(corpus, query, { limit: corpus.length })
.map((row) => row.url)
.sort()
).toEqual(reference)
}
})
})
describe('rankBrowserTopSites', () => {