diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 2339348f4fd..44e7462debc 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -2797,24 +2797,15 @@ "src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 52, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts": 375, "src/main/native-chat/agent-session-wire/observations/testing/agent-session-current-requests.test.ts": 271, - "src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-overflow-prefix.test.ts": 105, "src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-protected.test.ts": 68, "src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts": 61, - "src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-empty-delta-retention.test.ts": 362, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts": 837, - "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-accounting.test.ts": 1200, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-limits.test.ts": 858, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-conversation-window.test.ts": 3502, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts": 2439, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-large-identities.test.ts": 1124, - "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page-scaling.test.ts": 57, - "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page.test.ts": 1049, - "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-item-wire-form.test.ts": 2246, "src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-stop-note-paging.test.ts": 1337, "src/main/native-chat/agent-session-wire/options/testing/at-rest-command-demand.test.ts": 43, - "src/main/native-chat/agent-session-wire/providers/testing/hook-failure-provider-frame.test.ts": 234, - "src/main/native-chat/agent-session-wire/publication/history/testing/json-string-shortening.test.ts": 47, - "src/main/native-chat/agent-session-wire/providers/testing/provider-frame-disposition.test.ts": 82, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-accept-then-deliver.test.ts": 7805, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-acquire-isolation.test.ts": 1897, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-adopted-import.test.ts": 2881, @@ -2826,7 +2817,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-clear-queued-command.test.ts": 2168, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-close-aborts-start.test.ts": 1806, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-codex-held-goal.test.ts": 2081, - "src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-command-publication.test.ts": 1350, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-create-adoption-budget.test.ts": 4134, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-create-adoption.test.ts": 1670, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-current-requests.test.ts": 3325, @@ -2844,7 +2834,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-late-settlement-cancelled.test.ts": 2020, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-lease-renewer.test.ts": 875, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-newer-content-host.test.ts": 1997, - "src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-operation-start-refusal.test.ts": 1210, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-options-read-scaling.test.ts": 1006, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-prompt-cancel-repeat.test.ts": 1493, "src/main/native-chat/agent-session-wire/providers/testing/structured-agent-session-provider-naming.test.ts": 297, @@ -2866,14 +2855,11 @@ "src/main/native-chat/agent-session-wire/journal-ingress/testing/structured-agent-session-resolved-append.test.ts": 683, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-audience.test.ts": 2056, "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts": 731, - "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-context-clear.test.ts": 538, - "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-continuation.test.ts": 533, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-failure-filing.test.ts": 2350, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-after-reattach.test.ts": 1903, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-endings.test.ts": 2091, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-newer-orca.test.ts": 2824, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-ownership.test.ts": 4640, - "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-resume.test.ts": 575, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-teardown.test.ts": 2163, "src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-resumed-child-owner.test.ts": 682, "src/main/native-chat/agent-session-wire/rewind/testing/structured-agent-session-rewind-provenance.test.ts": 626, @@ -2885,18 +2871,11 @@ "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts": 305, "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts": 972, "src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-prompts.test.ts": 665, - "src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-start-flight.test.ts": 48, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-startup-attempt-reports.test.ts": 1857, - "src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-launch-directory.test.ts": 856, - "src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-stopping.test.ts": 831, - "src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts": 1849, "src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts": 1147, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-stop-event.test.ts": 1644, - "src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-opened-turn.test.ts": 1446, - "src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-withdrawn-send.test.ts": 1566, "src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-background-tasks.test.ts": 829, "src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-oversized-removal.test.ts": 717, - "src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-publication.test.ts": 732, "src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-target-reconcile.test.ts": 786, "src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts": 5574, "src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-thread-goal.test.ts": 3029, @@ -2908,7 +2887,6 @@ "src/main/native-chat/agent-session-wire/host/testing/structured-conversation-command-child-work.test.ts": 1810, "src/main/native-chat/agent-session-wire/host/testing/structured-conversation-command.test.ts": 2989, "src/main/native-chat/agent-session-wire/rewind/testing/structured-rewind-cut.test.ts": 672, - "src/main/native-chat/agent-session-wire/providers/testing/unhandled-provider-frame.test.ts": 231, "src/main/native-chat/chat-launcher-command.test.ts": 651, "src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 373, "src/main/native-chat/host-readable-transcript-path.test.ts": 150, @@ -6533,7 +6511,6 @@ "src/renderer/src/components/pull-request-page/page/use-details.test.tsx": 745, "src/renderer/src/components/quick-open-file-history.test.ts": 1716, "src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1941, - "src/renderer/src/components/quick-open-file-list-metadata.react.test.tsx": 2100, "src/renderer/src/components/quick-open-file-list.react.test.tsx": 2215, "src/renderer/src/components/quick-open-file-list.test.ts": 54, "src/renderer/src/components/quick-open-history-ranking.test.ts": 112, @@ -6616,14 +6593,12 @@ "src/renderer/src/components/right-sidebar/source-control/commit/use-commit-message-generation.cancel.test.ts": 1992, "src/renderer/src/components/right-sidebar/source-control/listing/open-uncommitted-entry.test.ts": 900, "src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 703, - "src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1912, "src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.test.tsx": 866, "src/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-operation-target.test.ts": 1735, "src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-jira-title.test.tsx": 1956, "src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 2720, "src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 268, "src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 266, - "src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-wsl-wake.test.tsx": 2095, "src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx": 585, "src/renderer/src/components/right-sidebar/ai-vault/search/use-ai-vault-search.test.tsx": 688, "src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-terminal-worktree.test.ts": 2116, @@ -6749,7 +6724,6 @@ "src/renderer/src/components/sidebar/CommentMarkdown.parse-validity.test.tsx": 7186, "src/renderer/src/components/sidebar/CommentMarkdown.unknown-html.test.tsx": 3140, "src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 2113, - "src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 389, "src/renderer/src/components/sidebar/workspace-actions/deletion/DeleteNestedWorktreesDialog.test.tsx": 1807, "src/renderer/src/components/sidebar/HostRemoveDialog.test.tsx": 6650, "src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 2025, @@ -6789,7 +6763,6 @@ "src/renderer/src/components/sidebar/worktree-list/grouping/host-section-reveal.test.tsx": 2294, "src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 52, "src/renderer/src/components/sidebar/worktree-list/listing/manual-sort-switch-toast.test.ts": 1753, - "src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 543, "src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 230, "src/renderer/src/components/sidebar/worktree-list/grouping/pinned-section-worktrees.test.ts": 446, "src/renderer/src/components/sidebar/workspace-actions/deletion/preserved-branch-batch-toast.test.tsx": 627, @@ -8951,11 +8924,6 @@ "src/shared/host-balanced-listing-scaling.test.ts": 64, "src/shared/host-setting-overrides.test.ts": 55, "src/shared/host-spawn-admission.test.ts": 183, - "src/shared/source-control/hosted-review/hosted-review-create-confirmation.test.ts": 43, - "src/shared/source-control/hosted-review/hosted-review-create-seed.test.ts": 83, - "src/shared/source-control/hosted-review/hosted-review-creation-providers.test.ts": 35, - "src/shared/source-control/hosted-review/hosted-review-github.test.ts": 40, - "src/shared/source-control/hosted-review/hosted-review-push-target.test.ts": 52, "src/shared/source-control/hosted-review/hosted-review-refs.test.ts": 57, "src/shared/html-file-url.test.ts": 49, "src/shared/image-data-uri.test.ts": 59, @@ -9244,11 +9212,7 @@ "src/shared/powershell-native-argument.test.ts": 34, "src/shared/powershell-native-argument.windows.test.ts": 67, "src/shared/source-control/review-comments/pr-bot-author-overrides.test.ts": 76, - "src/shared/source-control/checks/pr-check-severity-order.test.ts": 51, "src/shared/source-control/checks/pr-checks-fix-prompt.test.ts": 93, - "src/shared/source-control/review-comments/pr-comment-audience.test.ts": 51, - "src/shared/source-control/review-comments/pr-comment-groups.test.ts": 35, - "src/shared/source-control/review-comments/pr-comment-time.test.ts": 40, "src/shared/preferred-git-remote.test.ts": 42, "src/shared/priority-semaphore.test.ts": 43, "src/shared/process-growth-breaker.test.ts": 47, @@ -9432,14 +9396,8 @@ "src/shared/skill-package-manifest.test.ts": 188, "src/shared/skill-path-containment.test.ts": 49, "src/shared/sleeping-record-legacy-pane-key.test.ts": 59, - "src/shared/source-control/decisions/source-control-create-review-intent.test.ts": 54, "src/shared/source-control/listing/source-control-discard-all-sequence.test.ts": 50, - "src/shared/source-control/listing/source-control-entry-actions.test.ts": 54, - "src/shared/source-control/listing/source-control-group-order.test.ts": 31, - "src/shared/source-control/decisions/source-control-next-step-decision.test.ts": 99, - "src/shared/source-control/decisions/source-control-primary-action-decision.test.ts": 67, "src/shared/source-control/recovery/source-control-push-failure.test.ts": 51, - "src/shared/source-control/listing/source-control-section-order.test.ts": 63, "src/shared/source-control/listing/source-control-tree.test.ts": 63, "src/shared/source-scan/source-tree-scan.test.ts": 50, "src/shared/source-scan/source-tree-walk.test.ts": 62, diff --git a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs index 19bdf866346..80ade2ab259 100644 --- a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs +++ b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs @@ -152,7 +152,6 @@ const NODE_RUNTIME_INCLUDE = [ 'src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx', 'src/main/native-chat/agent-session-journal/**/*.test.ts', 'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts', - 'src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts', 'src/main/runtime/agent-session/records/agent-session-conversation-name-store.test.ts', 'src/main/runtime/rpc/ws-transport.test.ts', 'src/main/runtime/rpc/dispatcher-transport-parity.test.ts', diff --git a/mobile/src/hooks/mobile-dictation-on-device.test.tsx b/mobile/src/hooks/mobile-dictation-on-device.test.tsx index d7f9c020804..cca33fcf3c9 100644 --- a/mobile/src/hooks/mobile-dictation-on-device.test.tsx +++ b/mobile/src/hooks/mobile-dictation-on-device.test.tsx @@ -149,29 +149,6 @@ beforeEach(() => { held.errors = [] }) -describe('a dictation the phone can hear', () => { - it('records without the computer and inserts the phone’s text', async () => { - const rpc = createFakeRpcClient() - const sent: SentRequest[] = [] - const transcripts: string[] = [] - mount(rpc, (text) => transcripts.push(text)) - await act(async () => { - await dictation().start() - }) - expect(dictation().status).toBe('recording') - speak([1, 2, 3, 4]) - seam.finish = { text: ' hello phone ', failure: null } - await act(async () => { - const stopped = dictation().stop() - await pump(rpc, sent) - await stopped - }) - expect(transcripts).toEqual(['hello phone']) - expect(sent).toEqual([]) - expect(dictation().status).toBe('idle') - }) -}) - describe('a phone recognizer that fails at the finish', () => { it('replays what was said to the computer and inserts its text', async () => { const rpc = createFakeRpcClient() @@ -280,19 +257,3 @@ describe('a stop while a failed phone dictation is reaching the computer', () => expect(dictation().status).toBe('idle') }) }) - -describe('a dictation when the user chose Computer', () => { - it('goes to the computer and never arms the phone', async () => { - seam.preference = 'computer' - const rpc = createFakeRpcClient() - const sent: SentRequest[] = [] - mount(rpc, () => {}) - await act(async () => { - const started = dictation().start() - await pump(rpc, sent) - await started - }) - expect(sent.map((request) => request.method)).toEqual(['speech.dictation.start']) - expect(seam.calls).toEqual([]) - }) -}) diff --git a/mobile/src/platform/native-speech.test.ts b/mobile/src/platform/native-speech.test.ts index 66eeccfcff7..62a5be12d16 100644 --- a/mobile/src/platform/native-speech.test.ts +++ b/mobile/src/platform/native-speech.test.ts @@ -24,11 +24,6 @@ describe('a page asking for the microphone when the user chose This phone', () = await server.serve({ action: 'cancel' }) expect(await server.mayCapture()).toBe(false) }) - - it('is allowed under Automatic, where the computer is a fallback', async () => { - const server = createNativeSpeechServer(engineWith('automatic')) - expect(await server.mayCapture()).toBe(true) - }) }) describe('a recognizer another page took over', () => { diff --git a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-launch-directory.test.ts b/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-launch-directory.test.ts deleted file mode 100644 index 00502670c12..00000000000 --- a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-launch-directory.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { AgentSessionExecutionLocation } from '../../../../../shared/agent-session/lifecycle/agent-session-record' -import type { AgentSessionStatusEvent } from '../../../../../shared/agent-session/agent-session-wire' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' -import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import type { AgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store' -import { openTestAgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store-test-harness' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' -import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed' - -const SESSION = 'session-alpha' -const NOW = 1_800_000_000_000 - -const FLOATING: AgentSessionExecutionLocation = { - executionHostId: 'local', - wslDistro: null, - workspaceId: FLOATING_TERMINAL_WORKTREE_ID, - workspaceKind: 'folder' -} -const WORKTREE: AgentSessionExecutionLocation = { - ...FLOATING, - workspaceId: 'repo-1::/repos/one', - workspaceKind: 'git-worktree' -} - -let root: string -let store: AgentSessionRecordStore -const journals = createTrackedJournalOpener() - -beforeEach(async () => { - root = await mkdtemp(join(tmpdir(), 'orca-status-feed-workspace-path-')) - store = await openTestAgentSessionRecordStore(root) -}) - -afterEach(async () => { - await journals.closeAll() - await rm(root, { recursive: true, force: true }) -}) - -async function feedFor(location: AgentSessionExecutionLocation) { - await store.reserveOwner({ - sessionId: SESSION, - location, - provider: 'codex', - accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' }, - expectedFence: null, - spawnToken: 'spawn-a', - claimKeyId: 'key-1', - handoffOperationId: null, - probe: { outcome: 'reservation-unused' }, - operation: { - callerKey: 'client-1', - operationId: `${NOW}-${'1'.padStart(32, '0')}`, - fingerprint: 'fp-1' - }, - now: NOW - }) - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: location.workspaceId, - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - stateDirectory: root - }) - const feed = new StructuredAgentSessionStatusFeed({ - logger: createStructuredAgentSessionLogger(), - sessions: new Map([[SESSION, { journal, params: { location, provider: 'codex' } }]]), - getConversation: (sessionId) => store.getConversation(sessionId), - now: () => NOW - }) - const events: AgentSessionStatusEvent[] = [] - feed.subscribe({ id: 'renderer', emit: (event) => events.push(event) }) - return { feed, events } -} - -function lastSummary(events: readonly AgentSessionStatusEvent[]) { - const event = events.at(-1) - if (event?.type === 'status') { - return event.session - } - if (event?.type === 'snapshot') { - return event.sessions.find((session) => session.sessionId === SESSION) - } - throw new Error('no status publication') -} - -describe('status feed pinned launch directory', () => { - it('publishes a floating session its pinned folder once the launch pins it', async () => { - const { feed, events } = await feedFor(FLOATING) - expect(lastSummary(events)).toMatchObject({ sessionId: SESSION }) - expect(lastSummary(events)).not.toHaveProperty('launchDirectory') - - await store.pinLaunchDirectory(SESSION, '/home/me/original-floating') - feed.publish(SESSION) - - expect(events.at(-1)).toMatchObject({ - type: 'status', - session: { sessionId: SESSION, launchDirectory: '/home/me/original-floating' } - }) - }) - - it('omits the pin for a worktree session, whose id is where it launches', async () => { - const { feed, events } = await feedFor(WORKTREE) - await store.pinLaunchDirectory(SESSION, '/repos/one') - feed.publish(SESSION) - - expect(lastSummary(events)).toMatchObject({ sessionId: SESSION }) - expect(lastSummary(events)).not.toHaveProperty('launchDirectory') - }) -}) diff --git a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-stopping.test.ts b/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-stopping.test.ts deleted file mode 100644 index eeefe17974e..00000000000 --- a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-stopping.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { agentSessionConversationView } from '../../../../../shared/agent-session/lifecycle/agent-session-conversation-view' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import type { AgentSessionStatusSummary } from '../../../../../shared/agent-session/agent-session-wire' -import { agentSessionRecordFixture } from '../../../../../shared/agent-session/lifecycle/agent-session-record.test-fixture' -import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { indexedStatusFeedSession } from './structured-agent-session-status-feed-test-session' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' -import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed' - -const SESSION = 'status-session' - -let root: string -const journals = createTrackedJournalOpener() - -beforeEach(async () => { - root = await mkdtemp(join(tmpdir(), 'orca-status-feed-stopping-')) -}) - -afterEach(async () => { - await journals.closeAll() - await rm(root, { recursive: true, force: true }) -}) - -describe("the status feed's Stopping", () => { - it('publishes it while the stopped turn runs, and drops it when the host lets go', async () => { - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - stateDirectory: join(root, SESSION) - }) - const fence = agentSessionRecordFixture().lease.runtimeFence - const published: AgentSessionStatusSummary[] = [] - const session = indexedStatusFeedSession({ - journal, - child: { phase: 'ready', generation: 'child-1', fence } - }) - const feed = new StructuredAgentSessionStatusFeed({ - logger: createStructuredAgentSessionLogger(), - sessions: new Map([[SESSION, session]]), - statusSink: () => ({ publish: (summary) => published.push(summary), forget: () => {} }), - getConversation: () => agentSessionConversationView(agentSessionRecordFixture()), - now: () => 1_000 - }) - await journal.appendItem( - { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 }, - { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: 1_000 }, - { fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - feed.publish(SESSION) - expect(published.at(-1)).toMatchObject({ status: 'working' }) - expect(published.at(-1)).not.toHaveProperty('stopping') - - await journal.appendStopEvent({ reason: 'user-stop', turnId: 'turn-1' }, fence) - feed.publish(SESSION) - expect(published.at(-1)).toMatchObject({ status: 'working', stopping: true }) - - feed.revokeLive(SESSION) - expect(published.at(-1)).toMatchObject({ status: 'working' }) - expect(published.at(-1)).not.toHaveProperty('stopping') - }) - - it('reads a turn waiting on the person as attention, never Stopping', async () => { - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - stateDirectory: join(root, SESSION) - }) - const fence = agentSessionRecordFixture().lease.runtimeFence - const published: AgentSessionStatusSummary[] = [] - const feed = new StructuredAgentSessionStatusFeed({ - logger: createStructuredAgentSessionLogger(), - sessions: new Map([[SESSION, indexedStatusFeedSession({ journal })]]), - statusSink: () => ({ publish: (summary) => published.push(summary), forget: () => {} }), - getConversation: () => agentSessionConversationView(agentSessionRecordFixture()), - now: () => 1_000 - }) - const turn = { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1' } as const - await journal.appendItem( - { ...turn, ordinal: 0 }, - { kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: 1_000 }, - { fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - await journal.appendStopEvent({ reason: 'user-stop', turnId: 'turn-1' }, fence) - await journal.appendItem( - { ...turn, ordinal: 1 }, - { - kind: 'approval', - title: 'Run command?', - detail: null, - options: [{ id: 'yes', label: 'Allow' }], - resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } - }, - { fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - - feed.publish(SESSION) - - expect(published.at(-1)).toMatchObject({ status: 'attention' }) - expect(published.at(-1)).not.toHaveProperty('stopping') - }) -}) diff --git a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts b/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts deleted file mode 100644 index 1e13d1f753c..00000000000 --- a/src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { savedStructuredAgentSessionStatus } from '../structured-agent-session-saved-status' -import { releasedStructuredStatusSummary } from '../structured-agent-session-status-summary' -import { agentSessionConversationView } from '../../../../../shared/agent-session/lifecycle/agent-session-conversation-view' -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { AgentSessionRecord } from '../../../../../shared/agent-session/lifecycle/agent-session-record' -import { agentSessionRecordFixture } from '../../../../../shared/agent-session/lifecycle/agent-session-record.test-fixture' -import type { AgentSessionStatusEvent } from '../../../../../shared/agent-session/agent-session-wire' -import type { AgentChildWorkView } from '../../../../../shared/agent-status/child-work/agent-status-child-work-view' -import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { indexedStatusFeedSession as indexed } from './structured-agent-session-status-feed-test-session' -import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed' -import type { StructuredAgentSessionStatusFeedDeps } from '../structured-agent-session-status-feed-deps' -import type { StructuredAgentSessionStatusSink } from '../structured-agent-session-status-ownership' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' - -const SESSION = 'status-session' -const USER_IDENTITY = { - provider: 'codex', - threadId: 'thread-1', - turnId: 'turn-1', - ordinal: 1 -} as const - -let root: string -const journals = createTrackedJournalOpener() - -beforeEach(async () => { - root = await mkdtemp(join(tmpdir(), 'orca-agent-status-feed-')) -}) - -afterEach(async () => { - await journals.closeAll() - await rm(root, { recursive: true, force: true }) -}) - -async function openJournal(sessionId = SESSION, now?: () => number) { - return journals.open({ - identity: { - sessionId, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - now, - stateDirectory: join(root, sessionId) - }) -} - -function feedFor( - sessions: Map[0]>, - record: Partial | null = null, - onStatusChanged?: StructuredAgentSessionStatusFeedDeps['onStatusChanged'], - readChildWork?: () => AgentChildWorkView[], - statusSink?: StructuredAgentSessionStatusSink -) { - let now = 1_000 - const sink = - statusSink ?? - (readChildWork ? { publish: () => {}, forget: () => {}, readChildWork } : undefined) - const feed = new StructuredAgentSessionStatusFeed({ - logger: createStructuredAgentSessionLogger(), - ...(onStatusChanged ? { onStatusChanged } : {}), - ...(sink ? { statusSink: () => sink } : {}), - sessions: { - get: (sessionId: string) => { - const session = sessions.get(sessionId) - return session ? indexed(session) : undefined - }, - [Symbol.iterator]: function* () { - for (const [sessionId, session] of sessions) { - yield [sessionId, indexed(session)] as const - } - } - } as unknown as ReadonlyMap>, - // A partial record still has a lease: the feed reads the conversation's fence off it. - getConversation: () => - record ? agentSessionConversationView({ ...agentSessionRecordFixture(), ...record }) : null, - now: () => (now += 1) - }) - const events: AgentSessionStatusEvent[] = [] - const dispose = feed.subscribe({ id: 'list-1', emit: (event) => events.push(event) }) - return { feed, events, dispose } -} - -describe('StructuredAgentSessionStatusFeed', () => { - it('ends a closed subscriber and keeps publishing to the rest', async () => { - const journal = await openJournal() - const { feed, events, dispose } = feedFor(new Map([[SESSION, { journal }]])) - const others: AgentSessionStatusEvent[] = [] - feed.subscribe({ id: 'list-2', emit: (event) => others.push(event) }) - - dispose() - await journal.appendItem( - USER_IDENTITY, - { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - feed.publish(SESSION) - - expect(events.at(-1)).toEqual({ type: 'end' }) - expect(others.at(-1)).toEqual({ - type: 'status', - session: expect.objectContaining({ status: 'idle' }) - }) - }) -}) - -// Broadcast cache retains closed history; roster reads must intersect it with live sessions. -describe('the status sink sees the roster the broadcast cache deliberately lacks', () => { - it('keeps publishing to subscribers when the sink throws', async () => { - const journal = await openJournal() - const sink: StructuredAgentSessionStatusSink = { - publish: () => { - throw new Error('store down') - }, - forget: () => { - throw new Error('store down') - } - } - const { feed, events } = feedFor( - new Map([[SESSION, { journal }]]), - null, - undefined, - undefined, - sink - ) - await journal.appendItem( - USER_IDENTITY, - { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - feed.publish(SESSION, journal) - expect(() => feed.forget(SESSION)).not.toThrow() - expect(events.at(-1)).toMatchObject({ - type: 'status', - session: expect.objectContaining({ sessionId: SESSION, status: 'idle' }) - }) - }) -}) - -it('publishes a saved mode-only change without journal activity and retains it in reconnect summaries', async () => { - const journal = await openJournal() - const sessions = new Map[0]>([[SESSION, { journal }]]) - const record: Partial = { options: {} } - const { feed, events, dispose } = feedFor(sessions, record) - const cursor = journal.cursor() - record.options = { interactionMode: 'plan' } - feed.publish(SESSION, journal) - expect(events.at(-1)).toEqual({ - type: 'status', - session: expect.objectContaining({ interactionMode: 'plan' }) - }) - expect(journal.cursor()).toEqual(cursor) - const saved = savedStructuredAgentSessionStatus( - agentSessionConversationView({ ...agentSessionRecordFixture(), options: record.options }) - ) - expect(saved.interactionMode).toBe('plan') - expect(releasedStructuredStatusSummary(saved).interactionMode).toBe('plan') - record.options = { interactionMode: 'default' } - feed.publish(SESSION, journal) - expect(events.at(-1)).toEqual({ - type: 'status', - session: expect.objectContaining({ interactionMode: 'default' }) - }) - dispose() -}) diff --git a/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-opened-turn.test.ts b/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-opened-turn.test.ts deleted file mode 100644 index 9e0de7f4a66..00000000000 --- a/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-opened-turn.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { agentJournalItemKey } from '../../../../../shared/agent-session/journal/agent-session-journal-item-key' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import type { AgentSessionJournalIdentity } from '../../../../../shared/agent-session/journal/agent-session-journal-identity' -import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import type { StructuredAgentSessionAdapter } from '../../providers/structured-agent-session-adapter' -import { NO_STRUCTURED_AGENTS } from '../../providers/testing/structured-agent-session-adapter-router-test-support' -import { structuredAgentSessionStopNoteIdentity } from '../structured-agent-session-command-turn' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' -import '../structured-agent-session-turns' -import { performCancel } from '../structured-agent-session-turns-cancel' -import type { AgentSessionTurnContext } from '../structured-agent-session-turns' - -const IDENTITY: AgentSessionJournalIdentity = { - sessionId: 'session-1', - workspaceId: 'workspace-1', - hostId: 'host-1', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') -} -const TURN = { provider: 'codex' as const, threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 } - -let root: string | null = null -const journals = createTrackedJournalOpener() - -afterEach(async () => { - await journals.closeAll() - if (root) { - await rm(root, { recursive: true, force: true }) - root = null - } -}) - -async function stopWhileTheTurnOpens( - outcome: Awaited> -) { - root = await mkdtemp(join(tmpdir(), 'orca-stop-note-opened-turn-')) - const journal = await journals.open({ identity: IDENTITY, stateDirectory: root }) - const ctx: AgentSessionTurnContext = { - logger: createStructuredAgentSessionLogger(), - sessionId: 'session-1', - journal, - fence: 1, - agents: NO_STRUCTURED_AGENTS, - agent: 'codex', - adapter: { - open: async () => ({ - reportedOptions: { model: '' } - }), - acquire: vi.fn(), - dispatch: vi.fn(), - closeSession: vi.fn(), - cancelTurn: vi.fn(async () => { - await journal.appendItem( - TURN, - { kind: 'turn', turnId: 'turn-1', state: 'running' }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - return outcome - }), - answerPrompt: vi.fn(), - setOption: vi.fn() - }, - persistOptions: async () => undefined, - resolvedBy: 'client-1', - publish: vi.fn(), - now: () => 1 - } - await performCancel(ctx, { clientOperationId: 'stop-1', endSession: () => {} }) - return journal - .snapshot() - .items.flatMap((item) => - item.body.kind === 'status' ? [{ itemId: item.itemId, turnScope: item.turnScope }] : [] - ) -} - -describe('a Stop that waited for a turn to open', () => { - it('keeps the request before the turn that opens during interruption', async () => { - expect(await stopWhileTheTurnOpens({ cancelled: true, turnId: 'turn-1' })).toEqual([ - { - itemId: agentJournalItemKey(structuredAgentSessionStopNoteIdentity('stop-1')), - turnScope: AGENT_JOURNAL_THREAD_SCOPE - } - ]) - }) - - it("leaves a refused Stop's row with the conversation, as before", async () => { - expect( - await stopWhileTheTurnOpens({ - cancelled: false, - refusal: { detail: { text: 'failed to interrupt turn', audience: 'person' } } - }) - ).toEqual([ - { - itemId: agentJournalItemKey(structuredAgentSessionStopNoteIdentity('stop-1')), - turnScope: AGENT_JOURNAL_THREAD_SCOPE - } - ]) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-withdrawn-send.test.ts b/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-withdrawn-send.test.ts deleted file mode 100644 index 769fda64943..00000000000 --- a/src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-withdrawn-send.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Accepted Stop retains its request note even when child exit takes back the send. - -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { agentSessionFailureFact } from '../../../../../shared/agent-session/failures/agent-session-failure' -import { agentSessionFailureWords } from '../../../../../shared/agent-session/failures/agent-session-failure-words' -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import type { AgentSessionJournalIdentity } from '../../../../../shared/agent-session/journal/agent-session-journal-identity' -import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' -import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' -import '../structured-agent-session-turns' -import { performCancel } from '../structured-agent-session-turns-cancel' -import type { AgentSessionTurnContext } from '../structured-agent-session-turns' -import { NO_STRUCTURED_AGENTS } from '../../providers/testing/structured-agent-session-adapter-router-test-support' - -const IDENTITY: AgentSessionJournalIdentity = { - sessionId: 'session-1', - workspaceId: 'workspace-1', - hostId: 'host-1', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') -} - -let root: string | null = null -const journals = createTrackedJournalOpener() - -afterEach(async () => { - await journals.closeAll() - if (root) { - await rm(root, { recursive: true, force: true }) - root = null - } -}) - -async function stopEndingTheChild(options: { - turnRunning: boolean - takesSendBack: boolean - earlierProcessSendInDoubt?: boolean -}) { - root = await mkdtemp(join(tmpdir(), 'orca-stop-note-withdrawn-')) - const journal = await journals.open({ identity: IDENTITY, stateDirectory: root }) - if (options.turnRunning) { - await journal.appendItem( - { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 }, - { kind: 'turn', turnId: 'turn-1', state: 'running' }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - } - if (options.earlierProcessSendInDoubt) { - await journal.appendSubmission({ - clientMessageId: 'send-0', - payloadFingerprint: 'send-0', - body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'before the crash' }] }, - fence: 1 - }) - await journal.resolveDispatch({ - clientMessageId: 'send-0', - state: 'unknown', - reason: 'host_restarted', - fence: 1, - recovered: true - }) - } - await journal.appendSubmission({ - clientMessageId: 'send-1', - payloadFingerprint: 'send-1', - body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'look around' }] }, - fence: 1 - }) - const ctx: AgentSessionTurnContext = { - logger: createStructuredAgentSessionLogger(), - sessionId: 'session-1', - journal, - fence: 1, - agents: NO_STRUCTURED_AGENTS, - agent: 'codex', - adapter: { - open: async () => ({ - reportedOptions: { model: '' } - }), - acquire: vi.fn(), - dispatch: vi.fn(), - closeSession: vi.fn(), - cancelTurn: vi.fn(async () => ({ - cancelled: false, - refusal: { detail: { text: 'failed to interrupt turn', audience: 'person' as const } } - })), - answerPrompt: vi.fn(), - setOption: vi.fn() - }, - persistOptions: async () => undefined, - resolvedBy: 'client-1', - publish: vi.fn(), - now: () => 1 - } - const result = await performCancel(ctx, { - clientOperationId: 'stop-1', - stopChild: async () => { - if (options.takesSendBack) { - await journal.resolveDispatch({ - clientMessageId: 'send-1', - state: 'rejected', - ...agentSessionFailureWords(agentSessionFailureFact('cancelled'), { - surface: 'rejection' - }), - fence: 1, - recovered: true - }) - } - } - }) - const rows = journal - .snapshot() - .items.flatMap((item) => (item.body.kind === 'status' ? [item.body.text] : [])) - return { cancelled: result.ok && result.value.cancelled, rows } -} - -describe('a Stop that ends the child after its interrupt failed', () => { - it("keeps Stop when the only other send in doubt was an earlier process's", async () => { - expect( - await stopEndingTheChild({ - turnRunning: false, - takesSendBack: true, - earlierProcessSendInDoubt: true - }) - ).toEqual({ cancelled: true, rows: ['Cancellation requested.'] }) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/options/testing/structured-agent-session-send-options.test.ts b/src/main/native-chat/agent-session-wire/options/testing/structured-agent-session-send-options.test.ts deleted file mode 100644 index cc639471066..00000000000 --- a/src/main/native-chat/agent-session-wire/options/testing/structured-agent-session-send-options.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, it } from 'vitest' -import { - captureStructuredExecutionOptions, - openingMessageOptions -} from '../structured-agent-session-execution-options' -import { claudeAndCodexAgents } from '../../providers/testing/structured-agent-session-adapter-router-test-support' - -it('inherits a saved Plan for older phone selections without adding model intent', () => { - expect( - captureStructuredExecutionOptions( - { - agents: claudeAndCodexAgents(), - agent: 'claude', - persistedOptions: { interactionMode: 'plan' } - }, - undefined, - {} - ) - ).toEqual({ options: { interactionMode: 'plan', fastMode: 'false' } }) - expect(openingMessageOptions({ interactionMode: 'plan' })).toEqual({ - options: { interactionMode: 'plan' } - }) -}) diff --git a/src/main/native-chat/agent-session-wire/providers/testing/hook-failure-provider-frame.test.ts b/src/main/native-chat/agent-session-wire/providers/testing/hook-failure-provider-frame.test.ts deleted file mode 100644 index 50b14d037e8..00000000000 --- a/src/main/native-chat/agent-session-wire/providers/testing/hook-failure-provider-frame.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { projectStructuredItemToNativeChat } from '../../../../../shared/agent-session/view/structured-agent-session-projection' -import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame' - -const CODEX_KIND = 'notification:hook/completed' -const CLAUDE_KIND = 'message:system:hook_response' - -// Shape captured from codex-cli 0.162.0 with a SessionStart command that exits 1. -const CODEX_FAILURE = { - threadId: 'thread-1', - turnId: 'turn-1', - run: { - id: 'session-start:0:hooks.json', - eventName: 'sessionStart', - handlerType: 'command', - executionMode: 'sync', - scope: 'thread', - sourcePath: 'hooks.json', - source: 'user', - status: 'failed', - entries: [{ kind: 'error', text: 'hook exited with code 1' }] - } -} - -describe('failed hook provider frames', () => { - it('projects the Codex hook name and nested failure with its original Details', () => { - const row = unhandledProviderFrameJournalItem('codex', CODEX_KIND, CODEX_FAILURE) - expect(row).toMatchObject({ - classification: 'error-surface', - body: { - kind: 'status', - tone: 'error', - text: 'Codex SessionStart hook failed: hook exited with code 1' - } - }) - expect(row?.body.providerFrame?.payload.head).toBe(JSON.stringify(CODEX_FAILURE)) - expect(row?.body.providerFrame?.payload.truncated).toBe(false) - if (!row) { - throw new Error('Expected failed hook row') - } - expect( - projectStructuredItemToNativeChat({ - itemId: 'failed-hook', - revision: 1, - sequence: 1, - observedAt: 1, - body: row.body - }) - ).toMatchObject({ - role: 'system', - blocks: [ - { type: 'text', tone: 'error', text: row.body.text, providerFrame: row.body.providerFrame } - ] - }) - }) - - it('preserves multiple error entries without displaying hook progress as the error', () => { - const payload = { - run: { - eventName: 'preToolUse', - status: 'failed', - statusMessage: 'Running hook', - entries: [ - { kind: 'info', text: 'Running hook' }, - { kind: 'error', text: ' First failure ' }, - null, - { kind: 'error', text: 1 }, - { kind: 'error', text: 'Second failure' } - ] - } - } - expect(unhandledProviderFrameJournalItem('codex', CODEX_KIND, payload)?.body.text).toBe( - 'Codex PreToolUse hook failed: First failure\nSecond failure' - ) - }) - - it('shows Claude stderr and the exit code instead of the wire event name', () => { - expect( - unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, { - hook_name: 'PostToolUse:Bash', - hook_event: 'PostToolUse', - outcome: 'error', - exit_code: 1, - stderr: ' Script not found\n', - output: 'Other output' - })?.body - ).toMatchObject({ - tone: 'error', - text: 'Claude PostToolUse:Bash hook failed (exit code 1): Script not found' - }) - }) - - it.each([ - { - stderr: ' ', - output: 'Hook output', - stdout: 'Other stdout', - exit_code: null, - expected: 'Hook output' - }, - { stderr: null, output: ' ', stdout: 'Hook stdout', exit_code: '1', expected: 'Hook stdout' }, - { - stderr: {}, - output: null, - message: 'Hook could not start', - exit_code: 0, - expected: 'Hook could not start' - } - ])('uses available Claude output when stderr is empty: $expected', ({ expected, ...fields }) => { - expect( - unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, { - hook_event: 'SessionStart', - outcome: 'error', - ...fields - })?.body.text - ).toBe(`Claude SessionStart hook failed: ${expected}`) - }) - - it('keeps successful hooks hidden even when they produce diagnostic output', () => { - expect( - unhandledProviderFrameJournalItem('codex', CODEX_KIND, { - run: { status: 'completed', entries: [{ kind: 'info', text: 'Hook completed' }] } - }) - ).toBeNull() - expect( - unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, { - hook_name: 'SessionStart', - outcome: 'success', - exit_code: 0, - stderr: 'Diagnostic output' - }) - ).toBeNull() - }) - - it('keeps long hook error text whole and bounds only the raw Details', () => { - const row = unhandledProviderFrameJournalItem( - 'codex', - CODEX_KIND, - { - run: { status: 'failed', entries: [{ kind: 'error', text: 'x'.repeat(1000) }] } - }, - { inlineHeadBytes: 64 } - ) - expect(row?.body.text).toBe(`Codex hook failed: ${'x'.repeat(1000)}`) - expect(row?.body.providerFrame?.payload).toMatchObject({ truncated: true }) - expect( - Buffer.byteLength(row?.body.providerFrame?.payload.head ?? '', 'utf8') - ).toBeLessThanOrEqual(64) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/providers/testing/provider-frame-disposition.test.ts b/src/main/native-chat/agent-session-wire/providers/testing/provider-frame-disposition.test.ts deleted file mode 100644 index 0837f102d24..00000000000 --- a/src/main/native-chat/agent-session-wire/providers/testing/provider-frame-disposition.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { classifyProviderFrame } from '../provider-frame-disposition' -import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame' - -describe('provider frame classification catalog', () => { - it("surfaces a failed hook from the hook's own outcome field", () => { - expect( - classifyProviderFrame('codex', 'notification:hook/completed', { - run: { status: 'failed' } - }) - ).toBe('error-surface') - expect( - classifyProviderFrame('claude', 'message:system:hook_response', { - outcome: 'error', - stderr: 'hook failed' - }) - ).toBe('error-surface') - }) - - it('structurally diverts unknown future delta kinds from generic rows', () => { - expect(classifyProviderFrame('codex', 'notification:item/newThing/outputDelta', {})).toBe( - 'stream-into-item' - ) - expect(classifyProviderFrame('claude', 'message:future_delta', {})).toBe('stream-into-item') - }) - - it('suppresses subAgentActivity once the roster renders it, but never collabAgentToolCall', () => { - expect( - classifyProviderFrame('codex', 'item:subAgentActivity', { - id: 'a-1', - kind: 'started', - agentThreadId: 'thread-child', - agentPath: '/root/list_directory' - }) - // The roster already renders this lifecycle; raw fallback would duplicate it. - ).toBe('status-chrome') - expect( - classifyProviderFrame('codex', 'item:collabAgentToolCall', { - id: 'c-1', - tool: 'spawn', - status: 'inProgress', - senderThreadId: 'thread-root', - receiverThreadIds: ['thread-child'], - agentsStates: {} - }) - ).toBe('timeline-substantive') - }) -}) - -describe('failure is decided by kind, never by searching the payload', () => { - it('writes no row for a Claude session start that lists MCP servers which failed to connect', () => { - // Captured Claude 2.1.289 init includes failed MCP servers, not a failed session (#25477). - const init = { - type: 'system', - subtype: 'init', - mcp_servers: [ - { name: 'MCP_DOCKER', status: 'failed', source: 'user' }, - { name: 'codegraph', status: 'connected', source: 'user' }, - { name: 'jira', status: 'failed', source: 'user' } - ] - } - expect(unhandledProviderFrameJournalItem('claude', 'message:system:init', init)).toBeNull() - }) - - it('never turns a nested failure-looking value into an error row', () => { - const failed = { task_id: 't-1', status: 'failed', error: 'exit code 1' } - expect( - unhandledProviderFrameJournalItem('claude', 'message:system:task_notification', failed) - ).toBeNull() - expect( - unhandledProviderFrameJournalItem('codex', 'notification:mcpServer/startupStatus/updated', { - name: 'github', - status: 'failed', - error: 'auth expired' - }) - ).toBeNull() - expect(unhandledProviderFrameJournalItem('claude', 'message:system:future', failed)).toBeNull() - }) -}) diff --git a/src/main/native-chat/agent-session-wire/providers/testing/unhandled-provider-frame.test.ts b/src/main/native-chat/agent-session-wire/providers/testing/unhandled-provider-frame.test.ts deleted file mode 100644 index 32629fff8c2..00000000000 --- a/src/main/native-chat/agent-session-wire/providers/testing/unhandled-provider-frame.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame' - -describe('unhandled provider frame journal fallback', () => { - it('keeps a compact label and bounds the expandable payload without dropping it', () => { - const item = unhandledProviderFrameJournalItem( - 'future-provider', - 'notification:new/event', - { body: 'abcdefghij' }, - { inlineHeadBytes: 8 } - ) - - expect(item).not.toBeNull() - if (!item) { - throw new Error('expected substantive provider frame') - } - expect(item.body).toMatchObject({ - kind: 'status', - text: 'future-provider · notification:new/event', - providerFrame: { - provider: 'future-provider', - kind: 'notification:new/event', - payload: { byteLength: 21, truncated: true } - } - }) - expect( - Buffer.byteLength(item.body.providerFrame?.payload.head ?? '', 'utf8') - ).toBeLessThanOrEqual(8) - }) - - it('turns an unserializable message-shaped payload into an explicit visible value', () => { - const cyclic: { warning?: unknown } = {} - cyclic.warning = cyclic - - const item = unhandledProviderFrameJournalItem('codex', 'frame', cyclic) - - expect(item?.body.text).toBe('codex · frame') - expect(item?.body.providerFrame?.payload.head).toContain('unserializable payload') - }) -}) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts index 729c45f227c..5750ec4081a 100644 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts +++ b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts @@ -31,55 +31,6 @@ function replyBytes(value: unknown): number { } describe('conversation outline read', () => { - it('drops previews before it drops entries, then drops the oldest entries, never passing the budget', () => { - const items = Array.from({ length: 40 }, (_, index) => prompt(index + 1, 'x'.repeat(500))) - const full = readAgentSessionConversationOutline(snapshot(items), Number.MAX_SAFE_INTEGER) - const shortBudget = replyBytes( - full.entries.map((entry) => ({ ...entry, preview: entry.preview.slice(0, 60) })) - ) - expect(shortBudget).toBeLessThan(replyBytes(full.entries)) - const shortened = readAgentSessionConversationOutline(snapshot(items), shortBudget) - expect(shortened.entries).toHaveLength(40) - expect(shortened.entries.every((entry) => entry.preview.length === 60)).toBe(true) - expect(replyBytes(shortened.entries)).toBeLessThanOrEqual(shortBudget) - - const bare = readAgentSessionConversationOutline( - snapshot(items), - replyBytes(full.entries.map((entry) => ({ ...entry, preview: '' }))) - ) - expect(bare.entries).toHaveLength(40) - expect(bare.entries.every((entry) => entry.preview === '')).toBe(true) - - const budget = 10 * replyBytes(bare.entries[0]) - const trimmed = readAgentSessionConversationOutline(snapshot(items), budget) - expect(trimmed.omittedEntries).toBeGreaterThan(0) - expect(trimmed.entries.at(-1)?.sequence).toBe(40) - expect(trimmed.entries[0]?.sequence).toBe(trimmed.omittedEntries + 1) - expect(replyBytes(trimmed.entries)).toBeLessThanOrEqual(budget) - }) - - it('carries the reply to each prompt, and drops replies before it shortens a preview', () => { - const items = Array.from({ length: 10 }, (_, index): AgentJournalRenderItem[] => [ - prompt(index * 2 + 1, 'x'.repeat(100)), - { - itemId: `reply-${index}`, - revision: 1, - sequence: index * 2 + 2, - observedAt: index * 2 + 2, - body: { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text: 'Answer' }] } - } - ]).flat() - const full = readAgentSessionConversationOutline(snapshot(items), Number.MAX_SAFE_INTEGER) - expect(full.entries.map((entry) => entry.reply)).toEqual(Array(10).fill('Answer')) - - // Budget includes array brackets and entry separators. - const tight = readAgentSessionConversationOutline(snapshot(items), replyBytes(full.entries) - 2) - expect(tight.entries.every((entry) => entry.reply === undefined)).toBe(true) - expect(tight.entries.map((entry) => entry.preview)).toEqual( - full.entries.map((entry) => entry.preview) - ) - }) - it('stays inside the history page budget for a journal far past it', () => { const items = Array.from({ length: 10_000 }, (_, index) => prompt(index + 1, `${index} ${'y'.repeat(1_000)}`) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts index cf7141b97ef..38bee3a8aa2 100644 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts +++ b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts @@ -1,6 +1,3 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { agentJournalSubmissionKey } from '../../../../../../shared/agent-session/journal/agent-session-journal-item-key' import type { AgentJournalItemIdentity } from '../../../../../../shared/agent-session/journal/agent-session-journal-identity' @@ -9,23 +6,10 @@ import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../../shared/agent-session/journal/agent-session-journal-types' import { structuredAgentSessionPayloadFingerprint } from '../../../../../../shared/agent-session/lifecycle/structured-agent-session-mutation' -import { - closeTestJournalHostDatabase, - createTrackedJournalOpener -} from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { projectJournalBatch } from '../agent-session-journal-batch' + import { readAgentSessionHistory } from '../agent-session-history-page' -import { - fixture, - IDENTITY, - appendItems, - body, - item, - tick -} from './agent-session-history-contract-test-fixture' - -const outcomeJournals = createTrackedJournalOpener() +import { fixture, IDENTITY, body, item } from './agent-session-history-contract-test-fixture' describe('projectJournalBatch', () => { it('publishes every nested lifecycle mutation atomically at the outer cursor', async () => { @@ -85,81 +69,6 @@ describe('projectJournalBatch', () => { expect(tail.page.items.map((entry) => entry.body)).toEqual([body('one'), body('two')]) }) - it('accepts sparse outcome sequence groups', async () => { - // The shared fixture pins the input format; sparse groups exist only in an outcome epoch. - const root = await mkdtemp(join(tmpdir(), 'orca-history-outcomes-')) - const journal = await outcomeJournals.open({ - identity: IDENTITY, - stateDirectory: root, - now: tick - }) - try { - for (let ordinal = 1; ordinal <= 3; ordinal += 1) { - await journal.appendItem(item(ordinal), body(`item-${ordinal}`), { - fence: 1, - turnScope: AGENT_JOURNAL_THREAD_SCOPE - }) - } - const since = journal.readSince({ epoch: journal.epoch, sequence: 0 }) - if (!since.ok) { - throw new Error(`expected rows, got reset ${since.reset}`) - } - expect(since.rows.every((row) => row.kind === 'outcome')).toBe(true) - const withHole = since.rows.filter((row) => row.seq !== since.rows[1]?.seq) - expect( - projectJournalBatch({ rows: withHole, snapshot: journal.snapshot(), afterSequence: 0 }) - ).toMatchObject({ ok: true }) - } finally { - await journal.close() - await closeTestJournalHostDatabase(root) - await rm(root, { recursive: true, force: true }) - } - }) - - it('publishes touched items at their current reduced state, not as a delta', async () => { - await appendItems(1) - const cursor = fixture.journal.cursor() - await fixture.journal.appendItem(item(1), body('revised'), { - fence: 1, - turnScope: AGENT_JOURNAL_THREAD_SCOPE - }) - const since = fixture.journal.readSince(cursor) - if (!since.ok) { - throw new Error(`expected rows, got reset ${since.reset}`) - } - const projected = projectJournalBatch({ - rows: since.rows, - snapshot: fixture.journal.snapshot(), - afterSequence: cursor.sequence - }) - if (!projected.ok) { - throw new Error(`expected a batch, got reset ${projected.reset}`) - } - expect(projected.batch.items).toHaveLength(1) - expect(projected.batch.items[0]).toMatchObject({ body: body('revised'), revision: 2 }) - expect(projected.batch.cursor).toEqual(fixture.journal.cursor()) - }) - - it('lists a tombstoned item as removed', async () => { - await appendItems(1) - const cursor = fixture.journal.cursor() - await fixture.journal.appendTombstone(item(1), { fence: 1 }) - const since = fixture.journal.readSince(cursor) - if (!since.ok) { - throw new Error(`expected rows, got reset ${since.reset}`) - } - const projected = projectJournalBatch({ - rows: since.rows, - snapshot: fixture.journal.snapshot(), - afterSequence: cursor.sequence - }) - if (!projected.ok) { - throw new Error(`expected a batch, got reset ${projected.reset}`) - } - expect(projected.batch.removedItemIds).toHaveLength(1) - expect(projected.batch.items).toHaveLength(0) - }) - it('publishes a mismatched provider echo under its submission slot', async () => { const message: AgentJournalMessageItem = { kind: 'message', diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-accounting.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-accounting.test.ts deleted file mode 100644 index b8fd3a06a81..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-accounting.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - AGENT_JOURNAL_THREAD_SCOPE, - type AgentJournalItemBody -} from '../../../../../../shared/agent-session/journal/agent-session-journal-types' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, expect, it } from 'vitest' -import type { - AgentJournalItemIdentity, - AgentSessionJournalIdentity -} from '../../../../../../shared/agent-session/journal/agent-session-journal-identity' -import { createTrackedJournalOpener } from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import type { AgentSessionJournal } from '../../../../agent-session-journal/journal-store' -import { readAgentSessionHistory } from '../agent-session-history-page' -import { codexProviderHandle } from '../../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' - -const IDENTITY: AgentSessionJournalIdentity = { - sessionId: 'session-1', - workspaceId: 'ws-1', - hostId: 'host-1', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') -} - -const journals = createTrackedJournalOpener() -let root: string -let clock = 1_000 -let epochs = 0 -let journal: AgentSessionJournal - -function tick(): number { - clock += 1 - return clock -} - -function item(ordinal: number): AgentJournalItemIdentity { - return { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal } -} - -function body(text: string): AgentJournalItemBody { - return { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text }] } -} - -async function appendItems(count: number, text: string): Promise { - for (let ordinal = 1; ordinal <= count; ordinal += 1) { - await journal.appendItem(item(ordinal), body(`${text}-${ordinal}`), { - fence: 1, - turnScope: AGENT_JOURNAL_THREAD_SCOPE - }) - } -} - -beforeEach(async () => { - root = await mkdtemp(join(tmpdir(), 'orca-wire-history-')) - clock = 1_000 - epochs = 0 - journal = await journals.open({ - identity: IDENTITY, - stateDirectory: root, - now: tick, - mintEpoch: () => { - epochs += 1 - return `epoch-${epochs}` - } - }) -}) - -afterEach(async () => { - await journals.closeAll() - await rm(root, { recursive: true, force: true }) -}) - -it.each([1, 100, 200])('serializes each of %i unchanged forward page items once', async (count) => { - const cursor = journal.cursor() - await appendItems(count, 'x'.repeat(8_000)) - const snapshot = journal.snapshot() - const stringify = JSON.stringify - // Method-shaped type: the JSON.stringify overloads split on replacer shape and reject a forwarded one. - const forwardStringify: { - stringify(value: unknown, replacer?: unknown, space?: unknown): string - }['stringify'] = stringify - let itemSerializations = 0 - JSON.stringify = (value: unknown, replacer?: unknown, space?: unknown): string => { - if (value && typeof value === 'object' && 'itemId' in value && 'body' in value) { - itemSerializations++ - } - return forwardStringify(value, replacer, space) - } - try { - const result = readAgentSessionHistory( - journal, - { - sessionId: 'session-1', - direction: 'after', - limit: count, - cursor - }, - { snapshot } - ) - expect(result.ok).toBe(true) - expect(result.page.items).toHaveLength(count) - expect(itemSerializations).toBe(count) - } finally { - JSON.stringify = stringify - } -}) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts index 58f86c0f111..07d3a6eb193 100644 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts +++ b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts @@ -73,31 +73,6 @@ async function seedJournal(count: number) { } describe('forward history SQL read budget', () => { - it('keeps an exact final page final and preserves unlimited journal readers', async () => { - const journal = await seedJournal(6) - const cursor = { epoch: journal.epoch, sequence: 1 } - expect(journal.readSince(cursor)).toMatchObject({ ok: true, rows: expect.any(Array) }) - const first = readAgentSessionHistory(journal, { - sessionId: identity.sessionId, - direction: 'after', - cursor, - limit: 3 - }) - expect(first).toMatchObject({ ok: true, page: { hasNewer: true } }) - if (!first.ok) { - throw new Error('Expected first page') - } - const last = readAgentSessionHistory(journal, { - sessionId: identity.sessionId, - direction: 'after', - cursor: first.page.window.nextCursor, - limit: 3 - }) - expect(last).toMatchObject({ ok: true, page: { hasNewer: false } }) - const unlimited = journal.readSince(cursor) - expect(unlimited.ok && unlimited.rows).toHaveLength(6) - }) - it('reports a sequence gap when the next page reaches it', async () => { const journal = await seedJournal(6) const { db } = openTestJournalHostDatabase(root!) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page-scaling.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page-scaling.test.ts deleted file mode 100644 index 45294052c01..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page-scaling.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, it } from 'vitest' -import type { AgentJournalRenderItem } from '../../../../../../shared/agent-session/journal/agent-session-journal-types' -import { - boundHistoryItemsByBytes, - newestWholeSequenceGroups -} from '../agent-session-history-page-bounds' - -function item(index: number): AgentJournalRenderItem { - return { - itemId: String(index), - revision: 1, - sequence: index, - observedAt: index, - body: { kind: 'status', text: 'ok' } - } -} - -it('retains entire boundary groups and preserves oversized first-group truncation', () => { - const items = [item(1), { ...item(2), sequence: 1 }, item(3), { ...item(4), sequence: 3 }] - expect(newestWholeSequenceGroups(items, 1)).toEqual(items.slice(2)) - expect(newestWholeSequenceGroups(items, 3)).toEqual(items.slice(2)) - expect(boundHistoryItemsByBytes(items, 'oldest', new Map(), 1)).toMatchObject({ - dropped: 2, - items: [{ itemId: '1' }, { itemId: '2' }] - }) - expect(boundHistoryItemsByBytes(items, 'newest', new Map(), 1)).toMatchObject({ - dropped: 2, - items: [{ itemId: '3' }, { itemId: '4' }] - }) -}) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page.test.ts deleted file mode 100644 index 05f399362bd..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { readAgentSessionHistory } from '../agent-session-history-page' -import { fixture, appendItems, item } from './agent-session-history-contract-test-fixture' - -describe('readAgentSessionHistory', () => { - it('carries tombstones in a forward catch-up page', async () => { - await appendItems(1) - const cursor = fixture.journal.cursor() - await fixture.journal.appendTombstone(item(1), { fence: 1 }) - - const page = readAgentSessionHistory(fixture.journal, { - sessionId: 'session-1', - direction: 'after', - cursor - }) - if (!page.ok) { - throw new Error(`expected a page, got reset ${page.reset}`) - } - expect(page.page.items).toHaveLength(0) - expect(page.page.removedItemIds).toEqual(['codex:thread-1:turn-1:1']) - }) - - it('reports a forward read with no cursor as cursor_ahead rather than serving the tail', async () => { - await appendItems(1) - expect( - readAgentSessionHistory(fixture.journal, { sessionId: 'session-1', direction: 'after' }) - ).toMatchObject({ ok: false, reset: 'cursor_ahead' }) - }) - - it('resets a cursor from a previous epoch', async () => { - await appendItems(1) - const stale = fixture.journal.cursor() - await fixture.journal.rollEpoch('legacy_import', 2) - for (const direction of ['before', 'after'] as const) { - expect( - readAgentSessionHistory(fixture.journal, { - sessionId: 'session-1', - direction, - cursor: stale - }) - ).toMatchObject({ ok: false, reset: 'epoch_changed' }) - } - }) - - it('resets a cursor ahead of the journal', async () => { - await appendItems(1) - const ahead = { epoch: fixture.journal.epoch, sequence: fixture.journal.cursor().sequence + 10 } - expect( - readAgentSessionHistory(fixture.journal, { - sessionId: 'session-1', - direction: 'before', - cursor: ahead - }) - ).toMatchObject({ ok: false, reset: 'cursor_ahead' }) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-item-wire-form.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-item-wire-form.test.ts deleted file mode 100644 index 3c7f3ce7a68..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-item-wire-form.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect, it } from 'vitest' -import type { AgentJournalRenderItem } from '../../../../../../shared/agent-session/journal/agent-session-journal-types' -import { journalPayload } from '../../../../agent-session-journal/item-lifecycle/journal-payload-bounds' -import { HISTORY_PAGE_CONTENT_BUDGET_BYTES } from '../agent-session-history-page-bounds' -import { agentJournalItemForWire, shortenedAgentJournalItem } from '../agent-session-item-wire-form' - -it('preserves the full previously readable desktop message range', () => { - const item: AgentJournalRenderItem = { - itemId: 'message', - revision: 1, - sequence: 1, - observedAt: 1, - body: { - kind: 'message', - role: 'assistant', - blocks: [{ type: 'text', text: `${'x'.repeat(1_200_000)}TAIL` }] - } - } - const formed = agentJournalItemForWire(item) - expect(formed.item).toBe(item) - expect(formed.bytes).toBe(Buffer.byteLength(JSON.stringify(item), 'utf8')) -}) - -it.each([73_728, HISTORY_PAGE_CONTENT_BUDGET_BYTES, 4 * 1024 * 1024])( - 'keeps tool identity and output when cardinality exceeds %i bytes', - (budget) => { - const item: AgentJournalRenderItem = { - itemId: 'call', - revision: 1, - sequence: 1, - observedAt: 1, - body: { - kind: 'tool-call', - callId: 'call', - name: 'query', - state: 'completed', - input: { values: Array(Math.ceil(budget / 2) + 1_000).fill(0) }, - output: journalPayload('done') - } - } - const sent = shortenedAgentJournalItem(item, budget) - expect(sent).toMatchObject({ - itemId: 'call', - shortened: true, - body: { - kind: 'tool-call', - name: 'query', - input: { truncated: true }, - output: { head: 'done' } - } - }) - expect(Buffer.byteLength(JSON.stringify(sent))).toBeLessThanOrEqual(budget) - } -) diff --git a/src/main/native-chat/agent-session-wire/publication/history/testing/json-string-shortening.test.ts b/src/main/native-chat/agent-session-wire/publication/history/testing/json-string-shortening.test.ts deleted file mode 100644 index 94367d10308..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/history/testing/json-string-shortening.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, it } from 'vitest' -import { jsonBytes, shortenJsonStringsToFit } from '../json-string-shortening' - -it('cuts long strings to fit and marks them, never splitting a surrogate pair', () => { - const value = { text: '😀'.repeat(2_000), other: 'x'.repeat(4_000) } - const fitted = shortenJsonStringsToFit(value, 2_000) - expect(fitted).not.toBeNull() - expect(jsonBytes(fitted)).toBeLessThanOrEqual(2_000) - expect(fitted?.text.endsWith('…')).toBe(true) - // A lone high surrogate would serialize as an escape and render as a broken glyph. - expect(fitted?.text).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/) -}) - -it('leaves identifiers whole', () => { - const id = 'i'.repeat(3_000) - const fitted = shortenJsonStringsToFit({ id, itemId: id, text: 'x'.repeat(3_000) }, 7_000) - expect(fitted).toMatchObject({ id, itemId: id }) -}) - -it('is null when even the shortest cut cannot fit', () => { - const many = Array.from({ length: 1_000 }, () => 'x'.repeat(200)) - expect(shortenJsonStringsToFit(many, 1_000)).toBeNull() -}) diff --git a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-overflow-prefix.test.ts b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-overflow-prefix.test.ts deleted file mode 100644 index 6a11513a206..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-overflow-prefix.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { - AGENT_SESSION_STREAMED_TEXT_MAX_BYTES, - AGENT_SESSION_STREAMED_TEXT_TRUNCATION_MARKER as marker, - createAgentSessionDeltaCoalescer -} from '../agent-session-delta-coalescer' - -describe('streamed output overflow prefix', () => { - it('encodes only the retained prefix when 64 KiB command deltas cross the default limit', () => { - const delta = 'x'.repeat(64 * 1024) - const emitted: string[] = [] - const instance = createAgentSessionDeltaCoalescer({ - emit: (_key, text) => emitted.push(text), - schedule: () => () => {} - }) - for (let index = 0; index < 256; index += 1) { - instance.append('command', delta) - } - const from = vi.spyOn(Buffer, 'from') - const concat = vi.spyOn(Buffer, 'concat') - const allocate = vi.spyOn(Buffer, 'allocUnsafe') - try { - instance.append('command', delta) - expect(concat).not.toHaveBeenCalled() - expect(from).toHaveBeenCalledExactlyOnceWith(marker, 'utf8') - expect(allocate).toHaveBeenCalledExactlyOnceWith( - AGENT_SESSION_STREAMED_TEXT_MAX_BYTES - Buffer.byteLength(marker) - ) - } finally { - from.mockRestore() - concat.mockRestore() - allocate.mockRestore() - } - const text = 'x'.repeat(AGENT_SESSION_STREAMED_TEXT_MAX_BYTES - marker.length) + marker - expect(instance.snapshot('command')).toEqual({ - text, - observedBytes: 257 * delta.length, - truncated: true - }) - expect(instance.flushAll()).toBe(true) - expect(emitted).toEqual([text]) - instance.dispose() - }) - - it('leaves ordinary appends unencoded and joins split surrogates before overflow', () => { - const instance = createAgentSessionDeltaCoalescer({ - emit: () => true, - schedule: () => () => {} - }) - const from = vi.spyOn(Buffer, 'from') - const concat = vi.spyOn(Buffer, 'concat') - const allocate = vi.spyOn(Buffer, 'allocUnsafe') - try { - instance.append('command', '\ud800') - instance.append('command', '') - instance.append('command', '\udc00') - expect(from).not.toHaveBeenCalled() - expect(concat).not.toHaveBeenCalled() - expect(allocate).not.toHaveBeenCalled() - } finally { - from.mockRestore() - concat.mockRestore() - allocate.mockRestore() - } - expect(instance.snapshot('command')).toEqual({ - text: '\ud800\udc00', - observedBytes: 6, - truncated: false - }) - instance.dispose() - }) - - it.each([ - { cap: 0, chunks: ['abcdef'], text: '' }, - { cap: 1, chunks: ['abcdef'], text: '\n' }, - { cap: 33, chunks: ['x'.repeat(40)], text: marker.slice(0, 33) }, - { cap: 34, chunks: ['x'.repeat(40)], text: marker }, - { cap: 35, chunks: ['é', 'x'.repeat(40)], text: marker }, - { cap: 36, chunks: ['a€', 'x'.repeat(40)], text: `a${marker}` }, - { cap: 37, chunks: ['a€', 'x'.repeat(40)], text: `a${marker}` }, - { cap: 38, chunks: ['a€', 'x'.repeat(40)], text: `a€${marker}` }, - { cap: 40, chunks: ['\ud800', '', '\udc00', 'x'.repeat(40)], text: `\ufffd\ufffd${marker}` }, - { cap: 39, chunks: ['a😀', 'x'.repeat(40)], text: `a😀${marker}` }, - { cap: 38, chunks: ['a😀', 'x'.repeat(40)], text: `a${marker}` } - ])('preserves chunk encoding and UTF-8 clipping at a $cap byte cap', ({ cap, chunks, text }) => { - const instance = createAgentSessionDeltaCoalescer({ - maxRetainedBytes: cap, - emit: () => true, - schedule: () => () => {} - }) - for (const chunk of chunks) { - instance.append('command', chunk) - } - expect(instance.snapshot('command')).toEqual({ - text, - observedBytes: chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0), - truncated: true - }) - instance.dispose() - }) -}) diff --git a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts index a903a02939b..b1f80c3953d 100644 --- a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts +++ b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts @@ -26,55 +26,7 @@ function manualClock() { } } -function coalescer(clock: ReturnType, windowMs?: number) { - const emitted: [string, string][] = [] - const instance = createAgentSessionDeltaCoalescer({ - emit: (key, text) => emitted.push([key, text]), - schedule: clock.schedule, - ...(windowMs === undefined ? {} : { windowMs }) - }) - return { instance, emitted } -} - describe('agent-session delta coalescer', () => { - it('emits the accumulated snapshot again, not the increment, on the next window', () => { - const clock = manualClock() - const { instance, emitted } = coalescer(clock) - - instance.append('item-1', 'he') - clock.fire() - instance.append('item-1', 'llo') - clock.fire() - - expect(emitted).toEqual([ - ['item-1', 'he'], - ['item-1', 'hello'] - ]) - }) - - it('retains dirty state and surfaces admission failure for retry', () => { - const clock = manualClock() - let reject = true - const emitted: string[] = [] - const instance = createAgentSessionDeltaCoalescer({ - schedule: clock.schedule, - emit: (_key, text) => { - if (reject) { - return false - } - emitted.push(text) - return true - } - }) - - instance.append('item-1', 'retry me') - expect(instance.flush('item-1')).toBe(false) - expect(emitted).toEqual([]) - reject = false - expect(instance.flush('item-1')).toBe(true) - expect(emitted).toEqual(['retry me']) - }) - it('does not evict buffered output when flushing the oldest stream is backpressured', () => { const clock = manualClock() let reject = true diff --git a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-empty-delta-retention.test.ts b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-empty-delta-retention.test.ts deleted file mode 100644 index a8d1a991eba..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-empty-delta-retention.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../../shared/agent-session/journal/agent-session-journal-types' -import { describe, expect, it, vi } from 'vitest' -import { createCodexStructuredItemStreams } from '../../../../../codex/structured/items/codex-structured-item-streams' -import { createAgentSessionDeltaCoalescer } from '../agent-session-delta-coalescer' - -describe('empty streamed deltas', () => { - it('does not retain empty array slots through repeated Codex publications', () => { - const prefix = 'empty-delta-retention-prefix' - const streams = createCodexStructuredItemStreams({ - sink: { appendItem() {}, appendTombstone() {}, publish() {} }, - turnIdFor: () => 'turn', - identityFor: () => ({ provider: 'codex', threadId: 'thread', turnId: 'turn', ordinal: 0 }), - attributionFor: () => ({ turnScope: AGENT_JOURNAL_THREAD_SCOPE }), - schedule: () => () => {} - }) - const append = (delta: string) => - streams.handle('thread', 'item/agentMessage/delta', { itemId: 'item', delta }) - append(prefix) - try { - for (let batch = 0; batch < 4; batch += 1) { - for (let index = 0; index < 16_384; index += 1) { - append('') - } - expect(streams.flush()).toBe(true) - } - const originalJoin = Array.prototype.join - let retainedSlots = -1 - const spy = vi.spyOn(Array.prototype, 'join').mockImplementation(function ( - this: unknown[], - separator - ) { - // Byte counters cannot detect empty entries retained by the stream's chunk array. - if (separator === '' && this[0] === prefix) { - retainedSlots = this.length - } - return originalJoin.call(this, separator) - }) - let snapshot: ReturnType - try { - snapshot = streams.snapshot('thread', 'item') - } finally { - spy.mockRestore() - } - expect(retainedSlots).toBe(1) - expect(snapshot).toEqual({ - text: prefix, - observedBytes: Buffer.byteLength(prefix), - truncated: false - }) - append('é') - expect(streams.snapshot('thread', 'item')?.text).toBe(`${prefix}é`) - streams.forget('thread', 'item') - expect(streams.snapshot('thread', 'item')).toBeNull() - } finally { - streams.dispose() - } - }) - - it('preserves empty stream snapshots, scheduled publication and explicit flushes', () => { - const pending = new Set<() => void>() - const emitted: { key: string; text: string }[] = [] - const instance = createAgentSessionDeltaCoalescer({ - schedule: (run) => { - pending.add(run) - return () => { - pending.delete(run) - } - }, - emit: (key, text) => emitted.push({ key, text }) - }) - try { - expect(instance.append('empty', '')).toBe(true) - expect(instance.snapshot('empty')).toEqual({ - text: '', - observedBytes: 0, - truncated: false - }) - expect(pending.size).toBe(1) - expect(emitted).toEqual([]) - expect(instance.flushAll()).toBe(true) - expect(pending.size).toBe(0) - expect(emitted).toEqual([{ key: 'empty', text: '' }]) - instance.append('empty', 'visible') - instance.append('empty', '') - expect(pending.size).toBe(1) - expect(instance.flush('empty')).toBe(true) - expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' }) - instance.append('empty', '') - expect(instance.flushAll()).toBe(true) - expect(emitted).toHaveLength(3) - expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' }) - } finally { - instance.dispose() - } - expect(pending.size).toBe(0) - }) - - it('still refuses a new empty stream while the oldest output is backpressured', () => { - let accepting = false - const emitted: [string, string][] = [] - const instance = createAgentSessionDeltaCoalescer({ - maxStreams: 1, - schedule: () => () => {}, - emit: (key, text) => { - if (!accepting) { - return false - } - emitted.push([key, text]) - return true - } - }) - try { - instance.append('first', 'preserved') - expect(instance.append('second', '')).toBe(false) - expect(instance.snapshot('second')).toBeNull() - expect(instance.snapshot('first')?.text).toBe('preserved') - accepting = true - expect(instance.append('second', '')).toBe(true) - expect(instance.snapshot('first')).toBeNull() - expect(instance.snapshot('second')?.text).toBe('') - expect(instance.flushAll()).toBe(true) - expect(emitted).toEqual([ - ['first', 'preserved'], - ['second', ''] - ]) - } finally { - instance.dispose() - } - }) -}) diff --git a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-command-publication.test.ts b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-command-publication.test.ts deleted file mode 100644 index cc21c52a7ec..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-command-publication.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { expect, it } from 'vitest' - -import type { - AgentSessionSlashCommand, - AgentSessionSubscribeEvent -} from '../../../../../../shared/agent-session/agent-session-wire' -import type { AgentSessionBackgroundTaskState } from '../../../../../../shared/agent-session/agent-session-background-task-wire' -import { createStructuredAgentSessionEventCoalescer } from '../../../../../../shared/agent-session/structured-agent-session-coalescer' -import { - EMPTY_STRUCTURED_AGENT_SESSION, - reduceStructuredAgentSession -} from '../../../../../../shared/agent-session/structured-agent-session-reducer' -import { createTrackedJournalOpener } from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support' -import { AgentSessionSubscribers } from '../structured-agent-session-subscribers' -import { identityFor } from '../../../../../claude/structured/claude-structured-session-test-support' - -it('delivers catalog changes through existing frames without resending them on ordinary output', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-command-publication-')) - const journals = createTrackedJournalOpener() - const events: AgentSessionSubscribeEvent[] = [] - let state = EMPTY_STRUCTURED_AGENT_SESSION - const coalescer = createStructuredAgentSessionEventCoalescer((event) => { - events.push(event) - state = reduceStructuredAgentSession(state, { type: 'event', event }) - }) - try { - const journal = await journals.open({ identity: identityFor(), stateDirectory: root }) - const sessionId = identityFor().sessionId - let commands: AgentSessionSlashCommand[] | undefined = [ - { name: 'loaded', kind: 'command', kindUnspecified: true } - ] - let roster: AgentSessionBackgroundTaskState | null = null - const subscribers = new AgentSessionSubscribers({ - readCommands: () => commands, - readBackgroundTasks: () => roster - }) - const republishRoster = (): void => { - roster = roster ? null : { state: 'monitoring' } - subscribers.republishBackgroundTasks(sessionId, 7) - } - const close = subscribers.open({ - id: 'one', - sessionId, - journal, - fence: 7, - emit: coalescer.push - }) - expect(state.commands).toEqual(commands) - for (let i = 0; i < 25; i++) { - republishRoster() - } - coalescer.flush() - expect(events.filter((event) => 'commands' in event)).toHaveLength(1) - commands = [] - subscribers.publish(sessionId, journal) - republishRoster() - coalescer.flush() - expect(state.commands).toEqual([]) - expect(events.filter((event) => 'commands' in event)).toHaveLength(2) - commands = undefined - subscribers.publish(sessionId, journal) - coalescer.flush() - expect(state.commands).toBeNull() - close() - commands = [{ name: 'reconnected', kind: 'skill' }] - subscribers.open({ - id: 'two', - sessionId, - journal, - cursor: journal.cursor(), - fence: 8, - emit: coalescer.push - }) - coalescer.flush() - expect(state.commands).toEqual(commands) - subscribers.snapshot(sessionId, journal, 8) - expect(state.commands).toEqual(commands) - commands = undefined - subscribers.open({ - id: 'three', - sessionId, - journal, - cursor: journal.cursor(), - fence: 9, - emit: coalescer.push - }) - coalescer.flush() - expect(state.commands).toBeNull() - } finally { - coalescer.dispose() - await journals.closeAll() - await rm(root, { recursive: true, force: true }) - } -}) diff --git a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-publication.test.ts b/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-publication.test.ts deleted file mode 100644 index efaed10f397..00000000000 --- a/src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-publication.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { join } from 'node:path' -import { expect, it } from 'vitest' -import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../../shared/agent-session/journal/agent-session-journal-types' -import type { - AgentSessionStatusEvent, - AgentSessionSubscribeEvent -} from '../../../../../../shared/agent-session/agent-session-wire' -import type { AgentSessionBackgroundTaskState } from '../../../../../../shared/agent-session/agent-session-background-task-wire' -import { StructuredAgentSessionStatusFeed } from '../../../observations/structured-agent-session-status-feed' -import { AgentSessionSubscribers } from '../structured-agent-session-subscribers' -import { createStructuredAgentSessionLogger } from '../../../host/structured-agent-session-logger' -import { - claudeProviderHandle, - codexProviderHandle -} from '../../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding' - -import { - fixture, - SESSION, - journals -} from './structured-agent-session-subscriber-contract-test-fixture' - -it('settles a session nobody is reading, from running to idle', async () => { - // Never open a subscriber: status must publish independently of transcript readers. - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - stateDirectory: join(fixture.root, 'unread-journal') - }) - const statusFeed = new StructuredAgentSessionStatusFeed({ - logger: createStructuredAgentSessionLogger(), - sessions: new Map([ - [ - SESSION, - { - journal, - params: { - location: { - executionHostId: 'local', - wslDistro: null, - workspaceId: 'workspace-1', - workspaceKind: 'git-worktree' - }, - provider: 'codex' - } - } - ] - ]), - getConversation: () => null, - now: () => 1_000 - }) - const subscribers = new AgentSessionSubscribers({ - onJournalPublished: (sessionId, published) => statusFeed.publish(sessionId, published) - }) - const statuses: AgentSessionStatusEvent[] = [] - statusFeed.subscribe({ id: 'session-list', emit: (event) => statuses.push(event) }) - const turn = { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 } as const - - await journal.appendItem( - { ...turn, ordinal: 1 }, - { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'write a poem' }] }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - await journal.appendItem( - turn, - { kind: 'status', text: 'Working', turnLifecycle: { turnId: 'turn-1', state: 'running' } }, - { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - subscribers.publish(SESSION, journal) - - expect(statuses.at(-1)).toEqual({ - type: 'status', - session: expect.objectContaining({ status: 'working', latestPrompt: 'write a poem' }) - }) - - await journal.appendTombstone(turn, { fence: 1 }) - subscribers.publish(SESSION, journal) - - expect(statuses.at(-1)).toEqual({ - type: 'status', - session: expect.objectContaining({ status: 'idle' }) - }) -}) - -it('publishes background lifecycle without advancing the journal and carries its fence forward', async () => { - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'claude', - providerHandle: claudeProviderHandle('provider-1', null) - }, - stateDirectory: join(fixture.root, 'background-journal') - }) - let roster: AgentSessionBackgroundTaskState | null = null - const subscribers = new AgentSessionSubscribers({ readBackgroundTasks: () => roster }) - const events: AgentSessionSubscribeEvent[] = [] - subscribers.open({ - id: 'subscriber-1', - sessionId: SESSION, - journal, - fence: 1, - emit: (event) => events.push(event) - }) - const cursor = journal.cursor() - - const backgroundTasks: AgentSessionBackgroundTaskState = { - state: 'monitoring', - tasks: [{ id: 'task-1', kind: 'command', description: 'run the build' }] - } - roster = backgroundTasks - subscribers.republishBackgroundTasks(SESSION, 2) - - expect(journal.cursor()).toEqual(cursor) - expect(events.at(-1)).toEqual({ - type: 'batch', - sessionId: SESSION, - batch: { cursor, items: [], removedItemIds: [], submissions: [] }, - fence: 2, - hostNow: expect.any(Number), - backgroundTasks - }) - - await journal.appendItem( - { provider: 'orca', clientMessageId: 'after-background-fence' }, - { kind: 'status', text: 'After background state' }, - { fence: 2, turnScope: AGENT_JOURNAL_THREAD_SCOPE } - ) - subscribers.publish(SESSION, journal) - - expect(events.at(-1)).toMatchObject({ type: 'batch', fence: 2 }) -}) - -it('publishes latest turn activity without advancing or adding journal rows', async () => { - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: codexProviderHandle('thread-1') - }, - stateDirectory: join(fixture.root, 'activity-journal') - }) - const subscribers = new AgentSessionSubscribers() - const events: AgentSessionSubscribeEvent[] = [] - subscribers.open({ - id: 'subscriber-1', - sessionId: SESSION, - journal, - fence: 1, - emit: (event) => events.push(event) - }) - const cursor = journal.cursor() - - subscribers.publish(SESSION, journal, { - turnId: 'turn-1', - text: 'Inspecting the session wire' - }) - - expect(journal.cursor()).toEqual(cursor) - expect(events.at(-1)).toEqual({ - type: 'batch', - sessionId: SESSION, - batch: { cursor, items: [], removedItemIds: [], submissions: [] }, - fence: 1, - hostNow: expect.any(Number), - activity: { turnId: 'turn-1', text: 'Inspecting the session wire' } - }) - - subscribers.close(SESSION, 'subscriber-1') - subscribers.publish(SESSION, journal, null) - subscribers.open({ - id: 'reconnected', - sessionId: SESSION, - journal, - fence: 1, - cursor, - emit: (event) => events.push(event) - }) - expect(journal.cursor()).toEqual(cursor) - expect(events.at(-1)).toMatchObject({ activity: null }) -}) diff --git a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-recovery-resolution.test.ts b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-recovery-resolution.test.ts index a356da00dcd..eb3f171ba41 100644 --- a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-recovery-resolution.test.ts +++ b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-recovery-resolution.test.ts @@ -113,26 +113,6 @@ function deps( } describe('structured session recovery resolution', () => { - it('releases an ownerless reservation: nothing it recorded can be holding it', async () => { - const store = await openStore() - await reserve(store) - await latch(store) - - const result = await resolveStructuredSessionRecovery( - deps(store, () => ({ outcome: 'indeterminate', reason: 'no scan' })), - SESSION - ) - - expect(result).toBe('resolved') - expect(store.getRecord(SESSION)?.lease).toMatchObject({ - claimStatus: 'released', - handoffStage: null, - runtimeFence: 2, - reservedSpawnToken: null, - deathEvidence: null - }) - }) - it('releases an owner whose identity cannot be verified, and signals nothing', async () => { const store = await openStore() await liveOwner(store) diff --git a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts index d6d3d2b2acc..1e722273747 100644 --- a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts +++ b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts @@ -14,43 +14,6 @@ type Handover = { dispatchState: string; reason?: string | null } | undefined const LIMIT = 3 -it('publishes queued chats separately from the chat holding the start slot', async () => { - const first = Promise.withResolvers() - const phases = new Map() - const action = resumeStructuredAgentSessionsFromRestart( - { - admission: new StructuredAgentSessionResumeAdmission(1), - onPhase: (sessionId, phase) => phases.set(sessionId, phase), - consumeMarker: async () => true, - resume: async (sessionId) => { - if (sessionId === 'a') { - await first.promise - } - } - }, - ['a', 'b'].map((sessionId) => ({ - sessionId, - workspaceId: 'folder', - executionHostId: 'local', - workspaceKind: 'folder', - agent: 'codex', - work: { kind: 'turn', id: 'turn' }, - trigger: 'quit', - recordedAt: 1, - latestPrompt: '' - })), - 'modal' - ) - try { - await vi.waitFor(() => expect(phases.get('a')).toBe('starting')) - expect(phases.get('b')).toBe('queued') - } finally { - first.resolve() - await action - } - expect(phases.get('b')).toBe('starting') -}) - it('holds each slot from accept until handover or rejection, and frees it when the wait ends (P2-27)', async () => { const handovers = new Map>() let waiting = 0 diff --git a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-context-clear.test.ts b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-context-clear.test.ts deleted file mode 100644 index bf3323f41be..00000000000 --- a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-context-clear.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { structuredAgentSessionResumableSet } from '../structured-agent-session-restart-resume-set' -import { - structuredAgentSessionWorkingAtStop, - structuredAgentSessionWorkInFlight -} from '../../termination/structured-agent-session-working-at-teardown' -import { - record, - marker, - journal, - turnItem, - submission, - NOW, - SESSION, - TEARDOWN_CURRENT -} from './structured-agent-session-restart-resume-test-harness' -import type { AgentJournalRenderItem } from '../../../../../shared/agent-session/journal/agent-session-journal-types' - -const boundary = { operationId: 'clear', afterFence: 1, clearedAt: NOW } -const clear: AgentJournalRenderItem = { - itemId: 'clear', - sequence: 5, - revision: 1, - observedAt: NOW, - body: { kind: 'status', text: 'Context cleared', contextClear: boundary } -} -const value = (): ReturnType & { providerContextBoundary: typeof boundary } => ({ - ...record(), - providerHandleChain: [], - providerContextBoundary: boundary -}) -const set = (m = marker(), r = value()) => - structuredAgentSessionResumableSet({ - markers: [m], - getRecord: () => r, - supportsRecord: () => true, - latestPrompt: () => 'prior prompt', - movedOn: () => false, - savedByNewerOrca: () => false - }) - -describe('restart offers after context clear', () => { - it('retires the old-context offer before any new provider handle exists', () => { - expect(set()).toEqual({ candidates: [], superseded: [marker()] }) - expect(set({ ...marker(), contextClearOperationId: 'previous-clear' })).toEqual({ - candidates: [], - superseded: [{ ...marker(), contextClearOperationId: 'previous-clear' }] - }) - }) - - it('does not treat an old unknown send as new-context work during teardown', () => { - const unknown = { ...submission('old-send', 'unknown'), acceptedSequence: 2 } - expect( - structuredAgentSessionWorkInFlight([turnItem('old', 'running'), clear], [unknown]) - ).toBeNull() - expect( - structuredAgentSessionWorkingAtStop({ - sessionId: SESSION, - session: { - journal: journal([turnItem('old', 'running'), clear], [unknown]), - child: { fence: 2, phase: 'ready' } - }, - getRecord: value, - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - ).toBeNull() - }) - - it('stamps a current-context offer and keeps it eligible after restart', () => { - const proved = { ...record().providerHandleChain[0], linkId: 'new', mintedAtFence: 2 } - const currentRecord = { - ...value(), - providerHandleChain: [proved] - } - const turn = { ...turnItem('new-turn', 'running'), sequence: 6 } - const offer = structuredAgentSessionWorkingAtStop({ - sessionId: SESSION, - session: { journal: journal([clear, turn]), child: { fence: 2, phase: 'ready' } }, - getRecord: () => currentRecord, - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - })! - expect(offer.contextClearOperationId).toBe('clear') - expect(set(offer, currentRecord).candidates).toHaveLength(1) - }) -}) diff --git a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-continuation.test.ts b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-continuation.test.ts deleted file mode 100644 index 626c0881dca..00000000000 --- a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-continuation.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { expect, it, vi } from 'vitest' -import { marker, SESSION } from './structured-agent-session-restart-resume-test-harness' -import { - AGENT_SESSION_RESTART_CONTINUATION_REFUSED_NOTE, - AGENT_SESSION_RESTART_CONTINUATION_UNCONFIRMED_NOTE, - AGENT_SESSION_RESTART_NOT_CONNECTED_NOTE -} from '../../../../../shared/agent-session/lifecycle/agent-session-restart-continuation' -import { - RestartContinuationSupersededError, - startStructuredAgentSessionContinuation, - type StructuredAgentSessionContinuationDeps -} from '../structured-agent-session-restart-continuation' -import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger' - -async function continueStructuredAgentSessionAfterRestart( - ...args: Parameters -) { - const started = await startStructuredAgentSessionContinuation(...args) - return 'done' in started ? started.done : started.verdict() -} - -function dependencies( - settledDispatch: 'accepted' | 'pending' | 'unknown' | 'rejected', - handedOver: 'pending' | 'rejected' | undefined = 'pending' -): StructuredAgentSessionContinuationDeps & { - note: ReturnType - send: ReturnType -} { - return { - currentFence: () => 1, - send: vi.fn(async () => ({ - ok: true, - value: { submission: { dispatchState: 'pending' } } - })), - awaitSettlement: vi.fn(async () => ({ - dispatchState: settledDispatch, - reason: settledDispatch === 'rejected' ? 'provider_refused' : null - })), - awaitHandedOver: vi.fn(async () => - handedOver - ? { - dispatchState: handedOver, - reason: handedOver === 'rejected' ? 'Codex could not start.' : null - } - : undefined - ), - note: vi.fn(async () => undefined), - logger: createStructuredAgentSessionLogger() - } -} - -const UNCONFIRMED = [SESSION, AGENT_SESSION_RESTART_CONTINUATION_UNCONFIRMED_NOTE, 'warning'] -const REFUSED = [SESSION, AGENT_SESSION_RESTART_CONTINUATION_REFUSED_NOTE, 'error'] -const NOT_CONNECTED = [SESSION, AGENT_SESSION_RESTART_NOT_CONNECTED_NOTE, 'error'] - -it.each([ - ['pending', { sessionId: SESSION, outcome: 'pending' }, UNCONFIRMED], - ['unknown', { sessionId: SESSION, outcome: 'unknown' }, UNCONFIRMED], - ['rejected', { sessionId: SESSION, outcome: 'refused', reason: 'provider_refused' }, REFUSED] -] as const)( - 'preserves a %s settlement and notes it in the chat instead of the success note', - async (settled, expected, note) => { - const deps = dependencies(settled) - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).resolves.toEqual(expected) - expect(deps.note).toHaveBeenCalledExactlyOnceWith(...note) - } -) - -it("writes nothing in the chat when the user's own message came first, and still refuses", async () => { - const deps = dependencies('accepted') - deps.send.mockRejectedValue(new RestartContinuationSupersededError()) - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).rejects.toBeInstanceOf(RestartContinuationSupersededError) - expect(deps.note).not.toHaveBeenCalled() -}) - -// Ownership refusal also blocks personal sends; do not advise sending one. -it.each([ - ['agent_session_conflict', NOT_CONNECTED], - ['agent_session_operation_invalid', REFUSED] -] as const)('reports a %s send refusal without waiting for settlement', async (code, note) => { - const deps = dependencies('accepted') - deps.send.mockResolvedValue({ ok: false, refusal: { code } }) - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).resolves.toEqual({ - sessionId: SESSION, - outcome: 'refused', - reason: code, - refusal: { code } - }) - expect(deps.awaitSettlement).not.toHaveBeenCalled() - expect(deps.note).toHaveBeenCalledExactlyOnceWith(...note) -}) - -it('reports a newer-Orca send refusal with its reason', async () => { - const deps = dependencies('accepted') - const refusal = { - code: 'agent_session_journal_unreadable', - details: { reason: 'journalWrittenByNewerOrca' } - } as const - deps.send.mockResolvedValue({ ok: false, refusal: { ...refusal, message: 'Update Orca.' } }) - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).resolves.toEqual({ - sessionId: SESSION, - outcome: 'refused', - reason: 'agent_session_journal_unreadable', - refusal - }) -}) - -it('reports an unattached chat without sending', async () => { - const deps = dependencies('accepted') - deps.currentFence = () => null - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).resolves.toEqual({ - sessionId: SESSION, - outcome: 'refused', - reason: 'agent_session_not_attached' - }) - expect(deps.send).not.toHaveBeenCalled() -}) - -it('reports a continuation rejected at handover without waiting for the provider', async () => { - const deps = dependencies('accepted', 'rejected') - - await expect( - continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1') - ).resolves.toEqual({ sessionId: SESSION, outcome: 'refused', reason: 'Codex could not start.' }) - expect(deps.awaitSettlement).not.toHaveBeenCalled() - expect(deps.note).toHaveBeenCalledExactlyOnceWith(...REFUSED) -}) - -it('returns at handover and leaves the provider verdict to be awaited', async () => { - const deps = dependencies('accepted') - - const started = await startStructuredAgentSessionContinuation( - deps, - SESSION, - marker(), - 'operation-1' - ) - - expect(deps.awaitHandedOver).toHaveBeenCalledOnce() - expect(deps.awaitSettlement).not.toHaveBeenCalled() - if (!('verdict' in started)) { - throw new Error('expected a handed-over continuation') - } - await expect(started.verdict()).resolves.toEqual({ sessionId: SESSION, outcome: 'continued' }) - expect(deps.note).toHaveBeenCalledOnce() -}) diff --git a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-resume.test.ts b/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-resume.test.ts deleted file mode 100644 index 2077e15e883..00000000000 --- a/src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-resume.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { structuredAgentSessionResumableSet } from '../structured-agent-session-restart-resume-set' -import { structuredAgentSessionWorkingAtStop } from '../../termination/structured-agent-session-working-at-teardown' -import { - childRecord, - CLAUDE_ROOT, - claudeRecord, - journal, - TEARDOWN_CURRENT, - marker, - NOW, - record, - resumableSet, - SESSION, - submission, - turnItem -} from './structured-agent-session-restart-resume-test-harness' - -type StopInput = Parameters[0] - -function markersAtTeardown( - input: Omit & { - sessions: ReadonlyMap> - } -) { - return [...input.sessions].flatMap(([sessionId, session]) => { - const recorded = structuredAgentSessionWorkingAtStop({ ...input, sessionId, session }) - return recorded ? [recorded] : [] - }) -} - -describe('deriving what was working at teardown', () => { - // Persisted running rows prove no live execution; only this runtime’s child can justify a witness. - it('marks nothing for a stale running row this host was not executing', () => { - expect( - markersAtTeardown({ - sessions: new Map([ - [SESSION, { journal: journal([turnItem('turn-1', 'running')]), child: null }] - ]), - getRecord: () => record(), - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - ).toEqual([]) - }) - - // Capture lost prompts with the same work snapshot so blocked offers name what they lost. - - it('marks nothing for a settled lead whose tasks have all finished', () => { - expect( - markersAtTeardown({ - sessions: new Map([ - [ - SESSION, - { - journal: journal([turnItem('turn-1', 'completed')]), - child: { fence: 1, phase: 'ready' as const } - } - ] - ]), - getRecord: () => record(), - childWorkOpen: () => false, - childWork: () => [ - childRecord({ id: 'task-a', kind: 'agent', state: 'done' }), - childRecord({ id: 'task-b', kind: 'command', state: 'idle' }) - ], - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - ).toEqual([]) - }) - - it('marks nothing for a session that never proved a provider cursor', () => { - expect( - markersAtTeardown({ - sessions: new Map([ - [ - SESSION, - { - journal: journal([turnItem('turn-1', 'running')]), - child: { fence: 1, phase: 'ready' as const } - } - ] - ]), - getRecord: () => record({ chain: [] }), - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - ).toEqual([]) - }) - - // Claude may echo user input long after dispatch; mark the send before any turn ID exists. - - it('marks nothing for a session whose only work is a message still queued', () => { - const queued = { ...submission('msg-1', 'pending'), handoverRecorded: true as const } - expect( - markersAtTeardown({ - sessions: new Map([ - [ - SESSION, - { - journal: journal([turnItem('turn-0', 'completed')], [queued]), - child: { fence: 1, phase: 'ready' as const } - } - ] - ]), - getRecord: () => claudeRecord(null), - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - ).toEqual([]) - }) - - it('marks a handed-over message over a newer queued one, and a turn a queued one waits behind', () => { - const handedOver = { - ...submission('msg-1', 'pending'), - handoverRecorded: true as const, - handedOverAt: NOW - } - const queued = { ...submission('msg-2', 'pending'), handoverRecorded: true as const } - const markers = markersAtTeardown({ - sessions: new Map([ - [ - SESSION, - { - journal: journal([turnItem('turn-0', 'completed')], [handedOver, queued]), - child: { fence: 1, phase: 'ready' as const } - } - ], - [ - 'session-running', - { - journal: journal([turnItem('turn-1', 'running')], [queued]), - child: { fence: 1, phase: 'ready' as const } - } - ] - ]), - getRecord: () => claudeRecord(null), - childWorkOpen: () => false, - childWork: () => undefined, - trigger: 'quit', - teardownId: TEARDOWN_CURRENT, - now: NOW - }) - - expect(markers.map((entry) => entry.work)).toEqual([ - { kind: 'submission', id: 'msg-1' }, - { kind: 'turn', id: 'turn-1' } - ]) - }) -}) - -describe('the resumable set', () => { - // Same-teardown close advances Claude’s leaf; comparing full keys would permanently reject continuity. - it('still offers a Claude session whose leaf advanced after the marker was written', () => { - const { candidates } = structuredAgentSessionResumableSet({ - markers: [marker({ providerHandleRoot: CLAUDE_ROOT })], - getRecord: () => claudeRecord('5aed93d6-advanced-leaf'), - supportsRecord: () => true, - latestPrompt: () => '', - movedOn: () => false, - savedByNewerOrca: () => false - }) - - expect(candidates).toHaveLength(1) - }) - - it('withdraws and reports for deletion a Claude session that forked to a different root', () => { - const forked = marker({ providerHandleRoot: CLAUDE_ROOT }) - const set = structuredAgentSessionResumableSet({ - markers: [forked], - getRecord: () => claudeRecord(null, 'prov-session-2'), - supportsRecord: () => true, - latestPrompt: () => '', - movedOn: () => false, - savedByNewerOrca: () => false - }) - - expect(set.candidates).toEqual([]) - expect(set.superseded).toEqual([forked]) - }) - - it("neither offers nor spends a newer Orca's chat, and offers it once this build can open it", () => { - const offer = marker() - expect(resumableSet({ markers: [offer], savedByNewerOrca: [SESSION] })).toEqual({ - candidates: [], - superseded: [] - }) - expect(resumableSet({ markers: [offer] }).candidates).toHaveLength(1) - }) - - // Structural refusal must preserve a durable offer the user still owns. - it('does not report a structurally refused marker for deletion', () => { - expect(resumableSet({ markers: [marker()], chain: [] }).superseded).toEqual([]) - }) - - // Provider-authored restore rows cannot overturn teardown’s observed work. -}) diff --git a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-operation-start-refusal.test.ts b/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-operation-start-refusal.test.ts deleted file mode 100644 index 52fc3021644..00000000000 --- a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-operation-start-refusal.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { foundAgentSessionRecord } from '../../../../runtime/agent-session/records/agent-session-record-founding' -import { describe, expect, it, vi } from 'vitest' -import type { AgentSessionRecord } from '../../../../../shared/agent-session/lifecycle/agent-session-record' -import type { StructuredAgentSessionAttachContext } from '../../acquisition/structured-agent-session-attach-context' -import { startStructuredAgentSessionAgentForOperation } from '../structured-agent-session-agent-start' -import { StructuredAgentSessionAcquireAborts } from '../structured-agent-session-acquire-aborts' -import { StructuredAgentSessionStartupAttempts } from '../structured-agent-session-startup-attempt' -import { StructuredAgentSessionStartFlights } from '../structured-agent-session-start-flight' -import '../../options/structured-agent-session-options-read' -import { recordStructuredAgentSessionOptionIntent } from '../../options/structured-agent-session-option-intent' -import { claudeAndCodexAgents } from '../../providers/testing/structured-agent-session-adapter-router-test-support' -import { recordingStructuredAgentSessionLogger } from '../../host/testing/structured-agent-session-logger-test-support' - -const SESSION = 'session-1' - -describe('an operation whose agent start throws', () => { - it('is refused as a failed restart, with the error only in the log', async () => { - const log = recordingStructuredAgentSessionLogger() - const cause = new Error('EACCES: permission denied, open /Users/me/.orca/leases.json') - const context = { - deps: { - logger: log.logger, - store: { - requireWritableRecord: () => - foundAgentSessionRecord( - { - sessionId: SESSION, - provider: 'codex', - accountHome: { variable: 'CODEX_HOME', path: '/test/codex' }, - location: { - workspaceId: 'workspace-1', - workspaceKind: 'git-worktree', - executionHostId: 'local', - wslDistro: null - } - }, - { claimKeyId: 'test-key', now: 0 } - ) - } - }, - sessions: new Map(), - publishStatus: vi.fn(), - serialize: (_sessionId: string, task: () => Promise) => task(), - tasks: { trackAttach: (start: Promise) => start }, - runtimeState: { - acquireAborts: new StructuredAgentSessionAcquireAborts(), - startFlights: new StructuredAgentSessionStartFlights(), - startupAttempts: new StructuredAgentSessionStartupAttempts({ expire: () => {} }) - }, - reconcileLeases: () => Promise.reject(cause) - } - - const refused = await startStructuredAgentSessionAgentForOperation( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial context double; the store returns a complete record; the start throws at `reconcileLeases` before the remaining context is read. - context as unknown as StructuredAgentSessionAttachContext, - SESSION - ) - - expect(refused).toEqual({ - ok: false, - failure: { kind: 'hostFault' }, - refusal: { - code: 'agent_session_owner_restart_failed', - message: "The agent couldn't restart." - } - }) - expect(log.entries.map((entry) => entry.fields)).toEqual([ - { scope: 'agent-start', sessionId: SESSION, error: cause } - ]) - }) -}) - -describe('an option picked while the chat is at rest', () => { - it('refuses a key the provider would not accept as a rejected option', async () => { - const reviseSessionOptions = vi.fn() - const refused = await recordStructuredAgentSessionOptionIntent( - { - store: { - getRecord: () => - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the intent reads only the record's provider and options. - ({ provider: 'codex', options: {} }) as unknown as AgentSessionRecord, - replaceSessionOptions: vi.fn(), - reviseSessionOptions - }, - agents: claudeAndCodexAgents() - }, - { sessionId: SESSION, fence: 1, publish: () => {}, now: () => 0 }, - { key: 'notAnOption', value: 'x' } - ) - - expect(refused).toMatchObject({ - ok: false, - refusal: { - code: 'agent_session_operation_invalid', - details: { reason: 'optionRejected' } - } - }) - expect(reviseSessionOptions).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-provider-started-outcome.test.ts b/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-provider-started-outcome.test.ts index 021736ac09d..f586e217a98 100644 --- a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-provider-started-outcome.test.ts +++ b/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-provider-started-outcome.test.ts @@ -62,16 +62,6 @@ it("keeps a required read's final answer over the child's later death", async () expect(outcome).toMatchObject({ retry: 'never', cause: answer }) }) -it('answers an unanswered death after a successful open with the adapter verdict', async () => { - ending = died - expect(await settleStructuredAgentSessionProviderStarted(context(), OPENED)).toBe(died) - expect(died.retry).toBe('safe') -}) - -it('retires only its own owner once the child was replaced', async () => { - expect(await settleStructuredAgentSessionProviderStarted(context('g-2'), OPENED)).toBe('stale') -}) - it('ends the start on a reported death while its bookkeeping is still blocked', async () => { ending = died const blocked = context() diff --git a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-start-flight.test.ts b/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-start-flight.test.ts deleted file mode 100644 index ab06a3b5274..00000000000 --- a/src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-start-flight.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { StructuredAgentSessionStartFlights } from '../structured-agent-session-start-flight' - -describe('start flight child retention', () => { - it.each([10, 100])('retires a flight after %i duplicate publications', (count) => { - const flights = new StructuredAgentSessionStartFlights() - const flight = { - attemptId: 'start', - abort: () => {}, - outcome: Promise.resolve({ ok: true as const }) - } - flights.register('chat', flight) - const child = { generation: 'child', fence: 1 } - for (let n = 0; n < count; n++) { - flights.published('chat', { ...child }) - } - expect(flights.get('chat')?.children).toEqual([child]) - expect(flights.ownsChild('chat', child)).toBe(true) - const successor = { ...child, fence: 2 } - flights.published('chat', successor) - flights.published('chat', { ...successor }) - expect(flights.get('chat')?.children).toEqual([child, successor]) - flights.end('chat', flight.attemptId) - flights.published('chat', child) - expect(flights.has('chat')).toBe(false) - expect(flights.ownsChild('chat', child)).toBe(false) - }) -}) diff --git a/src/renderer/src/components/native-chat/native-chat-conversation-projection.test.ts b/src/renderer/src/components/native-chat/native-chat-conversation-projection.test.ts deleted file mode 100644 index 3dac734336e..00000000000 --- a/src/renderer/src/components/native-chat/native-chat-conversation-projection.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { expect, it } from 'vitest' -import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types' -import { createNativeChatConversationProjection } from './native-chat-conversation-projection' - -function text(id: string): NativeChatMessage { - return { - id, - role: 'assistant', - blocks: [{ type: 'text', text: id }], - timestamp: 1, - source: 'transcript' - } -} - -it('preserves task conversion and mode-dependent goal omission across revisions and structural changes', () => { - const frame = (id: string, kind: string, head: string): NativeChatMessage => ({ - ...text(id), - role: 'system', - blocks: [ - { - type: 'text', - text: 'provider notice', - providerFrame: { - provider: 'codex', - kind, - payload: { head, byteLength: head.length, digest: id, truncated: false } - } - } - ] - }) - const plan = frame( - 'plan', - 'notification:turn/plan/updated', - '{"plan":[{"step":"Inspect","status":"completed"}]}' - ) - const goal = frame('goal', 'notification:thread/goal/cleared', '{}') - const invalid = frame('invalid', 'notification:turn/plan/updated', 'invalid json') - const project = createNativeChatConversationProjection() - expect(project([plan, goal, invalid], true).map((row) => row.id)).toEqual(['plan', 'invalid']) - expect(project([plan], false)[0]?.blocks).toEqual([ - { - type: 'tool-call', - name: 'update_plan', - input: '{"plan":[{"step":"Inspect","status":"completed"}]}', - state: 'completed' - } - ]) - const changedRole = { ...goal, role: 'assistant' as const } - const truncated = frame('truncated', 'notification:turn/plan/updated', '{"plan":[]}') - if (truncated.blocks[0]?.type === 'text' && truncated.blocks[0].providerFrame) { - truncated.blocks[0].providerFrame.payload.truncated = true - } - const planOutput = { - id: 'plan', - role: 'assistant', - blocks: [ - { - type: 'tool-call', - name: 'update_plan', - input: '{"plan":[{"step":"Inspect","status":"completed"}]}', - state: 'completed' - } - ] - } - const goalOutput = { - id: 'goal', - role: 'system', - blocks: [ - { - type: 'text', - text: 'provider notice', - providerFrame: { - provider: 'codex', - kind: 'notification:thread/goal/cleared', - payload: { head: '{}', byteLength: 2, digest: 'goal', truncated: false } - } - } - ] - } - const invalidOutput = { - id: 'invalid', - role: 'system', - blocks: [ - { - type: 'text', - text: 'provider notice', - providerFrame: { - provider: 'codex', - kind: 'notification:turn/plan/updated', - payload: { head: 'invalid json', byteLength: 12, digest: 'invalid', truncated: false } - } - } - ] - } - const assistantGoalOutput = { ...goalOutput, role: 'assistant' } - const truncatedOutput = { - id: 'truncated', - role: 'system', - blocks: [ - { - type: 'text', - text: 'provider notice', - providerFrame: { - provider: 'codex', - kind: 'notification:turn/plan/updated', - payload: { head: '{"plan":[]}', byteLength: 11, digest: 'truncated', truncated: true } - } - } - ] - } - for (const [rows, visible, hidden] of [ - [ - [plan, goal, invalid], - [planOutput, goalOutput, invalidOutput], - [planOutput, invalidOutput] - ], - [[goal, plan], [goalOutput, planOutput], [planOutput]], - [ - [changedRole, invalid], - [assistantGoalOutput, invalidOutput], - [assistantGoalOutput, invalidOutput] - ], - [ - [truncated, plan, plan], - [truncatedOutput, planOutput, planOutput], - [truncatedOutput, planOutput, planOutput] - ], - [[], [], []] - ] as const) { - expect(project([...rows], false).map(({ id, role, blocks }) => ({ id, role, blocks }))).toEqual( - visible - ) - expect(project([...rows], true).map(({ id, role, blocks }) => ({ id, role, blocks }))).toEqual( - hidden - ) - } -}) diff --git a/src/renderer/src/components/native-chat/native-chat-install-slot.test.ts b/src/renderer/src/components/native-chat/native-chat-install-slot.test.ts deleted file mode 100644 index 3779caa1f3f..00000000000 --- a/src/renderer/src/components/native-chat/native-chat-install-slot.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, it } from 'vitest' -import { nativeChatInstallSlotState } from './NativeChatInstallSlot' - -it("offers Orca's install only to chats that run the managed runtime", () => { - const notInstalled = { - kind: 'active' as const, - subject: { target: { kind: 'local' as const }, runtimeId: null, agent: 'antigravity' }, - removable: false, - snapshot: { - kind: 'known' as const, - state: { - agent: 'antigravity', - revision: 1, - available: true, - version: '1.4.1', - installedVersion: null, - installedCurrent: false, - source: null, - operation: null - } - } - } - expect(nativeChatInstallSlotState(notInstalled, 'strip', false).kind).toBe('install') - expect(nativeChatInstallSlotState(notInstalled, 'strip', true)).toEqual({ kind: 'none' }) -}) diff --git a/src/renderer/src/components/native-chat/native-chat-sign-in.test.tsx b/src/renderer/src/components/native-chat/native-chat-sign-in.test.tsx index 8590afb9be7..7ce3803659e 100644 --- a/src/renderer/src/components/native-chat/native-chat-sign-in.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-sign-in.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment happy-dom -import { act, cleanup, fireEvent, render, renderHook, waitFor } from '@testing-library/react' -import { Profiler } from 'react' +import { act, cleanup, renderHook, waitFor } from '@testing-library/react' import { afterEach, expect, it, vi } from 'vitest' +import { useNativeChatSignIn } from './native-chat-sign-in' const { state, remoteCreate, persistOrder, startHost, notifyFailure } = vi.hoisted(() => ({ state: { @@ -34,45 +34,12 @@ vi.mock('@/runtime/hosted-terminals/creation/web-runtime-terminal-create-failure })) vi.mock('@/lib/wsl-host-user-start', () => ({ startStoppedWslHostForUserAct: startHost })) vi.mock('@/lib/claude-account-sign-in', () => ({ signInToClaudeAccount: vi.fn() })) -import { NativeChatSignInContext, useNativeChatSignIn } from './native-chat-sign-in' -import { NativeChatSignInButton } from './NativeChatSignInButton' afterEach(() => { cleanup() vi.clearAllMocks() }) -it.each([ - ['opencode', 'opencode auth login'], - ['pi', 'pi'], - ['omp', 'omp'] -])( - 'opens %s sign-in as a plain terminal in the originating folder and pane', - async (agent, command) => { - const { result } = renderHook(() => - useNativeChatSignIn({ - agent, - target: { kind: 'local' }, - worktreeId: 'folder:login-project', - groupId: 'chat-pane', - failure: null, - startFailures: [] - }) - ) - act(() => result.current?.signIn()) - await waitFor(() => expect(result.current?.signingIn).toBe(false)) - expect(state.createTab).toHaveBeenCalledExactlyOnceWith( - 'folder:login-project', - 'chat-pane', - undefined, - { pendingStartup: { command }, viewMode: 'terminal' } - ) - expect(state.setActiveTabType).toHaveBeenCalledWith('terminal', 'folder:login-project') - expect(persistOrder).toHaveBeenCalledWith('folder:login-project', 'login-terminal') - expect(remoteCreate).not.toHaveBeenCalled() - } -) - it('runs sign-in on the paired chat host instead of making a local terminal', async () => { const { result } = renderHook(() => useNativeChatSignIn({ @@ -141,62 +108,3 @@ it('leaves a failed WSL start to its existing notification owner', async () => { expect(remoteCreate).not.toHaveBeenCalled() expect(notifyFailure).not.toHaveBeenCalled() }) - -it.each([20, 200])('shares one sign-in operation across %i row actions', async (count) => { - const pending = Promise.withResolvers<{ status: 'created' }>() - remoteCreate.mockReturnValueOnce(pending.promise) - const eligibleRender = vi.fn() - const unrelatedRender = vi.fn() - const rows = Array.from({ length: count }, (_, index) => ( -
- - - - - - -
- )) - function Chat({ revision: _revision }: { revision: number }) { - const signIn = useNativeChatSignIn({ - agent: 'pi', - target: { kind: 'environment', environmentId: 'paired-host' }, - worktreeId: 'remote-project', - failure: null, - startFailures: [] - }) - return ( - {rows} - ) - } - const view = render() - eligibleRender.mockClear() - unrelatedRender.mockClear() - await act(async () => view.rerender()) - expect(eligibleRender).not.toHaveBeenCalled() - expect(unrelatedRender).not.toHaveBeenCalled() - expect(remoteCreate).not.toHaveBeenCalled() - expect(state.createTab).not.toHaveBeenCalled() - const buttons = view.getAllByRole('button') - expect(buttons).toHaveLength(count) - await act(async () => { - for (const button of buttons) { - fireEvent.click(button) - } - }) - expect(startHost).toHaveBeenCalledOnce() - expect(remoteCreate).toHaveBeenCalledOnce() - expect(unrelatedRender).not.toHaveBeenCalled() - expect(eligibleRender).toHaveBeenCalledTimes(count) - expect(buttons.every((button) => button instanceof HTMLButtonElement && button.disabled)).toBe( - true - ) - eligibleRender.mockClear() - await act(async () => pending.resolve({ status: 'created' })) - expect(eligibleRender).toHaveBeenCalledTimes(count) - expect(unrelatedRender).not.toHaveBeenCalled() - expect(buttons.every((button) => button instanceof HTMLButtonElement && !button.disabled)).toBe( - true - ) - expect(remoteCreate).toHaveBeenCalledOnce() -}) diff --git a/src/renderer/src/components/native-chat/native-chat-split-flow.test.ts b/src/renderer/src/components/native-chat/native-chat-split-flow.test.ts index 3880b3660c4..1f543abed51 100644 --- a/src/renderer/src/components/native-chat/native-chat-split-flow.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-split-flow.test.ts @@ -222,13 +222,3 @@ it('keeps floating chat splits local while a remote workspace is selected', asyn expect(createRemote).not.toHaveBeenCalled() expect(store.getState().activeWorktreeId).toBe('remote-workspace') }) - -it('retains the existing terminal-backed chat split route', () => { - expect( - runNativeChatSplitTarget({ kind: 'terminal-pane', terminalTabId: 'terminal' }, 'down') - ).toBe(true) - expect(requestSplit).toHaveBeenCalledExactlyOnceWith({ - tabId: 'terminal', - direction: 'horizontal' - }) -}) diff --git a/src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx b/src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx index 360e4d44682..f2db7202450 100644 --- a/src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx @@ -32,39 +32,3 @@ it('routes an authenticated-start refusal to sign-in instead of retrying the sam expect(signIn).toHaveBeenCalledOnce() expect(retry).not.toHaveBeenCalled() }) - -it.each(['failed', 'visibility-unknown'] as const)( - 'says the create hand-back failure once and keeps Retry for %s', - (lifecycle) => { - const retry = vi.fn() - const message = 'The message is back in the composer.' - const notices = structuredSessionNotices({ - launch: { lifecycle, failure: null, retry }, - agentLabel: 'Codex', - sessionError: message, - composerError: null - }) - render() - expect(screen.getAllByText(message)).toHaveLength(1) - expect(screen.queryByText('Chat could not be started.')).toBeNull() - expect(screen.queryByText('Chat connection could not be confirmed.')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: 'Retry' })) - expect(retry).toHaveBeenCalledOnce() - } -) - -it('keeps unrelated composer errors beside the hand-back notice', () => { - render( - {} }, - agentLabel: 'Codex', - sessionError: 'The message is back in the composer.', - composerError: { text: 'Attachment could not be added.', onDismiss: () => {} } - })} - /> - ) - expect(screen.getByText('The message is back in the composer.')).toBeTruthy() - expect(screen.getByText('Attachment could not be added.')).toBeTruthy() - expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy() -}) diff --git a/src/renderer/src/components/native-chat/use-native-chat-history-retention.test.ts b/src/renderer/src/components/native-chat/use-native-chat-history-retention.test.ts index 284c7174e47..de54b3a8d6f 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-history-retention.test.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-history-retention.test.ts @@ -3,36 +3,6 @@ import { act, renderHook } from '@testing-library/react' import { expect, it, vi } from 'vitest' import { useNativeChatHistoryRetention } from './use-native-chat-history-retention' -it('holds parked history through hidden renders and releases on return, rebind and unmount', () => { - const release = vi.fn() - const retainHistory = vi.fn(() => release) - const trimHistory = vi.fn() - const view = renderHook( - (source) => - useNativeChatHistoryRetention({ ...source, root: { current: null }, sourceKey: 'test' }), - { - initialProps: { retainHistory, trimHistory } - } - ) - act(() => { - view.result.current.leaveEnd() - view.result.current.leaveEnd() - }) - view.rerender({ retainHistory, trimHistory }) - expect(retainHistory).toHaveBeenCalledOnce() - expect(release).not.toHaveBeenCalled() - act(() => view.result.current.returnToEnd()) - expect(release).toHaveBeenCalledOnce() - act(() => view.result.current.leaveEnd()) - const nextRelease = vi.fn() - const nextRetain = vi.fn(() => nextRelease) - view.rerender({ retainHistory: nextRetain, trimHistory }) - expect(release).toHaveBeenCalledTimes(2) - act(() => view.result.current.leaveEnd()) - view.unmount() - expect(nextRelease).toHaveBeenCalledOnce() -}) - it.each([false, true])( 'preserves a mounted selection until it clears (shared source: %s)', (shared) => { diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-options.interaction-mode.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session-options.interaction-mode.test.tsx index b4c8cba8456..bc421ee5fa5 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-options.interaction-mode.test.tsx +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-options.interaction-mode.test.tsx @@ -1,21 +1,15 @@ import type * as RuntimeCapabilityProofModule from '@/runtime/rpc/compatibility/runtime-capability-proof' // @vitest-environment happy-dom import { - mocks, answer, mutateWith, renderOptions, REOPENED, currentValue } from './use-structured-agent-session-options.test-fixture' -import { replaceRuntimeEnvironmentRevisions } from '@/runtime/rpc/environment-revisions/runtime-environment-revision' import { forgetStructuredSessionSelection } from '@/lib/agent-session/options/structured-agent-session-selection-controller' -import { resetStructuredAgentSessionStatusFeedsForTests } from '@/runtime/structured-agent-session-status-feed' -import type { AgentSessionStatusEvent } from '../../../../shared/agent-session/agent-session-wire' -import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' -import { useStructuredAgentSessionOptionStamp } from './structured-agent-session-status-hooks' -import { act, renderHook, waitFor } from '@testing-library/react' +import { act, waitFor } from '@testing-library/react' import { expect, it, vi } from 'vitest' vi.mock('@/runtime/rpc/compatibility/runtime-capability-proof', async (importOriginal) => ({ @@ -23,115 +17,6 @@ vi.mock('@/runtime/rpc/compatibility/runtime-capability-proof', async (importOri runtimeEnvironmentSupportsCapability: async () => true })) -it.each([2, 20])('isolates mode refresh reads and renders with %s mounted chats', async (count) => { - resetStructuredAgentSessionStatusFeedsForTests() - replaceRuntimeEnvironmentRevisions([{ id: 'server-1', createdAt: 1 }]) - forgetStructuredSessionSelection('session-1') - for (let i = 0; i < count - 1; i++) { - forgetStructuredSessionSelection(`other-${i}`) - } - const emitters = new Map void>() - mocks.subscribeStatus.mockImplementation(async (target, onEvent) => { - emitters.set(target.kind, onEvent) - return { unsubscribe: () => {} } - }) - let localMode: 'default' | 'plan' = 'default' - let remoteMode: 'default' | 'plan' = 'default' - const read = vi.fn(async (target: RuntimeClientTarget) => ({ - models: [], - current: { - interactionMode: target.kind === 'local' ? localMode : remoteMode, - acceptedInputSources: { interactionMode: 'saved' } - } - })) - answer({ options: read }) - const { mutate } = mutateWith(async () => null) - const stampRenders = vi.fn() - const stamp = renderHook(() => { - stampRenders() - return useStructuredAgentSessionOptionStamp('session-1', { kind: 'local' }) - }) - const affectedRenders = vi.fn() - const unaffectedRenders = vi.fn() - const remoteRenders = vi.fn() - const first = renderOptions({ ...REOPENED, onRender: affectedRenders }, mutate) - const sameChat = renderOptions({ ...REOPENED, onRender: affectedRenders }, mutate) - const others = Array.from({ length: count - 1 }, (_, i) => - renderOptions({ ...REOPENED, sessionId: `other-${i}`, onRender: unaffectedRenders }, mutate) - ) - const remote = renderOptions({ ...REOPENED, paired: true, onRender: remoteRenders }, mutate) - const mounted = [first, sameChat, ...others, remote] - const summary = { - sessionId: 'session-1', - workspaceId: 'workspace-1', - agent: 'codex', - status: 'idle' as const, - latestPrompt: '', - updatedAt: 1 - } - const publish = (host: string, session = summary, mode: 'default' | 'plan' = 'default') => - act(async () => - emitters.get(host)?.({ type: 'status', session: { ...session, interactionMode: mode } }) - ) - try { - for (const hook of mounted) { - await waitFor(() => - expect(currentValue(hook.result.current.optionSnapshot, 'interactionMode')).toBe('default') - ) - } - await publish('local') - await publish('environment') - for (let i = 0; i < count - 1; i++) { - await publish('local', { ...summary, sessionId: `other-${i}` }) - } - await act(async () => {}) - read.mockClear() - stampRenders.mockClear() - affectedRenders.mockClear() - unaffectedRenders.mockClear() - remoteRenders.mockClear() - await publish('local', { ...summary, sessionId: 'unmounted' }, 'plan') - await publish('local', { ...summary, latestPrompt: 'transcript changed' }) - expect(read).not.toHaveBeenCalled() - expect(stampRenders).not.toHaveBeenCalled() - expect(unaffectedRenders).not.toHaveBeenCalled() - expect(remoteRenders).not.toHaveBeenCalled() - - localMode = 'plan' - await publish('local', summary, 'plan') - for (const hook of [first, sameChat]) { - await waitFor(() => - expect(currentValue(hook.result.current.optionSnapshot, 'interactionMode')).toBe('plan') - ) - } - expect(read.mock.calls.length).toBeGreaterThan(0) - expect(read.mock.calls.length).toBeLessThanOrEqual(2) - expect(affectedRenders).toHaveBeenCalled() - expect(stampRenders).toHaveBeenCalled() - expect(unaffectedRenders).not.toHaveBeenCalled() - expect(remoteRenders).not.toHaveBeenCalled() - - read.mockClear() - stampRenders.mockClear() - affectedRenders.mockClear() - remoteMode = 'plan' - await publish('environment', summary, 'plan') - await waitFor(() => - expect(currentValue(remote.result.current.optionSnapshot, 'interactionMode')).toBe('plan') - ) - expect(read).toHaveBeenCalledTimes(1) - expect(remoteRenders).toHaveBeenCalled() - expect(affectedRenders).not.toHaveBeenCalled() - expect(stampRenders).not.toHaveBeenCalled() - expect(unaffectedRenders).not.toHaveBeenCalled() - } finally { - stamp.unmount() - mounted.forEach((hook) => hook.unmount()) - resetStructuredAgentSessionStatusFeedsForTests() - mocks.subscribeStatus.mockImplementation(async () => ({ unsubscribe: vi.fn() })) - } -}) - it('projects transmitted mode admission to the optimistic exit control and recovers after settlement', async () => { const sessionId = 'mode-admission-projection' forgetStructuredSessionSelection(sessionId) diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-actions.test.tsx b/src/renderer/src/components/onboarding/use-onboarding-flow-actions.test.tsx index 32ff021713e..e54504a39b0 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-actions.test.tsx +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-actions.test.tsx @@ -136,48 +136,6 @@ function renderActions( } describe('onboarding completion destination', () => { - it.each(['claude', 'codex'] as const)( - 'opens the requested %s in the selected workspace after close', - async (agent) => { - const { result, openModal, update, onOnboardingChange } = renderActions( - true, - STEPS.length - 1, - agent - ) - await act(() => result.current.next()) - expect(update).toHaveBeenCalledWith( - expect.objectContaining({ outcome: 'completed', lastCompletedStep: 5 }) - ) - expect(launch).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ - agent, - worktreeId: 'folder:home', - workspaceExecutionHostId: 'runtime:chosen', - launchSource: 'onboarding', - groupId: 'group', - focusIntent - }) - ) - expect(onOnboardingChange.mock.invocationCallOrder[0]).toBeLessThan( - launch.mock.invocationCallOrder[0] - ) - launch.mock.calls[0][0].beforeSurfaceOpen() - expect(reveal).toHaveBeenCalledWith( - 'folder:home', - expect.objectContaining({ - agent, - providesInitialSurface: true, - executionHostId: 'runtime:chosen' - }) - ) - expect(openModal).not.toHaveBeenCalled() - expect(track).toHaveBeenCalledWith( - 'onboarding_completed', - expect.objectContaining({ path: 'workspace' }) - ) - } - ) - it('reports a captured workspace disappearing while completion is saved, without opening a surface', async () => { const { result, persistCurrentStep } = renderActions(true, STEPS.length - 1, 'claude') const saved = Promise.withResolvers<{ ok: boolean }>() @@ -200,45 +158,6 @@ describe('onboarding completion destination', () => { expect(toastError).toHaveBeenCalledOnce() }) - it('retains Add Project when no workspace is selected', async () => { - const { result, openModal } = renderActions(false) - await act(() => result.current.next()) - expect(launch).not.toHaveBeenCalled() - expect(openModal).toHaveBeenCalledWith('add-repo') - expect(track).toHaveBeenCalledWith( - 'onboarding_completed', - expect.objectContaining({ path: 'add_project_modal' }) - ) - }) - - it.each([true, false])( - 'returns to the main page when setting up later, selected workspace=%s', - async (hasSelectedWorkspace) => { - const { result, openModal, update, updateSettings } = renderActions( - hasSelectedWorkspace, - STEPS.findIndex((step) => step.id === 'agent'), - 'claude' - ) - await act(() => result.current.setUpLater()) - expect(updateSettings).toHaveBeenCalledWith({ defaultTuiAgent: 'claude' }) - expect(update).toHaveBeenCalledWith( - expect.objectContaining({ - outcome: 'completed', - lastCompletedStep: 5, - checklist: { dismissed: false } - }) - ) - expect(openModal).not.toHaveBeenCalled() - if (hasSelectedWorkspace) { - expect(launch).toHaveBeenCalledWith( - expect.objectContaining({ agent: 'claude', worktreeId: 'folder:home' }) - ) - } else { - expect(launch).not.toHaveBeenCalled() - } - } - ) - it('keeps onboarding open for retry when setting up later cannot save completion', async () => { const { result, update, onOnboardingChange, openModal } = renderActions( true, @@ -303,66 +222,6 @@ describe('onboarding completion destination', () => { ) }) - it.each(['chat', 'terminal'] as const)( - 'confirms first-page %s mode and launches the saved agent instead of a hidden auto-pick', - async (surface) => { - const { result, openModal } = renderActions(true, 0, 'claude', { - surface, - defaultAgent: 'codex' - }) - await act(() => result.current.setUpLater()) - expect(state.settings).toEqual( - expect.objectContaining({ - agentDefaultSurface: surface, - defaultTuiAgent: 'codex' - }) - ) - expect(launch).toHaveBeenCalledWith( - expect.objectContaining({ agent: 'codex', worktreeId: 'folder:home' }) - ) - expect(openModal).not.toHaveBeenCalled() - } - ) - - it.each(['agent_surface', 'theme'] as const)( - 'preserves saved Blank on %s despite a hidden auto-pick', - async (step) => { - const { result } = renderActions( - true, - STEPS.findIndex((value) => value.id === step), - 'claude', - { - surface: 'chat', - defaultAgent: 'blank' - } - ) - await act(() => result.current.setUpLater()) - expect(state.settings?.defaultTuiAgent).toBe('blank') - expect(launch).not.toHaveBeenCalled() - } - ) - - it('confirms a visible agent or Blank without substituting an installed agent', async () => { - const agentPage = STEPS.findIndex((step) => step.id === 'agent') - const chosen = renderActions(true, agentPage, 'aider', { defaultAgent: 'codex' }) - await act(() => chosen.result.current.setUpLater()) - expect(state.settings?.defaultTuiAgent).toBe('aider') - expect(launch).toHaveBeenCalledWith(expect.objectContaining({ agent: 'aider' })) - cleanup() - launch.mockClear() - const blank = renderActions(true, agentPage, null, { defaultAgent: 'codex' }) - await act(() => blank.result.current.setUpLater()) - expect(state.settings?.defaultTuiAgent).toBe('blank') - expect(launch).not.toHaveBeenCalled() - }) - - it('resolves ordinary Auto without replacing the saved preference', async () => { - const { result } = renderActions(true, 0, 'codex') - await act(() => result.current.setUpLater()) - expect(state.settings?.defaultTuiAgent).toBeNull() - expect(launch).toHaveBeenCalledWith(expect.objectContaining({ agent: 'claude' })) - }) - it('retains the clicked workspace owner through deferred save and post-close discovery', async () => { const { result, updateSettings, onOnboardingChange } = renderActions(true, 0, 'claude', { surface: 'chat', diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.tsx b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.tsx index caf75517ec2..853aac8280a 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.tsx +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.tsx @@ -6,7 +6,6 @@ import { useAppStore } from '@/store' import { getDefaultSettings } from '../../../../shared/constants' import { getDefaultOnboardingState } from '../../../../shared/onboarding-defaults' import { useOnboardingFlow } from './use-onboarding-flow' -import { usePersistCurrentStep } from './use-onboarding-flow-persistence' beforeEach(() => { useAppStore.setState(useAppStore.getInitialState(), true) @@ -60,56 +59,4 @@ describe('onboarding completion persistence', () => { expect(openModal).not.toHaveBeenCalled() expect(notify).toHaveBeenCalledOnce() }) - - it.each([ - ['claude', 'claude'], - [null, 'blank'] - ] as const)( - 'saves the visible %s selection when completing without visiting the agent page', - async (selectedAgent, expectedAgent) => { - const savedProgress = getDefaultOnboardingState() - const saveProgress = vi.fn().mockResolvedValue(savedProgress) - Object.defineProperty(window, 'api', { - configurable: true, - value: { onboarding: { update: saveProgress } } - }) - vi.spyOn(useAppStore.getState(), 'recordFeatureInteraction').mockResolvedValue(undefined) - const updateSettings = vi.fn().mockResolvedValue(undefined) - const onOnboardingChange = vi.fn() - const { result } = renderHook(() => - usePersistCurrentStep({ - currentStepId: 'notifications', - agentSurface: 'terminal', - selectedAgent, - agentInteractedRef: { current: false }, - yoloPermissions: true, - yoloPermissionsChosen: { current: false }, - theme: 'dark', - settings: { - ...getDefaultSettings('/tmp'), - defaultTuiAgent: 'codex', - agentDefaultSurface: 'chat' - }, - updateSettings, - onboardingChecklist: savedProgress.checklist, - onOnboardingChange, - setError: vi.fn() - }) - ) - - await act(async () => { - expect(await result.current()).toEqual({ ok: true }) - }) - expect(updateSettings).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ defaultTuiAgent: expectedAgent, agentDefaultSurface: 'terminal' }) - ) - expect(saveProgress).toHaveBeenCalledWith( - expect.objectContaining({ - lastCompletedStep: 5, - checklist: expect.objectContaining({ choseAgent: selectedAgent !== null }) - }) - ) - expect(onOnboardingChange).toHaveBeenCalledWith(savedProgress) - } - ) }) diff --git a/src/renderer/src/components/plugins/presentation/plugin-author.test.ts b/src/renderer/src/components/plugins/presentation/plugin-author.test.ts deleted file mode 100644 index da197a7f774..00000000000 --- a/src/renderer/src/components/plugins/presentation/plugin-author.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { PluginHostListEntry } from '../../../../../preload/api-types' -import { - pluginListEntryFromInstalled, - pluginListEntryFromListing -} from '../list/plugin-list-entries' -import { pluginAuthor, pluginAuthorCatalogId, pluginsBySamePublisher } from './plugin-author' -import { - pluginAuthorGithubUrl, - pluginSourceGitUrl, - pluginSourceReadmeUrl -} from './plugin-source-links' - -function listed(pluginKey: string, url: string, marketplaceSourceId = 'community') { - return pluginListEntryFromListing({ - pluginKey, - marketplaceSourceId, - marketplaceName: 'Community', - marketplaceOwner: 'catalog-owner', - marketplaceCommit: 'abc', - source: { kind: 'git', url, ref: 'main' }, - categories: [], - official: false, - bundled: false - }) -} - -function installed( - source: Pick, 'kind' | 'reference'> & - Partial>, - isDev = false -) { - return pluginListEntryFromInstalled( - { - pluginKey: 'acme.notes', - name: 'Notes', - publisher: 'acme', - version: '1.0.0', - status: 'running', - consentFingerprint: null, - needsReconsent: false, - isDev, - official: false, - bundled: false, - capabilities: [], - panels: [], - commands: [], - hasWorker: false, - restarts: 0, - source: { resolvedCommit: null, contentHash: 'b'.repeat(64), ...source } - }, - listed('acme.notes', 'https://github.com/acme/notes').listing - ) -} - -describe('plugin publisher and source provenance', () => { - it('links GitHub publishers only when the repository owner matches the publisher', () => { - expect(pluginAuthorGithubUrl(listed('acme.notes', 'https://github.com/Acme/notes.git'))).toBe( - 'https://github.com/acme' - ) - expect(pluginAuthorGithubUrl(listed('acme.notes', 'git@github.com:acme/notes.git'))).toBe( - 'https://github.com/acme' - ) - expect( - pluginAuthorGithubUrl(listed('victim.notes', 'https://github.com/mirror/notes')) - ).toBeNull() - expect(pluginAuthorGithubUrl(listed('acme.notes', 'https://gitlab.com/acme/notes'))).toBeNull() - expect( - pluginAuthorGithubUrl(installed({ kind: 'local-path', reference: '/plugins/notes' })) - ).toBeNull() - }) - - it('recommends only the same publisher label within the same catalog', () => { - const entry = listed('acme.notes', 'https://github.com/acme/notes') - const entries = { - installed: [], - available: [ - listed('acme.icons', 'https://github.com/Acme/icons'), - listed('acme.other-host', 'https://gitlab.com/acme/other'), - listed('acme.other-catalog', 'https://github.com/acme/other', 'unrelated') - ] - } - expect(pluginsBySamePublisher(entry, entries).map((item) => item.pluginKey)).toEqual([ - 'acme.icons', - 'acme.other-host' - ]) - }) - - it('keeps installed mirror authors and recommendations tied to the recorded release', () => { - const entry = installed({ - kind: 'marketplace', - reference: 'https://github.com/mirrors/plugins.git (plugins/notes)', - url: 'https://github.com/mirrors/plugins.git', - path: 'plugins/notes', - authorRepository: 'https://github.com/acme/notes', - marketplace: { - reference: 'https://github.com/catalog/plugins', - resolvedCommit: 'a'.repeat(40) - } - }) - const other = { - ...entry, - pluginKey: 'acme.icons', - installed: { ...entry.installed!, pluginKey: 'acme.icons' } - } - expect(pluginAuthor(entry)).toEqual({ - publisher: 'acme', - catalog: 'Community', - provenance: 'catalog' - }) - expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/notes') - expect( - pluginsBySamePublisher(entry, { installed: [other], available: [] }).map( - (item) => item.pluginKey - ) - ).toEqual(['acme.icons']) - // A newer catalog record must not replace the installed release's attribution. - const withoutListing = { ...entry, listing: null, updatable: false } - expect(pluginSourceGitUrl(withoutListing)).toBe('https://github.com/acme/notes') - expect(pluginAuthorCatalogId(withoutListing)).toBeNull() - }) - - it('uses structured monorepo sources instead of parsing their display label', () => { - const entry = installed({ - kind: 'marketplace', - reference: 'https://github.com/acme/plugins.git (plugins/notes)', - url: 'https://github.com/acme/plugins.git', - path: 'plugins/notes' - }) - expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/plugins.git') - }) - - it('revokes a recorded repository only when the installed catalog release marks it unavailable', () => { - const entry = installed({ - kind: 'marketplace', - reference: 'https://github.com/mirrors/plugins.git', - url: 'https://github.com/mirrors/plugins.git', - resolvedCommit: 'a'.repeat(40), - authorRepository: 'https://github.com/acme/notes', - marketplace: { - reference: 'https://github.com/catalog/plugins', - resolvedCommit: 'c'.repeat(40) - } - }) - entry.listing = { - ...entry.listing!, - source: { kind: 'git', url: 'https://github.com/mirrors/plugins.git', ref: 'main' }, - release: { - version: '1.0.0', - commit: 'a'.repeat(40), - contentHash: 'b'.repeat(64), - execution: 'data', - minimumOrca: '1.0.0', - repository: 'https://github.com/acme/notes', - sourceUnavailable: true - } - } - expect(pluginSourceGitUrl(entry)).toBe('https://github.com/mirrors/plugins.git') - expect(pluginSourceGitUrl({ ...entry, listing: null })).toBe('https://github.com/acme/notes') - expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBe('https://github.com/acme/notes') - entry.listing.release!.commit = 'd'.repeat(40) - expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/notes') - }) - - it('borrows a catalog repository only from a source and release matched listing', () => { - const entry = installed({ - kind: 'marketplace', - reference: 'Mirror · Notes', - url: 'https://github.com/mirrors/plugins.git', - path: 'plugins/notes', - resolvedCommit: 'a'.repeat(40), - marketplace: { - reference: 'https://github.com/catalog/plugins', - resolvedCommit: 'c'.repeat(40) - } - }) - entry.listing = { - ...entry.listing!, - source: { - kind: 'git', - url: 'https://github.com/mirrors/plugins.git', - ref: 'main', - path: 'plugins/notes' - }, - release: { - version: '1.0.0', - commit: 'a'.repeat(40), - contentHash: 'b'.repeat(64), - execution: 'data', - minimumOrca: '1.0.0', - repository: 'https://github.com/acme/notes' - } - } - for (const listing of [ - { ...entry.listing, source: { ...entry.listing.source, path: 'plugins/other' } }, - { - ...entry.listing, - source: { ...entry.listing.source, url: 'https://github.com/other/plugins' } - }, - { ...entry.listing, release: { ...entry.listing.release!, commit: 'd'.repeat(40) } }, - { ...entry.listing, release: { ...entry.listing.release!, contentHash: 'e'.repeat(64) } }, - { ...entry.listing, release: { ...entry.listing.release!, sourceUnavailable: true as const } } - ]) { - expect(pluginSourceGitUrl({ ...entry, listing })).toBe( - 'https://github.com/mirrors/plugins.git' - ) - } - expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBe( - 'https://github.com/mirrors/plugins.git' - ) - }) - - it('keeps a mirror catalog claim separate from an account identity', () => { - const entry = listed('victim.plugin', 'https://github.com/mirror/plugins') - entry.listing!.release = { - version: '1.0.0', - commit: 'a'.repeat(40), - contentHash: 'b'.repeat(64), - execution: 'data', - minimumOrca: '1.0.0', - repository: 'https://github.com/victim/repo' - } - expect(pluginAuthor(entry)).toEqual({ - publisher: 'victim', - catalog: 'Community', - provenance: 'catalog' - }) - }) - - it.each([ - { - reference: 'https://github.com/acme/notes', - url: 'https://github.com/acme/notes', - path: undefined, - readme: 'https://github.com/acme/notes/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#readme' - }, - { - reference: 'https://github.com/acme/plugins (plugins/notes)', - url: 'https://github.com/acme/plugins', - path: 'plugins/notes', - readme: - 'https://github.com/acme/plugins/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme' - } - ])( - 'restores older-host links only for the recorded location $reference', - ({ reference, path, url, readme }) => { - const entry = installed({ - kind: 'marketplace', - reference, - resolvedCommit: 'a'.repeat(40), - marketplace: { - reference: 'https://github.com/catalog/plugins', - resolvedCommit: 'c'.repeat(40) - } - }) - entry.listing!.source = { kind: 'git', url, ref: 'main', path } - expect(pluginSourceGitUrl(entry)).toBe(url) - expect(pluginSourceReadmeUrl(entry)).toBe(readme) - expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBeNull() - expect(pluginSourceGitUrl({ ...entry, listing: null })).toBeNull() - entry.listing!.source.path = 'plugins/other' - expect(pluginSourceGitUrl(entry)).toBeNull() - expect(pluginSourceReadmeUrl(entry)).toBeNull() - } - ) - - it('links an installed mirror README at its recorded path and commit, even after catalog changes', () => { - const entry = installed({ - kind: 'marketplace', - reference: 'mirror', - url: 'https://gitlab.com/mirror/plugins', - path: 'plugins/notes', - resolvedCommit: 'a'.repeat(40), - authorRepository: 'https://github.com/acme/notes' - }) - expect(pluginSourceReadmeUrl(entry)).toBe( - 'https://gitlab.com/mirror/plugins/-/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme' - ) - expect(pluginSourceReadmeUrl({ ...entry, listing: null })).toBe( - 'https://gitlab.com/mirror/plugins/-/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme' - ) - entry.installed!.source!.resolvedCommit = null - expect(pluginSourceReadmeUrl(entry)).toBeNull() - }) - - it('does not borrow a listing profile or repository for local and dev installs', () => { - for (const isDev of [true, false]) { - const entry = installed({ kind: 'local-path', reference: '/plugins/notes' }, isDev) - expect(pluginSourceGitUrl(entry)).toBeNull() - } - }) - - it('uses a Git install source even when an unrelated listing is attached', () => { - const entry = installed({ kind: 'git', reference: 'https://gitlab.com/acme/notes' }) - expect(pluginSourceGitUrl(entry)).toBe('https://gitlab.com/acme/notes') - expect(pluginAuthor(entry)).toEqual({ publisher: 'acme', catalog: null, provenance: 'git' }) - expect(pluginAuthor({ ...entry, listing: null })).toEqual({ - publisher: 'acme', - catalog: null, - provenance: 'git' - }) - }) - - it('attributes local and dev publisher labels to the manifest', () => { - for (const isDev of [true, false]) { - const entry = installed({ kind: 'local-path', reference: '/plugins/notes' }, isDev) - expect(pluginAuthor(entry)).toEqual({ - publisher: 'acme', - catalog: null, - provenance: 'manifest' - }) - } - }) - - it('keeps unnamed catalog and unknown install sources distinct', () => { - const entry = installed({ kind: 'marketplace', reference: 'catalog' }) - entry.listing = null - expect(pluginAuthor(entry)).toEqual({ - publisher: 'acme', - catalog: null, - provenance: 'catalog' - }) - entry.installed!.source = undefined - expect(pluginAuthor(entry)).toEqual({ - publisher: 'acme', - catalog: null, - provenance: 'unknown' - }) - }) -}) diff --git a/src/renderer/src/components/plugins/presentation/plugin-error-presentation.test.ts b/src/renderer/src/components/plugins/presentation/plugin-error-presentation.test.ts deleted file mode 100644 index d85623af579..00000000000 --- a/src/renderer/src/components/plugins/presentation/plugin-error-presentation.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, fallback: string) => fallback -})) - -import { pluginInstallErrorMessage } from './plugin-error-presentation' - -// Why: keep presentation tests locked to pluginInstallTrustError production strings -// so a "git" substring cannot silently re-route reserved errors to installGit. -const reservedRefusal = (pluginKey: string): string => - `${pluginKey} uses a name reserved for official Orca plugins; Orca installs it only from its official catalog` - -describe('pluginInstallErrorMessage', () => { - it('explains how to rename a local fork of an official template', () => { - const message = pluginInstallErrorMessage( - new Error(reservedRefusal('stablyai.orca-portuguese')) - ) - expect(message).toContain('reserved for official Orca plugins') - expect(message).toContain('orca-plugin.json') - expect(message).toContain('"orca-"') - expect(message).toContain('"publisher"') - }) - - it('does not mistake a reserved key containing "git" for a fetch failure', () => { - const message = pluginInstallErrorMessage(new Error(reservedRefusal('github.tools'))) - expect(message).toContain('reserved for official Orca plugins') - expect(message).not.toContain("Couldn't fetch") - }) - - it('still maps genuine git fetch failures to the installGit message', () => { - const message = pluginInstallErrorMessage( - new Error('git fetch failed: could not clone repository from remote') - ) - expect(message).toContain("Couldn't fetch this Git revision") - }) -}) diff --git a/src/renderer/src/components/plugins/presentation/plugin-name-disambiguation.test.ts b/src/renderer/src/components/plugins/presentation/plugin-name-disambiguation.test.ts deleted file mode 100644 index cc00ec1f004..00000000000 --- a/src/renderer/src/components/plugins/presentation/plugin-name-disambiguation.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - disambiguatedContentPluginNames, - disambiguatedPluginContentLabels -} from './plugin-name-disambiguation' - -describe('plugin content labels', () => { - it('suffixes the community theme, never the official one, by its key publisher', () => { - const labels = disambiguatedPluginContentLabels([ - { - id: 'plugin:stablyai.orca-catppuccin-theme/mocha', - pluginKey: 'stablyai.orca-catppuccin-theme', - label: 'Catppuccin Mocha' - }, - { - id: 'plugin:vale-c.catppuccin/mocha', - pluginKey: 'vale-c.catppuccin', - label: 'Catppuccin Mocha' - }, - { - id: 'plugin:vale-c.catppuccin/latte', - pluginKey: 'vale-c.catppuccin', - label: 'Catppuccin Latte' - } - ]) - expect(labels.get('plugin:stablyai.orca-catppuccin-theme/mocha')).toBe('Catppuccin Mocha') - expect(labels.get('plugin:vale-c.catppuccin/mocha')).toBe('Catppuccin Mocha (vale-c)') - expect(labels.get('plugin:vale-c.catppuccin/latte')).toBe('Catppuccin Latte') - }) - - it("keeps Orca's built-in names plain and suffixes a plugin entry sharing one", () => { - const labels = disambiguatedPluginContentLabels( - [{ id: 'plugin:acme.dracula/d', pluginKey: 'acme.dracula', label: 'Dracula' }], - ['Dracula'] - ) - expect(labels.get('plugin:acme.dracula/d')).toBe('Dracula (acme)') - }) - - it('names clashing plugins once each', () => { - const names = disambiguatedContentPluginNames([ - { pluginKey: 'stablyai.orca-catppuccin-theme', pluginName: 'Catppuccin for Orca' }, - { pluginKey: 'vale-c.catppuccin', pluginName: 'Catppuccin for Orca' }, - { pluginKey: 'vale-c.catppuccin', pluginName: 'Catppuccin for Orca' } - ]) - expect(names.get('stablyai.orca-catppuccin-theme')).toBe('Catppuccin for Orca') - expect(names.get('vale-c.catppuccin')).toBe('Catppuccin for Orca (vale-c)') - }) -}) diff --git a/src/renderer/src/components/plugins/state/use-plugin-page-model.test.tsx b/src/renderer/src/components/plugins/state/use-plugin-page-model.test.tsx deleted file mode 100644 index 294fc36147f..00000000000 --- a/src/renderer/src/components/plugins/state/use-plugin-page-model.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// @vitest-environment happy-dom - -import { act } from 'react' -import { createRoot } from 'react-dom/client' -import { expect, it } from 'vitest' -import { pluginListEntryFromListing, type PluginListEntry } from '../list/plugin-list-entries' -import type { PluginPagePreview } from './use-plugin-catalog' -import { usePluginPageModel } from './use-plugin-page-model' - -it('pins catalog provenance with its listing until the newer entry is selected', async () => { - const original = pluginListEntryFromListing( - { - pluginKey: 'acme.notes', - marketplaceSourceId: 'first', - marketplaceName: 'Community', - marketplaceOwner: 'catalog', - marketplaceCommit: 'a', - source: { kind: 'git', url: 'https://github.com/acme/notes', ref: 'main' }, - categories: [], - official: false, - bundled: false - }, - ['first'], - 'https://github.com/first/catalog' - ) - const newer = pluginListEntryFromListing( - { - ...original.listing!, - marketplaceSourceId: 'second', - marketplaceCommit: 'b' - }, - ['second'], - 'https://gitlab.com/second/catalog' - ) - const fetchPreview = () => new Promise(() => undefined) - let model: ReturnType | undefined - function Harness({ entry }: { entry: PluginListEntry }) { - model = usePluginPageModel({ entry, fetchPreview, refreshing: false }) - return null - } - const root = createRoot(document.createElement('div')) - try { - await act(async () => root.render()) - expect(model?.entry.verifiedSource).toEqual({ - kind: 'catalog', - catalog: 'Community', - owner: 'github.com/first' - }) - await act(async () => root.render()) - expect(model?.entry.verifiedSource).toEqual({ - kind: 'catalog', - catalog: 'Community', - owner: 'github.com/first' - }) - expect(model?.listing?.marketplaceSourceId).toBe('first') - await act(async () => model?.showNewer()) - expect(model?.entry.verifiedSource).toEqual({ - kind: 'catalog', - catalog: 'Community', - owner: 'gitlab.com/second' - }) - expect(model?.listing?.marketplaceSourceId).toBe('second') - } finally { - act(() => root.unmount()) - } -}) diff --git a/src/renderer/src/components/plugins/state/use-plugin-page-navigation.test.tsx b/src/renderer/src/components/plugins/state/use-plugin-page-navigation.test.tsx index 58256da6cca..76329f55c0c 100644 --- a/src/renderer/src/components/plugins/state/use-plugin-page-navigation.test.tsx +++ b/src/renderer/src/components/plugins/state/use-plugin-page-navigation.test.tsx @@ -93,30 +93,6 @@ describe('usePluginPageNavigation', () => { expect(document.activeElement).toBe(container.querySelector('[data-plugin-row-name]')) }) - it('clears history for catalog requests and discards removed pages', async () => { - await mountOpen() - await act(async () => nav?.open('acme.icons')) - await act(async () => nav?.leave('acme.notes')) - expect(nav?.openKey).toBe('acme.icons') - await act(async () => nav?.back()) - expect(nav?.openKey).toBeNull() - await act(async () => nav?.open('acme.notes')) - await act(async () => nav?.open('acme.icons')) - await act(async () => nav?.toCatalog()) - expect(nav?.openKey).toBeNull() - await act(async () => nav?.open('acme.third')) - await act(async () => nav?.back()) - expect(nav?.openKey).toBeNull() - }) - - it('returns to the originating detail page after replacing a completed review', async () => { - await mountOpen() - await act(async () => nav?.open('acme.tasks')) - await act(async () => nav?.open('acme.third', { replace: true })) - await act(async () => nav?.back()) - expect(nav?.openKey).toBe('acme.notes') - }) - it('pauses A when visiting B, keeps B decisions outside the queue, and resumes A before the next review', async () => { await mountOpen() await act(async () => reviews?.enqueue(['acme.notes', 'acme.third'])) diff --git a/src/renderer/src/components/pr-check-owner-presentation.test.tsx b/src/renderer/src/components/pr-check-owner-presentation.test.tsx index 2a9d96f3c61..b965a822fe3 100644 --- a/src/renderer/src/components/pr-check-owner-presentation.test.tsx +++ b/src/renderer/src/components/pr-check-owner-presentation.test.tsx @@ -1,13 +1,7 @@ // @vitest-environment happy-dom import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, expect, it, vi } from 'vitest' -import { CircleDashed } from 'lucide-react' import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner' -import { getCheckCounts, getOverallCheckCounts, getChecksSummaryLabel } from './pr-check-counts' -import { ChecksTabLayouts } from './pull-request-page/checks/layouts' -import { ChecksCompactHeader } from './pull-request-page/checks/toolbar' -import { ChecksTabCompactHeader } from './github-item-dialog/inspect-pull-request/checks-tab-header' -import { getChecksTabSummaryPresentation } from './github-item-dialog/inspect-pull-request/checks-tab-summary' import { PRTriageStrip } from './right-sidebar/checks-panel/presentation/triage-strip' import { i18n } from '@/i18n/i18n' import type { PRCheckDetail } from '../../../shared/github/check-types' @@ -41,138 +35,6 @@ afterEach(async () => { await i18n.changeLanguage('en') }) -it.each([ChecksCompactHeader, ChecksTabCompactHeader])( - 'keeps row approval chips while only current associated rows explain an owner failure', - (Header) => { - const accepted = acceptPRCheckObservation({ ...owner, checksSummary: blocked }, null, context)! - const counts = getCheckCounts(rows) - expect(counts).toMatchObject({ passing: 0, failing: 0, needsAction: 1 }) - const overall = getOverallCheckCounts(rows, accepted, accepted) - render( -
Refresh} - rerunAction={null} - secondaryActions={null} - /> - ) - expect(screen.getByText('1 check needs action')).toBeTruthy() - cleanup() - const newer = acceptPRCheckObservation( - { ...owner, checksSummary: { ...blocked } }, - accepted, - context - )! - const nextCounts = getOverallCheckCounts(rows, newer, accepted) - render( -
Refresh} - rerunAction={null} - secondaryActions={null} - /> - ) - expect(screen.getByText('1 check failing')).toBeTruthy() - expect( - getOverallCheckCounts( - rows, - acceptPRCheckObservation( - { ...owner, checksSummary: { ...blocked, failed: 2, total: 102 } }, - null, - context - )!, - accepted - ).failing - ).toBe(2) - } -) - -it.each(['page', 'compact'] as const)( - 'does not claim zero whole checks in the empty %s renderer', - async (variant) => { - i18n.addResourceBundle( - 'es', - 'translation', - { - auto: { - components: { - right: { sidebar: { ChecksPanel: { '976cefd02f': 'Comprobaciones no disponibles' } } }, - prCheckCounts: { noCheckDetails: 'No hay detalles de comprobaciones' } - } - } - }, - true, - true - ) - await i18n.changeLanguage('es') - const accepted = acceptPRCheckObservation(owner, null, context)! - const summaryLabel = getChecksSummaryLabel([], accepted) - render( - {summaryLabel}} - actions={} - SummaryIcon={CircleDashed} - summaryColor="" - summaryLabel={summaryLabel} - counts={getCheckCounts([])} - summaryPending={false} - sorted={[]} - renderCheckRow={() =>
} - fixChecksAgentDialog={} - /> - ) - expect(screen.getByText('Comprobaciones no disponibles')).toBeTruthy() - expect(screen.getByText('No hay detalles de comprobaciones')).toBeTruthy() - expect(screen.queryByText('No checks found')).toBeNull() - } -) - -it('keeps the row Fix action available when the complete owner is unavailable', () => { - const accepted = acceptPRCheckObservation(owner, null, context)! - render( - {}} - isFixingChecksWithAI={false} - onFixChecksWithAI={() => {}} - /> - ) - expect(screen.getByText('Checks unavailable')).toBeTruthy() - expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(false) -}) - -it('keeps a successful whole owner presentation when independent rows still contain an old failure', () => { - const accepted = acceptPRCheckObservation( - { ...owner, checksSummary: { ...blocked, state: 'success', total: 1, passed: 1, failed: 0 } }, - null, - context - )! - render( - {}} - isFixingChecksWithAI={false} - onFixChecksWithAI={() => {}} - /> - ) - expect(screen.getByText('No blocking PR action')).toBeTruthy() - expect(screen.queryByText('0 failing checks')).toBeNull() -}) - it.each([false, true])('keeps a failed parent pipeline fixable with empty jobs: %s', (empty) => { const fix = vi.fn() const props = { diff --git a/src/renderer/src/components/pr-check-tab-actions.test.tsx b/src/renderer/src/components/pr-check-tab-actions.test.tsx index d3be4cd613d..80db3bd8849 100644 --- a/src/renderer/src/components/pr-check-tab-actions.test.tsx +++ b/src/renderer/src/components/pr-check-tab-actions.test.tsx @@ -7,7 +7,6 @@ import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as capability } from '. import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { PRCheckDetail } from '../../../shared/github/check-types' import { TooltipProvider } from '@/components/ui/tooltip' -import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner' import { patchCachedPRChecks, subscribeWorkItemDetailsCache, @@ -155,53 +154,3 @@ it.each([PageChecks, DialogChecks])( expect(screen.getByText('1 check failing')).toBeTruthy() } ) - -it.each([PageChecks, DialogChecks])( - 'keeps approval explanation across its cache echo and retires it for a newer owner', - async (Tab) => { - lookup.mockResolvedValue({ ...initial, checksSummary: summary }) - rows.mockResolvedValue([ - { - name: 'Approval', - status: 'completed', - conclusion: 'action_required', - url: null, - observedHeadSha: 'a'.repeat(40) - } - ]) - const view = render() - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Refresh' })) - }) - expect(screen.getByText('1 check needs action')).toBeTruthy() - const newer = acceptPRCheckObservation({ ...initial, checksSummary: { ...summary } }, null, { - scope: 'workspace:local:local', - complete: true, - current: true - })! - view.rerender() - expect(screen.getByText('1 check failing')).toBeTruthy() - } -) - -it.each([PageChecks, DialogChecks])( - 'keeps a mismatched approval head conservative after its cache echo', - async (Tab) => { - lookup.mockResolvedValue({ ...initial, checksSummary: summary }) - rows.mockResolvedValue([ - { - name: 'Approval', - status: 'completed', - conclusion: 'action_required', - url: null, - observedHeadSha: 'b'.repeat(40) - } - ]) - render() - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Refresh' })) - }) - expect(screen.getByText('1 check failing')).toBeTruthy() - expect(screen.getByText('Approval')).toBeTruthy() - } -) diff --git a/src/renderer/src/components/provider-accounts/AntigravityChatAuthForm.test.tsx b/src/renderer/src/components/provider-accounts/AntigravityChatAuthForm.test.tsx index 0cc9addd102..c85d85d22e0 100644 --- a/src/renderer/src/components/provider-accounts/AntigravityChatAuthForm.test.tsx +++ b/src/renderer/src/components/provider-accounts/AntigravityChatAuthForm.test.tsx @@ -301,61 +301,6 @@ it('refreshes the saved method after reconnecting to a restarted host with the s expect(screen.getByRole('combobox')).toHaveTextContent('Gemini Enterprise') }) -it.each(['accounts', 'provider'] as const)( - '%s preserves another client’s active and settled sign-in status', - async (surface) => { - const target = { kind: 'environment', environmentId: `other-client-${surface}` } as const - streams.initial.set(key(target), { - revision: 0, - auth: 'unknown', - flow: { phase: 'waiting', flowId: null } - }) - vi.mocked(callAntigravityChatAuthConfig).mockResolvedValue(personal) - renderSurface(surface, target) - await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled()) - expect(screen.getByRole('button', { name: 'Sign in' })).toBeDisabled() - expect(screen.getByRole('status')).toHaveTextContent( - 'Sign-in is in progress in another client.' - ) - await act(async () => { - streams.handlers - .get(key(target)) - ?.onEvent({ revision: 1, auth: 'unknown', flow: { phase: 'expired', flowId: null } }) - }) - expect(screen.getByRole('status')).toHaveTextContent('Sign-in timed out. Try again.') - } -) - -it.each(['accounts', 'provider'] as const)( - '%s preserves signed-in host context and explains a changed method', - async (surface) => { - const target = { kind: 'environment', environmentId: `signed-in-${surface}` } as const - streams.initial.set(key(target), { revision: 0, auth: 'signedIn', flow: null }) - let current: AntigravityChatAuthConfig = personal - vi.mocked(callAntigravityChatAuthConfig).mockImplementation(async (_owner, config) => { - if (config) { - current = config - } - return current - }) - const user = userEvent.setup() - renderSurface(surface, target) - await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled()) - expect(screen.getByRole('status')).toHaveTextContent('Signed in on Test host.') - expect(screen.getByRole('button', { name: 'Change account' })).toBeEnabled() - await user.click(screen.getByRole('combobox')) - await user.click(screen.getByRole('option', { name: 'Gemini Enterprise' })) - expect(screen.getByRole('status')).toHaveTextContent('Sign in again to use Gemini Enterprise.') - await user.type(screen.getByLabelText('Google Cloud project'), enterprise.gcpProject) - await user.type(screen.getByLabelText('Google Cloud location'), enterprise.gcpLocation) - await user.tab() - await act(async () => { - streams.handlers.get(key(target))?.onEvent({ revision: 1, auth: 'unknown', flow: null }) - }) - expect(screen.getByRole('status')).toHaveTextContent('Sign in again to use Gemini Enterprise.') - } -) - it.each(['accounts', 'provider'] as const)( '%s uses its single Sign in action to reread a failed load before preparing Enterprise', async (surface) => { diff --git a/src/renderer/src/components/quick-open-file-list-folder-twin.react.test.tsx b/src/renderer/src/components/quick-open-file-list-folder-twin.react.test.tsx deleted file mode 100644 index 4ff72585956..00000000000 --- a/src/renderer/src/components/quick-open-file-list-folder-twin.react.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// @vitest-environment happy-dom -import { cleanup, renderHook } from '@testing-library/react' -import { afterEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { useNativeChatMentionFiles } from './native-chat/use-native-chat-mention-files' -import { - flushEffects, - listRuntimeFilesMock, - makeFolderWorkspace -} from './quick-open-file-list-test-harness' -import { folderWorkspaceKey } from '../../../shared/workspace-scope' - -vi.mock('@/runtime/files/search/runtime-file-search-client', async () => { - const mocks = await import('./__mocks__/quick-open-runtime-file-client') - return { - listRuntimeFiles: mocks.listRuntimeFilesMock, - cancelRuntimeFileList: mocks.cancelRuntimeFileListMock, - searchRuntimeFilePaths: mocks.searchRuntimeFilePathsMock - } -}) - -globalThis.IS_REACT_ACT_ENVIRONMENT = true -afterEach(cleanup) - -it.each([ - { hostId: 'local', path: '/local/repo', connectionId: undefined }, - { hostId: 'ssh:box', path: '/remote/repo', connectionId: 'box' } -] as const)( - 'lists chat mentions from the selected $hostId twin of a folder workspace on two hosts', - async ({ hostId, path, connectionId }) => { - const twin = { id: 'fw-twin' } - useAppStore.setState({ - activeWorktreeId: folderWorkspaceKey('fw-twin'), - activeWorkspaceExecutionHostId: hostId, - repos: [], - projectGroups: [], - worktreesByRepo: {}, - folderWorkspaces: [ - makeFolderWorkspace({ ...twin, folderPath: '/local/repo', executionHostId: 'local' }), - makeFolderWorkspace({ - ...twin, - folderPath: '/remote/repo', - connectionId: 'box', - executionHostId: 'ssh:box' - }) - ] - }) - renderHook(() => - useNativeChatMentionFiles({ - // A typed query debounces the remote search; empty lists the catalog at once. - query: '', - terminalTabId: 'chat', - structuredWorktreeId: folderWorkspaceKey('fw-twin') - }) - ) - await flushEffects() - expect(listRuntimeFilesMock).toHaveBeenCalledWith( - expect.objectContaining({ worktreePath: path, connectionId }), - expect.anything() - ) - } -) diff --git a/src/renderer/src/components/quick-open-file-list-metadata.react.test.tsx b/src/renderer/src/components/quick-open-file-list-metadata.react.test.tsx deleted file mode 100644 index 6af853583b5..00000000000 --- a/src/renderer/src/components/quick-open-file-list-metadata.react.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// @vitest-environment happy-dom - -import { act } from 'react' -import { expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import type { Worktree } from '../../../shared/worktree/types' -import type { RuntimeFileListState } from './quick-open-file-list' -import { - cancelRuntimeFileListMock, - flushEffects, - listRuntimeFilesMock, - makeRemoteWorktree, - renderProbe, - searchRuntimeFilePathsMock, - seedRemoteWorktree -} from './quick-open-file-list-test-harness' - -vi.mock('@/runtime/files/search/runtime-file-search-client', async () => { - const mocks = await import('./__mocks__/quick-open-runtime-file-client') - return { - listRuntimeFiles: mocks.listRuntimeFilesMock, - cancelRuntimeFileList: mocks.cancelRuntimeFileListMock, - searchRuntimeFilePaths: mocks.searchRuntimeFilePathsMock - } -}) - -async function replaceWorktrees(worktrees: Worktree[]): Promise { - await act(async () => { - useAppStore.setState({ worktreesByRepo: { 'repo-remote': worktrees } }) - }) - await flushEffects() -} - -it('keeps a settled inventory through metadata updates', async () => { - seedRemoteWorktree() - const states: RuntimeFileListState[] = [] - await renderProbe({ enabled: true, states, worktreeId: 'wt-remote' }) - const settledFiles = states.at(-1)?.files - const initialRequest = listRuntimeFilesMock.mock.calls[0]?.[1] - const rendersBeforeUpdates = states.length - - await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-1' }]) - await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-2' }]) - - expect(listRuntimeFilesMock).toHaveBeenCalledOnce() - expect(cancelRuntimeFileListMock).not.toHaveBeenCalled() - expect(initialRequest?.signal?.aborted).toBe(false) - expect(states.slice(rendersBeforeUpdates).every((state) => !state.loading)).toBe(true) - expect(states.at(-1)?.files).toBe(settledFiles) -}) - -it('keeps an in-flight query through metadata updates but replaces it for changed exclusions', async () => { - vi.useFakeTimers() - seedRemoteWorktree() - searchRuntimeFilePathsMock.mockReturnValue(new Promise(() => {})) - try { - await renderProbe({ enabled: true, states: [], query: 'target', worktreeId: 'wt-remote' }) - await act(async () => vi.advanceTimersByTimeAsync(120)) - const initialRequest = searchRuntimeFilePathsMock.mock.calls[0]?.[1] - await replaceWorktrees([{ ...makeRemoteWorktree(), comment: 'Updated comment' }]) - await act(async () => vi.advanceTimersByTimeAsync(120)) - expect(initialRequest?.signal?.aborted).toBe(false) - expect(searchRuntimeFilePathsMock).toHaveBeenCalledOnce() - - const nested = { ...makeRemoteWorktree(), id: 'nested', path: '/srv/remote/nested' } - await replaceWorktrees([makeRemoteWorktree(), nested]) - expect(initialRequest?.signal?.aborted).toBe(true) - await act(async () => vi.advanceTimersByTimeAsync(120)) - expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(2) - expect(searchRuntimeFilePathsMock.mock.calls[1]?.[1].excludePaths).toEqual([nested.path]) - - await replaceWorktrees([makeRemoteWorktree()]) - await act(async () => vi.advanceTimersByTimeAsync(120)) - expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(3) - expect(searchRuntimeFilePathsMock.mock.calls[2]?.[1].excludePaths).toBeUndefined() - } finally { - vi.useRealTimers() - } -}) diff --git a/src/renderer/src/components/quick-open-recent-validation.test.ts b/src/renderer/src/components/quick-open-recent-validation.test.ts index b3ef36acf2d..486326acbe3 100644 --- a/src/renderer/src/components/quick-open-recent-validation.test.ts +++ b/src/renderer/src/components/quick-open-recent-validation.test.ts @@ -86,35 +86,3 @@ it('cancels the producer on close and ignores its late result after reopening', expect(await first).toBeUndefined() expect(mocks.list).toHaveBeenCalledTimes(2) }) -it('cancels eligibility on owner/root changes while preserving ordinary search on errors', async () => { - const cache: QuickOpenRecentCache = { current: null } - mocks.list.mockRejectedValue(new Error('Update host')) - const result = await mergeQuickOpenRecentCandidates({ - cache, - key: 'root-a', - context, - options: { rootPath: '/repo' }, - candidatePaths: ['recent.ts'], - result: { files: ['recent.ts', 'ordinary.ts'], truncated: true }, - cancelled: () => false - }) - expect(result?.files).toEqual(['recent.ts', 'ordinary.ts']) - expect(result?.recentError).toBe('Recent files could not be checked: Update host') -}) - -it('uses fresh complete inventory membership without a second traversal', async () => { - const result = { files: ['ordinary.ts', 'recent.ts'], truncated: false } - expect( - await mergeQuickOpenRecentCandidates({ - result, - completeInventory: true, - candidatePaths: ['recent.ts', 'deleted.ts'], - cache: { current: null }, - key: 'full', - context, - options: { rootPath: '/repo' }, - cancelled: () => false - }) - ).toBe(result) - expect(mocks.list).not.toHaveBeenCalled() -}) diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.body-work.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.body-work.test.tsx index d9bea4acc7f..4125ec78161 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.body-work.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.body-work.test.tsx @@ -14,10 +14,7 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { TooltipProvider } from '@/components/ui/tooltip' import FolderWorkspaceSourceControlPanel from './FolderWorkspaceSourceControlPanel' -import { SourceControlPanel } from './source-control/panel/panel' import { ConfirmationDialogContext } from '@/components/confirmation-dialog-context' -import { createRunningCommitMessageGenerationRecord } from '@/store/slices/commit-message-generation' -import type { PullRequestGenerationRecord } from '@/store/slices/hosted-review/generation/pull-request-generation-state' const probes = vi.hoisted(() => ({ status: vi.fn(), @@ -165,31 +162,6 @@ async function scrollTo(ids: readonly string[]): Promise { } }) } -function writeUnrelatedGeneration(): void { - const context = { - worktreeId: 'other::/elsewhere', - worktreePath: '/elsewhere', - requestId: 1, - runtimeTarget: { kind: 'local' as const } - } - const pr: PullRequestGenerationRecord = { - context: { ...context, repoId: 'other', branch: 'other' }, - seed: { base: 'main', title: '', body: '', draft: false }, - seedFieldRevisions: { base: 0, title: 0, body: 0, draft: 0 }, - requiresPushBeforeCreate: false, - status: 'running', - result: null, - error: null, - hydrated: false - } - useAppStore - .getState() - .setCommitMessageGenerationRecord( - context.worktreeId, - createRunningCommitMessageGenerationRecord(context) - ) - useAppStore.getState().setPullRequestGenerationRecord(context.worktreeId, pr) -} const BODY_OPERATIONS = ['compare', 'history', 'submodules', 'upstream', 'eligibility'] as const let root: Root @@ -318,224 +290,6 @@ async function render(node: ReactNode): Promise { }) } -function bodyWork(worktrees: readonly Worktree[]) { - const idByPath = new Map(worktrees.map((row) => [row.path, row.id])) - return Object.fromEntries( - BODY_OPERATIONS.map((operation) => { - const ids = probes[operation].mock.calls.map( - ([key]: unknown[]) => idByPath.get(String(key)) ?? String(key) - ) - return [operation, { calls: ids.length, worktrees: [...new Set(ids)].sort() }] - }) - ) -} - -it('runs expanded body work only for on-screen sections, at mount and over the safety interval', async () => { - const counts: unknown[] = [] - for (const count of [3, 30]) { - useAppStore.setState(useAppStore.getInitialState(), true) - const worktrees = setup(count) - const visible = [worktrees[0]!.id, worktrees[1]!.id] - probes.visible.clear() - for (const id of visible) { - probes.visible.add(id) - } - for (const probe of [probes.upstreamRef, ...BODY_OPERATIONS.map((name) => probes[name])]) { - probe.mockClear() - } - await render() - const atMount = bodyWork(worktrees) - expect(atMount.upstream!.calls).toBeGreaterThan(0) - expect(atMount.compare!.worktrees).toEqual(visible.toSorted()) - expect(atMount.compare!.calls).toBeGreaterThanOrEqual(visible.length) - const intervals: ReturnType[] = [] - for (let tick = 0; tick < 2; tick++) { - for (const operation of BODY_OPERATIONS) { - probes[operation].mockClear() - } - await act(async () => { - await vi.advanceTimersByTimeAsync(30_000) - }) - intervals.push(bodyWork(worktrees)) - } - for (const snapshot of [atMount, ...intervals]) { - for (const work of Object.values(snapshot)) { - expect(work.worktrees.every((id) => visible.includes(id))).toBe(true) - expect(work.calls).toBeLessThanOrEqual(visible.length) - } - } - // Only the selected worktree binds an upstream ref; sections never add resolutions. - expect(probes.upstreamRef).not.toHaveBeenCalled() - counts.push({ atMount, intervals }) - await act(async () => root.render(null)) - } - // Ten times the attached worktrees costs the same when the same sections are on screen. - expect(counts[1]).toEqual(counts[0]) -}) - -it.each([3, 30])( - 'freezes collapsed and off-screen bodies among %i sections on unrelated app writes', - async (count) => { - const worktrees = setup(count) - const [active, collapsed, offScreen] = worktrees - for (const row of worktrees) { - probes.visible.add(row.id) - } - await render() - const collapsedSection = container.querySelector( - `[data-source-control-worktree="${collapsed!.id}"]` - )! - await act(async () => - collapsedSection.querySelector('button[aria-expanded]')!.click() - ) - await scrollTo(worktrees.filter((row) => row !== offScreen).map((row) => row.id)) - const writes = [ - () => - useAppStore.setState((state) => ({ - worktreesByRepo: { - repo: state.worktreesByRepo.repo!.map((row) => - row.id === collapsed!.id ? { ...row, lastActivityAt: 99 } : row - ) - } - })), - () => - useAppStore.setState((state) => ({ - settings: { - ...state.settings!, - theme: state.settings?.theme === 'dark' ? 'light' : 'dark' - } - })), - () => - useAppStore.setState((state) => ({ - activeGroupIdByWorktree: { - ...state.activeGroupIdByWorktree, - 'folder:feature': 'other-group' - }, - activeTabTypeByWorktree: { - ...state.activeTabTypeByWorktree, - 'folder:feature': 'terminal' - } - })), - () => - useAppStore.setState((state) => ({ - groupsByWorktree: { - ...state.groupsByWorktree, - 'folder:feature': [ - { id: 'other-group', worktreeId: 'folder:feature', tabOrder: [], activeTabId: null } - ] - } - })), - () => - useAppStore.setState((state) => ({ - openFiles: [ - ...state.openFiles, - { - id: 'unrelated-file', - worktreeId: 'elsewhere', - filePath: '/elsewhere/other.ts', - relativePath: 'other.ts', - language: 'typescript', - mode: 'edit', - isDirty: false - } - ] - })), - () => - useAppStore.setState((state) => ({ - gitStatusByWorktree: { - ...state.gitStatusByWorktree, - elsewhere: [{ path: 'other.ts', status: 'modified', area: 'unstaged' }] - } - })), - writeUnrelatedGeneration - ] - for (const [writeIndex, write] of writes.entries()) { - probes.bodyRenders.clear() - probes.eligibility.mockClear() - await act(async () => write()) - for (const row of worktrees) { - expect(probes.bodyRenders.get(row.id) ?? 0, `write ${writeIndex}, owner ${row.id}`).toBe(0) - } - expect(probes.eligibility).not.toHaveBeenCalled() - } - probes.bodyRenders.clear() - await act(async () => - useAppStore.setState((state) => ({ - openFiles: [ - ...state.openFiles, - { - id: 'active-file', - worktreeId: active!.id, - filePath: `${active!.path}/a.ts`, - relativePath: 'a.ts', - language: 'typescript', - mode: 'edit', - isDirty: false - } - ], - activeFileIdByWorktree: { - ...state.activeFileIdByWorktree, - 'folder:feature': 'active-file' - }, - activeTabTypeByWorktree: { ...state.activeTabTypeByWorktree, 'folder:feature': 'editor' } - })) - ) - expect( - container - .querySelector( - `[data-source-control-worktree="${active!.id}"] [data-source-control-path="a.ts"]` - ) - ?.getAttribute('data-current') - ).toBe('true') - expect(probes.bodyRenders.get(active!.id)).toBeGreaterThan(0) - for (const row of worktrees.slice(1)) { - expect(probes.bodyRenders.get(row.id) ?? 0).toBe(0) - } - // The frozen views ignore their own status and owner changes, while a relevant live update is visible. - probes.bodyRenders.clear() - await act(async () => - useAppStore.setState((state) => ({ - gitStatusByWorktree: { - ...state.gitStatusByWorktree, - [active!.id]: [ - ...state.gitStatusByWorktree[active!.id]!, - { path: 'visible.ts', status: 'modified', area: 'unstaged' } - ], - [offScreen!.id]: [ - ...state.gitStatusByWorktree[offScreen!.id]!, - { path: 'hidden.ts', status: 'modified', area: 'unstaged' } - ] - } - })) - ) - expect(probes.bodyRenders.get(active!.id)).toBeGreaterThan(0) - expect(probes.bodyRenders.get(collapsed!.id) ?? 0).toBe(0) - expect(probes.bodyRenders.get(offScreen!.id) ?? 0).toBe(0) - expect( - container.querySelector(`[data-source-control-worktree="${active!.id}"]`)!.textContent - ).toContain('visible.ts') - await scrollTo(worktrees.map((row) => row.id)) - expect( - container.querySelector(`[data-source-control-worktree="${offScreen!.id}"]`)!.textContent - ).toContain('hidden.ts') - } -) - -it('keeps the selected workspace panel idle when another worktree writes generation records', async () => { - const [worktree] = setup(1) - useAppStore.setState({ - activeWorktreeId: worktree!.id, - activeWorkspaceKey: `worktree:${worktree!.id}` - }) - await render() - expect(probes.eligibility).toHaveBeenCalled() - probes.eligibility.mockClear() - renders = 0 - await act(async () => writeUnrelatedGeneration()) - expect(probes.eligibility).not.toHaveBeenCalled() - expect(renders).toBe(0) -}) - it.each(['off-screen', 'collapsed'])( 'keeps selection and draft while %s and resumes the current owner snapshot', async (inactive) => { diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.test.tsx index 5d135c6e51a..9e01411ce25 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.test.tsx @@ -158,79 +158,6 @@ async function render(visible = true) { ) } -it.each([3, 30])( - 'bounds observation, watches and unrelated renders for %i attached worktrees', - async (count) => { - const worktrees = setup(count) - await render() - // One status per visible repository is necessary for its badge; collapsed bodies do no compare work. - expect(probes.status).toHaveBeenCalledTimes(count) - expect(probes.body).not.toHaveBeenCalled() - // Every mounted section watches its tree, collapsed ones included (R4). - expect(probes.watches).toEqual(worktrees.map((row) => row.id)) - probes.header.mockClear() - await act(async () => useAppStore.setState({ activeFileId: 'unrelated-file' })) - expect(probes.header).not.toHaveBeenCalled() - await act(async () => - useAppStore.setState({ - sshConnectionStates: new Map([ - [ - 'other-host', - { targetId: 'other-host', status: 'connected', error: null, reconnectAttempt: 0 } - ] - ]) - }) - ) - expect(probes.header).not.toHaveBeenCalled() - await act(async () => - useAppStore.setState((state) => ({ - gitStatusByWorktree: { - ...state.gitStatusByWorktree, - [worktrees[0]!.id]: [{ path: 'a.ts', status: 'modified', area: 'unstaged' }] - } - })) - ) - expect(probes.header.mock.calls).toEqual([['feature-0']]) - expect(probes.body).not.toHaveBeenCalled() - // Agent changes wake the matching collapsed badge, using the existing scheduler's coalescing. - probes.status.mockClear() - await act(async () => { - window.dispatchEvent( - new CustomEvent(ORCA_WORKTREE_FILE_CHANGE_EVENT, { - detail: { - runtimeEnvironmentId: null, - payload: { - worktreePath: worktrees[0]!.path, - events: [{ absolutePath: `${worktrees[0]!.path}/a.ts`, kind: 'change' }] - } - } - }) - ) - await vi.advanceTimersByTimeAsync(4000) - }) - expect(probes.status).toHaveBeenCalledTimes(1) - expect(probes.status.mock.calls[0]?.[0]?.worktreeId).toBe(worktrees[0]!.id) - const toggles = container.querySelectorAll('button[aria-expanded]') - for (const toggle of toggles) { - await act(async () => toggle.click()) - } - expect(probes.body).toHaveBeenCalledTimes(count) - expect(probes.watches).toEqual(worktrees.map((row) => row.id)) - await render(false) - probes.status.mockClear() - await act(async () => { - await vi.advanceTimersByTimeAsync(120000) - }) - expect(probes.status).not.toHaveBeenCalled() - expect(probes.watches).toEqual([]) - } -) - -function ActiveStatusObserver() { - useWorktreeGitStatusObservation(null) - return null -} - function ScopedStatusObserver({ owner }: { owner: SourceControlScope }) { useWorktreeGitStatusObservation(owner, { enabled: true }) return null @@ -272,48 +199,6 @@ it('filters file events by the section runtime host when a same-id local twin ex } }) -it('refreshes an open diff after a file event with the sidebar closed', async () => { - vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mac') - const [worktree] = setup(1) - const path = '/Users/test/Library/Containers/app/worktree' - useAppStore.setState({ - activeWorktreeId: worktree!.id, - activeWorkspaceKey: `worktree:${worktree!.id}`, - worktreesByRepo: { repo: [{ ...worktree!, path }] }, - rightSidebarOpen: false, - openFiles: [ - { - id: 'open-diff', - filePath: `${path}/a.ts`, - relativePath: 'a.ts', - worktreeId: worktree!.id, - language: 'typescript', - isDirty: false, - mode: 'diff' - } - ] - }) - await act(async () => root.render()) - expect(probes.status).toHaveBeenCalledTimes(1) - probes.status.mockClear() - await act(async () => { - window.dispatchEvent( - new CustomEvent(ORCA_WORKTREE_FILE_CHANGE_EVENT, { - detail: { - runtimeEnvironmentId: null, - payload: { - worktreePath: path, - events: [{ absolutePath: `${path}/a.ts`, kind: 'change' }] - } - } - }) - ) - await vi.advanceTimersByTimeAsync(4000) - }) - expect(probes.status).toHaveBeenCalledTimes(1) - expect(probes.status.mock.calls[0]?.[0]?.worktreeId).toBe(worktree!.id) -}) - it('includes a paired-runtime descendant of a local attached worktree as a section', async () => { const [parent] = setup(1) const remote = { diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.watch-coverage.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.watch-coverage.test.tsx index 912671a0147..85c70670791 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.watch-coverage.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspaceSourceControlPanel.watch-coverage.test.tsx @@ -274,68 +274,3 @@ it.each(['explicit', 'repository'] as const)( expect(stream!.unsubscribe).toHaveBeenCalled() } ) - -it('keeps ordinary warm watches, shares roots and releases only section-owned ones', async () => { - const children = Array.from({ length: 30 }, (_, i) => worktreeRow('repo', `/child-${i}`, i)) - const others = [1, 2].map((i) => worktreeRow('other', `/other-${i}`, i)) - const openFile = (worktree: Worktree) => ({ - id: `${worktree.id}::file`, - filePath: `${worktree.path}/file.ts`, - relativePath: 'file.ts', - worktreeId: worktree.id, - language: 'typescript', - isDirty: false, - mode: 'edit' as const - }) - setup({ - children, - others, - openFiles: [...others, children[0]!].map(openFile), - // The three recently used workspaces: two ordinary ones and one that is also a section. - lastVisitedAtByWorktreeId: Object.fromEntries( - [...others, children[0]!].map((row, i) => [row.id, 100 + i]) - ) - }) - await render() - const watched = watchedPaths(watchWorktree.mock.calls) - // Thirty sections never take the recently used workspaces' slots. - for (const row of others) { - expect(watched).toContain(row.path) - } - // The section and an open file in the same worktree share one root. - expect(watched.filter((path) => path === children[0]!.path)).toHaveLength(1) - expect(unwatchWorktree).not.toHaveBeenCalled() - - await render(false) - const released = watchedPaths(unwatchWorktree.mock.calls).sort() - expect(released).toEqual( - children - .slice(1) - .map((row) => row.path) - .sort() - ) -}) - -it.each(['direct', 'nested'] as const)( - 'does not claim a watch for unserved %s SSH', - async (route) => { - const child = worktreeRow('rrepo', '/remote/child', 0, { hostId: 'ssh:nested-ssh' }) - setup({ children: [child] }) - useAppStore.setState({ - repos: useAppStore.getState().repos.map((repo) => - repo.id === 'rrepo' - ? { - ...repo, - connectionId: 'nested-ssh', - executionHostId: route === 'nested' ? 'runtime:env-1' : 'ssh:nested-ssh' - } - : repo - ) - }) - await render() - expect(watchWorktree).not.toHaveBeenCalled() - expect(runtimeStreams).toEqual([]) - // Scheduling is preserved; unsupported SSH status still reaches the original remote rejection boundary. - expect(probes.status.mock.calls.map(([request]) => request.worktreeId)).toEqual([child.id]) - } -) diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.test.tsx index d6c30b80266..eb38ea32e04 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.test.tsx @@ -244,42 +244,4 @@ describe('Checks comment edit/delete ownership', () => { ) expect(comments().map((c) => c.id)).toEqual([9001, 9002]) }) - - it('reports a GitHub failure from the owning server without changing the list', async () => { - mocks.callRuntimeRpc.mockResolvedValue({ - ok: false, - error: { type: 'permission_denied', message: 'Resource not accessible by integration' } - }) - const { hook, comments } = render({ - repo: repoRow({ executionHostId: 'runtime:owner-server' }) - }) - - await act(async () => { - await expect(hook.result.current.handleEditComment(COMMENT, 'new body')).resolves.toBe(false) - }) - - expect(mocks.toastError).toHaveBeenCalledWith('Resource not accessible by integration') - expect(comments().map((c) => c.body)).toEqual(['old body', 'other']) - }) - - it('drops a result that lands after the panel moved to another review', async () => { - let finish: (value: { ok: false; error: { message: string } }) => void = () => {} - mocks.callRuntimeRpc.mockReturnValue(new Promise((resolve) => (finish = resolve))) - const { hook, comments, moveAway } = render({ - repo: repoRow({ executionHostId: 'runtime:owner-server' }) - }) - - let pending: Promise = Promise.resolve(true) - act(() => { - pending = hook.result.current.handleEditComment(COMMENT, 'new body') - }) - moveAway() - await act(async () => { - finish({ ok: false, error: { message: 'stale failure' } }) - await pending - }) - - expect(mocks.toastError).not.toHaveBeenCalled() - expect(comments().map((c) => c.body)).toEqual(['old body', 'other']) - }) }) diff --git a/src/renderer/src/components/right-sidebar/plugin-panels/declarative/use-plugin-declarative-state.test.tsx b/src/renderer/src/components/right-sidebar/plugin-panels/declarative/use-plugin-declarative-state.test.tsx index a44582698b6..541b6a21bb1 100644 --- a/src/renderer/src/components/right-sidebar/plugin-panels/declarative/use-plugin-declarative-state.test.tsx +++ b/src/renderer/src/components/right-sidebar/plugin-panels/declarative/use-plugin-declarative-state.test.tsx @@ -90,14 +90,6 @@ afterEach(() => { }) describe('usePluginDeclarativeState', () => { - it('stores a write made before a workspace switch under the workspace it was made in', async () => { - const panel = await mountReadyPanel() - await panel.switchTo('wt-b') - // The guest flushes the edit it made in A as the switch's hydrate reaches it. - panel.persist({ hydration: panel.first, value: 'typed in A' }) - expect(panel.writes()).toEqual([expect.objectContaining({ workspace: 'wt-a' })]) - }) - it('keeps each write with its own workspace across a quick A, B, A switch', async () => { const panel = await mountReadyPanel() const inB = await panel.switchTo('wt-b') @@ -131,13 +123,4 @@ describe('usePluginDeclarativeState', () => { panel.persist({ hydration: latest + 1, value: 'never sent' }) expect(panel.stored()).toEqual([['wt-b', 'eight back']]) }) - - it('still stores plugin-scope entries tagged with an earlier hydration', async () => { - const panel = await mountReadyPanel() - await panel.switchTo('wt-b') - panel.persist({ hydration: panel.first, scope: 'plugin', value: 'global' }) - expect(panel.writes()).toEqual([ - expect.objectContaining({ entries: [expect.objectContaining({ scope: 'plugin' })] }) - ]) - }) }) diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts index 65959fd532c..267be69db04 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts @@ -53,17 +53,6 @@ describe('showSourceControlEntryFailureToast', () => { storeState.activeWorktreeId = 'wt-1' }) - it('still reports a failure belonging to a worktree the user has left, naming it', () => { - // Suppress stale recovery actions after switches, but still report the original workspace's failure. - storeState.activeWorktreeId = 'wt-2' - show({ worktreeId: 'wt-1', worktreeName: 'feature-a', onRetry: vi.fn() }) - - expect(toastError).toHaveBeenCalledTimes(1) - expect(lastToast().title).toBe('Failed to stage “src/app.ts” in feature-a') - expect(lastToast().options.action).toBeUndefined() - expect(lastToast().options.duration).toBeUndefined() - }) - it('retires a Retry action that became stale after a worktree switch', () => { const onRetry = vi.fn() show({ onRetry }) diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx deleted file mode 100644 index 8557ba1c65b..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// @vitest-environment happy-dom - -import { act, useState, type ReactNode } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, describe, expect, it } from 'vitest' -import { useAppStore } from '@/store' -import { readStoreListenerCount } from '@/store/store-listener-census' -import { useSourceControlStoreActions, type SourceControlStoreActions } from './use-store-actions' - -const originalState = useAppStore.getState() - -let root: Root | null = null -let container: HTMLDivElement | null = null - -function mount(node: ReactNode): void { - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - act(() => root?.render(node)) -} - -function unmount(): void { - if (root) { - act(() => root?.unmount()) - } - root = null - container?.remove() - container = null -} - -function listenerCount(): number { - const count = readStoreListenerCount() - if (count === null) { - throw new Error('store listener census unavailable') - } - return count -} - -afterEach(() => { - unmount() - useAppStore.setState(originalState, true) -}) - -describe('useSourceControlStoreActions store subscriptions', () => { - it('subscribes to no store slices', () => { - const baseline = listenerCount() - - function Probe(): null { - useSourceControlStoreActions() - return null - } - mount() - - // Fixed-identity actions come from getState(); generation records are selected per owner by their consumers. - expect(listenerCount() - baseline).toBe(0) - - unmount() - expect(listenerCount()).toBe(baseline) - }) - - it('returns the same object across an unrelated store write and re-render', () => { - let latest: SourceControlStoreActions | null = null - let rerender: (() => void) | null = null - - function Probe(): null { - const [, setTick] = useState(0) - rerender = () => setTick((t) => t + 1) - latest = useSourceControlStoreActions() - return null - } - mount() - - const first = latest - expect(first).not.toBeNull() - - act(() => { - useAppStore.setState({ - rightSidebarOpen: !originalState.rightSidebarOpen - }) - }) - act(() => rerender?.()) - - expect(latest).toBe(first) - }) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.placement.test.tsx b/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.placement.test.tsx deleted file mode 100644 index 65a570abfba..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.placement.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// @vitest-environment happy-dom - -import { cleanup, renderHook } from '@testing-library/react' -import type { ReactNode } from 'react' -import { afterEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import type { AppState } from '@/store/types' -import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures' -import { SourceControlScopeProvider } from '../panel/worktree-scope' -import { useSourceControlNoteOpening } from './use-note-opening' - -const initialAppState = useAppStore.getInitialState() -afterEach(() => { - cleanup() - useAppStore.setState(initialAppState, true) -}) - -it('opens a folder section note in the folder group while the child keeps the content', () => { - const openFile = vi.fn(() => 'file') - useAppStore.setState({ - openFile, - activeWorktreeId: 'folder:feature', - activeGroupIdByWorktree: { 'folder:feature': 'folder-group' } - }) - const worktree = makeWorktree('wt-child', 'wt-child', { instanceId: 'instance-1' }) - const owner = { - worktreeId: 'wt-child', - repoId: 'repo-1', - executionHostId: 'ssh:box' as const, - worktree, - attachedAt: 1, - repo: { ...makeRepo(), connectionId: 'box' }, - placementWorkspaceId: 'folder:feature' - } - const { result } = renderHook( - () => - useSourceControlNoteOpening({ - activeWorktreeId: 'wt-child', - worktreePath: '/tmp/wt-child', - entries: [], - branchEntries: [], - branchSummary: null, - handleOpenDiff: vi.fn(), - openCommittedDiff: vi.fn(), - sourceControlRef: { current: null } - }), - { - wrapper: ({ children }: { children: ReactNode }) => ( - {children} - ) - } - ) - - result.current.handleOpenComment({ - id: 'note-1', - worktreeId: 'wt-child', - filePath: 'README.md', - source: 'markdown', - lineNumber: 3, - body: 'check', - createdAt: 1, - side: 'modified' - }) - - expect(openFile).toHaveBeenCalledWith( - expect.objectContaining({ worktreeId: 'wt-child', filePath: '/tmp/wt-child/README.md' }), - { - placementWorkspaceId: 'folder:feature', - executionHostId: 'ssh:box', - targetGroupId: 'folder-group' - } - ) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control/panel/worktree-operation-state.test.tsx b/src/renderer/src/components/right-sidebar/source-control/panel/worktree-operation-state.test.tsx index 43cf8fc6762..73c65c22998 100644 --- a/src/renderer/src/components/right-sidebar/source-control/panel/worktree-operation-state.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/panel/worktree-operation-state.test.tsx @@ -5,9 +5,6 @@ import { act, cleanup, renderHook } from '@testing-library/react' import type { ReactNode } from 'react' import { afterEach, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' -import type { AppState } from '@/store/types' -import { useSourceControlHostedReviewCreated } from '../review/use-hosted-review-created' -import { useSourceControlStoreActions } from '../listing/use-store-actions' import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures' import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { useSourceControlCommitAction } from '../commit/use-commit-action' @@ -207,95 +204,3 @@ it('retires only the removed host owner on authoritative purge and fences late d .purgeWorktreeTerminalState([hostWorktreeTarget({ id: 'wt-removed', hostId: 'local' })]) expect(getWorktreeOperationStore('wt-removed', 'local', 'instance-1').getState().draft).toBe('') }) - -it('writes the base ref and opens aggregate diffs on the section host while the folder stays active', async () => { - const updateRepo = vi.fn() - const openAllDiffs = vi.fn() - useAppStore.setState({ - activeWorktreeId: 'folder:feature', - activeWorkspaceExecutionHostId: 'local', - updateRepo, - openAllDiffs - }) - const section = renderHook(() => useSourceControlStoreActions(), { - wrapper: sectionWrapper('wt-twin', 'ssh:box') - }) - await section.result.current.updateRepo('repo-1', { worktreeBaseRef: 'release' }) - expect(updateRepo).toHaveBeenCalledWith( - 'repo-1', - { worktreeBaseRef: 'release' }, - { hostId: 'ssh:box' } - ) - section.result.current.openAllDiffs('wt-twin', '/remote/repo') - expect(openAllDiffs).toHaveBeenCalledWith( - 'wt-twin', - '/remote/repo', - undefined, - undefined, - undefined, - { - placementWorkspaceId: 'folder:feature', - runtimeEnvironmentId: null, - executionHostId: 'ssh:box' - } - ) - expect(useAppStore.getState().activeWorktreeId).toBe('folder:feature') -}) - -it('opens a section file here and its checks in the folder overview without changing workspace', () => { - const openFile = vi.fn(() => 'file') - const revealInExplorer = vi.fn() - const setRightSidebarOpen = vi.fn() - const setRightSidebarTab = vi.fn() - useAppStore.setState({ - activeWorktreeId: 'folder:feature', - activeGroupIdByWorktree: { 'folder:feature': 'folder-group' }, - openFile, - revealInExplorer, - setRightSidebarOpen, - setRightSidebarTab - }) - const section = renderHook( - () => { - const actions = useSourceControlStoreActions() - const checks = useSourceControlHostedReviewCreated({ - activeRepo: null, - activeWorktreeId: 'wt-child', - branchName: 'feature', - fallbackGitHubPRNumber: null, - linkedAzureDevOpsPR: null, - linkedBitbucketPR: null, - linkedGitHubPR: 123, - linkedGitLabMR: null, - linkedGiteaPR: null, - fetchHostedReviewForBranch: actions.fetchHostedReviewForBranch, - fetchPRForBranch: actions.fetchPRForBranch, - refreshActiveGitStatusAfterMutation: async () => {}, - setRightSidebarOpen: actions.setRightSidebarOpen, - setRightSidebarTab: actions.setRightSidebarTab, - updateWorktreeMeta: actions.updateWorktreeMeta - }) - return { actions, checks } - }, - { wrapper: sectionWrapper('wt-child', 'ssh:box') } - ) - section.result.current.actions.revealInExplorer('wt-child', '/remote/repo/a.ts') - expect(openFile).toHaveBeenCalledWith( - expect.objectContaining({ - filePath: '/remote/repo/a.ts', - worktreeId: 'wt-child', - mode: 'edit' - }), - { - placementWorkspaceId: 'folder:feature', - executionHostId: 'ssh:box', - runtimeEnvironmentId: null, - targetGroupId: 'folder-group', - preview: false - } - ) - expect(revealInExplorer).not.toHaveBeenCalled() - section.result.current.checks.openHostedReviewInChecks() - expect(setRightSidebarTab).toHaveBeenCalledWith('pr-checks') - expect(useAppStore.getState().activeWorktreeId).toBe('folder:feature') -}) diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-wsl-wake.test.tsx b/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-wsl-wake.test.tsx deleted file mode 100644 index 83aea1b5bbd..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-wsl-wake.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -// @vitest-environment happy-dom - -import { act, cleanup, renderHook } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - noteUserWorkspaceSwitch, - resetWorkspaceSwitchUserActsForTests -} from '@/lib/workspace-switch-user-act' - -const getRuntimeGitHistoryMock = vi.hoisted(() => vi.fn()) -vi.mock('@/runtime/source-control/git/runtime-git-status-client', () => ({ - getRuntimeGitHistory: getRuntimeGitHistoryMock -})) -vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null })) - -import { useSourceControlGitHistory } from './use-git-history' - -type Props = Parameters[0] - -const props = (overrides: Partial = {}): Props => ({ - activeRepoTarget: { kind: 'local' }, - activeWorktreeId: 'wt-1', - worktreePath: '\\\\wsl.localhost\\Ubuntu\\repo', - branchName: 'main', - compareBaseRef: 'main', - isFolder: false, - isBranchVisible: true, - isGitHistoryExpanded: true, - isGitHistoryVisible: true, - worktreeMap: new Map([ - ['wt-1', {}], - ['wt-2', {}] - ]), - ...overrides -}) - -const wakes = (): unknown[] => - getRuntimeGitHistoryMock.mock.calls.map((call) => call[1].wakeWslDistro) - -beforeEach(() => { - getRuntimeGitHistoryMock.mockReset().mockResolvedValue({ items: [] }) - resetWorkspaceSwitchUserActsForTests() -}) -afterEach(cleanup) - -// E9: only the user's own act may start a stopped WSL distro for a Commits read. -describe('Commits history and a stopped WSL distro', () => { - it('stays background on a mount or remount with Commits already open', async () => { - const first = renderHook(() => useSourceControlGitHistory(props())) - await act(async () => {}) - first.unmount() - renderHook(() => useSourceControlGitHistory(props())) - await act(async () => {}) - expect(wakes()).toEqual([undefined, undefined]) - }) - - it('wakes once for the user switching to the workspace, never again on a remount', async () => { - noteUserWorkspaceSwitch('wt-1') - const first = renderHook(() => useSourceControlGitHistory(props())) - await act(async () => {}) - first.unmount() - renderHook(() => useSourceControlGitHistory(props())) - await act(async () => {}) - expect(wakes()).toEqual([true, undefined]) - }) - - it('wakes when the user expands Commits, not when Orca re-shows it or a refresh follows', async () => { - const { result, rerender } = renderHook((p: Props) => useSourceControlGitHistory(p), { - initialProps: props({ isGitHistoryExpanded: false }) - }) - await act(async () => {}) - rerender(props()) - await act(async () => {}) - // Hidden and shown again by Orca (a sidebar re-show), with no switch. - rerender(props({ isGitHistoryVisible: false })) - rerender(props()) - await act(async () => {}) - await act(async () => result.current.refreshGitHistoryRef.current()) - expect(wakes()).toEqual([true, undefined, undefined]) - }) - - it("does not take another worktree's restored expanded view as a click", async () => { - const { rerender } = renderHook((p: Props) => useSourceControlGitHistory(p), { - initialProps: props({ isGitHistoryExpanded: false }) - }) - await act(async () => {}) - rerender(props({ activeWorktreeId: 'wt-2' })) - await act(async () => {}) - expect(wakes()).toEqual([undefined]) - }) - - it("keeps the Refresh button's explicit wake", async () => { - const { result } = renderHook(() => useSourceControlGitHistory(props())) - await act(async () => {}) - await act(async () => result.current.refreshGitHistory({ wakeWslDistro: true })) - expect(wakes()).toEqual([undefined, true]) - }) -}) diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx index 5e2d5d5e262..4c2597a28b5 100644 --- a/src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx +++ b/src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx @@ -2,7 +2,6 @@ import { cleanup, render } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import CommentMarkdown from './CommentMarkdown' -import type { DocumentCodeBlockRenderer } from './comment-markdown-element-renderers' const mocks = vi.hoisted(() => ({ mermaid: vi.fn(() => { @@ -18,88 +17,7 @@ afterEach(() => { mocks.mermaid.mockClear() }) -/** Ignore inter-block line breaks because they have no rendered layout. */ -function blocksOf(container: HTMLElement): string[] { - return Array.from(container.firstElementChild?.children ?? [], (block) => block.outerHTML) -} - -const REPLY = [ - 'First paragraph with **bold** and a [link](https://example.com).', - '', - '```ts', - 'const a = 1', - '```', - '', - '- one', - '- two', - '', - '| a | b |', - '| - | - |', - '| 1 | 2 |', - '', - 'Last paragraph.' -].join('\n') - describe('growing comment markdown', () => { - it('renders a streamed reply to the same document as the finished one', () => { - const whole = render() - const expected = blocksOf(whole.container) - whole.unmount() - - const streamed = render() - for (let end = 5; end < REPLY.length + 5; end += 5) { - streamed.rerender( - - ) - } - streamed.rerender() - expect(blocksOf(streamed.container)).toEqual(expected) - }) - - it('does not render a finished block again as later text arrives', () => { - const renderCodeBlock = vi.fn(({ children }) => ( -
{children}
- )) - const fence = '```ts\nconst a = 1\n```\n\n' - const view = render( - - ) - const afterFence = renderCodeBlock.mock.calls.length - expect(afterFence).toBeGreaterThan(0) - - let content = `${fence}Next` - for (const more of [' paragraph', ' keeps', ' growing.\n\nAnd another.']) { - content += more - view.rerender( - - ) - } - expect(renderCodeBlock.mock.calls.length).toBe(afterFence) - }) - - it('does not remount earlier text when a kept tag closes', () => { - const props = { variant: 'document', unknownHtmlAsText: true, growing: true } as const - const view = render(Ctrl'} {...props} />) - const press = view.container.querySelectorAll('p')[1] - for (const content of [ - 'Intro.\n\nPress Ctrl to copy.', - 'Intro.\n\nPress Ctrl to copy.\n\nTail.' - ]) { - view.rerender() - expect(view.container.querySelectorAll('p')[1]).toBe(press) - } - }) - it('links a path in an earlier block once the host confirms it', () => { const content = 'Edited src/a.ts.\n\nThen more text' const view = render( diff --git a/src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx b/src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx deleted file mode 100644 index 8e44b49fbbb..00000000000 --- a/src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// @vitest-environment happy-dom -import { act, cleanup, render } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { resetSystemPrefersDarkSubscriptionForTests } from '@/hooks/theme/use-system-prefers-dark' - -const mermaidProps: { current: { isDark: boolean } | null } = vi.hoisted(() => ({ current: null })) - -vi.mock('@/components/editor/MermaidBlock', () => ({ - default: (props: { isDark: boolean }) => { - mermaidProps.current = props - return null - } -})) -vi.mock('@/store', () => ({ - useAppStore: (selector: (state: Record) => unknown) => - selector({ settings: { theme: 'system' } }) -})) - -import CommentMermaidBlock from './CommentMermaidBlock' - -const originalMatchMedia = window.matchMedia - -afterEach(() => { - cleanup() - mermaidProps.current = null - resetSystemPrefersDarkSubscriptionForTests() - window.matchMedia = originalMatchMedia -}) - -describe('CommentMermaidBlock system theme', () => { - it('follows a system color-scheme change while mounted', () => { - let matches = false - const listeners = new Set() - const media = { - get matches() { - return matches - }, - addEventListener: (_type: string, listener: EventListener) => listeners.add(listener), - removeEventListener: (_type: string, listener: EventListener) => listeners.delete(listener) - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook only reads `matches` and (un)subscribes to `change`. - window.matchMedia = (() => media) as unknown as typeof window.matchMedia - - render() - expect(mermaidProps.current?.isDark).toBe(false) - - act(() => { - matches = true - for (const listener of listeners) { - listener(new MediaQueryListEvent('change', { matches: true })) - } - }) - expect(mermaidProps.current?.isDark).toBe(true) - }) -}) diff --git a/src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts b/src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts deleted file mode 100644 index 54b55a56e1b..00000000000 --- a/src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { htmlRendersAsText } from './comment-markdown-unknown-html-as-text' -import { splitMarkdownTopLevelBlocks, type MarkdownBlockSplit } from './markdown-top-level-blocks' - -const REPLY = [ - 'Here is the plan.', - '', - '## Steps', - '', - '- first item', - '- second item', - '', - ' continued inside the list', - '', - '1. one', - '2. two', - '', - 'A loose list follows.', - '', - '1. first', - '', - '2. second', - '', - ' with a paragraph of its own', - '', - '3. third', - '', - '> a quote', - 'that continues lazily', - '', - '```ts', - 'const a = 1', - '', - '## not a heading', - '```', - '', - 'Intro to the table', - '| a | b |', - '| - | - |', - '| 1 | 2 |', - '', - 'Setext title', - '===', - '', - 'Closing words with `code` and **bold**.', - '' -].join('\n') - -function streamed(source: string, step: number): string[] { - let split: MarkdownBlockSplit | null = null - for (let end = step; end < source.length + step; end += step) { - split = splitMarkdownTopLevelBlocks(source.slice(0, end), split) - } - return (split?.blocks ?? []).map((block) => block.text) -} - -function cut(source: string, previous: MarkdownBlockSplit | null = null): string[] { - return splitMarkdownTopLevelBlocks(source, previous).blocks.map((block) => block.text) -} - -describe('splitMarkdownTopLevelBlocks', () => { - it.each([1, 3, 7])('reaches the same cut when streamed %i characters at a time', (step) => { - expect(streamed(REPLY, step)).toEqual(cut(REPLY)) - }) - - it.each([ - ['a link definition', 'See [the docs][docs].\n\nMore.\n\n[docs]: https://example.com\n'], - ['a footnote', 'A claim.[^1]\n\nMore.\n\n[^1]: The source.\n'], - ['a nested definition', 'See [the docs][docs].\n\n> [docs]: https://example.com\n'], - ['raw HTML', 'Before.\n\n
\n\nhidden **text**\n\n
\n'], - ['raw HTML inside a quote', 'Before.\n\n>
cell\n\nAfter.\n'], - ['an inline tag left open', 'Before low.\n\nAfter.\n'] - ])('leaves a document with %s whole, however it arrived', (_name, source) => { - expect(cut(source)).toEqual([source]) - expect(streamed(source, 2)).toEqual([source]) - }) - - describe('when unknown HTML renders as text', () => { - const rendersAsText = (html: string): boolean => - htmlRendersAsText(html, new Set(['b', 'sub', 'details'])) - function streamedAsText(source: string): string[] { - let split: MarkdownBlockSplit | null = null - for (let end = 2; end < source.length + 2; end += 2) { - split = splitMarkdownTopLevelBlocks( - source.slice(0, end), - split, - true, - undefined, - undefined, - rendersAsText - ) - } - return (split?.blocks ?? []).map((block) => block.text) - } - - it.each([ - ['a type parameter', 'A Vec of Promise.\n\nAfter.\n', 2], - ['an unknown tag block', '\n\nInside.\n\n\n', 3] - ])('keeps cutting a document with %s', (_name, source, blocks) => { - expect(streamedAsText(source)).toHaveLength(blocks) - expect(streamedAsText(source).join('')).toBe(source) - }) - - it.each([ - ['a kept tag', 'Before low.\n\nAfter.\n'], - ['a kept tag beside an unknown one', 'A bold.\n\nAfter.\n'], - [ - 'bold the HTML parser carries out of a table', - '
one
\n\nAfter.\n' - ], - ['a comment', 'oops -->\n\nAfter.\n'], - ['a malformed kept tag', '
\n\nAfter **x**\n'] - ])('leaves a document with %s whole', (_name, source) => { - expect(streamedAsText(source)).toEqual([source]) - }) - - it('leaves raw unknown HTML whole when it does not render as text', () => { - expect(cut('