diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 4e30f7c7cfd..91585adc093 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -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, diff --git a/src/renderer/src/lib/agent-row-tool-preview.test.ts b/src/renderer/src/lib/agent-row-tool-preview.test.ts deleted file mode 100644 index 5be5e982ba8..00000000000 --- a/src/renderer/src/lib/agent-row-tool-preview.test.ts +++ /dev/null @@ -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('') - }) -}) diff --git a/src/renderer/src/lib/agent-send-title-status.test.ts b/src/renderer/src/lib/agent-send-title-status.test.ts deleted file mode 100644 index f4e2247dadc..00000000000 --- a/src/renderer/src/lib/agent-send-title-status.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/lib/agent-session-fork-context.test.ts b/src/renderer/src/lib/agent-session-fork-context.test.ts deleted file mode 100644 index 088a27ac866..00000000000 --- a/src/renderer/src/lib/agent-session-fork-context.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/src/renderer/src/lib/agent-session-launch-plan.test.ts b/src/renderer/src/lib/agent-session-launch-plan.test.ts deleted file mode 100644 index 7e1f45f8ab0..00000000000 --- a/src/renderer/src/lib/agent-session-launch-plan.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-after-failure.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-after-failure.test.ts index b3b789f3264..723e9151efc 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-after-failure.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-after-failure.test.ts @@ -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() - }) }) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-create-message.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-create-message.test.ts index 6804691d583..95756733ada 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-create-message.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-create-message.test.ts @@ -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, { diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-different-request.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-different-request.test.ts index dd328e4492c..7d14f80b1ab 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-different-request.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-different-request.test.ts @@ -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) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt.test.ts index 5fdc3936fe9..8071653ecca 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-prompt.test.ts @@ -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() - }) }) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-reload.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-reload.test.ts deleted file mode 100644 index 2319098a382..00000000000 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-reload.test.ts +++ /dev/null @@ -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( - '@/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( - '@/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 { - 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> - optionsMode?: 'replace-model-selection' - options?: Readonly> - }) => ({ - ...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) - ) - }) -}) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-resume-identity.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-resume-identity.test.ts index f1d3d05f0d6..7d906d3c30a 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-resume-identity.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-resume-identity.test.ts @@ -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') - }) }) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-unconfirmed-or-retried.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-unconfirmed-or-retried.test.ts index c3b00d58b65..9200374177d 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-unconfirmed-or-retried.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch-unconfirmed-or-retried.test.ts @@ -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 }) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch.test.ts index b1740974b32..0d0ed1e60df 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-launch.test.ts @@ -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) diff --git a/src/renderer/src/lib/agent-session/launch/structured-agent-session-new-chat-own-launch.test.ts b/src/renderer/src/lib/agent-session/launch/structured-agent-session-new-chat-own-launch.test.ts index 8fb36e09c4e..0edd8de8a40 100644 --- a/src/renderer/src/lib/agent-session/launch/structured-agent-session-new-chat-own-launch.test.ts +++ b/src/renderer/src/lib/agent-session/launch/structured-agent-session-new-chat-own-launch.test.ts @@ -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') diff --git a/src/renderer/src/lib/agent-session/structured-agent-session-draft-position-lifetime.test.tsx b/src/renderer/src/lib/agent-session/structured-agent-session-draft-position-lifetime.test.tsx index 5c693f850f7..7ca2f5fe132 100644 --- a/src/renderer/src/lib/agent-session/structured-agent-session-draft-position-lifetime.test.tsx +++ b/src/renderer/src/lib/agent-session/structured-agent-session-draft-position-lifetime.test.tsx @@ -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() - const view = render( - - - - ) - 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 () => { diff --git a/src/renderer/src/lib/agent-session/structured-agent-session-ordinary-draft.test.tsx b/src/renderer/src/lib/agent-session/structured-agent-session-ordinary-draft.test.tsx index c47843b8ed2..50ebd53a733 100644 --- a/src/renderer/src/lib/agent-session/structured-agent-session-ordinary-draft.test.tsx +++ b/src/renderer/src/lib/agent-session/structured-agent-session-ordinary-draft.test.tsx @@ -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() - clearNativeChatComposerDraftsForTests() - const loaded = Promise.withResolvers>() - setNativeChatComposerDraftStorageForTests({ ...storage, loadAll: () => loaded.promise }) - const loading = hydrateNativeChatComposerDrafts() - const view = render( - - - - ) - 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) => { diff --git a/src/renderer/src/lib/agent-session/structured-agent-session-tab-activation.test.ts b/src/renderer/src/lib/agent-session/structured-agent-session-tab-activation.test.ts deleted file mode 100644 index acad33e8a90..00000000000 --- a/src/renderer/src/lib/agent-session/structured-agent-session-tab-activation.test.ts +++ /dev/null @@ -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 -})) - -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' } - ) - }) -}) diff --git a/src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts b/src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts index 856cb058cfa..a89650a6e45 100644 --- a/src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts +++ b/src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts @@ -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 } } -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) - }) }) diff --git a/src/renderer/src/lib/agent-skill-nav-install-status.test.ts b/src/renderer/src/lib/agent-skill-nav-install-status.test.ts deleted file mode 100644 index d629dbd5825..00000000000 --- a/src/renderer/src/lib/agent-skill-nav-install-status.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/src/renderer/src/lib/agent-status-connection-ownership.test.ts b/src/renderer/src/lib/agent-status-connection-ownership.test.ts index 06a2373cf45..8ac508c15ee 100644 --- a/src/renderer/src/lib/agent-status-connection-ownership.test.ts +++ b/src/renderer/src/lib/agent-status-connection-ownership.test.ts @@ -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() diff --git a/src/renderer/src/lib/agent-status-count.test.ts b/src/renderer/src/lib/agent-status-count.test.ts deleted file mode 100644 index 49f9b0e672d..00000000000 --- a/src/renderer/src/lib/agent-status-count.test.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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({}) - }) -}) diff --git a/src/renderer/src/lib/agent-status-epoch-clock.test.ts b/src/renderer/src/lib/agent-status-epoch-clock.test.ts deleted file mode 100644 index 30dd7033627..00000000000 --- a/src/renderer/src/lib/agent-status-epoch-clock.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/renderer/src/lib/agent-status-terminal-title.test.ts b/src/renderer/src/lib/agent-status-terminal-title.test.ts deleted file mode 100644 index 756dd79b9e3..00000000000 --- a/src/renderer/src/lib/agent-status-terminal-title.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/lib/agent-status.test.ts b/src/renderer/src/lib/agent-status.test.ts index 3aec7e07360..9c3f909375f 100644 --- a/src/renderer/src/lib/agent-status.test.ts +++ b/src/renderer/src/lib/agent-status.test.ts @@ -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() - }) }) diff --git a/src/renderer/src/lib/agent-tab-shortcuts.test.ts b/src/renderer/src/lib/agent-tab-shortcuts.test.ts deleted file mode 100644 index f1fbec9a9ef..00000000000 --- a/src/renderer/src/lib/agent-tab-shortcuts.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/lib/agent-verdict-status-line.test.ts b/src/renderer/src/lib/agent-verdict-status-line.test.ts deleted file mode 100644 index c402603e38d..00000000000 --- a/src/renderer/src/lib/agent-verdict-status-line.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts b/src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts index 554bf28ac7a..470e0be1579 100644 --- a/src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts +++ b/src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts @@ -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' diff --git a/src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts b/src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts index 1836d26047a..9acd16c3eb5 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts @@ -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(), diff --git a/src/renderer/src/lib/ai-vault-resume-command.test.ts b/src/renderer/src/lib/ai-vault-resume-command.test.ts index ef83ae9cd9b..c5142c0bd87 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.test.ts @@ -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'") - }) }) diff --git a/src/renderer/src/lib/ai-vault-resume-platform.test.ts b/src/renderer/src/lib/ai-vault-resume-platform.test.ts index d2ad38b7ff7..adf2e0202d8 100644 --- a/src/renderer/src/lib/ai-vault-resume-platform.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-platform.test.ts @@ -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', diff --git a/src/renderer/src/lib/ai-vault-resume-target.test.ts b/src/renderer/src/lib/ai-vault-resume-target.test.ts index f60bc8f24ad..6102563c10a 100644 --- a/src/renderer/src/lib/ai-vault-resume-target.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-target.test.ts @@ -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') - }) }) diff --git a/src/renderer/src/lib/ai-vault-session-drag.test.ts b/src/renderer/src/lib/ai-vault-session-drag.test.ts index ed0326eb38b..a5b0928c891 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.test.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.test.ts @@ -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, { diff --git a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts index 278a56f0f29..05cc85fd4a8 100644 --- a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts +++ b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts @@ -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 } } }) diff --git a/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts b/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts index cd43e33871a..b2d540fd05c 100644 --- a/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts +++ b/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts @@ -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 { 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) - }) }) diff --git a/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts b/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts index a062b9df906..cf8310cb88f 100644 --- a/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts +++ b/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts @@ -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, diff --git a/src/renderer/src/lib/app-command-dispatch.test.ts b/src/renderer/src/lib/app-command-dispatch.test.ts index f898dd7cd89..3d64d07bc26 100644 --- a/src/renderer/src/lib/app-command-dispatch.test.ts +++ b/src/renderer/src/lib/app-command-dispatch.test.ts @@ -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) diff --git a/src/renderer/src/lib/app-menu-paste.test.ts b/src/renderer/src/lib/app-menu-paste.test.ts index 1a508ad7678..5ffb2cff1e4 100644 --- a/src/renderer/src/lib/app-menu-paste.test.ts +++ b/src/renderer/src/lib/app-menu-paste.test.ts @@ -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') diff --git a/src/renderer/src/lib/app-menu-selection-actions.test.ts b/src/renderer/src/lib/app-menu-selection-actions.test.ts deleted file mode 100644 index b807474d927..00000000000 --- a/src/renderer/src/lib/app-menu-selection-actions.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/renderer/src/lib/automation-session-observer.test.ts b/src/renderer/src/lib/automation-session-observer.test.ts index f4d5cd6eaa3..f38636bd506 100644 --- a/src/renderer/src/lib/automation-session-observer.test.ts +++ b/src/renderer/src/lib/automation-session-observer.test.ts @@ -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() - }) }) diff --git a/src/renderer/src/lib/automation-terminal-ownership.test.ts b/src/renderer/src/lib/automation-terminal-ownership.test.ts index 83658228690..acb8a823743 100644 --- a/src/renderer/src/lib/automation-terminal-ownership.test.ts +++ b/src/renderer/src/lib/automation-terminal-ownership.test.ts @@ -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) diff --git a/src/renderer/src/lib/base64-bytes.test.ts b/src/renderer/src/lib/base64-bytes.test.ts deleted file mode 100644 index 53434f03368..00000000000 --- a/src/renderer/src/lib/base64-bytes.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/lib/bounded-generation-map.test.ts b/src/renderer/src/lib/bounded-generation-map.test.ts index 7412a6de586..3eb15e7d1f4 100644 --- a/src/renderer/src/lib/bounded-generation-map.test.ts +++ b/src/renderer/src/lib/bounded-generation-map.test.ts @@ -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) - }) }) diff --git a/src/renderer/src/lib/browser-cookie-import-toast.test.ts b/src/renderer/src/lib/browser-cookie-import-toast.test.ts index 8f2b4d1f8cd..1b16d9b3d01 100644 --- a/src/renderer/src/lib/browser-cookie-import-toast.test.ts +++ b/src/renderer/src/lib/browser-cookie-import-toast.test.ts @@ -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( diff --git a/src/renderer/src/lib/browser-history-match.test.ts b/src/renderer/src/lib/browser-history-match.test.ts index 9a146c71088..e770d86a559 100644 --- a/src/renderer/src/lib/browser-history-match.test.ts +++ b/src/renderer/src/lib/browser-history-match.test.ts @@ -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', () => {