diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 9d67fa43b04..1275feb2feb 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -888,7 +888,6 @@ 1 test-timeout-needs-reason:src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts 4 test-timeout-needs-reason:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts 1 test-timeout-needs-reason:src/main/git/worktrees/removal/worktree-removal-real-git.test.ts -1 test-timeout-needs-reason:src/main/hermes/hook-service.test.ts 1 test-timeout-needs-reason:src/main/ipc/filesystem/filesystem-quick-open-options.integration.test.ts 1 test-timeout-needs-reason:src/main/ipc/preflight-runnable-local-cli.test.ts 1 test-timeout-needs-reason:src/main/ipc/worktrees/git/worktree-git-status-attribution-real-git.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 3591b8f57f4..58854098508 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -11780,14 +11780,14 @@ "providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local", "daemon", "ssh"], - "coverageNotes": "Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1623 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: A deterministic isolated runtime/SQLite/RPC harness covers Run and Dispatch ownership, exact handle and remint-stable pane recipients, fixed Delivery replay and acknowledgment, restart, concurrent pollers, filtered waiters, and built-CLI parity with direct DB state. Schema v28 durably records every coordinator handle through SQLite triggers, backfills current handles on open, recreates routing triggers after old table rebuilds, and atomically canonicalizes coordinator-addressed inserts to run:. A 125-row oracle and 151-row reconciliation suite cover paged old-handle backlogs, commit-without-notify recovery, duplicate Dispatch owners, cancellation, and lifecycle fencing. Federation tests use real databases, dropped acknowledgments, restart/reconnect, and protocol v1/v2 peers; duplicate read rows do not wake again. Daemon and SSH pointer settlement is deterministic, with SSH bounded at 60 seconds beyond the 20-second mux health window. No RPC parameter, stream opcode, or published payload changed. Live headed/headless paired-runtime, physical SSH, WSL, Linux, and Windows acceptance remain gaps. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/runtime/orchestration-mailbox-cold-park-idle.spec.ts removed. Loses fresh-output cold-park quiet continuation, same-incarnation deferred Enter, late pointer-write settlement and explicit-check batch-claim alarms, including durable delivered_at state checks mixed into registrations0/3. No equivalent coverage claim. src/main/runtime/orchestration-dispatch-mailbox-delivery.spec.ts reduced. Loses dispatch wake dedup, restart unread guidance, legacy ack behavior and ambiguous pointer write/Enter crash retention alarms. DB-only stable delivery identity does not replace those paths. src/main/runtime/orchestration-mailbox-delivery.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", + "coverageNotes": "Audit1734 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1623 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: A deterministic isolated runtime/SQLite/RPC harness covers Run and Dispatch ownership, exact handle and remint-stable pane recipients, fixed Delivery replay and acknowledgment, restart, concurrent pollers, filtered waiters, and built-CLI parity with direct DB state. Schema v28 durably records every coordinator handle through SQLite triggers, backfills current handles on open, recreates routing triggers after old table rebuilds, and atomically canonicalizes coordinator-addressed inserts to run:. A 125-row oracle and 151-row reconciliation suite cover paged old-handle backlogs, commit-without-notify recovery, duplicate Dispatch owners, cancellation, and lifecycle fencing. Federation tests use real databases, dropped acknowledgments, restart/reconnect, and protocol v1/v2 peers; duplicate read rows do not wake again. Daemon and SSH pointer settlement is deterministic, with SSH bounded at 60 seconds beyond the 20-second mux health window. No RPC parameter, stream opcode, or published payload changed. Live headed/headless paired-runtime, physical SSH, WSL, Linux, and Windows acceptance remain gaps. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/runtime/orchestration-mailbox-cold-park-idle.spec.ts removed. Loses fresh-output cold-park quiet continuation, same-incarnation deferred Enter, late pointer-write settlement and explicit-check batch-claim alarms, including durable delivered_at state checks mixed into registrations0/3. No equivalent coverage claim. src/main/runtime/orchestration-dispatch-mailbox-delivery.spec.ts reduced. Loses dispatch wake dedup, restart unread guidance, legacy ack behavior and ambiguous pointer write/Enter crash retention alarms. DB-only stable delivery identity does not replace those paths. src/main/runtime/orchestration-mailbox-delivery.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-4325", "https://github.com/stablyai/orca/issues/14522", "internal incident evidence: improve-vps-setup, 2026-08-10" ], "invariant": "Each message has one stable row ID and authoritative recipient; coordinator-addressed current-delivery inserts are atomically owned by run:. Pointer staging may set delivered_at but never consumes mail. Each Run consumer generation has at most one outstanding Delivery with a fixed ID and fixed message IDs; ordinary checks replay it until an explicit matching acknowledgment marks exactly those rows read. Rebinding fences the old generation, notification types/counts correspond to unread rows retrievable under the same authority, and federation replay imports each stable message identity once without re-waking an already-read duplicate.", - "oracle": "Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Seed status, dispatch, and worker_done rows across direct-handle and canonical Run recipients in an isolated DB. Compare pointer count, RPC and built-CLI check output, direct SQLite rows, unread/peek/all/type filters, concurrent pollers, fixed Delivery IDs, explicit acknowledgment, restart, filtered check --wait, and coordinator remint. Route a 125-row old-handle backlog, inject a commit without notification, and require startup repair. Exercise duplicate Run/Dispatch owners, stale panes, 50-row pages, cancellation, lifecycle fencing, and absent PTYs. Drop a federation ACK, reconnect/restart v1/v2 peers, and require stable import plus no duplicate read-row wake. Hold a healthy SSH write past five seconds but below the 60-second settlement deadline, then distinguish the three settlement outcomes end to end: only a proven refusal releases the reservation and drains a delivery parked behind the watermark; a dropped in-flight settlement must surface as unverifiable with bytes handed to the transport, preserve the durable write-attempted reservation, and emit no duplicate pointer after restart; a settled write that throws mid-pointer is unverifiable, not a refusal; and an Enter whose settlement is lost stays at enter-attempted so restart emits no second Enter. Install the production PTY controller and verify that it routes settled writes through the owning provider and refuses before any byte when the routed provider cannot settle. Census every production PTY provider class and reject a settlement synthesized from the fire-and-forget write.", + "oracle": "Audit1734 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Seed status, dispatch, and worker_done rows across direct-handle and canonical Run recipients in an isolated DB. Compare pointer count, RPC and built-CLI check output, direct SQLite rows, unread/peek/all/type filters, concurrent pollers, fixed Delivery IDs, explicit acknowledgment, restart, filtered check --wait, and coordinator remint. Route a 125-row old-handle backlog, inject a commit without notification, and require startup repair. Exercise duplicate Run/Dispatch owners, stale panes, 50-row pages, cancellation, lifecycle fencing, and absent PTYs. Drop a federation ACK, reconnect/restart v1/v2 peers, and require stable import plus no duplicate read-row wake. Hold a healthy SSH write past five seconds but below the 60-second settlement deadline, then distinguish the three settlement outcomes end to end: only a proven refusal releases the reservation and drains a delivery parked behind the watermark; a dropped in-flight settlement must surface as unverifiable with bytes handed to the transport, preserve the durable write-attempted reservation, and emit no duplicate pointer after restart; a settled write that throws mid-pointer is unverifiable, not a refusal; and an Enter whose settlement is lost stays at enter-attempted so restart emits no second Enter. Install the production PTY controller and verify that it routes settled writes through the owning provider and refuses before any byte when the routed provider cannot settle. Census every production PTY provider class and reject a settlement synthesized from the fire-and-forget write.", "commands": [ "pnpm run build:cli && pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-message-delivery-identity.test.ts --reporter=dot --testTimeout=5000", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orchestration-mailbox-routing.test.ts src/main/runtime/orchestration-mailbox-notification-consistency.test.ts src/main/runtime/orchestration-mailbox-delivery.test.ts src/main/ipc/pty/pty-controller-ownership-routing.test.ts src/main/runtime/orchestration/db/schema/run-coordinator-handle-migration.test.ts src/main/runtime/orchestration/db/runs/orchestration-run-delivery-db.test.ts src/main/runtime/orchestration/db/worker-dispatch/orchestration-worker-dispatch-db.test.ts src/main/runtime/orchestration/mailbox/formatting/message-formatting.test.ts src/main/runtime/orchestration/mailbox/terminal-pointer/mailbox-pointer-stage.test.ts src/main/daemon/transport/client/testing/client.test.ts src/main/daemon/providers/routing/testing/daemon-pty-router.test.ts src/main/daemon/providers/routing/testing/degraded-daemon-pty-provider.test.ts", @@ -11897,13 +11897,6 @@ "file": "src/main/runtime/orchestration/mailbox/terminal-pointer/mailbox-pointer-stage.test.ts", "assertions": ["a refused pointer write drains a delivery parked behind its watermark"] }, - { - "file": "src/main/daemon/transport/client/testing/client.test.ts", - "assertions": [ - "an asynchronous daemon socket write failure settles as unverifiable, never as a proven refusal", - "a wedged daemon socket write disconnects at its bounded settlement deadline" - ] - }, { "file": "src/main/runtime/orchestration/db/worker-dispatch/orchestration-worker-dispatch-db.test.ts", "assertions": [ @@ -11966,6 +11959,7 @@ "Accumulate stable soak runs before considering blocking promotion." ], "knownGaps": [ + "Audit1734 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded.", "Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded.", "Audit1623 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded.", "No headed Electron restart journey was required to prove the runtime/PTY/check boundary.", diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 9e97ec7dacf..00bd6577341 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -1945,7 +1945,6 @@ "src/main/emulator/serve-sim-helper-processes.test.ts": 59, "src/main/emulator/serve-sim-kill-contract.test.ts": 1284, "src/main/emulator/serve-sim-runtime-materializer.test.ts": 178, - "src/main/emulator/simctl-simulator-devices.test.ts": 59, "src/main/ephemeral-vm/recipe-runner.test.ts": 460, "src/main/ephemeral-vm/runtime-service.test.ts": 870, "src/main/ephemeral-vm/runtime-ssh-cleanup.test.ts": 188, @@ -2226,7 +2225,6 @@ "src/main/gitlab/client-mr-listing.test.ts": 558, "src/main/gitlab/client-mr-review-actions.test.ts": 482, "src/main/gitlab/client-work-items.test.ts": 492, - "src/main/gitlab/client.test.ts": 534, "src/main/gitlab/gitlab-known-host-backoff.test.ts": 477, "src/main/gitlab/gitlab-known-host-probe.test.ts": 389, "src/main/gitlab/gitlab-known-host-retirement.test.ts": 509, @@ -2529,7 +2527,6 @@ "src/main/ipc/skill-install-management-ipc-handlers.test.ts": 193, "src/main/ipc/skill-install-progress-ipc.test.ts": 48, "src/main/ipc/source-control/source-control-ai-linked-issue.test.ts": 60, - "src/main/ipc/speech.test.ts": 60, "src/main/ipc/ssh/ssh-app-shutdown.test.ts": 4455, "src/main/ipc/ssh/ssh-connect-decision-cancellation.test.ts": 6414, "src/main/ipc/ssh/ssh-credential-cancel.test.ts": 4604, @@ -6293,7 +6290,6 @@ "src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 692, "src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx": 3019, "src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 3399, - "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 2576, "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2702, "src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx": 2736, "src/renderer/src/components/native-chat/NativeChatPromptEditor.paragraph-selection.test.tsx": 652, @@ -6324,7 +6320,6 @@ "src/renderer/src/components/native-chat/NativeChatStructuredSession.wsl-host-stopped.test.tsx": 3657, "src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 583, "src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 544, - "src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 2047, "src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 3544, "src/renderer/src/components/native-chat/NativeChatToolOutput.test.tsx": 410, "src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 3128, @@ -6364,7 +6359,6 @@ "src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 6688, "src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1409, "src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1326, - "src/renderer/src/components/native-chat/native-chat-composer-mobile-hold.test.tsx": 899, "src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1724, "src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 526, "src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 1166, @@ -9144,10 +9138,8 @@ "src/shared/ndjson-retained-chunk-memory.test.ts": 327, "src/shared/nested-repo-telemetry-schema.test.ts": 196, "src/shared/nested-repo-telemetry.test.ts": 55, - "src/shared/nested-worker-depth.test.ts": 45, "src/shared/network-proxy.test.ts": 44, "src/shared/network/manual-address.test.ts": 68, - "src/shared/network/server-share-address.test.ts": 50, "src/shared/new-workspace-dialog-repo.test.ts": 50, "src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 64, "src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 67, diff --git a/src/main/antigravity/antigravity-host-acp.test.ts b/src/main/antigravity/antigravity-host-acp.test.ts index 3372e454510..3a53cd4e659 100644 --- a/src/main/antigravity/antigravity-host-acp.test.ts +++ b/src/main/antigravity/antigravity-host-acp.test.ts @@ -4,7 +4,6 @@ import { join } from 'node:path' import { afterEach, expect, it, vi } from 'vitest' import { selectAcpHostAccountHome } from '../acp/acp-launch-command' import type { AcpHostBackendSettings } from '../acp/acp-launch-specs' -import { resolveAcpLaunchInvocation } from '../acp/acp-structured-launch-resolution' import { ANTIGRAVITY_LAUNCH_SPEC } from './antigravity-acp-launch-spec' // agy's own sign-in, read from the host's keyring or token file. @@ -127,26 +126,3 @@ it.skipIf(process.platform === 'win32')( ).resolves.toBeNull() } ) - -it('a chat launches on the backend its account home pins', async () => { - const resolve = (path: string) => - resolveAcpLaunchInvocation( - ANTIGRAVITY_LAUNCH_SPEC, - { variable: 'GEMINI_HOME', path }, - { - resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }), - resolveCommand: (command) => `/resolved/${command}`, - dataDirectory: '/orca-data' - } - ) - const host = await resolve('/home/user/.gemini') - expect(host.command).toBe('/resolved/agy') - expect(host.spec.args({ fullAccess: false, pluginDir: null })).toEqual(['acp']) - expect(host.env.GEMINI_HOME).toBeUndefined() - expect(host.env.AGY_ACP_FORCE_FILE_STORAGE).toBeUndefined() - - const managed = await resolve('/orca-data/antigravity-chat') - expect(managed.spec.managedRuntime).toBeDefined() - expect(managed.env.GEMINI_HOME).toBe('/orca-data/antigravity-chat') - expect(managed.env.AGY_ACP_FORCE_FILE_STORAGE).toBe('1') -}) diff --git a/src/main/automations/automation-terminal-launch.test.ts b/src/main/automations/automation-terminal-launch.test.ts index 6984d784fdc..4ec24f834ee 100644 --- a/src/main/automations/automation-terminal-launch.test.ts +++ b/src/main/automations/automation-terminal-launch.test.ts @@ -46,30 +46,6 @@ function launch(runtime: ReturnType, reuseSession = false) { } describe('headless automation agent launch', () => { - it.each([ - ['is refused', async () => false, "Aider started, but Orca could not send this run's prompt."], - [ - 'throws', - async () => { - throw new Error('writer broke') - }, - 'writer broke' - ] - ])( - 'opens the terminal and reports the prompt undelivered when delivery %s', - async (_, deliver, error) => { - const opened = await launch(fakeRuntime(deliver)) - - expect(opened).toMatchObject({ - terminalHandle: 'term-1', - terminalSessionId: 'tab-1', - terminalPaneKey: 'tab-1:leaf-1', - terminalPtyId: 'pty-1' - }) - await expect(opened.delivery).resolves.toEqual({ error }) - } - ) - it.each([ ['resolves on a paired server', { hostId: 'runtime:env-1' }, null], ['is gone', null, new Error('selector_not_found')] diff --git a/src/main/automations/automation-workspace-create.test.ts b/src/main/automations/automation-workspace-create.test.ts index 3e85f409fc8..351eb671a7c 100644 --- a/src/main/automations/automation-workspace-create.test.ts +++ b/src/main/automations/automation-workspace-create.test.ts @@ -68,44 +68,6 @@ const automation: Automation = { } describe('headless automation workspace create args', () => { - it('stamps automation provenance for serve-mode new-per-run workspaces', () => { - const args = buildAutomationWorktreeCreateArgs({ - automation, - run: { - id: 'run-1', - title: 'Nightly review run', - scheduledFor: Date.UTC(2026, 0, 2, 3, 4, 5) - }, - repo, - createdAt: 123 - }) - - expect(args).toMatchObject({ - repoSelector: { selector: 'id:repo-1', hostId: 'ssh:ssh-target-1' }, - name: 'auto-nightly-review-run-20260102T0304', - baseBranch: 'origin/main', - setupDecision: 'skip', - activate: false, - createdWithAgent: 'codex', - startupAgent: 'codex', - startupPrompt: 'Review changes', - telemetrySource: 'unknown', - automationProvenance: { - kind: 'created-by-automation', - automationId: 'automation-1', - automationNameSnapshot: 'Nightly review', - automationRunId: 'run-1', - automationRunTitleSnapshot: 'Nightly review run', - createdAt: 123, - executionTargetType: 'ssh', - executionTargetId: 'ssh-target-1', - projectId: 'project-1', - repoId: 'repo-1', - hostId: 'ssh:ssh-target-1' - } - }) - }) - it('skips setup when the created branch differs from the content approved for the automation', async () => { const worktreePath = mkdtempSync(path.join(tmpdir(), 'orca-automation-setup-')) try { diff --git a/src/main/automations/automation-zero-grace-tick-latency.test.ts b/src/main/automations/automation-zero-grace-tick-latency.test.ts index ffad65094f8..753f51546a4 100644 --- a/src/main/automations/automation-zero-grace-tick-latency.test.ts +++ b/src/main/automations/automation-zero-grace-tick-latency.test.ts @@ -89,22 +89,7 @@ describe('AutomationService zero-grace tick latency', () => { expect(await statusAt(lateMs)).toBe('dispatched') }) - it('still skips a zero-grace occurrence that came due during a long sleep', async () => { - expect(await statusAt(4 * 60 * 60 * 1000)).toBe('skipped_missed') - }) - it('skips once lateness exceeds the tick-latency tolerance', async () => { expect(await statusAt(2 * 60_000 + 1)).toBe('skipped_missed') }) - - it('dispatches after a restart that crosses the occurrence within tolerance', async () => { - vi.setSystemTime(new Date('2026-05-13T08:00:00')) - const store = await createStore() - store.addRepo(makeRepo()) - const automation = makeZeroGrace(store) - await evaluateAt(store, DUE - 30_000) - expect(store.listAutomationRuns(automation.id)).toHaveLength(0) - await evaluateAt(store, DUE + 30_000) - expect(store.listAutomationRuns(automation.id)[0]?.status).toBe('dispatched') - }) }) diff --git a/src/main/automations/retained-run-reconciliation.test.ts b/src/main/automations/retained-run-reconciliation.test.ts index a233867c18c..380f8af4ba1 100644 --- a/src/main/automations/retained-run-reconciliation.test.ts +++ b/src/main/automations/retained-run-reconciliation.test.ts @@ -158,27 +158,6 @@ describe('reconciling retained runs against a graph that has not published yet', service.stop() }) - it('does not fail a serve-startup retained run whose host pane has not reattached', async () => { - const store = await createStore() - const automation = createAutomation(store) - const retained = retainDispatchedRun(store, automation) - const surface = createPaneSurface() - const service = new AutomationService(store, { - terminalLauncher: OPEN_LAUNCHER, - terminalObserver: surface.observer - }) - - service.start() - await vi.advanceTimersByTimeAsync(30_000) - expect(readRun(store, automation.id, retained.id).status).toBe('dispatched') - - surface.mountPane() - await vi.advanceTimersByTimeAsync(5_000) - expect(surface.observedHandles).toEqual(['handle-1']) - expect(readRun(store, automation.id, retained.id).status).toBe('dispatched') - service.stop() - }) - it('still closes out a run the ready surface cannot find after the settle window', async () => { const store = await createStore() const automation = createAutomation(store) diff --git a/src/main/automations/run-terminal-retention.test.ts b/src/main/automations/run-terminal-retention.test.ts index 10f835ae784..7af5974bc8e 100644 --- a/src/main/automations/run-terminal-retention.test.ts +++ b/src/main/automations/run-terminal-retention.test.ts @@ -1,11 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AutomationRun, AutomationRunStatus } from '../../shared/automations-types' import { runKeyedSerializedOperation } from '../cli/keyed-promise-queue' -import { - createRunTerminalRetention, - RUN_TERMINAL_GRACE_MS, - RUN_TERMINALS_KEPT_PER_AUTOMATION -} from './run-terminal-retention' +import { createRunTerminalRetention, RUN_TERMINAL_GRACE_MS } from './run-terminal-retention' function makeRun( id: string, @@ -87,19 +83,6 @@ afterEach(() => { }) describe('headless run terminal retention', () => { - it('closes finished run terminals past the grace period, keeping the newest few viewable', async () => { - const runs = [0, 1, 2, 3, 4, 5].map((hour) => makeRun(`r${hour}`, hour * 3_600_000)) - const h = harness(runs) - - await h.retention.sweep() - expect(h.closed).toEqual([]) - - vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS) - await h.retention.sweep() - expect(h.closed.toSorted()).toEqual(['tab-r0:1', 'tab-r1:1', 'tab-r2:1']) - expect(6 - h.closed.length).toBe(RUN_TERMINALS_KEPT_PER_AUTOMATION) - }) - it('never closes a run that has not finished, however old', async () => { const runs = [ makeRun('working', 0, 'dispatched'), @@ -142,18 +125,6 @@ describe('headless run terminal retention', () => { } ) - it('keeps the newest few per automation, not across all of them', async () => { - const runs = [ - ...[0, 1, 2].map((n) => makeRun(`a${n}`, n, 'completed', 'a')), - ...[0, 1, 2].map((n) => makeRun(`b${n}`, n, 'completed', 'b')) - ] - const h = harness(runs) - await h.retention.sweep() - vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS) - await h.retention.sweep() - expect(h.closed).toEqual([]) - }) - it('drains every completed, unused run terminal for an update, ignoring keep and grace', async () => { const runs = [ ...[0, 1, 2, 3, 4].map((n) => makeRun(`r${n}`, n)), @@ -190,30 +161,6 @@ describe('headless run terminal retention', () => { expect(h.forgotten[0]).toMatchObject({ id: 'missing', status: 'dispatch_failed' }) }) - it('closes a still-dispatched run whose agent exited, leaving its shell idle', async () => { - const runs = [ - makeRun('exited', 0, 'dispatched'), - ...[1, 2, 3].map((n) => makeRun(`done${n}`, n)) - ] - const h = harness(runs) - h.idleShell.add('exited') - await h.retention.sweep() - vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS) - await h.retention.sweep() - expect(h.closed).toEqual(['tab-exited:1']) - }) - - it('keeps the newest three live terminals, not counting ones already gone', async () => { - const runs = [0, 1, 2, 3, 4].map((n) => makeRun(`r${n}`, n)) - const h = harness(runs) - h.dead.add('r4') - await h.retention.sweep() - vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS) - await h.retention.sweep() - expect(h.closed).toEqual(['tab-r0:1']) - expect(h.forgotten.map((run) => run.id).toSorted()).toEqual(['r0', 'r4']) - }) - it('drains idle failed and exited runs for an update, never a live agent', async () => { const runs = [ makeRun('missing', 0, 'dispatch_failed'), @@ -329,27 +276,6 @@ describe('headless run terminal retention', () => { expect(h.forgotten.map((run) => run.id)).not.toContain('r0') }) - it('closes a failed run in its own workspace, and never sleeps or closes a used one', async () => { - const runs = [ - makeRun('failed', 0, 'dispatch_failed'), - makeRun('typed', 1), - ...[2, 3, 4].map((n) => makeRun(`done${n}`, n)) - ] - const h = harness(runs) - h.ownWorkspace.add('failed') - h.ownWorkspace.add('typed') - h.idleShell.add('failed') - h.use.set('typed', 'used') - await h.retention.drain() - expect(h.slept).toEqual([]) - expect(h.closed.toSorted()).toEqual([ - 'tab-done2:1', - 'tab-done3:1', - 'tab-done4:1', - 'tab-failed:1' - ]) - }) - it('keeps a run in its own workspace whose sleep declines or fails, retrying each sweep', async () => { const runs = [0, 1, 2, 3].map((n) => makeRun(`r${n}`, n)) const h = harness(runs) diff --git a/src/main/daemon/transport/client/testing/client.test.ts b/src/main/daemon/transport/client/testing/client.test.ts index 024bed72ef0..59e50d06524 100644 --- a/src/main/daemon/transport/client/testing/client.test.ts +++ b/src/main/daemon/transport/client/testing/client.test.ts @@ -1,16 +1,10 @@ -import { EventEmitter } from 'node:events' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createServer, type Server, type Socket } from 'node:net' import { tmpdir } from 'node:os' import { join } from 'node:path' import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' import { DaemonClient } from '../daemon-client' -import type { DaemonPendingRequests } from '../daemon-client-pending-requests' -import { - encodeNdjson, - NDJSON_MAX_LINE_BYTES, - NdjsonLineTooLongError -} from '../../../../../shared/main-process-ndjson-framer' +import { encodeNdjson } from '../../../../../shared/main-process-ndjson-framer' import { DaemonConnectionLostError } from '../../../protocol/daemon-errors' import type { HelloMessage } from '../../../protocol/daemon-hello-protocol' import type { DaemonRequest } from '../../../protocol/daemon-rpc-contract' @@ -147,23 +141,6 @@ describe('DaemonClient', () => { } describe('connect', () => { - it('captures one matching endpoint identity from both authenticated sockets', async () => { - const identity = { - pid: 123, - startedAtMs: 456, - launchNonce: 'launch-a', - entryPath: '/Applications/Orca.app/Contents/Resources/daemon-entry.js', - appVersion: '1.2.3', - spawnerExecPath: '/Applications/Orca.app/Contents/MacOS/Orca' - } - await startMockDaemon({ helloIdentity: () => identity }) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - expect(client.getDaemonIdentity()).toEqual(identity) - }) - it('rejects a v24 daemon that omits endpoint identity', async () => { await startMockDaemon({ omitHelloIdentity: true }) client = new DaemonClient({ socketPath, tokenPath }) @@ -196,30 +173,6 @@ describe('DaemonClient', () => { expect(client.getDaemonIdentity()).toBeNull() }) - it('removes socket startup listeners after connecting', async () => { - await startMockDaemon() - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - const connectedClient = client as unknown as { - controlSocket: Socket | null - streamSocket: Socket | null - } - for (const socket of [connectedClient.controlSocket, connectedClient.streamSocket]) { - expect(socket?.listenerCount('connect')).toBe(0) - // The post-handshake disconnect handler still needs one error listener. - expect(socket?.listenerCount('error')).toBe(1) - } - }) - - it('rejects on version mismatch', async () => { - await startMockDaemon({ rejectVersion: true }) - - client = new DaemonClient({ socketPath, tokenPath }) - await expect(client.ensureConnected()).rejects.toThrow() - }) - it('times out when the daemon never answers hello', async () => { let resolveHello: () => void = () => {} const helloReceived = new Promise((resolve) => { @@ -276,127 +229,9 @@ describe('DaemonClient', () => { expect(disconnected.controlSocket).toBeNull() expect(disconnected.streamSocket).toBeNull() }) - - it('removes hello startup listeners after timeout', async () => { - vi.useFakeTimers() - - client = new DaemonClient({ socketPath, tokenPath }) - const write = vi.fn(() => true) - const destroy = vi.fn() - const socket = new EventEmitter() as Socket - socket.write = write as unknown as Socket['write'] - socket.destroy = destroy as unknown as Socket['destroy'] - const sendHello = ( - client as unknown as { - sendHello( - socket: Socket, - token: string, - role: 'control' | 'stream', - timeoutMs: number - ): Promise - } - ).sendHello.bind(client) - - const promise = sendHello(socket, 'test-token-123', 'control', 5000) - const rejection = expect(promise).rejects.toThrow('Hello response timed out') - await vi.advanceTimersByTimeAsync(5000) - - await rejection - expect(write).toHaveBeenCalledOnce() - expect(destroy).toHaveBeenCalledOnce() - expect(socket.listenerCount('data')).toBe(0) - expect(socket.listenerCount('error')).toBe(0) - expect(socket.listenerCount('close')).toBe(0) - }) - - it('rejects when the daemon closes before hello completes', async () => { - await startMockDaemon({ closeOnHello: true }) - - client = new DaemonClient({ socketPath, tokenPath }) - await expect(client.ensureConnected()).rejects.toThrow( - 'Connection closed before hello response' - ) - }) - - it('rejects when the daemon closes immediately after connect', async () => { - await startMockDaemon({ closeOnConnect: true }) - - client = new DaemonClient({ socketPath, tokenPath }) - await expect(client.ensureConnected()).rejects.toThrow() - }) - - it('fails a retired endpoint at the connect, not at the token read', async () => { - rmSync(tokenPath) - client = new DaemonClient({ socketPath, tokenPath }) - - const error = (await client - .ensureConnected() - .catch((err: unknown) => err)) as NodeJS.ErrnoException - - expect(error.syscall).toBe('connect') - expect(['ENOENT', 'ECONNREFUSED']).toContain(error.code) - }) - - it('still attempts the handshake when the token is gone but a daemon is listening', async () => { - const hellos: HelloMessage[] = [] - await startMockDaemon({ onHello: (msg) => hellos.push(msg) }) - rmSync(tokenPath) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - expect(hellos.length).toBeGreaterThan(0) - expect(hellos[0]?.token).toBe('') - }) }) describe('RPC', () => { - it('rejects an oversized request before installing a timer or writing', async () => { - await startMockDaemon() - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - const internals = client as unknown as { - controlSocket: Socket - pendingRequests: DaemonPendingRequests - } - const writeSpy = vi.spyOn(internals.controlSocket, 'write') - const timerSpy = vi.spyOn(globalThis, 'setTimeout') - try { - await expect( - client.request('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) }) - ).rejects.toBeInstanceOf(NdjsonLineTooLongError) - - expect(timerSpy).not.toHaveBeenCalled() - expect(writeSpy).not.toHaveBeenCalled() - expect(internals.pendingRequests.size).toBe(0) - } finally { - timerSpy.mockRestore() - writeSpy.mockRestore() - } - }) - - it('sends request and receives response', async () => { - await startMockDaemon({ - onControlMessage: (msg) => { - const req = msg as { id: string; type: string } - if (req.type === 'listSessions') { - return encodeNdjson({ - id: req.id, - ok: true, - payload: { sessions: [] } - }) - } - return null - } - }) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - const result = await client.request('listSessions', undefined) - expect(result).toEqual({ sessions: [] }) - }) - it('survives a cancelCreateOrAttach the daemon refuses', async () => { // Old daemons reject cancellation; that refusal must not disconnect sibling sessions. const received: string[] = [] @@ -454,47 +289,6 @@ describe('DaemonClient', () => { await rejected }) - - it('rejects on error response', async () => { - await startMockDaemon({ - onControlMessage: (msg) => { - const req = msg as { id: string; type: string } - return encodeNdjson({ - id: req.id, - ok: false, - error: 'Something went wrong' - }) - } - }) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - await expect(client.request('listSessions', undefined)).rejects.toThrow( - 'Something went wrong' - ) - }) - - it('adds recovery hints to node-pty daemon diagnostics', async () => { - await startMockDaemon({ - onControlMessage: (msg) => { - const req = msg as { id: string; type: string } - return encodeNdjson({ - id: req.id, - ok: false, - error: - "node-pty: posix_spawn failed: ENOENT (errno 2, No such file or directory) - helper='/tmp/deleted/spawn-helper'" - }) - } - }) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - await expect(client.request('listSessions', undefined)).rejects.toThrow( - "Daemon's node-pty install is gone (worktree deleted?). Restart Orca. node-pty: posix_spawn failed: ENOENT" - ) - }) }) describe('events', () => { @@ -541,123 +335,7 @@ describe('DaemonClient', () => { }) }) - describe('disconnect', () => { - it('removes socket listeners when disconnecting', async () => { - await startMockDaemon() - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - const connectedClient = client as unknown as { - controlSocket: Socket | null - streamSocket: Socket | null - } - const sockets = [connectedClient.controlSocket, connectedClient.streamSocket] - for (const socket of sockets) { - expect(socket?.listenerCount('data')).toBe(1) - expect(socket?.listenerCount('close')).toBe(1) - expect(socket?.listenerCount('error')).toBe(1) - } - - client.disconnect() - - for (const socket of sockets) { - expect(socket?.listenerCount('data')).toBe(0) - expect(socket?.listenerCount('close')).toBe(0) - expect(socket?.listenerCount('error')).toBe(0) - } - }) - - it('emits disconnected when server destroys sockets', async () => { - const serverSockets: Socket[] = [] - await startMockDaemon() - server.on('connection', (socket) => serverSockets.push(socket)) - - client = new DaemonClient({ socketPath, tokenPath }) - const disconnected = vi.fn() - client.onDisconnected(disconnected) - await client.ensureConnected() - - await waitFor(() => serverSockets.length >= 2) - - for (const socket of serverSockets) { - socket.destroy() - } - - await waitFor(() => disconnected.mock.calls.length > 0, 3000) - expect(client.isConnected()).toBe(false) - }) - - it('disconnect() can be called safely when not connected', () => { - client = new DaemonClient({ socketPath, tokenPath }) - expect(() => client.disconnect()).not.toThrow() - }) - }) - describe('notify (fire-and-forget)', () => { - it('sends request with notify_ prefix without expecting response', async () => { - const received: unknown[] = [] - await startMockDaemon({ - onControlMessage: (msg) => { - received.push(msg) - return null - } - }) - - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - - const delivered = client.notify('write', { sessionId: 'session-1', data: 'hello' }) - expect(delivered).toBe(true) - - await waitFor(() => received.length > 0) - const msg = received[0] as { id: string; type: string } - expect(msg.id).toMatch(/^notify_/) - expect(msg.type).toBe('write') - }) - - it('reports a dropped delivery when not connected', () => { - client = new DaemonClient({ socketPath, tokenPath }) - expect(client.notify('write', { sessionId: 'session-1', data: 'hello' })).toBe(false) - }) - - it('reports a dropped delivery for an oversized payload without writing', async () => { - await startMockDaemon() - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - const internals = client as unknown as { controlSocket: Socket } - const writeSpy = vi.spyOn(internals.controlSocket, 'write') - - expect(client.notify('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) })).toBe(false) - expect(writeSpy).not.toHaveBeenCalled() - }) - - it('rejects an oversized settled notification without disconnecting', async () => { - await startMockDaemon() - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - const internals = client as unknown as { controlSocket: Socket } - const writeSpy = vi.spyOn(internals.controlSocket, 'write') - - await expect( - client.notifyWithSettlement('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) }) - ).resolves.toEqual({ outcome: 'refused', reason: 'encode_failed' }) - expect(writeSpy).not.toHaveBeenCalled() - expect(client.isConnected()).toBe(true) - }) - - it('reports a dropped delivery when the socket write throws', async () => { - await startMockDaemon() - client = new DaemonClient({ socketPath, tokenPath }) - await client.ensureConnected() - const internals = client as unknown as { controlSocket: Socket } - vi.spyOn(internals.controlSocket, 'write').mockImplementation(() => { - throw new Error('EPIPE') - }) - - expect(client.notify('write', { sessionId: 'session-1', data: 'hello' })).toBe(false) - }) - it('reports an asynchronous socket write failure at settlement', async () => { await startMockDaemon() client = new DaemonClient({ socketPath, tokenPath }) diff --git a/src/main/daemon/transport/framing/testing/daemon-stream-binary-framing.test.ts b/src/main/daemon/transport/framing/testing/daemon-stream-binary-framing.test.ts index 60d6b0f01eb..bbb8eee49ab 100644 --- a/src/main/daemon/transport/framing/testing/daemon-stream-binary-framing.test.ts +++ b/src/main/daemon/transport/framing/testing/daemon-stream-binary-framing.test.ts @@ -1,21 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { createBinaryStreamFrameReader, - encodeBinaryStreamDataFrame, - encodeBinaryStreamEventFrame + encodeBinaryStreamDataFrame } from '../daemon-stream-binary-framing' -import { createNdjsonParser } from '../../../../../shared/main-process-ndjson-framer' -import { streamFramesFor, type StreamWriteChunk } from '../daemon-stream-data-split' +import { streamFramesFor } from '../daemon-stream-data-split' import type { DaemonEvent } from '../../../protocol/daemon-stream-events' -function isDaemonEvent(record: unknown): record is DaemonEvent { - return ( - typeof record === 'object' && record !== null && 'type' in record && record.type === 'event' - ) -} - -const TERMINAL_TEXT = '\x1b[38;5;208m┌─ build ─┐\x1b[0m \u{1F680} ok\r\n"quoted" \\ tab\t' - function readAll(chunks: Buffer[], splitEvery?: number): { events: DaemonEvent[]; fatal: Error[] } { const events: DaemonEvent[] = [] const fatal: Error[] = [] @@ -31,53 +21,7 @@ function readAll(chunks: Buffer[], splitEvery?: number): { events: DaemonEvent[] return { events, fatal } } -function binaryEvents(write: (stream: { write(chunk: StreamWriteChunk): void }) => void) { - const frames: Buffer[] = [] - write({ write: (chunk) => frames.push(Buffer.from(chunk)) }) - return readAll(frames).events -} - -function ndjsonEvents(write: (stream: { write(chunk: StreamWriteChunk): void }) => void) { - const events: DaemonEvent[] = [] - const parser = createNdjsonParser( - (record) => { - if (isDaemonEvent(record)) { - events.push(record) - } - }, - () => {} - ) - write({ write: (chunk) => parser.feed(String(chunk)) }) - return events -} - describe('binary stream framing', () => { - it('round-trips data and event frames across any socket chunking', () => { - const frames = [ - encodeBinaryStreamDataFrame('session-1', TERMINAL_TEXT, { seq: 42, rawLength: 7 }), - encodeBinaryStreamEventFrame({ - type: 'event', - event: 'exit', - sessionId: 'session-1', - payload: { code: 0 } - }), - encodeBinaryStreamDataFrame('session-2', '') - ] - const expected = [ - { - type: 'event', - event: 'data', - sessionId: 'session-1', - payload: { seq: 42, rawLength: 7, data: TERMINAL_TEXT } - }, - { type: 'event', event: 'exit', sessionId: 'session-1', payload: { code: 0 } }, - { type: 'event', event: 'data', sessionId: 'session-2', payload: { data: '' } } - ] - for (const splitEvery of [undefined, 1, 3, 7]) { - expect(readAll(frames, splitEvery)).toEqual({ events: expected, fatal: [] }) - } - }) - it('drops an unparsable record but keeps reading the frames after it', () => { const broken = Buffer.from([1, 0, 0, 0, 3, 0x7b, 0x7b, 0x7b]) const next = encodeBinaryStreamDataFrame('session-1', 'after') @@ -102,64 +46,6 @@ describe('binary stream framing', () => { expect(oversized).toHaveBeenCalledTimes(1) }) - it.each([ - ['plain', undefined, undefined, false], - ['seq and raw length', 12_000, 90_000, false], - ['zero raw length', 0, 7, false], - ['transformed', 3, 11, true] - ] as const)( - 'follows the NDJSON per-chunk metadata rules for %s data', - (_label, raw, seq, transformed) => { - const data = TERMINAL_TEXT.repeat(400) - const rawLength = raw ?? data.length - const write = - (framing: 'ndjson' | 'binary-v1') => - (stream: { write(chunk: StreamWriteChunk): void }): void => - streamFramesFor(framing).data( - stream, - 'session-1', - data, - 4096, - rawLength, - seq, - transformed - ) - // Chunk boundaries differ by design (byte vs JSON budgets); the metadata rules must not. - const describeChunks = (events: DaemonEvent[]) => { - let consumed = 0 - return events.map((event) => { - if (event.event !== 'data') { - throw new Error('expected data events only') - } - const { data: chunk, ...meta } = event.payload - consumed += chunk.length - return { - meta, - expected: transformed - ? { seq, rawLength, sequenceChars: rawLength, transformed: true } - : { - ...(seq === undefined ? {} : { seq: seq - (data.length - consumed) }), - ...(raw === undefined && seq === undefined - ? {} - : { - rawLength: raw === 0 ? 0 : chunk.length, - sequenceChars: raw === 0 ? 0 : chunk.length - }) - } - } - }) - } - for (const events of [binaryEvents(write('binary-v1')), ndjsonEvents(write('ndjson'))]) { - expect( - events.map((event) => (event.event === 'data' ? event.payload.data : '')).join('') - ).toBe(data) - for (const { meta, expected } of describeChunks(events)) { - expect(meta).toEqual(expected) - } - } - } - ) - it('keeps every frame within the frame budget', () => { const frames: Buffer[] = [] streamFramesFor('binary-v1').data( diff --git a/src/main/daemon/transport/framing/testing/daemon-stream-data-split.test.ts b/src/main/daemon/transport/framing/testing/daemon-stream-data-split.test.ts index 0d33166bced..13b11210366 100644 --- a/src/main/daemon/transport/framing/testing/daemon-stream-data-split.test.ts +++ b/src/main/daemon/transport/framing/testing/daemon-stream-data-split.test.ts @@ -22,12 +22,6 @@ function write( } describe('writeStreamDataEvents wire parity', () => { - it('keeps JSON escaping, Unicode and newline framing byte-for-byte', () => { - expect(write('"\\\n\t\u0000é中🐙', 4096)).toEqual([ - '{"type":"event","event":"data","sessionId":"session-1","payload":{"data":"\\\"\\\\\\n\\t\\u0000é中🐙"}}\n' - ]) - }) - it('keeps split frames within the byte cap and preserves code points and sequence spans', () => { const data = '🐙é中\x1b[0m"\\\n'.repeat(100) for (const seq of [undefined, 9000]) { diff --git a/src/main/emulator/android/android-device-system-metadata.test.ts b/src/main/emulator/android/android-device-system-metadata.test.ts deleted file mode 100644 index 5f79eb0cbcd..00000000000 --- a/src/main/emulator/android/android-device-system-metadata.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, expect, it, vi } from 'vitest' -import { readAndroidDeviceSystemMetadata } from './android-device-system-metadata' - -let directory: string | undefined -afterEach(async () => { - vi.unstubAllEnvs() - if (directory) { - await rm(directory, { recursive: true, force: true }) - } -}) -it('reads the API of a stopped AVD from its configured custom data directory', async () => { - directory = await mkdtemp(join(tmpdir(), 'orca-avd-metadata-')) - const avdHome = join(directory, 'avds') - const data = join(directory, 'custom-data') - await mkdir(avdHome) - await mkdir(data) - await writeFile(join(avdHome, 'Pixel.ini'), `path=${data}\n`) - await writeFile( - join(data, 'config.ini'), - 'image.sysdir.1=system-images/android-36/google_apis/arm64-v8a/\n' - ) - vi.stubEnv('ANDROID_AVD_HOME', avdHome) - const runner = vi.fn() - expect( - await readAndroidDeviceSystemMetadata( - { - backend: 'android', - id: 'Pixel', - name: 'Pixel', - detail: 'avd', - state: 'shutdown', - isAvailable: true - }, - runner, - { sdkRoot: '/sdk', adb: '/sdk/adb', emulator: '/sdk/emulator' } - ) - ).toEqual({ apiLevel: '36' }) - expect(runner).not.toHaveBeenCalled() -}) diff --git a/src/main/emulator/emulator-opening-preference.test.ts b/src/main/emulator/emulator-opening-preference.test.ts deleted file mode 100644 index 242226101bb..00000000000 --- a/src/main/emulator/emulator-opening-preference.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - resolveEmulatorOpeningPreference, - rememberEmulatorDevice -} from './emulator-opening-preference' -import type { EmulatorDevice } from './backends/emulator-backend' -const devices: EmulatorDevice[] = [ - { backend: 'ios', id: 'iphone', name: 'iPhone', state: 'booted', isAvailable: true }, - { - backend: 'android', - id: 'emulator-5558', - name: 'Pixel9_API36', - detail: 'emulator', - state: 'booted', - isAvailable: true - } -] -describe('emulator opening preferences', () => { - it('asks on first use instead of choosing a running iPhone', () => { - expect(resolveEmulatorOpeningPreference({}, devices)).toEqual({ - selectionRequired: 'first-use' - }) - }) - it('preserves a legacy fixed device, but respects an explicit ask or last-used mode', () => { - const settings = { - mobileEmulatorDefaultDeviceUdid: 'iphone', - mobileEmulatorRecentDevices: rememberEmulatorDevice(devices[1], []) - } - expect(resolveEmulatorOpeningPreference(settings, devices)).toEqual({ device: 'iphone' }) - expect( - resolveEmulatorOpeningPreference({ ...settings, mobileEmulatorOpeningMode: 'ask' }, devices) - ).toEqual({ selectionRequired: 'ask' }) - expect( - resolveEmulatorOpeningPreference( - { ...settings, mobileEmulatorOpeningMode: 'last-used' }, - devices - ) - ).toEqual({ device: 'emulator-5558' }) - }) - it('does not replace a missing or unavailable preference with another platform', () => { - expect( - resolveEmulatorOpeningPreference({ mobileEmulatorDefaultDeviceUdid: 'removed' }, devices) - ).toEqual({ selectionRequired: 'missing' }) - expect( - resolveEmulatorOpeningPreference({ mobileEmulatorDefaultDeviceUdid: 'iphone' }, [ - { ...devices[0], isAvailable: false } - ]) - ).toEqual({ selectionRequired: 'unavailable' }) - }) - it('keeps physical serials distinct from AVD names and bounds recent preferences', () => { - const physical: EmulatorDevice = { - ...devices[1], - id: 'USB-123', - name: 'Pixel9_API36', - detail: 'device' - } - const recent = rememberEmulatorDevice(physical, rememberEmulatorDevice(devices[1], [])) - expect(recent.map((item) => item.id)).toEqual(['USB-123', 'Pixel9_API36']) - expect( - resolveEmulatorOpeningPreference({ mobileEmulatorRecentDevices: recent }, devices) - ).toEqual({ selectionRequired: 'missing' }) - let history = recent - for (let i = 0; i < 25; i++) { - history = rememberEmulatorDevice({ ...physical, id: `USB-${i}` }, history) - } - expect(history).toHaveLength(20) - }) -}) diff --git a/src/main/emulator/simctl-simulator-devices.test.ts b/src/main/emulator/simctl-simulator-devices.test.ts deleted file mode 100644 index 56ffc950405..00000000000 --- a/src/main/emulator/simctl-simulator-devices.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as Os from 'node:os' - -const { execFileMock, platformMock } = vi.hoisted(() => ({ - execFileMock: vi.fn(), - platformMock: vi.fn(() => 'darwin') -})) - -vi.mock('child_process', () => ({ - execFile: execFileMock -})) - -vi.mock('os', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - platform: platformMock - } -}) - -vi.mock('./serve-sim-execution', () => ({ - execServeSimCommand: vi.fn() -})) - -import { - listSimulatorDevices, - resolveSimulatorUdid, - shutdownSimulatorDevice -} from './simctl-simulator-devices' - -describe('listSimulatorDevices', () => { - beforeEach(() => { - execFileMock.mockReset() - platformMock.mockReset() - platformMock.mockReturnValue('darwin') - }) - - it('keeps lastBootedAt as usage evidence only when lastUsedAt is absent', async () => { - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - null, - JSON.stringify({ - devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-26-0': [ - { - udid: 'booted-before', - name: 'iPhone', - state: 'Shutdown', - lastBootedAt: '2026-10-29T10:00:00Z' - }, - { - udid: 'used-after', - name: 'iPhone', - state: 'Shutdown', - lastBootedAt: '2026-10-29T10:00:00Z', - lastUsedAt: '2026-10-30T11:00:00Z' - } - ] - } - }), - '' - ) - }) - const devices = await listSimulatorDevices() - expect(devices.map((device) => device.lastUsedAt)).toEqual([ - '2026-10-29T10:00:00Z', - '2026-10-30T11:00:00Z' - ]) - }) - - it('normalizes missing simctl errors for UI and CLI callers', async () => { - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - Object.assign(new Error('Command failed: xcrun simctl list devices -j'), { code: 72 }), - '', - 'xcrun: error: unable to find utility "simctl", not a developer tool or in PATH' - ) - }) - - const error = await listSimulatorDevices().catch((caught: unknown) => caught) - - expect(error).toMatchObject({ - code: 'emulator_simctl_unavailable', - message: expect.stringContaining('Xcode Simulator tools are unavailable') - }) - expect(error).toBeInstanceOf(Error) - expect((error as Error).message).not.toContain('Command failed') - }) -}) - -describe('resolveSimulatorUdid', () => { - beforeEach(() => { - execFileMock.mockReset() - platformMock.mockReturnValue('darwin') - }) - - it('prefers an exact name over an earlier device whose name only contains it', async () => { - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - null, - JSON.stringify({ - devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-27-0': [ - { - name: 'iPhone 17e', - udid: '645A2204-ED7C-4253-B298-2749820B1B7E', - state: 'Shutdown' - }, - { name: 'iPhone 17', udid: '1BD4732B-EC1A-4074-8C70-6AC369607185', state: 'Shutdown' } - ] - } - }), - '' - ) - }) - const serveSim = { command: 'serve-sim', baseArgs: [], usesElectronAsNode: false } - - await expect(resolveSimulatorUdid('iPhone 17', serveSim)).resolves.toBe( - '1BD4732B-EC1A-4074-8C70-6AC369607185' - ) - await expect(resolveSimulatorUdid('iPHONE 17', serveSim)).resolves.toBe( - '1BD4732B-EC1A-4074-8C70-6AC369607185' - ) - await expect(resolveSimulatorUdid('17e', serveSim)).resolves.toBe( - '645A2204-ED7C-4253-B298-2749820B1B7E' - ) - }) -}) - -describe('shutdownSimulatorDevice', () => { - beforeEach(() => { - execFileMock.mockReset() - platformMock.mockReset() - platformMock.mockReturnValue('darwin') - }) - - it('treats an already-shut-down device as success', async () => { - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - Object.assign( - new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'), - { code: 164 } - ), - '', - 'Unable to shutdown device in current state: Shutdown' - ) - }) - - await expect( - shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE') - ).resolves.toBeUndefined() - }) - - it('propagates real shutdown failures instead of swallowing them', async () => { - // Command text always contains shutdown; only the actual device state proves it is already off. - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - Object.assign( - new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'), - { killed: true, signal: 'SIGTERM' } - ), - '', - '' - ) - }) - - await expect( - shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE') - ).rejects.toMatchObject({ code: 'emulator_error' }) - }) - - it('rejects current-state failures that are not already shut down', async () => { - execFileMock.mockImplementation((_command, _args, _options, callback) => { - callback( - Object.assign( - new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'), - { code: 164 } - ), - '', - 'Unable to shutdown device in current state: Booting' - ) - }) - - await expect( - shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE') - ).rejects.toMatchObject({ code: 'emulator_error' }) - }) -}) diff --git a/src/main/ephemeral-vm/workspace-resume-user-disconnect.test.ts b/src/main/ephemeral-vm/workspace-resume-user-disconnect.test.ts index 78e68dd5b98..0715f51e7d1 100644 --- a/src/main/ephemeral-vm/workspace-resume-user-disconnect.test.ts +++ b/src/main/ephemeral-vm/workspace-resume-user-disconnect.test.ts @@ -53,7 +53,6 @@ import { resetSshUserDisconnectsForTests } from '../ssh/ssh-user-disconnect' import { getRuntimeOwnedSshTargetId } from '../ssh/connection/ssh-connection-store' -import { forgetRecipeVmWakePairingsForTests, recipeVmWakePairing } from './recipe-vm-wake-pairings' // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recipe context reader is mocked, so the resume never reads this store. const STORE = {} as Store @@ -67,15 +66,6 @@ describe("waking an SSH-connected recipe VM and its host's Disconnect", () => { }) // The reconnect can move the tunnel and re-pair the server; the open that waited expects that. - it('reconnects the host when nobody disconnected it, recording the pairing it left', async () => { - forgetRecipeVmWakePairingsForTests() - await resumeEphemeralVmWorkspace(STORE, 'workspace-1', { - sshHeld: false, - sshUserDisconnects: 0 - }) - expect(mocks.connect).toHaveBeenCalledTimes(1) - expect(recipeVmWakePairing('env-vm')).toBe(9) - }) it('leaves a Disconnect made during the wake in place, with the VM up', async () => { gates.resuming = Promise.withResolvers() @@ -117,12 +107,4 @@ describe("waking an SSH-connected recipe VM and its host's Disconnect", () => { // Suspend tore down the VM's SSH transport, so the wake reconnects it even when the recipe had // no resume step to run. - it('reconnects the host of a VM whose recipe resume was skipped', async () => { - gates.skipped = true - await resumeEphemeralVmWorkspace(STORE, 'workspace-1', { - sshHeld: false, - sshUserDisconnects: 0 - }) - expect(mocks.connect).toHaveBeenCalledTimes(1) - }) }) diff --git a/src/main/gitea/client.test.ts b/src/main/gitea/client.test.ts index 74ca7a82708..10541ebdb7b 100644 --- a/src/main/gitea/client.test.ts +++ b/src/main/gitea/client.test.ts @@ -9,18 +9,11 @@ vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({ ...(await importOriginal>()), gitExecFileAsync: gitExecFileAsyncMock })) - -import { - getGiteaAuthStatus, - getGiteaPullRequestForBranch, - getGiteaPullRequestForBranchOrThrow, - normalizeGiteaApiBaseUrl -} from './client' +import { getGiteaPullRequestForBranch, getGiteaPullRequestForBranchOrThrow } from './client' import { _resetGiteaRepoRefCache } from './repository-ref' import { _getGiteaPullRequestScanCacheSize, - _resetGiteaPullRequestScanCache, - scanGiteaPullRequests + _resetGiteaPullRequestScanCache } from './pull-request-scan-cache' import { __resetRepoDefaultBranchCacheForTests } from '../source-control/repo-default-branch' @@ -70,50 +63,6 @@ describe('Gitea client', () => { vi.unstubAllGlobals() }) - it('hides a stale closed PR whose source branch is the repo default branch (#9171)', async () => { - primeGitExecWithDefaultBranch() - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - return Response.json([{ ...giteaPr(7, 'main'), state: 'closed' }]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toBeNull() - }) - - it('hides a stale merged PR on the default branch but keeps an open one', async () => { - primeGitExecWithDefaultBranch() - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - return Response.json([{ ...giteaPr(9, 'main'), merged: true }]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toBeNull() - - _resetGiteaPullRequestScanCache() - __resetRepoDefaultBranchCacheForTests() - const openFetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - return Response.json([giteaPr(10, 'main')]) - }) - vi.stubGlobal('fetch', openFetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toMatchObject({ - number: 10, - state: 'open' - }) - }) - it('discards a closed default-branch shadow and refetches the linked PR via the fallback (#9171)', async () => { primeGitExecWithDefaultBranch() const fetchMock = vi.fn(async (url: string) => { @@ -133,124 +82,6 @@ describe('Gitea client', () => { ).resolves.toMatchObject({ number: 42 }) }) - it('normalizes Gitea API base URLs', () => { - expect(normalizeGiteaApiBaseUrl('https://git.example.com')).toBe( - 'https://git.example.com/api/v1' - ) - expect(normalizeGiteaApiBaseUrl('https://git.example.com/api/v1/')).toBe( - 'https://git.example.com/api/v1' - ) - }) - - it('fetches a branch pull request and commit status', async () => { - const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { - const parsed = new URL(url) - if (!init) { - throw new Error('expected request init') - } - expect((init.headers as Record).Authorization).toBe('token gitea-token') - if (parsed.pathname.endsWith('/commits/abc123/status')) { - return Response.json({ state: 'success' }) - } - return Response.json([giteaPr()]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect( - getGiteaPullRequestForBranch('/repo', 'refs/heads/feature/gitea') - ).resolves.toEqual({ - number: 7, - title: 'Add Gitea', - state: 'open', - url: 'https://git.example.com/team/repo/pulls/7', - status: 'success', - updatedAt: '2026-05-15T00:00:00Z', - mergeable: 'MERGEABLE', - headSha: 'abc123' - }) - - const listUrl = new URL(String(fetchMock.mock.calls[0]?.[0])) - expect(listUrl.origin).toBe('https://git.example.com') - expect(listUrl.pathname).toBe('/api/v1/repos/team/repo/pulls') - expect(listUrl.searchParams.get('state')).toBe('all') - expect(listUrl.searchParams.get('sort')).toBe('recentupdate') - expect(listUrl.searchParams.get('page')).toBe('1') - expect(listUrl.searchParams.get('limit')).toBe('50') - }) - - it('shares one /pulls scan across concurrent branch lookups (#8807)', async () => { - let listCalls = 0 - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - listCalls++ - return Response.json([giteaPr(7, 'feature/a'), giteaPr(8, 'feature/b')]) - }) - vi.stubGlobal('fetch', fetchMock) - - const [a, b, missing] = await Promise.all([ - getGiteaPullRequestForBranch('/repo', 'feature/a'), - getGiteaPullRequestForBranch('/repo', 'feature/b'), - getGiteaPullRequestForBranch('/repo', 'feature/none') - ]) - - expect(a?.number).toBe(7) - expect(b?.number).toBe(8) - expect(missing).toBeNull() - expect(listCalls).toBe(1) - }) - - it('reuses the cached /pulls scan for lookups inside the TTL', async () => { - let listCalls = 0 - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - listCalls++ - return Response.json([giteaPr()]) - }) - vi.stubGlobal('fetch', fetchMock) - - await getGiteaPullRequestForBranch('/repo', 'feature/gitea') - await getGiteaPullRequestForBranch('/repo', 'feature/gitea') - await getGiteaPullRequestForBranch('/repo', 'no-pr-branch') - - expect(listCalls).toBe(1) - }) - - it('retries a failed /pulls scan after only the short failure cooldown', async () => { - vi.useFakeTimers() - try { - let listCalls = 0 - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - listCalls++ - return listCalls === 1 - ? Response.json({ message: 'temporary failure' }, { status: 503 }) - : Response.json([giteaPr()]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toBeNull() - await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toBeNull() - expect(listCalls).toBe(1) - - await vi.advanceTimersByTimeAsync(3_001) - await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toMatchObject({ - number: 7 - }) - expect(listCalls).toBe(2) - } finally { - vi.useRealTimers() - } - }) - it('surfaces a lookup failure via getGiteaPullRequestForBranchOrThrow instead of null (finding 4)', async () => { const fetchMock = vi.fn(async () => Response.json({ message: 'unauthorized' }, { status: 403 })) vi.stubGlobal('fetch', fetchMock) @@ -279,16 +110,6 @@ describe('Gitea client', () => { expect(requested).toEqual(['/api/v1/repos/team/repo/pulls', '/api/v1/repos/team/repo/pulls/42']) }) - it('resolves null without throwing when the scan returns an empty list', async () => { - // Gitea has no head-branch filter, so "branch has no PR" arrives as a 200 - // with [] from the repo listing — never a 404. The throwing variant must - // still report that as a plain "no PR". - const fetchMock = vi.fn(async () => Response.json([])) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranchOrThrow('/repo', 'feature/gitea')).resolves.toBeNull() - }) - it('still throws on a 404 from the /pulls scan so a misconfigured host is not read as "no PR"', async () => { const fetchMock = vi.fn(async () => Response.json({ message: 'not found' }, { status: 404 })) vi.stubGlobal('fetch', fetchMock) @@ -300,38 +121,6 @@ describe('Gitea client', () => { ) }) - it('expires successful scans and bounds retained repository listings', async () => { - vi.useFakeTimers() - try { - let listCalls = 0 - const fetchMock = vi.fn(async (url: string) => { - const parsed = new URL(url) - if (parsed.pathname.endsWith('/status')) { - return Response.json({ state: 'success' }) - } - listCalls++ - return Response.json([giteaPr()]) - }) - vi.stubGlobal('fetch', fetchMock) - - await getGiteaPullRequestForBranch('/repo', 'feature/gitea') - expect(_getGiteaPullRequestScanCacheSize()).toBe(1) - await vi.advanceTimersByTimeAsync(30_001) - expect(_getGiteaPullRequestScanCacheSize()).toBe(0) - await getGiteaPullRequestForBranch('/repo', 'feature/gitea') - expect(listCalls).toBe(2) - - await Promise.all( - Array.from({ length: 40 }, (_, index) => - scanGiteaPullRequests(`repo-${index}`, async () => [], 50, 5) - ) - ) - expect(_getGiteaPullRequestScanCacheSize()).toBe(32) - } finally { - vi.useRealTimers() - } - }) - it('does not let an in-flight scan re-cache results from before an invalidation', async () => { let releaseFirstScan!: () => void const firstScanGate = new Promise((resolve) => { @@ -366,71 +155,6 @@ describe('Gitea client', () => { expect(listCalls).toBe(2) }) - it('uses an API base URL override for subpath or non-standard deployments', async () => { - process.env.ORCA_GITEA_API_BASE_URL = 'https://git.example.com/code' - const fetchMock = vi.fn(async (url: string | URL) => { - if (String(url).includes('/commits/abc123/status')) { - return Response.json({ state: 'pending' }) - } - return Response.json([giteaPr()]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toMatchObject({ - number: 7, - status: 'pending' - }) - expect(String(fetchMock.mock.calls[0]?.[0])).toContain( - 'https://git.example.com/code/api/v1/repos/team/repo/pulls' - ) - }) - - it('falls back to a linked PR number when branch lookup misses', async () => { - const fetchMock = vi.fn(async (url: string | URL) => { - const requestUrl = String(url) - if (requestUrl.includes('/commits/abc123/status')) { - return Response.json({ state: 'success' }) - } - if (requestUrl.endsWith('/pulls/42')) { - return Response.json(giteaPr(42, 'renamed-local-branch')) - } - return Response.json([]) - }) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaPullRequestForBranch('/repo', 'local-name', 42)).resolves.toMatchObject({ - number: 42, - status: 'success' - }) - }) - - it('reports configured token auth without a global API base URL', async () => { - await expect(getGiteaAuthStatus()).resolves.toEqual({ - configured: true, - authenticated: true, - account: null, - baseUrl: null, - tokenConfigured: true - }) - }) - - it('verifies token auth when a global API base URL is configured', async () => { - process.env.ORCA_GITEA_API_BASE_URL = 'https://git.example.com' - const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) => - Response.json({ login: 'gitea-user' }) - ) - vi.stubGlobal('fetch', fetchMock) - - await expect(getGiteaAuthStatus()).resolves.toEqual({ - configured: true, - authenticated: true, - account: 'gitea-user', - baseUrl: 'https://git.example.com/api/v1', - tokenConfigured: true - }) - expect(String(fetchMock.mock.calls[0]?.[0])).toBe('https://git.example.com/api/v1/user') - }) - it('cancels unread error-response bodies so bundled undici cannot crash on socket close', async () => { let cancelledBodies = 0 const fetchMock = vi.fn(async () => diff --git a/src/main/github/image-attachment-upload.test.ts b/src/main/github/image-attachment-upload.test.ts deleted file mode 100644 index ac06daecb61..00000000000 --- a/src/main/github/image-attachment-upload.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { uploadGitHubImage } from './image-attachment-upload' - -const { gh, acquire, release } = vi.hoisted(() => ({ - gh: vi.fn(), - acquire: vi.fn(), - release: vi.fn() -})) -vi.mock('./gh-utils', () => ({ - ghExecFileAsync: gh, - acquire, - release, - extractExecError: (error: unknown) => ({ stdout: '', stderr: String(error) }) -})) -const repository = { - ownerRepo: { owner: 'octo', repo: 'images', host: 'github.com' }, - ghOptions: { host: 'github.com' } -} -beforeEach(() => { - vi.resetAllMocks() -}) -describe('GitHub image attachment upload', () => { - it('posts unchanged binary bytes with their exact content length in the resolved gh context', async () => { - gh.mockResolvedValueOnce({ stdout: '{"id":123}' }).mockResolvedValueOnce({ - stdout: '{"url":"https://github.com/user-attachments/assets/a"}' - }) - await expect( - uploadGitHubImage({ - fileName: 'screen.png', - contentType: 'image/png', - bytes: Buffer.from([0, 128, 255]), - resolve: async () => repository - }) - ).resolves.toEqual({ url: 'https://github.com/user-attachments/assets/a' }) - expect(gh.mock.calls[0][0]).toEqual(['api', '--cache', '60s', 'repos/octo/images']) - expect(gh.mock.calls[1][0]).toEqual([ - 'api', - '-X', - 'POST', - 'https://uploads.github.com/user-attachments/assets?name=screen.png&content_type=image%2Fpng&repository_id=123', - '-H', - 'Content-Type: application/octet-stream', - '-H', - 'Content-Length: 3', - '--input', - '-' - ]) - expect(gh.mock.calls[1][1]).toMatchObject({ - host: 'github.com', - idempotent: false, - stdin: Buffer.from([0, 128, 255]) - }) - }) - it('reports a rejected upload without retrying the POST and releases admission', async () => { - gh.mockResolvedValueOnce({ stdout: '{"id":123}' }).mockRejectedValueOnce(new Error('HTTP 403')) - await expect( - uploadGitHubImage({ - fileName: 'screen.png', - contentType: 'image/png', - bytes: Buffer.from([1]), - resolve: async () => repository - }) - ).rejects.toThrow('selected account') - expect(gh.mock.calls.map((call) => call[0][0])).toEqual(['api', 'api']) - expect(release).toHaveBeenCalledOnce() - }) -}) diff --git a/src/main/gitlab/client.test.ts b/src/main/gitlab/client.test.ts deleted file mode 100644 index 8e661d6e73a..00000000000 --- a/src/main/gitlab/client.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as GlUtils from './gl-utils' - -const { - glabExecFileAsyncMock, - getGlabKnownHostsMock, - getProjectRefMock, - acquireMock, - releaseMock -} = vi.hoisted(() => ({ - glabExecFileAsyncMock: vi.fn(), - getGlabKnownHostsMock: vi.fn(), - getProjectRefMock: vi.fn(), - acquireMock: vi.fn(), - releaseMock: vi.fn() -})) - -vi.mock('./gl-utils', async () => { - const actual = await vi.importActual('./gl-utils') - return { - ...actual, - glabExecFileAsync: glabExecFileAsyncMock, - getGlabKnownHosts: getGlabKnownHostsMock, - getProjectRef: getProjectRefMock, - acquire: acquireMock, - release: releaseMock - } -}) - -import { getAuthenticatedViewer, getWorkItemByProjectRef, listTodos } from './client' - -describe('gitlab client — viewer & paste-URL lookup', () => { - beforeEach(() => { - glabExecFileAsyncMock.mockReset() - getGlabKnownHostsMock.mockReset() - getProjectRefMock.mockReset() - acquireMock.mockReset() - releaseMock.mockReset() - acquireMock.mockResolvedValue(undefined) - getGlabKnownHostsMock.mockResolvedValue(['gitlab.com']) - getProjectRefMock.mockResolvedValue(null) - }) - - describe('getAuthenticatedViewer', () => { - it('returns username + email when glab api user succeeds', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ username: 'alice', email: 'alice@example.com' }) - }) - await expect(getAuthenticatedViewer()).resolves.toEqual({ - username: 'alice', - email: 'alice@example.com' - }) - expect(glabExecFileAsyncMock).toHaveBeenCalledWith(['api', 'user']) - expect(acquireMock).toHaveBeenCalledOnce() - expect(releaseMock).toHaveBeenCalledOnce() - }) - - it('coerces a missing email to null', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ username: 'alice', email: null }) - }) - await expect(getAuthenticatedViewer()).resolves.toEqual({ - username: 'alice', - email: null - }) - }) - - it('returns null when glab fails', async () => { - glabExecFileAsyncMock.mockRejectedValueOnce(new Error('not authenticated')) - await expect(getAuthenticatedViewer()).resolves.toBeNull() - expect(releaseMock).toHaveBeenCalledOnce() - }) - - it('returns null when username is empty', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ username: ' ', email: null }) - }) - await expect(getAuthenticatedViewer()).resolves.toBeNull() - }) - }) - - describe('getWorkItemByProjectRef', () => { - it('fetches an MR and maps to GitLabWorkItem', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ - id: 100, - iid: 5, - title: 't', - state: 'opened', - web_url: 'https://gitlab.com/g/p/-/merge_requests/5', - source_branch: 'feat', - target_branch: 'main' - }) - }) - const item = await getWorkItemByProjectRef( - '/repo', - { host: 'gitlab.com', path: 'g/p' }, - 5, - 'mr' - ) - expect(item).toMatchObject({ type: 'mr', number: 5, branchName: 'feat' }) - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/merge_requests/5'], - { cwd: '/repo' } - ) - }) - - it('fetches an issue and maps to GitLabWorkItem', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ - id: 200, - iid: 9, - title: 'bug', - state: 'opened', - web_url: 'https://gitlab.com/g/p/-/issues/9' - }) - }) - const item = await getWorkItemByProjectRef( - '/repo', - { host: 'gitlab.com', path: 'g/p' }, - 9, - 'issue' - ) - expect(item).toMatchObject({ type: 'issue', number: 9 }) - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/issues/9'], - { cwd: '/repo' } - ) - }) - - it('passes self-hosted hostname for local pasted URL lookups', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ - id: 201, - iid: 9, - title: 'bug', - state: 'opened', - web_url: 'https://gitlab.internal/g/p/-/issues/9' - }) - }) - - await getWorkItemByProjectRef('/repo', { host: 'gitlab.internal', path: 'g/p' }, 9, 'issue') - - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', '--hostname', 'gitlab.internal', 'projects/g%2Fp/issues/9'], - { cwd: '/repo' } - ) - }) - - it('routes local WSL pasted-project lookup through glab execution options', async () => { - const localGitOptions = { wslDistro: 'Ubuntu' } - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ - id: 201, - iid: 9, - title: 'WSL bug', - state: 'opened', - web_url: 'https://gitlab.com/g/p/-/issues/9' - }) - }) - - await getWorkItemByProjectRef( - '/repo', - { host: 'gitlab.com', path: 'g/p' }, - 9, - 'issue', - null, - localGitOptions - ) - - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/issues/9'], - { - cwd: '/repo', - wslDistro: 'Ubuntu' - } - ) - }) - - it('returns null when the API errors', async () => { - glabExecFileAsyncMock.mockRejectedValueOnce(new Error('not found')) - const item = await getWorkItemByProjectRef( - '/repo', - { host: 'gitlab.com', path: 'g/p' }, - 9, - 'issue' - ) - expect(item).toBeNull() - }) - }) - - describe('listTodos', () => { - it('maps glab todos response to GitLabTodo shape', async () => { - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify([ - { - id: 1, - action_name: 'assigned', - target_type: 'MergeRequest', - target: { - iid: 42, - title: 'Add feature', - web_url: 'https://gitlab.com/g/p/-/merge_requests/42' - }, - target_url: 'https://gitlab.com/g/p/-/merge_requests/42', - author: { username: 'alice', avatar_url: 'https://example.com/a.png' }, - project: { path_with_namespace: 'g/p' }, - updated_at: '2026-05-08T10:00:00Z', - state: 'pending' - } - ]) - }) - - await expect(listTodos('/repo')).resolves.toEqual([ - { - id: 1, - actionName: 'assigned', - targetType: 'MergeRequest', - targetIid: 42, - targetTitle: 'Add feature', - targetUrl: 'https://gitlab.com/g/p/-/merge_requests/42', - projectPath: 'g/p', - authorUsername: 'alice', - authorAvatarUrl: 'https://example.com/a.png', - updatedAt: '2026-05-08T10:00:00Z', - state: 'pending' - } - ]) - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', 'todos?state=pending&per_page=50'], - { cwd: '/repo' } - ) - }) - - it('routes local WSL todos through project resolution and glab execution options', async () => { - const localGitOptions = { wslDistro: 'Ubuntu' } - getProjectRefMock.mockResolvedValueOnce({ host: 'gitlab.com', path: 'g/p' }) - glabExecFileAsyncMock.mockResolvedValueOnce({ stdout: '[]' }) - - await expect(listTodos('/repo', null, localGitOptions)).resolves.toEqual([]) - - expect(getProjectRefMock).toHaveBeenCalledWith('/repo', ['gitlab.com'], null, localGitOptions) - expect(glabExecFileAsyncMock).toHaveBeenCalledWith( - ['api', 'todos?state=pending&per_page=50'], - { cwd: '/repo', wslDistro: 'Ubuntu' } - ) - }) - - it('coerces non-pending state values to pending (defensive)', async () => { - // Preserve the pending/done union even if future API output ignores the pending filter. - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify([ - { id: 2, action_name: 'mentioned', target_type: 'Issue', state: 'weird' } - ]) - }) - const result = await listTodos('/repo') - expect(result[0].state).toBe('pending') - }) - - it('falls back to empty list when glab errors', async () => { - glabExecFileAsyncMock.mockRejectedValueOnce(new Error('auth failed')) - await expect(listTodos('/repo')).resolves.toEqual([]) - }) - - it('handles missing target / project / author fields gracefully', async () => { - // Commit/Note todos can omit target; retain a well-formed display record. - glabExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify([{ id: 3, action_name: 'build_failed', target_type: 'Commit' }]) - }) - const result = await listTodos('/repo') - expect(result[0]).toMatchObject({ - targetIid: null, - targetTitle: '', - targetUrl: '', - projectPath: '', - authorUsername: '', - authorAvatarUrl: '' - }) - }) - }) -}) diff --git a/src/main/grok/hook-service.test.ts b/src/main/grok/hook-service.test.ts index c4d7a1c4bcd..95962b53c54 100644 --- a/src/main/grok/hook-service.test.ts +++ b/src/main/grok/hook-service.test.ts @@ -26,13 +26,8 @@ vi.mock('os', async () => { homedir: homedirMock } }) - -import { getGrokToolEventMatcherForTests, GrokHookService } from './hook-service' +import { GrokHookService } from './hook-service' import { buildWindowsGrokHookScript } from './windows-grok-hook-script' -import { - POSIX_HOOK_JSON_STDIN_PRELUDE, - POSIX_HOOK_JSON_STDIN_READER -} from '../agent-hooks/hook-stdin-contract' const GROK_SCRIPT_FILE_NAME = process.platform === 'win32' ? 'grok-hook.cmd' : 'grok-hook.sh' const WINDOWS_POWERSHELL_LAUNCHER = @@ -129,21 +124,6 @@ describe('GrokHookService', () => { rmSync(homeDir, { recursive: true, force: true }) }) - it('guards Windows GROK_HOME substring checks when empty (#9358)', () => { - const script = buildWindowsGrokHookScript() - expect(script).toContain('set "ORCA_GROK_HOME="') - expect(script).toContain('if not defined GROK_HOME goto :orca_grok_home_ready') - expect(script).toContain('%GROK_HOME:~4096,1%') - expect(script).toContain('set "ORCA_GROK_HOME=%GROK_HOME:"=%"') - expect(script).toContain('%ORCA_GROK_HOME:~4096,1%') - expect(script).toContain(':orca_grok_home_ready') - expect(script).toContain('if not defined ORCA_GROK_HOME goto :orca_grok_home_ready') - expect(script).toContain('if "%ORCA_GROK_HOME:~-1%"=="\\"') - expect(script).toContain('if not "%GROK_HOME:~4096,1%"=="" goto :orca_grok_home_ready') - // Parenthesized blocks still expand percent variables before their guard executes. - expect(script).not.toMatch(/if defined GROK_HOME \(/) - }) - it.skipIf(process.platform !== 'win32')( 'generated grok-hook.cmd exits 0 when GROK_HOME is unset (#9358)', async () => { @@ -229,126 +209,7 @@ describe('GrokHookService', () => { } ) - it('installs a dedicated global Grok hook config and managed script', () => { - const status = new GrokHookService().install() - - expect(status.state).toBe('installed') - expect(status.configPath).toBe(join(homeDir, '.grok', 'hooks', 'orca-status.json')) - expect(status.managedHooksPresent).toBe(true) - - const config = JSON.parse( - readFileSync(join(homeDir, '.grok', 'hooks', 'orca-status.json'), 'utf8') - ) as { - hooks: Record - } - expect(Object.keys(config.hooks).sort()).toEqual( - [ - 'Notification', - 'PreToolUse', - 'PostToolUse', - 'PostToolUseFailure', - 'SessionEnd', - 'SessionStart', - 'Stop', - 'StopCancelled', - 'StopFailure', - 'UserPromptSubmit' - ].sort() - ) - // PreToolUse is needed for in-flight tools and question waits. - expect(config.hooks.PreToolUse[0].matcher).toBe('.*') - expect(registersManagedGrokScript(config.hooks.PreToolUse[0].hooks[0].command)).toBe(true) - // Grok uses real regex matchers; bare * is invalid. - expect(config.hooks.PostToolUseFailure[0].matcher).toBe('.*') - expect(config.hooks.PostToolUse[0].matcher).toBe('.*') - expect(config.hooks.StopCancelled[0].matcher).toBeUndefined() - expect(config.hooks.StopFailure[0].matcher).toBeUndefined() - expect(config.hooks.Notification[0].matcher).toBeUndefined() - expect(getGrokToolEventMatcherForTests()).toBe('.*') - expect(getGrokToolEventMatcherForTests()).not.toBe('*') - expect(new RegExp(getGrokToolEventMatcherForTests()).test('run_terminal_command')).toBe(true) - // Construct invalid regex dynamically to avoid static lint rejection. - const bareStar = ['*', ''].join('') - expect(() => new RegExp(bareStar)).toThrow() - expect(registersManagedGrokScript(config.hooks.PostToolUse[0].hooks[0].command)).toBe(true) - if (process.platform !== 'win32') { - const command = config.hooks.PostToolUse[0].hooks[0].command - expect(command).toContain(join(homeDir, '.orca')) - expect(command).toMatch(/^if \[ -n "\$\{ORCA_PANE_KEY-\}" \] && /) - } - - const script = readFileSync( - join(homeDir, '.orca', 'agent-hooks', GROK_SCRIPT_FILE_NAME), - 'utf8' - ) - expect(script).toContain('/hook/grok') - if (process.platform === 'win32') { - expect(script).toContain('%SystemRoot%\\System32\\curl.exe') - expect(script).toContain('set "ORCA_GROK_HOME=%GROK_HOME:"=%"') - expect(script).toContain('--data-urlencode "grokHome=%ORCA_GROK_HOME%"') - } else { - // Stream payload to curl stdin; reader variables must be initialized before capture. - const prelude = POSIX_HOOK_JSON_STDIN_PRELUDE.join('\n') - expect(script).toContain(prelude) - expect(script.indexOf(prelude)).toBeLessThan( - script.indexOf(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`) - ) - expect(script).toContain(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`) - expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl') - expect(script).toContain('--data-urlencode "payload@-"') - expect(script).toContain('${#GROK_HOME}" -le 4096') - expect(script).toContain('--data-urlencode "grokHome=${grok_home}"') - expect(script).not.toContain('--data-urlencode "payload=${payload}"') - } - }) - // Profile-path spaces need the encoded fallback so Grok cannot split the managed path. - it.skipIf(process.platform !== 'win32')( - 'wraps the managed hook command to survive spaces in the profile path (#6078)', - () => { - const spaceHome = join(tmpdir(), 'orca grok home with spaces') - mkdirSync(spaceHome, { recursive: true }) - homedirMock.mockReturnValue(spaceHome) - try { - expect(new GrokHookService().install().state).toBe('installed') - - const config = JSON.parse( - readFileSync(join(spaceHome, '.grok', 'hooks', 'orca-status.json'), 'utf8') - ) as { hooks: Record } - - for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) { - const command = config.hooks[eventName]?.[0]?.hooks?.[0]?.command - expect(command).toMatch(WINDOWS_POWERSHELL_LAUNCHER) - } - } finally { - rmSync(spaceHome, { recursive: true, force: true }) - } - } - ) - - it('installs hooks under GROK_HOME when set', () => { - const grokHome = mkdtempSync(join(tmpdir(), 'orca-grok-home-env-')) - const previous = process.env.GROK_HOME - process.env.GROK_HOME = grokHome - try { - const status = new GrokHookService().install() - expect(status.state).toBe('installed') - expect(status.configPath).toBe(join(grokHome, 'hooks', 'orca-status.json')) - expect(readFileSync(join(grokHome, 'hooks', 'orca-status.json'), 'utf8')).toContain( - 'SessionStart' - ) - expect(() => - readFileSync(join(homeDir, '.grok', 'hooks', 'orca-status.json'), 'utf8') - ).toThrow() - } finally { - if (previous === undefined) { - delete process.env.GROK_HOME - } else { - process.env.GROK_HOME = previous - } - rmSync(grokHome, { recursive: true, force: true }) - } - }) it('does not restore a user-cleared global hook config on the next launch', () => { const service = new GrokHookService() @@ -375,15 +236,6 @@ describe('GrokHookService', () => { expect(readFileSync(configPath, 'utf8')).toBe(userClearedConfig) }) - it('removes the empty managed config when hooks are disabled', () => { - const service = new GrokHookService() - const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json') - - service.install() - expect(service.remove().state).toBe('not_installed') - expect(() => readFileSync(configPath, 'utf8')).toThrow() - }) - it('removes the managed config even when a non-hook key remains', () => { const service = new GrokHookService() const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json') @@ -400,18 +252,6 @@ describe('GrokHookService', () => { }) // Respect startup opt-out while allowing explicit re-enabling to override it. - it('leaves a user-cleared config alone on a startup install', () => { - const service = new GrokHookService() - const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json') - const userCleared = '{ "hooks" : {} }\n' - - expect(service.install().state).toBe('installed') - writeFileSync(configPath, userCleared, 'utf8') - - service.install() - - expect(readFileSync(configPath, 'utf8')).toBe(userCleared) - }) it('restores a user-cleared config when the user turns hooks back on', () => { const service = new GrokHookService() diff --git a/src/main/hermes/hook-service.test.ts b/src/main/hermes/hook-service.test.ts index 116eefa6d95..35026b358eb 100644 --- a/src/main/hermes/hook-service.test.ts +++ b/src/main/hermes/hook-service.test.ts @@ -31,24 +31,6 @@ describe('HermesHookService', () => { rmSync(homeDir, { recursive: true, force: true }) }) - it('installs the managed Hermes plugin and enables it in config.yaml', () => { - const status = new HermesHookService().install() - - expect(status).toMatchObject({ - agent: 'hermes', - state: 'installed', - managedHooksPresent: true, - detail: null - }) - const pluginDir = join(homeDir, 'plugins', _internals.HERMES_PLUGIN_NAME) - expect(readFileSync(join(pluginDir, 'plugin.yaml'), 'utf-8')).toContain('provides_hooks:') - expect(readFileSync(join(pluginDir, '__init__.py'), 'utf-8')).toContain('/hook/hermes') - const config = parse(readFileSync(join(homeDir, 'config.yaml'), 'utf-8')) as { - plugins: { enabled: string[] } - } - expect(config.plugins.enabled).toContain(_internals.HERMES_PLUGIN_NAME) - }) - it('preserves other enabled plugins and removes Orca from disabled list', () => { writeFileSync( join(homeDir, 'config.yaml'), @@ -217,38 +199,6 @@ describe('HermesHookService', () => { expect(config.plugins.disabled).toBe('not-a-list') }) - it('reports partial when the plugin exists but is not enabled', () => { - new HermesHookService().install() - const update = _internals.updateConfigContent( - readFileSync(join(homeDir, 'config.yaml'), 'utf-8'), - _internals.disablePlugin - ) - expect(update.content).toBeTruthy() - writeFileSync(join(homeDir, 'config.yaml'), update.content!, 'utf-8') - - const status = new HermesHookService().getStatus() - - expect(status.state).toBe('partial') - expect(status.detail).toContain('not enabled') - }) - - it('is visible to the real hermes CLI when hermes is installed', () => { - const hermesAvailable = spawnSync('hermes', ['--version'], { encoding: 'utf-8' }).status === 0 - if (!hermesAvailable) { - return - } - new HermesHookService().install() - - const output = execFileSync('hermes', ['plugins', 'list'], { - env: { ...process.env, HERMES_HOME: homeDir }, - encoding: 'utf-8', - timeout: 15_000 - }) - - expect(output).toContain(_internals.HERMES_PLUGIN_NAME) - expect(output.toLowerCase()).toContain('enabled') - }, 20_000) - it('registered plugin hooks post normalized JSON to Orca', async () => { const pythonAvailable = spawnSync('python3', ['--version'], { encoding: 'utf-8' }).status === 0 if (!pythonAvailable) { diff --git a/src/main/ipc/browser/desktop-browser-bridge-host-control.test.ts b/src/main/ipc/browser/desktop-browser-bridge-host-control.test.ts index bbf8dfbb8af..aaa228f967c 100644 --- a/src/main/ipc/browser/desktop-browser-bridge-host-control.test.ts +++ b/src/main/ipc/browser/desktop-browser-bridge-host-control.test.ts @@ -5,8 +5,6 @@ vi.mock('electron', () => ({ ipcMain: { handle: vi.fn(), removeHandler: vi.fn() import type { SshTarget } from '../../../shared/ssh-types' import { disallowDesktopBrowserBridgeHost, - noticeDesktopBrowserBridgeHostControl, - stopDesktopBrowserBridgeHost, type DesktopBrowserBridgeHostControlDeps } from './desktop-browser-bridge-host-control' import { @@ -15,8 +13,6 @@ import { setDesktopBrowserBridgeHostStopped } from './desktop-browser-bridge-reconcile' -const NOTICE_CHANNEL = 'browser:remoteHostControlNotice' - function setup(targets: SshTarget[] = []) { let noticed: string[] | undefined const byId = new Map(targets.map((target) => [target.id, target])) @@ -74,52 +70,6 @@ describe('desktop browser bridge host control', () => { setDesktopBrowserBridgeControl(null) }) - it('tells the person once per host, and waits for a window before counting it shown', () => { - const { deps, sent, mainWindow } = setup([userHost, orcaOwnedHost]) - const host = { targetId: userHost.id, hostLabel: 'build-box' } - - noticeDesktopBrowserBridgeHostControl(host, () => null, deps) - expect(sent).toEqual([]) - - noticeDesktopBrowserBridgeHostControl(host, mainWindow, deps) - noticeDesktopBrowserBridgeHostControl(host, mainWindow, deps) - noticeDesktopBrowserBridgeHostControl( - { targetId: orcaOwnedHost.id, hostLabel: 'sandbox' }, - mainWindow, - deps - ) - - expect(sent).toEqual([ - { - channel: NOTICE_CHANNEL, - payload: { targetId: 'ssh-1', hostLabel: 'build-box', canDisallow: true } - }, - { - channel: NOTICE_CHANNEL, - payload: { targetId: 'runtime-ssh-1', hostLabel: 'sandbox', canDisallow: false } - } - ]) - expect(deps.settings?.getSettings().desktopBrowserBridgeNoticedTargetIds).toEqual([ - 'ssh-1', - 'runtime-ssh-1' - ]) - }) - - it('Stop detaches the host and clears its tabs without touching its setting', () => { - const { deps, reconcile, byId } = setup([userHost]) - - stopDesktopBrowserBridgeHost(userHost.id, deps) - - expect(isDesktopBrowserBridgeHostStopped(userHost.id)).toBe(true) - expect(reconcile).toHaveBeenCalledTimes(1) - expect(deps.endHostActivity).toHaveBeenCalledWith('ssh-1') - expect(byId.get(userHost.id)?.allowRemoteCliControl).toBeUndefined() - - setDesktopBrowserBridgeHostStopped(userHost.id, false) - expect(isDesktopBrowserBridgeHostStopped(userHost.id)).toBe(false) - expect(reconcile).toHaveBeenCalledTimes(2) - }) - it("Don't allow turns the host's setting off, and only stops hosts Orca created", () => { const { deps, byId } = setup([userHost, orcaOwnedHost]) diff --git a/src/main/ipc/launchable-file-type.test.ts b/src/main/ipc/launchable-file-type.test.ts index d8167ee84c2..de505329b39 100644 --- a/src/main/ipc/launchable-file-type.test.ts +++ b/src/main/ipc/launchable-file-type.test.ts @@ -106,14 +106,4 @@ describe('classifyLinkedFile', () => { } expect(classifyLinkedFile(facts, platform, PATHEXT)).toBe(expected) }) - - it('reveals sockets, devices and other non-regular files', () => { - const facts = { - names: ['/x/sock'], - isFile: false, - isDirectory: false, - mode: 0o755 - } - expect(classifyLinkedFile({ ...facts, head: new Uint8Array() }, 'linux')).toBe('reveal') - }) }) diff --git a/src/main/ipc/linked-file-inspection.test.ts b/src/main/ipc/linked-file-inspection.test.ts index b3a624c7e06..ea61f61d039 100644 --- a/src/main/ipc/linked-file-inspection.test.ts +++ b/src/main/ipc/linked-file-inspection.test.ts @@ -24,11 +24,6 @@ describe.skipIf(process.platform === 'win32')('inspectLinkedFile', () => { return path } - it('opens documents and shows scripts as text', async () => { - expect(await inspectLinkedFile(await file('notes.md', '# Notes\n'))).toBe('open') - expect(await inspectLinkedFile(await file('run.sh', 'echo hi\n'))).toBe('view-as-text') - }) - it('reads the content of +x files whatever their name', async () => { expect(await inspectLinkedFile(await file('notes.md', '#!/bin/sh\n', 0o755))).toBe( 'view-as-text' diff --git a/src/main/ipc/mobile-pairing-offer-handlers.test.ts b/src/main/ipc/mobile-pairing-offer-handlers.test.ts deleted file mode 100644 index 0cc4fc0fde0..00000000000 --- a/src/main/ipc/mobile-pairing-offer-handlers.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import '../runtime/rpc/unused-default-rpc-methods.test-fixture' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as NodeOs from 'node:os' - -// Pins the desktop's Settings pairing handlers: every branch's reply and the runtime calls it makes. -const { handleMock, networkInterfacesMock } = vi.hoisted(() => ({ - handleMock: vi.fn(), - networkInterfacesMock: vi.fn() -})) - -vi.mock('electron', () => ({ - app: { isPackaged: false }, - ipcMain: { handle: handleMock }, - shell: { openExternal: vi.fn() } -})) - -vi.mock('os', async (importOriginal) => ({ - ...(await importOriginal()), - networkInterfaces: networkInterfacesMock -})) - -import type { MobilePairingQrResult } from '../runtime/rpc/server/pairing/mobile-pairing-qr' -import { - MOBILE_PAIRING_OFFER_TTL_MS, - RUNTIME_PAIRING_OFFER_TTL_MS -} from '../runtime/rpc/server/pairing/pairing-offer-expiry' -import { registerMobileHandlers } from './mobile' - -const LAN = { - en0: [{ address: '192.168.1.20', family: 'IPv4', internal: false, netmask: '255.255.255.0' }] -} - -type Call = { method: string; args?: unknown } - -function fakeServer(options: { - mobileOffer?: Record - runtimeOffer?: Record - exposureFails?: boolean -}) { - const calls: Call[] = [] - const server = { - getNetworkExposureBindHost: () => '0.0.0.0', - ensureNetworkExposure: async () => { - calls.push({ method: 'ensureNetworkExposure' }) - if (options.exposureFails) { - throw new Error('bind refused') - } - }, - createMobilePairingOffer: async (args: Record) => { - calls.push({ method: 'createMobilePairingOffer', args: { ...args, name: typeof args.name } }) - return options.mobileOffer - }, - createPairingOffer: (args: Record) => { - calls.push({ method: 'createPairingOffer', args: { ...args, name: typeof args.name } }) - return options.runtimeOffer - } - } - return { server, calls } -} - -const MOBILE_OFFER = { - available: true, - pairingUrl: 'orca://pair#x', - endpoint: 'ws://192.168.1.20:6768', - deviceId: 'dev-1', - connectionMode: 'local-only', - webClientUrl: null, - offerExpiresAt: 1_000 -} -const RUNTIME_OFFER = { - available: true, - pairingUrl: 'orca://pair#r', - webClientUrl: 'http://192.168.1.20:6768/#r', - endpoint: 'ws://192.168.1.20:6768', - deviceId: 'dev-2', - offerExpiresAt: 2_000 -} - -describe('desktop pairing IPC handlers', () => { - const handlers = new Map unknown>() - const encodePairingQr = async (): Promise => ({ - ok: true, - qrDataUrl: 'data:qr', - qrSize: 21 - }) - - beforeEach(() => { - handlers.clear() - handleMock.mockReset() - handleMock.mockImplementation((channel: string, handler: (...args: unknown[]) => unknown) => { - handlers.set(channel, handler) - }) - networkInterfacesMock.mockReset().mockReturnValue(LAN) - vi.spyOn(console, 'error').mockImplementation(() => {}) - }) - - async function call( - channel: string, - server: ReturnType['server'], - args?: unknown - ) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handlers under test call only the four methods the fake implements. - registerMobileHandlers(server as never, { - encodePairingQr, - getDefaultRouteInterfaceNames: async () => new Set(['en0']) - }) - return handlers.get(channel)?.({}, args) - } - - describe('mobile:getPairingQR', () => { - it('mints at an explicit address', async () => { - const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER }) - expect( - await call('mobile:getPairingQR', server, { - address: '100.64.1.20', - connectionMode: 'local-only', - rotate: true - }) - ).toEqual({ - available: true, - qrDataUrl: 'data:qr', - qrSize: 21, - pairingUrl: 'orca://pair#x', - endpoint: 'ws://192.168.1.20:6768', - deviceId: 'dev-1', - connectionMode: 'local-only', - expiresAt: 1_000 - }) - expect(calls).toEqual([ - { - method: 'createMobilePairingOffer', - args: { - address: '100.64.1.20', - connectionMode: 'local-only', - rotate: true, - name: 'string', - offerTtlMs: MOBILE_PAIRING_OFFER_TTL_MS - } - } - ]) - }) - - it('falls back to the default LAN address', async () => { - const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER }) - await call('mobile:getPairingQR', server, { connectionMode: 'automatic' }) - expect(calls[0]?.args).toMatchObject({ address: '192.168.1.20' }) - }) - - it('refuses LAN-only pairing with no usable address and mints nothing', async () => { - networkInterfacesMock.mockReturnValue({}) - const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER }) - expect(await call('mobile:getPairingQR', server, { connectionMode: 'local-only' })).toEqual({ - available: false, - reason: 'invalid_advertised_endpoint', - guidance: - 'No reachable network address is available for pairing. Connect to Wi‑Fi or Tailscale, or pick an address manually.' - }) - expect(calls).toEqual([]) - }) - - it('still asks for a Relay offer with no address, and hides the unadvertised endpoint', async () => { - networkInterfacesMock.mockReturnValue({}) - const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER }) - expect(await call('mobile:getPairingQR', server, { connectionMode: 'automatic' })).toEqual( - expect.objectContaining({ available: true, endpoint: null }) - ) - expect(calls[0]?.args).toMatchObject({ address: null, connectionMode: 'automatic' }) - }) - - it('passes through an unavailable offer with its Relay failure', async () => { - const relayFailure = { kind: 'install_missing' } - const { server } = fakeServer({ - mobileOffer: { available: false, reason: 'relay_unavailable', guidance: 'g', relayFailure } - }) - expect(await call('mobile:getPairingQR', server, { connectionMode: 'automatic' })).toEqual({ - available: false, - reason: 'relay_unavailable', - guidance: 'g', - relayFailure - }) - }) - }) - - describe('mobile:getRuntimePairingUrl', () => { - it('widens, then mints a network-reach runtime link', async () => { - const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER }) - expect(await call('mobile:getRuntimePairingUrl', server, { rotate: true })).toEqual({ - available: true, - pairingUrl: 'orca://pair#r', - webClientUrl: 'http://192.168.1.20:6768/#r', - endpoint: 'ws://192.168.1.20:6768', - deviceId: 'dev-2', - expiresAt: 2_000 - }) - expect(calls).toEqual([ - { method: 'ensureNetworkExposure' }, - { - method: 'createPairingOffer', - args: { - address: '192.168.1.20', - rotate: true, - name: 'string', - scope: 'runtime', - reach: 'network', - offerTtlMs: RUNTIME_PAIRING_OFFER_TTL_MS - } - } - ]) - }) - - it('keeps this-computer reach on loopback without widening', async () => { - const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER }) - await call('mobile:getRuntimePairingUrl', server, { - address: '127.0.0.1', - reach: 'this-computer' - }) - expect(calls).toEqual([ - { - method: 'createPairingOffer', - args: expect.objectContaining({ address: '127.0.0.1', reach: 'this-computer' }) - } - ]) - }) - - it('widens for this-computer intent at a non-loopback address', async () => { - const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER }) - await call('mobile:getRuntimePairingUrl', server, { - address: '192.168.1.20', - reach: 'this-computer' - }) - expect(calls.map((entry) => entry.method)).toEqual([ - 'ensureNetworkExposure', - 'createPairingOffer' - ]) - expect(calls[1]?.args).toMatchObject({ reach: 'network' }) - }) - - it('reports a failed widening with guidance and mints nothing', async () => { - const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER, exposureFails: true }) - expect(await call('mobile:getRuntimePairingUrl', server)).toEqual({ - available: false, - reason: 'network_exposure_failed', - guidance: expect.any(String) - }) - expect(calls).toEqual([{ method: 'ensureNetworkExposure' }]) - }) - - it('answers a bare unavailable with no address or no offer', async () => { - networkInterfacesMock.mockReturnValue({}) - const none = fakeServer({ runtimeOffer: RUNTIME_OFFER }) - expect(await call('mobile:getRuntimePairingUrl', none.server)).toEqual({ available: false }) - expect(none.calls).toEqual([]) - - networkInterfacesMock.mockReturnValue(LAN) - const refused = fakeServer({ - runtimeOffer: { available: false, reason: 'device_registry_unavailable', guidance: 'g' } - }) - expect(await call('mobile:getRuntimePairingUrl', refused.server)).toEqual({ - available: false - }) - }) - }) -}) diff --git a/src/main/ipc/runtime-environments/runtime-environment-recipe-vm-wake.test.ts b/src/main/ipc/runtime-environments/runtime-environment-recipe-vm-wake.test.ts index 628f5cb5570..7dd7409d4f4 100644 --- a/src/main/ipc/runtime-environments/runtime-environment-recipe-vm-wake.test.ts +++ b/src/main/ipc/runtime-environments/runtime-environment-recipe-vm-wake.test.ts @@ -109,14 +109,6 @@ describe('a request to a recipe VM environment', () => { rmSync(userDataPath, { recursive: true, force: true }) }) - it('goes through to a running VM, holding a workspace open with its owner', async () => { - recordVm('running') - await expect(codeOf('none')).resolves.toBeNull() - expect(holdOpen).not.toHaveBeenCalled() - await expect(codeOf('wake')).resolves.toBeNull() - expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', false, 180_000, undefined) - }) - it.each(['suspended', 'resume_failed'] as const)( 'leaves a %s VM asleep for a request that does not open its workspace', async (status) => { @@ -127,18 +119,6 @@ describe('a request to a recipe VM environment', () => { } ) - it.each([ - ['up', null], - ['waking', 'runtime_environment_waking'], - ['wake-failed', 'runtime_environment_wake_failed'], - ['slept', 'runtime_environment_asleep'] - ] as const)('answers a held open that ended %s', async (opening, code) => { - recordVm('suspended') - ownerOpening = opening - await expect(codeOf('wake')).resolves.toBe(code) - expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', true, 180_000, undefined) - }) - it.each([ ['waking', 'runtime_environment_waking'], ['asleep', 'runtime_environment_asleep'] @@ -152,11 +132,6 @@ describe('a request to a recipe VM environment', () => { ) // A phone gives up after 30 seconds, so its open is held for less. - it("holds an open only as long as its caller's preparation allows", async () => { - recordVm('suspended') - await prepareRecipeVm(userDataPath, VM_ENVIRONMENT, 'wake', 25_000) - expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', true, 25_000, undefined) - }) // A Disconnect made before the open holds: no client's open wakes the VM behind that server. it('refuses an open of a server the user disconnected, before waking anything', async () => { @@ -211,10 +186,4 @@ describe('a request to a recipe VM environment', () => { await expect(codeOf('none', { id: 'env-manual', source: 'manual' })).resolves.toBeNull() expect(state).not.toHaveBeenCalled() }) - - it('says a suspended VM is asleep on a host that cannot wake it', async () => { - registerRecipeVmWaker(null) - recordVm('suspended') - await expect(codeOf('wake')).resolves.toBe('runtime_environment_asleep') - }) }) diff --git a/src/main/ipc/runtime-environments/runtime-environment-redundant-managed-removal.test.ts b/src/main/ipc/runtime-environments/runtime-environment-redundant-managed-removal.test.ts index a6c11b67ebe..2b5c065f67d 100644 --- a/src/main/ipc/runtime-environments/runtime-environment-redundant-managed-removal.test.ts +++ b/src/main/ipc/runtime-environments/runtime-environment-redundant-managed-removal.test.ts @@ -43,35 +43,6 @@ afterEach(() => { rmSync(userDataPath, { recursive: true, force: true }) }) -it('removes a managed entry through forgetRedundant while another entry manages the server', async () => { - const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key') - addManaged('managed-alias', 'ssh-2', 46_769, 'public-key') - forgetRedundant.mockResolvedValueOnce({ - outcome: 'forgotten', - verdict: 'unverifiable', - environmentId: 'managed', - sshTargetId: 'ssh-1' - }) - await expect(removeRedundantManagedPairing(userDataPath, environment)).resolves.toMatchObject({ - removed: { id: 'managed' } - }) - expect(forgetRedundant).toHaveBeenCalledWith('managed') -}) - -it('surfaces a refusal instead of reporting the entry removed', async () => { - const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key') - addManaged('managed-alias', 'ssh-2', 46_769, 'public-key') - forgetRedundant.mockResolvedValueOnce({ - outcome: 'refused', - verdict: 'live', - code: 'orcad_forget_no_managed_twin', - reason: 'No other saved entry manages this server anymore.' - }) - await expect(removeRedundantManagedPairing(userDataPath, environment)).rejects.toThrow( - 'No other saved entry manages this server anymore.' - ) -}) - it('leaves the pairing to the store guard when the other entry manages a different server', () => { const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key') addManaged('other', 'ssh-2', 46_769, 'other-key') diff --git a/src/main/ipc/runtime-environments/runtime-environment-request-preparation.test.ts b/src/main/ipc/runtime-environments/runtime-environment-request-preparation.test.ts index 5d476d8f7a6..18e6360355c 100644 --- a/src/main/ipc/runtime-environments/runtime-environment-request-preparation.test.ts +++ b/src/main/ipc/runtime-environments/runtime-environment-request-preparation.test.ts @@ -55,12 +55,4 @@ describe('a request whose own recipe VM wake re-paired its server', () => { } expect((await fence(1, 'before')).failure).toMatchObject({ ok: false }) }) - - it('holds a request that woke nothing to the current pairing', async () => { - prepared.current = { environment: AFTER_WAKE, before: AFTER_WAKE, wokenPairingRevision: null } - await expect(fence(2, 'after')).resolves.toMatchObject({ - failure: null, - expected: { pairingRevision: 2, runtimeId: 'after' } - }) - }) }) diff --git a/src/main/ipc/runtime-environments/runtime-environment-transport-recipe-vm-intent.test.ts b/src/main/ipc/runtime-environments/runtime-environment-transport-recipe-vm-intent.test.ts deleted file mode 100644 index d824d2e52ab..00000000000 --- a/src/main/ipc/runtime-environments/runtime-environment-transport-recipe-vm-intent.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -// #36923: the desktop's own calls and subscriptions tell the server preparation which request -// needs the server (only one that opens a workspace may wake a sleeping recipe VM) and hand it -// their abort signal, so a caller that gives up stops waiting on a wake. -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => ({ - resolveManaged: vi.fn( - async ( - _userDataPath: string, - id: string, - _preparation?: unknown, - _method?: string, - _signal?: AbortSignal - ) => ({ id, pairingRevision: 1 }) - ), - // The server comes back re-paired, so the caller's pairing fence ends the call right after - // its preparation: only what preparation was handed is under test. - prepareRequest: vi.fn( - async ( - _userDataPath: string, - id: string, - _preparation?: unknown, - _method?: string, - _signal?: AbortSignal - ) => ({ - environment: { id, pairingRevision: 2, createdAt: 0 }, - before: { id, pairingRevision: 2, createdAt: 0 }, - wokenPairingRevision: null - }) - ) -})) - -vi.mock('electron', () => ({ app: { getPath: () => '/user-data' } })) -vi.mock('../../../shared/remote-runtime/host-store/runtime-environment-store', () => ({ - resolveEnvironment: (_userDataPath: string, id: string) => ({ id, pairingRevision: 1 }), - markEnvironmentUsed: vi.fn() -})) -vi.mock('../../../shared/remote-runtime/host-catalog/runtime-environments', () => ({ - getPreferredPairedRuntimeConnection: () => ({ endpoint: 'ws://vm:6768' }) -})) -vi.mock('./runtime-environment-managed-tunnel', () => ({ - resolveManagedRuntimeEnvironment: mocks.resolveManaged, - prepareManagedRuntimeRequest: mocks.prepareRequest, - requireWslHostWithoutStart: vi.fn() -})) -vi.mock('../../../shared/remote-runtime/remote-runtime-client', () => ({ - subscribeRemoteRuntimeRequest: vi.fn(async () => ({ close: vi.fn() })) -})) -vi.mock('./runtime-environment-support-routing', () => ({ - shouldRouteSubscriptionBySupport: () => false -})) - -import { - callRuntimeEnvironment, - subscribeRuntimeEnvironment -} from './runtime-environment-transport-routing' - -const callbacks = { onEvent: vi.fn(), onClose: vi.fn() } - -describe('runtime environment transport and a sleeping recipe VM', () => { - beforeEach(() => { - mocks.resolveManaged.mockClear() - mocks.prepareRequest.mockClear() - }) - - it.each(['worktree.activate', 'worktree.ps'])( - 'prepares the server for a %s call with its method and signal', - async (method) => { - const signal = new AbortController().signal - const response = await callRuntimeEnvironment( - '/user-data', - 'env-vm', - method, - {}, - 1000, - 1, - undefined, - { preparation: { wslStart: 'never' }, signal } - ) - expect(response).toMatchObject({ ok: false, error: { code: 'runtime_environment_changed' } }) - expect(mocks.prepareRequest).toHaveBeenCalledWith( - '/user-data', - 'env-vm', - { wslStart: 'never' }, - method, - signal - ) - } - ) - - it('prepares the server for a subscription with its method and signal', async () => { - const signal = new AbortController().signal - await subscribeRuntimeEnvironment( - '/user-data', - 'env-vm', - 'runtime.clientEvents.subscribe', - undefined, - 1000, - callbacks, - () => true, - signal - ) - expect(mocks.resolveManaged).toHaveBeenCalledWith( - '/user-data', - 'env-vm', - undefined, - 'runtime.clientEvents.subscribe', - signal - ) - }) -}) diff --git a/src/main/ipc/speech.test.ts b/src/main/ipc/speech.test.ts deleted file mode 100644 index e73ea64bc4d..00000000000 --- a/src/main/ipc/speech.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { - handleMock, - fromWebContentsMock, - getSpeechModelManagerMock, - getSpeechSttServiceMock, - deleteLocalSpeechModelMock -} = vi.hoisted(() => ({ - handleMock: vi.fn(), - fromWebContentsMock: vi.fn(), - getSpeechModelManagerMock: vi.fn(), - getSpeechSttServiceMock: vi.fn(), - deleteLocalSpeechModelMock: vi.fn() -})) - -vi.mock('electron', () => ({ - app: { getPath: vi.fn(() => '/tmp/orca-speech-test') }, - BrowserWindow: { fromWebContents: fromWebContentsMock }, - ipcMain: { handle: handleMock, on: vi.fn() }, - safeStorage: { - decryptString: vi.fn(), - encryptString: vi.fn(() => Buffer.from('encrypted')), - isEncryptionAvailable: vi.fn(() => true) - }, - systemPreferences: { - getMediaAccessStatus: vi.fn(() => 'granted'), - askForMediaAccess: vi.fn(() => Promise.resolve(true)) - } -})) - -vi.mock('../speech/model-catalog', () => ({ - SPEECH_MODEL_CATALOG: [], - getCatalogModel: vi.fn(() => ({ id: 'model-1' })) -})) - -vi.mock('../speech/speech-runtime-service', () => ({ - getSpeechModelManager: getSpeechModelManagerMock, - getSpeechSttService: getSpeechSttServiceMock -})) - -vi.mock('../speech/speech-model-deletion', () => ({ - deleteLocalSpeechModel: deleteLocalSpeechModelMock -})) - -import { registerSpeechHandlers } from './speech' - -type SpeechDownloadHandler = (event: { sender: { id: number } }, modelId: string) => Promise - -function getHandler(channel: string): SpeechDownloadHandler { - const call = handleMock.mock.calls.find((entry) => entry[0] === channel) - if (!call) { - throw new Error(`${channel} handler not registered`) - } - return call[1] as SpeechDownloadHandler -} - -describe('registerSpeechHandlers', () => { - beforeEach(() => { - handleMock.mockReset() - fromWebContentsMock.mockReset() - getSpeechModelManagerMock.mockReset() - getSpeechSttServiceMock.mockReset() - deleteLocalSpeechModelMock.mockReset() - }) - - it('clears the model download progress callback when the window closes', async () => { - const clearProgressCallback = vi.fn() - let resolveDownload: () => void = () => {} - const closeHandlers: (() => void)[] = [] - const manager = { - setProgressCallback: vi.fn(() => clearProgressCallback), - downloadModel: vi.fn( - () => - new Promise((resolve) => { - resolveDownload = resolve - }) - ) - } - const window = { - isDestroyed: vi.fn(() => false), - webContents: { send: vi.fn() }, - once: vi.fn((_event: string, handler: () => void) => { - closeHandlers.push(handler) - }), - off: vi.fn() - } - getSpeechModelManagerMock.mockReturnValue(manager) - fromWebContentsMock.mockReturnValue(window) - registerSpeechHandlers({} as never) - - const pending = getHandler('speech:downloadModel')({ sender: { id: 7 } }, 'model-1') - closeHandlers[0]?.() - resolveDownload() - await pending - - expect(clearProgressCallback).toHaveBeenCalledTimes(1) - expect(window.off).toHaveBeenCalledWith('closed', expect.any(Function)) - }) -}) diff --git a/src/main/ipc/ssh/ssh-session-teardown-removal.test.ts b/src/main/ipc/ssh/ssh-session-teardown-removal.test.ts deleted file mode 100644 index b3ca18d94ac..00000000000 --- a/src/main/ipc/ssh/ssh-session-teardown-removal.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// #37949: every removal of a saved SSH target (Settings, `orca host rm`, a paired client, a recipe -// VM's cleanup) ends in removeRegisteredSshTarget, so the window's cleanup must happen there. -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => { - const settings: { hostSettingOverrides?: Record> } = {} - return { - removeTarget: vi.fn(), - send: vi.fn(), - settings, - updateSettings: vi.fn() - } -}) - -vi.mock('../../ssh/targets/ssh-target-registry', () => ({ - getSshTargetRegistryStore: () => ({ removeTarget: mocks.removeTarget }) -})) -vi.mock('./ssh-ipc-context', () => ({ - getCurrentMainWindow: () => ({ isDestroyed: () => false, webContents: { send: mocks.send } }), - connectionManager: { disconnect: vi.fn(async () => {}) }, - portForwardManager: null, - persistedStore: { - getSettings: () => mocks.settings, - updateSettings: mocks.updateSettings, - removeSshRemotePtyLeases: vi.fn() - } -})) -vi.mock('./ssh-target-lifecycle-queue', () => ({ - runTargetLifecycle: (_targetId: string, run: () => Promise) => run() -})) -vi.mock('../../managed-server/lifecycle/orcad-managed-tunnel', () => ({ - closeOrcadManagedTunnelsForTarget: vi.fn(async () => {}) -})) -vi.mock('../../browser/routing/local-host-partitions/local-ssh-browser-partitions', () => ({ - releaseLocalSshBrowserPartitionsForTarget: vi.fn(async () => {}) -})) -vi.mock('../../browser/routing/partitions/browser-route-partition-storage-runtime', () => ({ - clearBrowserRoutePartitionStorageForLocalSshTarget: vi.fn(async () => {}) -})) - -const { removeRegisteredSshTarget } = await import('./ssh-session-teardown') - -describe('removing a saved SSH target', () => { - beforeEach(() => { - vi.clearAllMocks() - mocks.settings.hostSettingOverrides = { - 'ssh:ssh-1': { displayLabel: 'Renamed', defaultWorktreeLocation: '/srv' }, - 'ssh:ssh-2': { displayLabel: 'Other' } - } - }) - - it("tells the window and drops the host's rename after the target is gone", async () => { - await removeRegisteredSshTarget('ssh-1') - - expect(mocks.removeTarget).toHaveBeenCalledWith('ssh-1') - expect(mocks.send).toHaveBeenCalledWith('ssh:target-removed', { targetId: 'ssh-1' }) - expect(mocks.removeTarget.mock.invocationCallOrder[0]).toBeLessThan( - mocks.send.mock.invocationCallOrder[0]! - ) - expect(mocks.updateSettings).toHaveBeenCalledWith( - { - hostSettingOverrides: { - 'ssh:ssh-1': { defaultWorktreeLocation: '/srv' }, - 'ssh:ssh-2': { displayLabel: 'Other' } - } - }, - { notifyListeners: true } - ) - }) -}) diff --git a/src/main/jcode/hook-service.test.ts b/src/main/jcode/hook-service.test.ts index c50bc144b21..349e169d5f5 100644 --- a/src/main/jcode/hook-service.test.ts +++ b/src/main/jcode/hook-service.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { parse as parseToml } from 'smol-toml' @@ -17,9 +17,7 @@ import { JcodeHookService } from './hook-service' import { getJcodeConfigPath, getJcodeManagedCommand, - getJcodeManagedScriptFileName, - getJcodeManagedScriptPath, - JCODE_HOOK_EVENTS + getJcodeManagedScriptPath } from './hook-settings' import { tomlQuoteString } from './hook-config' @@ -39,36 +37,6 @@ describe('JcodeHookService', () => { rmSync(homeDir, { recursive: true, force: true }) }) - it('reports not_installed before any install', () => { - const status = new JcodeHookService().getStatus() - expect(status).toMatchObject({ - agent: 'jcode', - state: 'not_installed', - configPath: getJcodeConfigPath() - }) - }) - - it('installs managed hooks into jcode config.toml and posts to /hook/jcode', () => { - const status = new JcodeHookService().install() - expect(status.state).toBe('installed') - expect(status.agent).toBe('jcode') - expect(status.configPath).toBe(getJcodeConfigPath()) - expect(status.managedHooksPresent).toBe(true) - - const config = readFileSync(getJcodeConfigPath(), 'utf8') - for (const event of JCODE_HOOK_EVENTS) { - // Commands are shell-quoted for jcode, then TOML-quoted for config storage. - expect(config).toContain( - `${event} = ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}` - ) - } - const script = readFileSync(getJcodeManagedScriptPath(), 'utf8') - expect(script).toContain('/hook/jcode') - expect(script).toContain('payload@-') - expect(script).toContain('$JCODE_HOOK_PAYLOAD') - expect(script).toContain('payload="$JCODE_HOOK_PAYLOAD"') - }) - it('reports a missing managed script and repairs it without changing config', () => { const service = new JcodeHookService() service.install() @@ -86,30 +54,6 @@ describe('JcodeHookService', () => { expect(readFileSync(getJcodeConfigPath(), 'utf8')).toBe(config) }) - it('reports missing scripts alongside incomplete event coverage and user hooks', () => { - const service = new JcodeHookService() - service.install() - writeFileSync( - getJcodeConfigPath(), - `[hooks]\nsession_start = ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}\nturn_end = "~/bin/my-turn-notify"\n`, - 'utf8' - ) - rmSync(getJcodeManagedScriptPath()) - - const status = service.getStatus() - expect(status.state).toBe('partial') - expect(status.detail).toContain('Managed hook script missing') - expect(status.detail).toContain( - 'Managed hook missing for events: turn_start, pre_tool, post_tool, session_end' - ) - expect(status.detail).toContain('User-owned hooks kept for events: turn_end') - expect(service.install().state).toBe('partial') - expect(service.getStatus().detail).not.toContain('script missing') - expect(readFileSync(getJcodeConfigPath(), 'utf8')).toContain( - 'turn_end = "~/bin/my-turn-notify"' - ) - }) - it.each([ '/Users/previous/.orca/agent-hooks/jcode-hook.sh', 'C:\\Users\\previous\\.orca\\agent-hooks\\jcode-hook.cmd' @@ -133,60 +77,6 @@ describe('JcodeHookService', () => { expect(readFileSync(configPath, 'utf8')).not.toContain('previous') }) - it('does not report stale managed commands as installed beside the current one', () => { - const service = new JcodeHookService() - service.install() - const staleCommand = getJcodeManagedCommand('/Users/previous/.orca/agent-hooks/jcode-hook.sh') - writeFileSync( - getJcodeConfigPath(), - `[hooks]\n${JCODE_HOOK_EVENTS.map((event) => `${event} = [${tomlQuoteString(staleCommand)}, ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}]`).join('\n')}\n`, - 'utf8' - ) - - const status = service.getStatus() - expect(status.state).toBe('partial') - expect(status.managedHooksPresent).toBe(true) - expect(status.detail).toBe( - `Managed hook command outdated for events: ${JCODE_HOOK_EVENTS.join(', ')}` - ) - expect(service.install().state).toBe('installed') - expect(readFileSync(getJcodeConfigPath(), 'utf8')).not.toContain('previous') - }) - - it('reports legacy unquoted commands as outdated even when the script exists', () => { - const service = new JcodeHookService() - service.install() - writeFileSync( - getJcodeConfigPath(), - `[hooks]\n${JCODE_HOOK_EVENTS.map((event) => `${event} = ${tomlQuoteString(getJcodeManagedScriptPath())}`).join('\n')}\n`, - 'utf8' - ) - - const status = service.getStatus() - expect(status.state).toBe('partial') - expect(status.detail).toContain('Managed hook command outdated for events:') - expect(status.detail).not.toContain('script missing') - expect(status.detail).not.toContain('User-owned') - expect(service.install().state).toBe('installed') - }) - - it('does not mistake a user command mentioning the managed script in a comment for Orca ownership', () => { - const configPath = getJcodeConfigPath() - mkdirSync(dirname(configPath), { recursive: true }) - writeFileSync( - configPath, - `[hooks]\nturn_end = "~/bin/my-turn-notify" # replaces agent-hooks/${getJcodeManagedScriptFileName()}\n`, - 'utf8' - ) - - const status = new JcodeHookService().getStatus() - expect(status.state).toBe('partial') - expect(status.managedHooksPresent).toBe(false) - expect(status.detail).toContain('User-owned hooks kept for events: turn_end') - expect(status.detail).not.toContain('command outdated') - expect(status.detail).not.toContain('script missing') - }) - it('preserves unrelated config tables when installing hooks', () => { const configPath = getJcodeConfigPath() mkdirSync(dirname(configPath), { recursive: true }) @@ -207,15 +97,6 @@ describe('JcodeHookService', () => { expect(readFileSync(configPath, 'utf8')).toContain('turn_end = "~/bin/my-turn-notify"') }) - it('reports error when the [hooks] table holds a non-scalar value', () => { - const configPath = getJcodeConfigPath() - mkdirSync(dirname(configPath), { recursive: true }) - writeFileSync(configPath, '[hooks]\nturn_end = """\nmultiline\n"""\n', 'utf8') - const status = new JcodeHookService().install() - expect(status.state).toBe('error') - expect(status.detail).toContain('config.toml') - }) - it('appends to user hook arrays and removes only its own element', () => { const configPath = getJcodeConfigPath() const source = `[hooks]\npre_tool = [\n "~/gates/pre-tool.sh", # policy\n "~/gates/memory-gate.sh",\n]\npre_tool_timeout_ms = 5000\n` @@ -244,16 +125,4 @@ describe('JcodeHookService', () => { 'pre_tool = [\n "~/gates/pre-tool.sh", # policy\n "~/gates/memory-gate.sh",\n]\n' ) }) - - it('remove clears only the managed entries', () => { - new JcodeHookService().install() - const before = readFileSync(getJcodeConfigPath(), 'utf8') - expect(before).toContain('turn_end') - const status = new JcodeHookService().remove() - expect(status.state).toBe('not_installed') - expect(status.detail).toBeNull() - expect(existsSync(getJcodeManagedScriptPath())).toBe(true) - const after = readFileSync(getJcodeConfigPath(), 'utf8') - expect(after).not.toContain(getJcodeManagedScriptPath()) - }) }) diff --git a/src/main/jira/client.test.ts b/src/main/jira/client.test.ts index 8cd106d5e1b..8abe41540ac 100644 --- a/src/main/jira/client.test.ts +++ b/src/main/jira/client.test.ts @@ -157,35 +157,6 @@ describe('Jira client credential storage', () => { ) }) - it('sends a non-browser User-Agent on Jira POST requests', async () => { - // Electron's Chrome UA trips Atlassian XSRF checks on token-authenticated writes. - const siteId = 'site-alpha' - writeJiraFiles(siteId, 'token-alpha') - netFetchMock.mockResolvedValueOnce( - new Response(JSON.stringify({ issues: [] }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }) - ) - const jira = await loadClientModule({ encryptionAvailable: true }) - const client = jira.getClients(siteId)[0] - - if (!client) { - throw new Error('Expected stored Jira client') - } - - await jira.jiraRequest(client, '/rest/api/3/search/jql', { - method: 'POST', - body: JSON.stringify({ jql: 'project = ALP' }) - }) - - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - const userAgent = headers.get('User-Agent') ?? '' - expect(netFetchMock.mock.calls[0]?.[1]?.method).toBe('POST') - expect(userAgent).toBe('Orca') - expect(userAgent).not.toMatch(/Mozilla|Chrome|Safari|AppleWebKit/i) - }) - it('removes cancelled queued reads so the request pool recovers', async () => { const jira = await loadClientModule() await Promise.all([jira.acquire(), jira.acquire(), jira.acquire(), jira.acquire()]) @@ -351,21 +322,6 @@ describe('Jira client credential storage', () => { expect(jira.getStatus().credentialError).toBeUndefined() }) - it('treats empty Jira token files as missing credentials', async () => { - const siteId = 'site-alpha' - writeJiraFiles(siteId, Buffer.alloc(0)) - const jira = await loadClientModule({ encryptionAvailable: false }) - - await expect(jira.testConnection(siteId)).resolves.toEqual({ - ok: false, - error: 'Not connected to Jira.', - errorCode: 'jira.notConnected' - }) - - expect(fetchMock).not.toHaveBeenCalled() - expect(jira.getStatus()).toMatchObject({ connected: false }) - }) - it('keeps healthy sites under the "all" selection when one site cannot be decrypted', async () => { writeMultiSiteFiles( [ @@ -389,24 +345,6 @@ describe('Jira client credential storage', () => { expect(jira.getStatus().credentialError).toContain('Could not decrypt') }) - it('rethrows the decrypt error for a specific site selection', async () => { - writeMultiSiteFiles( - [ - { id: 'good', token: 'token-good' }, - { id: 'bad', token: Buffer.from([0x76, 0x31, 0x30, 0xff, 0xfe]) } - ], - 'bad' - ) - const jira = await loadClientModule({ - encryptionAvailable: true, - decryptString: () => { - throw new Error('userCanceledErr') - } - }) - - expect(() => jira.getClients('bad')).toThrow('Could not decrypt') - }) - it('does not clear credentials when Electron transport fails after a network change', async () => { const siteId = 'site-alpha' const tokenPath = tokenPathForSite(siteId) @@ -432,156 +370,6 @@ describe('Jira client credential storage', () => { }) }) - it('does not treat Jira permission failures as credential revocation', async () => { - const jira = await loadClientModule() - - expect(jira.isAuthError(new jira.JiraApiError('Unauthorized', 401))).toBe(true) - expect(jira.isAuthError(new jira.JiraApiError('Forbidden', 403))).toBe(false) - }) - - it('connects to self-hosted Jira with a Bearer PAT against REST v2', async () => { - netFetchMock.mockResolvedValueOnce( - new Response( - JSON.stringify({ - name: 'wquintal', - key: 'JIRAUSER10101', - displayName: 'William', - emailAddress: 'william@example.com' - }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ) - ) - const jira = await loadClientModule() - - await expect( - jira.connect({ - siteUrl: 'jira.example.com', - email: '', - apiToken: 'pat-token', - authType: 'server' - }) - ).resolves.toMatchObject({ - ok: true, - // Server /myself has no accountId; the username stands in for it. - viewer: { displayName: 'William', accountId: 'wquintal' } - }) - - expect(netFetchMock).toHaveBeenCalledWith( - 'https://jira.example.com/rest/api/2/myself', - expect.objectContaining({ headers: expect.any(Headers) }) - ) - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - expect(headers.get('Authorization')).toBe('Bearer pat-token') - }) - - it('connects to self-hosted Jira with Basic username/password against REST v2', async () => { - netFetchMock.mockResolvedValueOnce( - new Response( - JSON.stringify({ name: 'jdoe', key: 'JIRAUSER20202', displayName: 'Jane Doe' }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ) - ) - const jira = await loadClientModule() - - await expect( - jira.connect({ - siteUrl: 'jira.example.com', - // Username selects classic Server/DC Basic auth; the token slot holds the password. - email: 'jdoe', - apiToken: 'account-password', - authType: 'server' - }) - ).resolves.toMatchObject({ - ok: true, - viewer: { displayName: 'Jane Doe', accountId: 'jdoe' } - }) - - expect(netFetchMock).toHaveBeenCalledWith( - 'https://jira.example.com/rest/api/2/myself', - expect.objectContaining({ headers: expect.any(Headers) }) - ) - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - expect(headers.get('Authorization')).toBe( - `Basic ${Buffer.from('jdoe:account-password').toString('base64')}` - ) - }) - - it('uses Basic auth for stored self-hosted sites that carry a username', async () => { - const siteId = 'site-server-basic' - const orcaDir = join(tempHome, '.orca') - mkdirSync(join(orcaDir, 'jira-tokens'), { recursive: true }) - writeFileSync( - join(orcaDir, 'jira-sites.json'), - JSON.stringify({ - version: 1, - activeSiteId: siteId, - selectedSiteId: siteId, - sites: [ - { - id: siteId, - siteUrl: 'https://jira.example.com', - email: 'jdoe', - displayName: 'Jane Doe', - accountId: 'jdoe', - authType: 'server' - } - ] - }), - { encoding: 'utf-8' } - ) - writeFileSync(tokenPathForSite(siteId), 'account-password') - netFetchMock.mockResolvedValueOnce( - new Response(JSON.stringify({ name: 'jdoe', displayName: 'Jane Doe' }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }) - ) - const jira = await loadClientModule() - - await expect(jira.testConnection(siteId)).resolves.toMatchObject({ ok: true }) - - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - expect(headers.get('Authorization')).toBe( - `Basic ${Buffer.from('jdoe:account-password').toString('base64')}` - ) - }) - - it('requires a token but not an email for self-hosted connections', async () => { - const jira = await loadClientModule() - - await expect( - jira.connect({ - siteUrl: 'jira.example.com', - email: '', - apiToken: '', - authType: 'server' - }) - ).resolves.toEqual({ - ok: false, - error: 'Personal access token is required.', - errorCode: 'jira.personalTokenRequired' - }) - expect(netFetchMock).not.toHaveBeenCalled() - }) - - it('requires a password when a username is present on self-hosted', async () => { - const jira = await loadClientModule() - - await expect( - jira.connect({ - siteUrl: 'jira.example.com', - email: 'jdoe', - apiToken: '', - authType: 'server' - }) - ).resolves.toEqual({ - ok: false, - error: 'Password is required.', - errorCode: 'jira.passwordRequired' - }) - expect(netFetchMock).not.toHaveBeenCalled() - }) - it('keeps distinct self-hosted PAT accounts on one host as separate sites', async () => { netFetchMock .mockResolvedValueOnce( @@ -620,79 +408,6 @@ describe('Jira client credential storage', () => { expect(stored.sites).toHaveLength(2) expect(stored.sites.map((site) => site.accountId).sort()).toEqual(['alice', 'bot']) }) - - it('uses Bearer auth and REST v2 for stored self-hosted sites', async () => { - const siteId = 'site-server' - const orcaDir = join(tempHome, '.orca') - mkdirSync(join(orcaDir, 'jira-tokens'), { recursive: true }) - writeFileSync( - join(orcaDir, 'jira-sites.json'), - JSON.stringify({ - version: 1, - activeSiteId: siteId, - selectedSiteId: siteId, - sites: [ - { - id: siteId, - siteUrl: 'https://jira.example.com', - email: '', - displayName: 'William', - accountId: 'wquintal', - authType: 'server' - } - ] - }), - { encoding: 'utf-8' } - ) - writeFileSync(tokenPathForSite(siteId), 'pat-token') - netFetchMock.mockResolvedValueOnce( - new Response(JSON.stringify({ name: 'wquintal', displayName: 'William' }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }) - ) - const jira = await loadClientModule() - - await expect(jira.testConnection(siteId)).resolves.toMatchObject({ - ok: true, - viewer: { displayName: 'William' } - }) - - expect(netFetchMock).toHaveBeenCalledWith( - 'https://jira.example.com/rest/api/2/myself', - expect.objectContaining({ headers: expect.any(Headers) }) - ) - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - expect(headers.get('Authorization')).toBe('Bearer pat-token') - }) - - it('bridges proxy environment settings before Jira connect requests', async () => { - netFetchMock.mockResolvedValueOnce( - new Response( - JSON.stringify({ - accountId: 'account-alpha', - displayName: 'Ada', - emailAddress: 'ada@example.com' - }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ) - ) - const jira = await loadClientModule() - - await expect( - jira.connect({ - siteUrl: 'example.atlassian.net', - email: 'ada@example.com', - apiToken: 'token-alpha' - }) - ).resolves.toMatchObject({ ok: true, viewer: { displayName: 'Ada' } }) - - expect(resolveProxyMock).toHaveBeenCalledWith('https://example.atlassian.net/rest/api/3/myself') - expect(netFetchMock).toHaveBeenCalledTimes(1) - const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers - expect(headers.get('User-Agent')).toBe('Orca') - expect(fetchMock).not.toHaveBeenCalled() - }) }) describe('Jira site file cross-process visibility', () => { @@ -813,18 +528,4 @@ describe('Jira site file cross-process visibility', () => { expect(jira.getStatus()).toMatchObject({ connected: false, sites: [] }) }) - - it('follows the selected site changing underneath it', async () => { - const sites = [ - { id: 'site-alpha', token: 'token-alpha' }, - { id: 'site-beta', token: 'token-beta' } - ] - writeMultiSiteFiles(sites, 'site-alpha') - const jira = await loadClientModule() - expect(jira.getStatus()).toMatchObject({ selectedSiteId: 'site-alpha' }) - - writeMultiSiteFiles(sites, 'site-beta') - - expect(jira.getStatus()).toMatchObject({ selectedSiteId: 'site-beta' }) - }) }) diff --git a/src/main/kiro/hook-service.test.ts b/src/main/kiro/hook-service.test.ts index 2039d2c9bb0..19282c9b7bc 100644 --- a/src/main/kiro/hook-service.test.ts +++ b/src/main/kiro/hook-service.test.ts @@ -1,10 +1,9 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { createManagedHookLocalFilesystem } from '../agent-hooks/managed-hook-local-filesystem.test-fixture' import { isPlainObject } from '../agent-hooks/installer-utils' -import { wrapWindowsBatchHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command' import { KiroHookService } from './hook-service' import { getKiroManagedCommandMatcher, KIRO_HOOK_EVENTS } from './hook-settings' @@ -45,13 +44,6 @@ afterEach(() => { const agentsDir = (): string => join(home, '.kiro', 'agents') const agentPath = (name: string): string => join(agentsDir(), `${name}.json`) -const scriptPath = (): string => - join( - home, - '.orca', - 'agent-hooks', - process.platform === 'win32' ? 'kiro-hook.cmd' : 'kiro-hook.sh' - ) function writeAgent(name: string, config: unknown): void { mkdirSync(agentsDir(), { recursive: true }) @@ -65,15 +57,6 @@ function readAgent(name: string): Record { const USER_STOP_HOOK = { command: 'say done' } describe('KiroHookService', () => { - it("reports the V2 default agent's gap as partial when no custom agents exist", () => { - const result = new KiroHookService().install() - expect(result.state).toBe('partial') - expect(result.detail).toContain('kiro-cli agent create') - expect(result.detail).toContain(agentsDir()) - // The launcher is still laid down so the refresher can keep it current. - expect(existsSync(scriptPath())).toBe(true) - }) - it('adds one managed hook per event to every agent and keeps user hooks', () => { writeAgent('caverna', { name: 'caverna', tools: ['*'], hooks: { stop: [USER_STOP_HOOK] } }) writeAgent('reviewer', { name: 'reviewer', tools: ['read'] }) @@ -118,29 +101,6 @@ describe('KiroHookService', () => { expect(readFileSync(agentPath('broken'), 'utf-8')).toBe('{ not json') }) - it('reports partial when an agent created after install has no hooks yet', () => { - writeAgent('first', { name: 'first' }) - const service = new KiroHookService() - service.install() - writeAgent('later', { name: 'later' }) - - const result = service.getStatus() - expect(result.state).toBe('partial') - expect(result.detail).toContain('later.json has no Orca hooks') - }) - - it('reports a missing launcher as partial instead of installed', () => { - writeAgent('tracked', { name: 'tracked' }) - const service = new KiroHookService() - service.install() - rmSync(scriptPath()) - expect(service.getStatus()).toMatchObject({ - state: 'partial', - managedHooksPresent: true, - detail: expect.stringContaining('script missing') - }) - }) - // Shape `kiro-cli agent upgrade` (2.29+) writes; edits to `hooks` keep the file "universal". it('converges list-form hooks in place and removes only its entries, locally and remotely', async () => { const orcaHook = (trigger: string, command: string) => ({ @@ -231,15 +191,6 @@ describe('KiroHookService', () => { expect(readAgent('plain')).toEqual({ name: 'plain' }) }) - it('patches the agents under $KIRO_HOME instead of ~/.kiro', () => { - const kiroHome = join(home, 'custom-kiro') - mkdirSync(join(kiroHome, 'agents'), { recursive: true }) - writeFileSync(join(kiroHome, 'agents', 'relocated.json'), JSON.stringify({ name: 'relocated' })) - process.env.KIRO_HOME = kiroHome - expect(new KiroHookService().install().state).toBe('installed') - expect(existsSync(agentsDir())).toBe(false) - }) - it('patches the remote agents under the probed KIRO_HOME, not ~/.kiro', async () => { const kiroHome = join(home, 'remote-kiro') mkdirSync(join(kiroHome, 'agents'), { recursive: true }) @@ -258,67 +209,5 @@ describe('KiroHookService', () => { expect(readAgent('stale').hooks).toBeUndefined() }) - it('falls back to /.kiro/agents when no KIRO_HOME was probed', async () => { - writeAgent('default', { name: 'default' }) - - const result = await new KiroHookService().installRemote( - createManagedHookLocalFilesystem(), - home - ) - - expect(result.state).toBe('installed') - expect(readAgent('default').hooks).toBeDefined() - }) - - it('reports a remote agent deleted after the listing as unreadable, not invalid JSON', async () => { - writeAgent('kept', { name: 'kept' }) - writeAgent('gone', { name: 'gone' }) - const sftp = createManagedHookLocalFilesystem() - const readFile = sftp.readFile - Object.assign(sftp, { - readFile: ( - path: string, - encoding: BufferEncoding, - callback: (error: Error | undefined, contents: Buffer) => void - ) => { - if (path.endsWith('gone.json')) { - rmSync(path) - } - return readFile(path, encoding, callback) - } - }) - - const result = await new KiroHookService().installRemote(sftp, home) - - expect(result.state).toBe('partial') - expect(result.detail).toContain('gone.json is could not be read') - expect(result.detail).not.toContain('not valid JSON') - expect(readAgent('kept').hooks).toBeDefined() - }) - // Check command shapes everywhere; windows-hook-payload-delivery.test.ts executes them under Kiro's shells. - it('registers the Windows batch hook command for every event', () => { - const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') - try { - writeAgent('mine', { name: 'mine' }) - expect(new KiroHookService().install().state).toBe('installed') - const hooks = readAgent('mine').hooks - if (!isPlainObject(hooks)) { - throw new Error('Expected hooks object') - } - const command = wrapWindowsBatchHookCommand(scriptPath()) - for (const event of KIRO_HOOK_EVENTS) { - expect(hooks[event]).toEqual([expect.objectContaining({ command })]) - } - } finally { - platform.mockRestore() - } - }) - - it('ignores non-JSON files such as the shipped example config', () => { - mkdirSync(agentsDir(), { recursive: true }) - writeFileSync(join(agentsDir(), 'agent_config.json.example'), '{}') - expect(new KiroHookService().install().state).toBe('partial') - expect(readFileSync(join(agentsDir(), 'agent_config.json.example'), 'utf-8')).toBe('{}') - }) }) diff --git a/src/main/linear/client.test.ts b/src/main/linear/client.test.ts index 75a33adb5da..942063afb4f 100644 --- a/src/main/linear/client.test.ts +++ b/src/main/linear/client.test.ts @@ -156,21 +156,6 @@ function mkdtempLike(prefix: string): string { } describe('Linear client workspace storage', () => { - it('forwards an optional detail deadline to both SDK clients', async () => { - const linear = await loadClientModule() - await linear.connect('token-alpha') - const signal = new AbortController().signal - const entry = linear.getClients('org-alpha', signal)[0] - expect(linearClientMock).toHaveBeenLastCalledWith({ apiKey: 'token-alpha', signal }) - linear.getPublicFileUrlClient(entry, signal) - expect(linearClientMock).toHaveBeenLastCalledWith({ - apiKey: 'token-alpha', - signal, - headers: { - 'public-file-urls-expire-in': String(linear.LINEAR_PUBLIC_FILE_URL_EXPIRY_SECONDS) - } - }) - }) it('requests temporary file URLs using only the selected workspace credential', async () => { const linear = await loadClientModule() await linear.connect('token-alpha') @@ -214,22 +199,6 @@ describe('Linear client workspace storage', () => { }) }) - it('reports a legacy single-token workspace without constructing a Linear client', async () => { - writeLegacyLinearFiles('token-alpha', { - displayName: 'Ada', - email: 'ada@example.com', - organizationName: 'Alpha' - }) - const linear = await loadClientModule() - - expect(linear.getStatus()).toMatchObject({ - connected: true, - selectedWorkspaceId: 'legacy', - workspaces: [{ id: 'legacy', organizationName: 'Alpha', isLegacy: true }] - }) - expect(linearClientMock).not.toHaveBeenCalled() - }) - it('migrates legacy token storage to a real workspace id when explicitly tested', async () => { writeLegacyLinearFiles('token-alpha', { displayName: 'Ada', @@ -364,24 +333,6 @@ describe('Linear client workspace storage', () => { expect(linear.getStatus().credentialError).toBeUndefined() }) - it('treats empty Linear token files as missing credentials', async () => { - writeLegacyLinearToken(Buffer.alloc(0), { - displayName: 'Ada', - email: 'ada@example.com', - organizationName: 'Alpha' - }) - const linear = await loadClientModule({ encryptionAvailable: false }) - - await expect(linear.testConnection('legacy')).resolves.toEqual({ - ok: false, - error: 'No API key stored.', - errorCode: 'linear.noApiKey' - }) - - expect(linearClientMock).not.toHaveBeenCalled() - expect(linear.getStatus()).toMatchObject({ connected: false }) - }) - it('keeps healthy workspaces under the "all" selection when one cannot be decrypted', async () => { writeMultiWorkspaceFiles( [ @@ -409,24 +360,6 @@ describe('Linear client workspace storage', () => { expect(clients.map((client) => client.workspace.id)).toEqual(['good']) expect(linear.getStatus().credentialError).toContain('Could not decrypt') }) - - it('rethrows the decrypt error for a specific workspace selection', async () => { - writeMultiWorkspaceFiles( - [ - { id: 'good', token: 'token-good' }, - { id: 'bad', token: Buffer.from([0x76, 0x31, 0x30, 0xff, 0xfe]) } - ], - 'bad' - ) - const linear = await loadClientModule({ - encryptionAvailable: true, - decryptString: () => { - throw new Error('userCanceledErr') - } - }) - - expect(() => linear.getClients('bad')).toThrow('Could not decrypt') - }) }) describe('Linear workspace file cross-process visibility', () => { @@ -548,18 +481,4 @@ describe('Linear workspace file cross-process visibility', () => { activeWorkspaceId: 'ws-alpha' }) }) - - it('follows the selected workspace changing underneath it', async () => { - const workspaces = [ - { id: 'ws-alpha', token: 'token-alpha' }, - { id: 'ws-beta', token: 'token-beta' } - ] - writeMultiWorkspaceFiles(workspaces, 'ws-alpha') - const linear = await loadClientModule() - expect(linear.getStatus()).toMatchObject({ selectedWorkspaceId: 'ws-alpha' }) - - writeMultiWorkspaceFiles(workspaces, 'ws-beta') - - expect(linear.getStatus()).toMatchObject({ selectedWorkspaceId: 'ws-beta' }) - }) }) diff --git a/src/main/media/remote-file-preview-reader.test.ts b/src/main/media/remote-file-preview-reader.test.ts index 78812092138..6f0bd64b10a 100644 --- a/src/main/media/remote-file-preview-reader.test.ts +++ b/src/main/media/remote-file-preview-reader.test.ts @@ -7,9 +7,7 @@ vi.mock('../ipc/runtime-environments/runtime-environment-transport-routing', () vi.mock('../ipc/runtime-environments/runtime-environment-registry-root', () => ({ getRuntimeEnvironmentRegistryRoot: () => '/user-data' })) - import { - assertRemoteChunkReadsSupported, readRemotePreviewRange, readRemotePreviewChunk, renewRemotePreviewGrant @@ -40,53 +38,7 @@ describe('remote file preview reader', () => { await expect(readRemotePreviewChunk(address, 0, 4, signal)).rejects.toThrow('corrupted') }) - it('proves chunk reads with one byte once per pairing, and re-proves after a re-pair', async () => { - callRuntimeEnvironment.mockResolvedValue( - ok({ contentBase64: 'AA==', bytesRead: 1, eof: false }) - ) - const pinned = { ...address, environmentId: 'env-probe' } - await assertRemoteChunkReadsSupported(pinned, signal) - await assertRemoteChunkReadsSupported(pinned, signal) - expect(callRuntimeEnvironment).toHaveBeenCalledTimes(1) - expect(callRuntimeEnvironment).toHaveBeenCalledWith( - '/user-data', - 'env-probe', - 'files.readChunk', - expect.objectContaining({ offset: 0, length: 1 }), - 20_000, - 7, - undefined, - { signal } - ) - await assertRemoteChunkReadsSupported( - { ...pinned, expectedEnvironmentPairingRevision: 8 }, - signal - ) - expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2) - }) - // Why: a host can answer files.readChunk yet lack the grant's paging method. - it('pages a granted file through its grant, proven apart from workspace reads', async () => { - callRuntimeEnvironment.mockResolvedValue( - ok({ contentBase64: 'AA==', bytesRead: 1, eof: false }) - ) - const workspace = { ...address, environmentId: 'env-kinds' } - const { relativePath: _relativePath, ...base } = workspace - const granted = { ...base, grantId: 'g-1', absolutePath: '/tmp/clip.mp4' } - await assertRemoteChunkReadsSupported(workspace, signal) - await assertRemoteChunkReadsSupported(granted, signal) - expect(callRuntimeEnvironment).toHaveBeenLastCalledWith( - '/user-data', - 'env-kinds', - 'files.readTerminalArtifactChunk', - { worktree: 'id:wt-1', grantId: 'g-1', absolutePath: '/tmp/clip.mp4', offset: 0, length: 1 }, - 20_000, - 7, - undefined, - { signal } - ) - expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2) - }) it('renews a granted file only at the path its first grant named', async () => { const { relativePath: _relativePath, ...base } = address diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-project-defaults.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-project-defaults.test.ts deleted file mode 100644 index f5dd6208862..00000000000 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-project-defaults.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { resolveHiddenProbeCwd } from '../../rate-limits/hidden-probe-cwd' -import { describe, expect, it } from 'vitest' -import { applyCodexConfiguredLaunchDefaults } from '../../codex/structured/launch/codex-configured-launch-defaults' -import type { AgentSessionModelCatalogResult } from '../../../shared/agent-session/agent-session-wire' -import { harness } from './agent-model-catalog-account-list-test-support' - -for (const agent of ['claude', 'codex'] as const) { - describe(`${agent} project defaults on the account listing`, () => { - it('shows the account’s configured model and effort in every folder, whatever its own settings pick', async () => { - // Every folder's settings may pick both; the picker still follows the account. - const h = harness(agent, { workspaceMayOverrideDefaultModel: async () => true }) - const efforts = ['low', 'medium', 'high'].map((value) => ({ value, label: value })) - const raw = [ - { - id: h.full[0], - label: 'Model', - isDefault: true, - efforts, - defaultEffort: 'medium', - declaredDefaultEffort: 'medium' - } - ] - h.byFolder[resolveHiddenProbeCwd()] = { - origin: 'probe', - complete: true, - configured: { model: h.full[0], effort: 'high' }, - // Codex folds configured defaults into rows before the catalog service sees them. - models: applyCodexConfiguredLaunchDefaults(raw, { model: h.full[0], effort: 'high' }) - } - await h.service.read({ agent, waitForListing: true }) - const row = (result: AgentSessionModelCatalogResult) => - result.origin === 'unknown' ? null : result.models[0] - for (const id of ['a', 'e']) { - expect(row(await h.read(id, { waitForListing: true }))).toMatchObject({ - isDefault: true, - defaultEffort: 'high' - }) - expect(h.service.peek(id)?.configured).toMatchObject({ modelId: h.full[0] }) - } - expect(h.runs).toEqual([resolveHiddenProbeCwd()]) - }) - - it('lists a workspace this host cannot inspect as the account does, never where Orca runs', async () => { - const h = harness(agent) - const result = await h.service.read({ agent, workspacePath: null, waitForListing: true }) - expect(result.origin === 'unknown' ? null : result.models).toEqual( - h.full.map((id) => expect.objectContaining({ id })) - ) - expect(h.runs).toEqual([resolveHiddenProbeCwd()]) - expect(h.unresolvedRuns).toEqual([]) - }) - }) -} diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts index 27890f46d0a..a0f0e749d2c 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts @@ -256,27 +256,6 @@ it('keeps a shared listing for an earlier-ending reader of the same folder when expect(h.run).toHaveBeenCalledOnce() }) -it('keeps a later reader of the same folder waiting for its own deadline, not the first one', async () => { - vi.useFakeTimers() - onTestFinished(() => { - vi.useRealTimers() - }) - const h = sharedListingHarness() - void h.service.demand({ agent: 'codex', workspacePath: '/a', deadline: Date.now() + 1_000 }) - await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce()) - const later = h.service.read({ - agent: 'codex', - workspacePath: '/a', - waitForListing: true, - deadline: Date.now() + 60_000 - }) - await vi.advanceTimersByTimeAsync(2_000) - h.discovery.resolve({ ...listing('shared'), complete: true, configured: {} }) - await vi.advanceTimersByTimeAsync(0) - expect(await later).toMatchObject({ models: [{ id: 'shared' }] }) - expect(h.run).toHaveBeenCalledOnce() -}) - function readOperation(h: ReturnType, deadline: number) { const controller = new AbortController() let current = true diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-setup.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-setup.test.ts index cdc4c1bca9a..b7e0e509a72 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-setup.test.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-setup.test.ts @@ -272,34 +272,6 @@ it('ends a shorter reader at its own deadline, and a stalled refresh lists nothi expect(h.run).not.toHaveBeenCalled() }) -it.each([10, 100])( - 'resolves setup and project settings once for %i readers of one request, before and after it settles', - async (readers) => { - const h = harness() - const gate = Promise.withResolvers() - h.folder.gate = gate - const reads = Array.from({ length: readers }, () => - h.service.demand({ agent: 'codex', sessionId: 'b', deadline: Date.now() + 30_000 }) - ) - gate.resolve() - await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce()) - // More readers arrive while the listing that setup found is still running. - reads.push( - ...Array.from({ length: readers }, () => - h.service.demand({ agent: 'codex', sessionId: 'b', deadline: Date.now() + 30_000 }) - ) - ) - h.discovery.resolve(listing('shared')) - await Promise.all(reads) - // One request needs one folder lookup, one CLI resolution and one project-settings check. - expect(h.recordWorkspacePath).toHaveBeenCalledOnce() - // A due listing resolves the CLI twice (a snapshot, then a fresh lookup), once per setup. - expect(h.resolve.mock.calls.length).toBeLessThanOrEqual(2) - expect(h.workspaceMayOverrideDefaultModel.mock.calls.length).toBeLessThanOrEqual(1) - expect(h.run).toHaveBeenCalledOnce() - } -) - it('holds readers of a stalled refresh only to their own deadlines, and starts no other until asked', async () => { vi.useFakeTimers() onTestFinished(() => { @@ -331,18 +303,6 @@ it('holds readers of a stalled refresh only to their own deadlines, and starts n expect(await c).toMatchObject({ models: [{ id: 'fresh' }] }) }) -it("resolves a moved folder afresh instead of reusing the old folder's setup", async () => { - const h = harness() - h.folder.cli = Promise.withResolvers() - void h.service.demand({ agent: 'codex', sessionId: 'b' }) - await vi.waitFor(() => expect(h.resolve).toHaveBeenCalledOnce()) - h.folder.path = '/new' - h.service.retireContext('ws') - void h.service.demand({ agent: 'codex', sessionId: 'b' }) - await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce()) - expect(h.resolve.mock.calls.at(-1)?.[4]).toBe('/new') -}) - it.each(['cancelled', 'stopped'] as const)( 'settles a reader at once when it is %s while its setup is stalled', async (how) => { @@ -396,22 +356,6 @@ it("keeps the folder's defaults when they arrive after a shorter last reader lef expect(await a).toMatchObject({ models: [{ id: 'listed', isDefault: true }] }) }) -it("reuses a settled setup after its starting reader's deadline while its listing still runs", async () => { - vi.useFakeTimers() - onTestFinished(() => { - vi.useRealTimers() - }) - const h = harness(agent) - void h.service.demand({ agent, sessionId: 'a', deadline: Date.now() + 1_000 }) - void h.service.demand({ agent, sessionId: 'b', deadline: Date.now() + 30_000 }) - await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce()) - await vi.advanceTimersByTimeAsync(2_000) - void h.service.demand({ agent, sessionId: 'b', deadline: Date.now() + 30_000 }) - await vi.advanceTimersByTimeAsync(0) - expect(h.recordWorkspacePath).toHaveBeenCalledOnce() - expect(h.workspaceMayOverrideDefaultModel).toHaveBeenCalledOnce() -}) - it("keeps the listing when the folder's settings stall, naming no default, within the reader's budget", async () => { vi.useFakeTimers() onTestFinished(() => { diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx index 27fdb136d2c..d02293c4a6c 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx @@ -442,45 +442,8 @@ describe.each([ }) // Steal focus after resume to exercise the grab’s caret restore; otherwise select-all retries can escape detection. - it('restores the caret, not a select-all, when the guest takes the bar back mid-grab', () => { - renderWorkspacePane() - const staged = startEditing('example.int') - act(() => staged.setSelectionRange(3, 7)) - - act(() => adoptOntoClient()) - act(() => flushFrames(1)) - - const adopted = addressBar() - expect(document.activeElement).toBe(adopted) - act(() => webview.focus()) - expect(document.activeElement).toBe(webview) - - act(() => flushFrames()) - - expect(document.activeElement).toBe(adopted) - expect(adopted.value).toBe('example.int') - expect([adopted.selectionStart, adopted.selectionEnd]).toEqual([3, 7]) - }) // An open list survives focus naturally; only a dismissed list proves dropdown state crosses the swap. - it('keeps a dismissed suggestion list dismissed through the swap', () => { - renderWorkspacePane() - const staged = startEditing('example.int') - act(() => { - fireEvent.keyDown(staged, { key: 'Escape' }) - }) - expect(staged.getAttribute('aria-expanded')).toBe('false') - expect(document.activeElement).toBe(staged) - - act(() => adoptOntoClient()) - act(() => flushFrames()) - - const adopted = addressBar() - expect(adopted.value).toBe('example.int') - expect(document.activeElement).toBe(adopted) - expect(adopted.getAttribute('aria-expanded')).toBe('false') - expect(screen.queryByRole('option', { name: /Example docs/ })).toBeNull() - }) // Only same-commit swaps may reclaim focus; later worktree revisits must not resume old edits. it('does not resume an edit into a pane that mounts in a later task', async () => { @@ -496,58 +459,9 @@ describe.each([ expect(document.activeElement).not.toBe(remounted) }) - it('leaves an idle address bar alone when the same swap happens', () => { - renderWorkspacePane() - const streamed = addressBar() - expect(document.activeElement).not.toBe(streamed) - - act(() => adoptOntoClient()) - act(() => flushFrames()) - - const adopted = addressBar() - expect(adopted.value).toBe('') - // Adoption without an active edit must not seize chrome focus or open suggestions. - expect(document.activeElement).not.toBe(adopted) - expect(adopted.getAttribute('aria-expanded')).toBe('false') - expect(screen.queryByRole('option', { name: /Example docs/ })).toBeNull() - }) - // Generation changes reattach in place; edits must survive without remounting. - it('carries a live edit through a host restart that bumps the page generation', () => { - renderWorkspacePane() - act(() => adoptOntoClient()) - act(() => flushFrames()) - - const before = startEditing('rebooting.internal') - act(() => before.setSelectionRange(4, 4)) - - act(() => adoptOntoClient(CLIENT_PLACEMENT.pageHostGeneration + 1)) - act(() => flushFrames()) - - const after = addressBar() - // The registry can rescue drafts across remounts; node identity is needed to prove the chrome was never rebuilt. - expect(after).toBe(before) - expect(after.value).toBe('rebooting.internal') - expect(document.activeElement).toBe(after) - expect([after.selectionStart, after.selectionEnd]).toEqual([4, 4]) - }) // Headless adoption stays streamed and must preserve the same live edit. - it('keeps a live edit when a headless host adopts the page in place', () => { - renderWorkspacePane() - const streamed = startEditing('headless.internal') - act(() => streamed.setSelectionRange(8, 8)) - - act(() => adoptOntoServer()) - act(() => flushFrames()) - - const after = addressBar() - expect(screen.getByTestId('streamed-viewport')).not.toBeNull() - expect(after).toBe(streamed) - expect(after.value).toBe('headless.internal') - expect(document.activeElement).toBe(after) - expect([after.selectionStart, after.selectionEnd]).toEqual([8, 8]) - }) it('holds a URL submitted against the staged page and loads it once the guest attaches', () => { renderWorkspacePane() @@ -567,30 +481,6 @@ describe.each([ }) // Only input identity exposes chrome remounts; save/resume can otherwise hide the repeated dropdown animation. - it('adopts a client-hosted page without remounting the chrome', () => { - stageClientHostedHandle() - renderWorkspacePane() - - // The client-hosted pane mounts from the first frame, so there is no pane to swap at adoption. - expect(screen.queryByTestId('streamed-viewport')).toBeNull() - // Do not attach before the host publishes the page; the retained registry would reject the unknown ID. - expect(mocks.attach).not.toHaveBeenCalled() - expect(screen.queryByText('Client-hosted browser unavailable')).toBeNull() - - const staged = startEditing('example.int') - act(() => staged.setSelectionRange(3, 7)) - expect(staged.getAttribute('aria-expanded')).toBe('true') - - act(() => adoptOntoClient()) - act(() => flushFrames()) - - expect(addressBar()).toBe(staged) - expect(staged.getAttribute('aria-expanded')).toBe('true') - expect(staged.value).toBe('example.int') - expect(document.activeElement).toBe(staged) - expect([staged.selectionStart, staged.selectionEnd]).toEqual([3, 7]) - expect(mocks.attach).toHaveBeenCalled() - }) // Continuity alone passes if nothing happens; verify generation changes really reattach the guest. it('re-attaches the guest when a host restart bumps the page generation', () => { @@ -618,23 +508,6 @@ describe.each([ }) // Do not consume a one-time tour before placement makes its controls usable. - it('does not burn the intro tour on a pane that is still connecting', () => { - const recordFeatureInteraction = vi.fn(async () => {}) - useAppStore.setState({ - persistedUIReady: true, - contextualToursSeenIds: ['client-hosted-browser'], - recordFeatureInteraction - } as unknown as Parameters[0]) - stageClientHostedHandle() - renderWorkspacePane() - - expect(recordFeatureInteraction).not.toHaveBeenCalled() - - act(() => adoptOntoClient()) - act(() => flushFrames()) - - expect(recordFeatureInteraction).toHaveBeenCalledWith('client-hosted-browser') - }) // Live host status can override cached client placement and swap to streaming; retain edit-session recovery for this path. it('carries a live edit when a staged client-hosted page is adopted onto the server', () => { @@ -656,22 +529,6 @@ describe.each([ expect([adopted.selectionStart, adopted.selectionEnd]).toEqual([4, 4]) }) - it('holds a URL submitted against a staged client-hosted page until its guest attaches', () => { - stageClientHostedHandle() - renderWorkspacePane() - const staged = startEditing('https://example.internal/client-deferred') - act(() => { - fireEvent.submit(staged.closest('form') ?? staged) - }) - - expect(webview.loadURL).not.toHaveBeenCalled() - - act(() => adoptOntoClient()) - act(() => flushFrames()) - - expect(webview.loadURL).toHaveBeenCalledWith('https://example.internal/client-deferred') - }) - // Restored client-placed pages must await recovery locally; hosts reject server screencasts for them. it('mounts a restored client-hosted page on the client pane while the host recovers it', () => { restoreClientHostedHandle() diff --git a/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx b/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx index c70c64c7aa3..8687923b78f 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx +++ b/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx @@ -11,12 +11,10 @@ import { act } from 'react' import { createRoot } from 'react-dom/client' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import { GitHistoryPanel } from '@/components/right-sidebar/source-control/sync/git-history-panel' import { GitGraphTab } from './git-graph-tab' import { GitGraphFilter } from './git-graph-filter' -import { useGitGraphHistory, gitGraphFilterSupported } from './use-git-graph-history' +import { useGitGraphHistory } from './use-git-graph-history' import type { GitHistoryResult } from '../../../../../shared/git/history/git-history-types' -import * as graph from '../../../../../shared/git/history/git-history-graph' const { getHistory, getRefs } = vi.hoisted(() => ({ getHistory: vi.fn(), getRefs: vi.fn() })) vi.mock('@/runtime/source-control/git/runtime-git-status-client', async (original) => ({ @@ -99,74 +97,6 @@ const result: GitHistoryResult = { limit: 50 } -it('keeps view models on expand and resize, rebuilding when history changes', async () => { - const build = vi.spyOn(graph, 'buildGitHistoryViewModels') - const render = (history: GitHistoryResult) => ( - - {}} - onRefresh={() => {}} - onLoadCommitFiles={async () => []} - onOpenCommitFile={() => {}} - /> - - ) - await act(async () => root.render(render(result))) - expect(build).toHaveBeenCalledTimes(1) - await act(async () => - container.querySelector('[data-testid="git-history-row"]')!.click() - ) - await act(async () => - container - .querySelector('[role="separator"]')! - .dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })) - ) - expect(build).toHaveBeenCalledTimes(1) - await act(async () => - root.render( - render({ - ...result, - items: [...result.items, { id: 'b', subject: 'next', message: 'next', parentIds: [] }] - }) - ) - ) - expect(build).toHaveBeenCalledTimes(2) - expect( - container.querySelector('[data-testid="git-history-row"]')?.getAttribute('aria-expanded') - ).toBe('true') -}) - -it('disables the branch filter when an older host omits the refs echo', async () => { - await act(async () => - root.render( - - {}} - /> - - ) - ) - expect(container.querySelector('button')?.disabled).toBe(true) - await act(async () => - root.render( - - {}} - /> - - ) - ) - expect(container.querySelector('button')?.disabled).toBe(false) -}) - it('recovers silently after each later token expiry and ignores refreshes for other worktrees', async () => { let history: ReturnType function History() { @@ -335,33 +265,6 @@ async function loadOlderHistory() { return { controller, items, page } } -it('keeps a selection pushed past the loaded depth with exactly one extra page', async () => { - const { controller, items } = await loadOlderHistory() - await act(async () => { - Array.from(container.querySelectorAll('button')) - .find((button) => button.textContent === 'Commit 149')! - .click() - }) - const refreshed = [ - { id: 'new-tip', subject: 'New commit', message: 'New commit', parentIds: [] }, - ...items - ] - getHistory.mockImplementation(async (_owner, options) => ({ - ...result, - refs: 'all', - historyToken: 'refreshed', - items: refreshed.slice(options.skip, options.skip + 50), - hasMore: true - })) - await act(async () => { - await controller.refresh() - }) - expect(getHistory).toHaveBeenCalledTimes(4) - expect(container.querySelectorAll('[data-current]')).toHaveLength(200) - expect(container.querySelector('[data-current="true"] button[title="Commit 149"]')).not.toBeNull() - expect(container.querySelector('aside h2')?.textContent).toBe('Commit 149') -}) - it('bounds refresh to one extra page when the selected commit has vanished', async () => { const { controller, items } = await loadOlderHistory() const remaining = items.filter((item) => item.id !== 'commit-110') diff --git a/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx b/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx index 2933f75e6bc..654b5905fba 100644 --- a/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx +++ b/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx @@ -121,45 +121,6 @@ describe('EmulatorDeviceFrame window-visibility gating', () => { expect(startFrameStream).toHaveBeenCalledTimes(2) }) - it('does not tear down on a quick hide/show within the park delay', async () => { - vi.useFakeTimers() - await renderFrame(true) - expect(startFrameStream).toHaveBeenCalledTimes(1) - - await act(async () => { - setDocumentVisibility('hidden') - }) - await act(async () => { - vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS - 100) - }) - await act(async () => { - setDocumentVisibility('visible') - }) - // Returning before the grace expires must cancel parking without reconnecting. - await act(async () => { - vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS) - }) - expect(stopFrameStream).not.toHaveBeenCalled() - expect(startFrameStream).toHaveBeenCalledTimes(1) - }) - - it('keeps streaming while hidden when occlusion state is proven stale (display-sleep wedge)', async () => { - vi.useFakeTimers() - await renderFrame(true) - expect(startFrameStream).toHaveBeenCalledTimes(1) - - // Real input disproves a wedged hidden/occluded signal; streaming must continue instead of leaving a black frame. - await act(async () => { - setDocumentVisibility('hidden') - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' })) - }) - await act(async () => { - vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS * 2) - }) - expect(stopFrameStream).not.toHaveBeenCalled() - expect(startFrameStream).toHaveBeenCalledTimes(1) - }) - it('resumes a parked stream when user input proves hidden visibility is stale', async () => { vi.useFakeTimers() await renderFrame(true) diff --git a/src/renderer/src/components/emulator-pane/emulator-device-picker-groups.test.ts b/src/renderer/src/components/emulator-pane/emulator-device-picker-groups.test.ts deleted file mode 100644 index 2fdc5a14c45..00000000000 --- a/src/renderer/src/components/emulator-pane/emulator-device-picker-groups.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { groupEmulatorDevices } from './emulator-device-picker-groups' -import { toSimulatorDeviceRows } from './emulator-device-row-mapping' - -describe('emulator picker inventory', () => { - it('keeps unavailable records out of used devices and retains every duplicate identity', () => { - const rows = toSimulatorDeviceRows([ - { - id: 'old', - name: 'iPhone 18 Pro', - backend: 'ios', - state: 'shutdown', - detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-0', - isAvailable: false, - lastUsedAt: '2026-10-30', - availabilityError: 'runtime removed' - }, - { - id: 'used', - name: 'iPhone 18 Pro', - backend: 'ios', - state: 'shutdown', - detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-2', - isAvailable: true, - lastUsedAt: '2026-10-31' - }, - { - id: 'unused', - name: 'iPhone 18 Pro', - backend: 'ios', - state: 'shutdown', - detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-2', - isAvailable: true - }, - { - id: 'pixel', - name: 'Pixel', - backend: 'android', - state: 'booted', - osVersion: '16', - apiLevel: '36', - isAvailable: true - } - ]) - const groups = groupEmulatorDevices(rows, 'old') - expect(groups.used.map((d) => d.udid)).toEqual(['used', 'pixel']) - expect(groups.other.map((d) => d.udid)).toEqual(['unused']) - expect(groups.unavailable.map((d) => d.udid)).toEqual(['old']) - expect(groups.unavailable[0].availabilityError).toBe('runtime removed') - }) - - it('includes the selected shutdown device and tolerates metadata from older hosts', () => { - const rows = toSimulatorDeviceRows([ - { id: 'legacy', name: 'Phone', state: 'shutdown' }, - { id: 'selected', name: 'Tablet', state: 'shutdown' } - ]) - const groups = groupEmulatorDevices(rows, 'selected') - expect(groups.used.map((d) => d.udid)).toEqual(['selected']) - expect(groups.other.map((d) => d.udid)).toEqual(['legacy']) - }) -}) diff --git a/src/renderer/src/components/emulator-pane/emulator-device-picker.test.tsx b/src/renderer/src/components/emulator-pane/emulator-device-picker.test.tsx deleted file mode 100644 index 863d052c51d..00000000000 --- a/src/renderer/src/components/emulator-pane/emulator-device-picker.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// @vitest-environment happy-dom -import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { EmulatorDevicePicker } from './emulator-device-picker' - -afterEach(cleanup) - -describe('EmulatorDevicePicker', () => { - it('reveals other devices during search, keeps unavailable rows disabled, and selects the exact duplicate', async () => { - const onSelect = vi.fn() - render( - - ) - fireEvent.click(screen.getByRole('combobox')) - expect(screen.queryByText('iPhone')).toBeNull() - fireEvent.change(screen.getByPlaceholderText('Search devices…'), { - target: { value: 'iPhone' } - }) - await waitFor(() => expect(screen.getAllByText('iPhone')).toHaveLength(2)) - const unavailable = screen.getByText('Missing runtime').closest('[cmdk-item]') - expect(unavailable?.getAttribute('aria-disabled')).toBe('true') - fireEvent.click(unavailable!) - expect(onSelect).not.toHaveBeenCalled() - fireEvent.click(screen.getByText('iOS 27.2 · Stopped')) - expect(onSelect).toHaveBeenCalledWith('new') - await waitFor(() => expect(screen.queryByPlaceholderText('Search devices…')).toBeNull()) - }) -}) diff --git a/src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx b/src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx index 6c2888257e6..67d0266bb47 100644 --- a/src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx +++ b/src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx @@ -1,6 +1,5 @@ import * as emulatorPreferences from '@/lib/emulator-opening-preferences-capability' import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities' -import { EMULATOR_OPENING_PREFERENCES_CAPABILITY } from '../../../../shared/emulator-runtime-capabilities' // @vitest-environment happy-dom import { act } from 'react' @@ -165,31 +164,6 @@ describe('useEmulatorPaneSession', () => { vi.restoreAllMocks() }) - it('asks the capable host on automatic open without sending a guessed device', async () => { - consumePrelaunchedSimulatorSession(WORKTREE_ID) - setLocalRuntimeCapabilitiesForTests([EMULATOR_OPENING_PREFERENCES_CAPABILITY]) - vi.mocked(window.api.runtime.call).mockImplementation(async (request) => { - if (request.method === 'emulator.listDevices') { - return runtimeSuccess(deviceList) - } - return runtimeSuccess({ attached: false, selectionRequired: 'first-use' }) - }) - await act(async () => { - root.render() - }) - await flushEffects() - expect(window.api.runtime.call).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'emulator.attach', - params: { worktree: WORKTREE_ID, focus: false, userInitiated: true } - }) - ) - expect(latest?.loading).toBe(false) - expect(latest?.selectedUdid).toBeNull() - expect(latest?.selectionRequired).toBe('first-use') - expect(latest?.error).toBeNull() - }) - it('retains an explicit session when an older automatic capability lookup finishes', async () => { consumePrelaunchedSimulatorSession(WORKTREE_ID) const capability = createDeferred() @@ -242,45 +216,6 @@ describe('useEmulatorPaneSession', () => { expect(latest?.displayName).toBe('iPhone B') }) - it('shows a connecting state instead of the old live preview while switching devices', async () => { - await act(async () => { - root.render() - }) - await flushEffects() - - expect(latest?.isLive).toBe(true) - expect(latest?.previewUrl).toBe('http://127.0.0.1:3100/stream.mjpeg') - - await act(async () => { - container.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - await Promise.resolve() - }) - - expect(latest?.loading).toBe(true) - expect(latest?.isLive).toBe(false) - expect(latest?.previewUrl).toBeUndefined() - expect(latest?.displayName).toBe('iPhone B') - expect(latest?.selectedUdid).toBe('device-b') - - await act(async () => { - attachDeferred.resolve({ - attached: true, - info: { - deviceUdid: 'device-b', - displayName: 'iPhone B', - streamUrl: 'http://127.0.0.1:3200/stream.mjpeg', - wsUrl: 'ws://127.0.0.1:3200/ws' - } - }) - await attachDeferred.promise - await Promise.resolve() - }) - - expect(latest?.loading).toBe(false) - expect(latest?.isLive).toBe(true) - expect(latest?.previewUrl).toBe('http://127.0.0.1:3200/stream.mjpeg') - }) - it('ignores a rotate response from the previous session after switching devices', async () => { await act(async () => { root.render() diff --git a/src/renderer/src/components/github/github-image-upload.test.ts b/src/renderer/src/components/github/github-image-upload.test.ts index 66d65646d9f..2b58a0871ca 100644 --- a/src/renderer/src/components/github/github-image-upload.test.ts +++ b/src/renderer/src/components/github/github-image-upload.test.ts @@ -64,51 +64,3 @@ it('refuses image intake on an older runtime before sending any bytes', async () ).rejects.toThrow('cannot upload images yet') expect(rpc.mock.calls.map((call) => call[1])).toEqual(['status.get']) }) - -it.each([2, 20])('checks capability once and uploads each of %i accepted images', async (count) => { - rpc.mockImplementation(async (_runtime, method) => { - if (method === 'status.get') { - return { capabilities: ['github.image-attachments.v1'] } - } - if (method === 'clipboard.startImageUpload') { - return { uploadId: 'image' } - } - if (method === 'github.uploadAttachment') { - return { url: 'https://github.com/user-attachments/assets/image' } - } - return {} - }) - const upload = captureGitHubImageUpload({ - runtime: { kind: 'local' }, - target: { kind: 'repo', repo: 'repo', source: 'origin' } - }) - for (let index = 0; index < count; index++) { - await upload(new File(['a'], `${index}.png`, { type: 'image/png' })) - } - expect(rpc.mock.calls.filter((call) => call[1] === 'status.get')).toHaveLength(1) - expect(rpc.mock.calls.filter((call) => call[1] === 'github.uploadAttachment')).toHaveLength(count) -}) - -it('stages a 5 MiB image in fourteen bounded chunks without putting bytes in Markdown', async () => { - rpc.mockImplementation(async (_runtime, method) => { - if (method === 'status.get') { - return { capabilities: ['github.image-attachments.v1'] } - } - if (method === 'clipboard.startImageUpload') { - return { uploadId: 'large' } - } - if (method === 'github.uploadAttachment') { - return { url: 'https://github.com/user-attachments/assets/large' } - } - return {} - }) - const url = await captureGitHubImageUpload({ - runtime: { kind: 'local' }, - target: { kind: 'repo', repo: 'repo', source: 'issue' } - })(new File([new Uint8Array(5 * 1024 * 1024)], 'large.png', { type: 'image/png' })) - expect(url).toBe('https://github.com/user-attachments/assets/large') - expect( - rpc.mock.calls.filter((call) => call[1] === 'clipboard.appendImageUploadChunk') - ).toHaveLength(14) - expect(rpc.mock.calls.filter((call) => call[1] === 'status.get')).toHaveLength(1) -}) diff --git a/src/renderer/src/components/github/use-github-textarea-images.test.tsx b/src/renderer/src/components/github/use-github-textarea-images.test.tsx index ba4c945e38e..7f2e385efaa 100644 --- a/src/renderer/src/components/github/use-github-textarea-images.test.tsx +++ b/src/renderer/src/components/github/use-github-textarea-images.test.tsx @@ -132,55 +132,6 @@ it('delivers a File drop through the registered owner and detaches an externally expect(view.container.querySelector('output')?.textContent).toBe('Replacement') }) -it.each([2, 20])( - 'keeps typing independent of %i historical labels and retires them with the draft', - async (count) => { - upload.mockReset().mockResolvedValue('https://github.com/user-attachments/assets/completed') - const changed = vi.fn() - const view = render() - const field = textareaField(view.getByRole('textbox')) - field.setSelectionRange(field.value.length, field.value.length) - fireEvent.paste(field, { - clipboardData: { - files: Array.from( - { length: count }, - (_, index) => new File(['png'], `history-${index}.png`, { type: 'image/png' }) - ) - } - }) - await waitFor(() => expect(upload).toHaveBeenCalledTimes(count)) - await waitFor(() => expect(field.value).not.toContain('Uploading')) - let projectionPasses = 0 - const originalReplace = String.prototype.replace - const replace = vi.spyOn(String.prototype, 'replace').mockImplementation(function ( - this: string, - pattern: Parameters[0], - replacement: Parameters[1] - ) { - if (pattern instanceof RegExp && pattern.source.startsWith('!\\[Uploading ')) { - projectionPasses++ - } - return originalReplace.call(this, pattern, replacement) - }) - changed.mockClear() - try { - fireEvent.change(field, { target: { value: `${field.value} typed` } }) - expect(projectionPasses).toBe(1) - expect(changed).toHaveBeenCalledOnce() - expect(field.value.endsWith(' typed')).toBe(true) - } finally { - replace.mockRestore() - } - fireEvent.change(field, { target: { value: `${field.value}![Uploading history-0.png…]()` } }) - expect(field.value).not.toContain('Uploading') - fireEvent.click(view.getByRole('button', { name: 'Replace' })) - fireEvent.change(field, { target: { value: 'Replacement ![Uploading history-0.png…]()' } }) - expect(view.container.querySelector('output')?.textContent).toBe( - 'Replacement ![Uploading history-0.png…]()' - ) - } -) - it('removes an undo-restored owned label after an incomplete user label prefix', async () => { const held = Promise.withResolvers() upload.mockReset().mockReturnValue(held.promise) diff --git a/src/renderer/src/components/jira-attachment-image.test.tsx b/src/renderer/src/components/jira-attachment-image.test.tsx index 21e4411e060..6e24c359a01 100644 --- a/src/renderer/src/components/jira-attachment-image.test.tsx +++ b/src/renderer/src/components/jira-attachment-image.test.tsx @@ -44,21 +44,6 @@ it('removes the previous image immediately when its mounted owner or reference c expect(release).toHaveBeenCalledOnce() }) -it('lets a new reference load after the previous reference failed', async () => { - acquire - .mockRejectedValueOnce(new Error('unavailable')) - .mockResolvedValueOnce({ url: 'blob:second', release }) - const view = render( - - ) - await waitFor(() => expect(acquire).toHaveBeenCalledOnce()) - await new Promise((resolve) => setTimeout(resolve, 0)) - view.rerender( - - ) - await waitFor(() => expect(screen.getByRole('img').getAttribute('src')).toBe('blob:second')) -}) - it('reacquires the same reference after disconnect and reconnect invalidations', async () => { acquire .mockResolvedValueOnce({ url: 'blob:first', release }) diff --git a/src/renderer/src/components/jira-issue-assignee-control.test.tsx b/src/renderer/src/components/jira-issue-assignee-control.test.tsx index 8ad90fc2d8b..36d489da874 100644 --- a/src/renderer/src/components/jira-issue-assignee-control.test.tsx +++ b/src/renderer/src/components/jira-issue-assignee-control.test.tsx @@ -93,31 +93,6 @@ beforeEach(() => { boundary.search.mockResolvedValue(users) }) describe('Existing Jira issue assignee', () => { - it('offers self first, alphabetic candidates, issue-scoped search, and unassignment', async () => { - const { mutateIssue } = mount() - fireEvent.click(screen.getByRole('button', { name: 'Assignee' })) - const search = await screen.findByPlaceholderText('Search users') - await screen.findByRole('button', { name: 'Ada' }) - const rows = screen.getAllByRole('button').map((button) => button.textContent) - expect(rows.indexOf('Zoe me')).toBeLessThan(rows.indexOf('Ada')) - expect(rows.indexOf('Ada')).toBeLessThan(rows.indexOf('Zoe')) - boundary.search.mockResolvedValue([{ accountId: 'beth', displayName: 'Beth' }]) - fireEvent.change(search, { target: { value: 'Beth' } }) - fireEvent.click(await screen.findByRole('button', { name: 'Beth' })) - expect(boundary.search).toHaveBeenLastCalledWith({ kind: 'local' }, 'ENG-1', 'Beth', 'site-1') - expect(mutateIssue).toHaveBeenCalledWith( - 'assignee', - { assigneeAccountId: 'beth' }, - { assignee: { accountId: 'beth', displayName: 'Beth' } } - ) - fireEvent.click(screen.getByRole('button', { name: 'Assignee' })) - fireEvent.click(screen.getByRole('button', { name: 'Unassigned' })) - expect(mutateIssue).toHaveBeenLastCalledWith( - 'assignee', - { assigneeAccountId: null }, - { assignee: undefined } - ) - }) it('loads the viewer from the selected source and hides it during an account switch', async () => { let resolve = (_status: JiraConnectionStatus) => {} boundary.readJiraStatus.mockReturnValue( @@ -210,12 +185,4 @@ describe('Existing Jira issue assignee', () => { 'site-1' ) }) - it('ignores assignment from a previously opened picker while another field saves', async () => { - const { props, rerender, mutateIssue } = mount() - fireEvent.click(screen.getByRole('button', { name: 'Assignee' })) - await screen.findByRole('button', { name: 'Ada' }) - rerender() - fireEvent.click(screen.getByRole('button', { name: 'Ada' })) - expect(mutateIssue).not.toHaveBeenCalled() - }) }) diff --git a/src/renderer/src/components/mobile/use-pairing-offer-expiry.test.ts b/src/renderer/src/components/mobile/use-pairing-offer-expiry.test.ts index 5a7ae23b7f9..5a55dfb5d9d 100644 --- a/src/renderer/src/components/mobile/use-pairing-offer-expiry.test.ts +++ b/src/renderer/src/components/mobile/use-pairing-offer-expiry.test.ts @@ -23,26 +23,6 @@ describe('usePairingOfferExpiry', () => { vi.useRealTimers() }) - it('fires one minute before expiry', () => { - const onDue = vi.fn() - renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue)) - act(() => vi.advanceTimersByTime(EXPIRES_AT - 60_000 - T0 - 1)) - expect(onDue).not.toHaveBeenCalled() - act(() => vi.advanceTimersByTime(1)) - expect(onDue).toHaveBeenCalledOnce() - }) - - it('waits while hidden and fires on return once due', () => { - const onDue = vi.fn() - renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue)) - act(() => setVisibility('hidden')) - act(() => vi.advanceTimersByTime(EXPIRES_AT - T0)) - expect(onDue).not.toHaveBeenCalled() - act(() => setVisibility('visible')) - act(() => vi.advanceTimersByTime(0)) - expect(onDue).toHaveBeenCalledOnce() - }) - it('fires soon after the computer wakes from sleep past the refresh time', () => { const onDue = vi.fn() renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue)) @@ -51,22 +31,4 @@ describe('usePairingOfferExpiry', () => { act(() => vi.advanceTimersByTime(15_000)) expect(onDue).toHaveBeenCalledOnce() }) - - it('rechecks when the window regains focus', () => { - const onDue = vi.fn() - renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue)) - vi.setSystemTime(EXPIRES_AT) - act(() => window.dispatchEvent(new Event('focus'))) - act(() => vi.advanceTimersByTime(0)) - expect(onDue).toHaveBeenCalledOnce() - }) - - it('stops after unmount and never runs without an expiry', () => { - const onDue = vi.fn() - const { unmount } = renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue)) - renderHook(() => usePairingOfferExpiry(null, onDue)) - unmount() - act(() => vi.advanceTimersByTime(EXPIRES_AT)) - expect(onDue).not.toHaveBeenCalled() - }) }) diff --git a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx b/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx index 6fc17144589..755f690410b 100644 --- a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx +++ b/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx @@ -92,31 +92,3 @@ it('holds Continue back from the settings wait until the opted-in launch names i expect(seen.at(-1)).toEqual({ pending: false, resuming: ['a', 'b'] }) expect(seen.some((entry) => !entry.pending && entry.resuming.length === 0)).toBe(false) }) - -it('holds nothing back once the launch found nothing to resume', async () => { - settings(true) - rpc.mockResolvedValue({ sessions: [] }) - await act(async () => root.render()) - expect(seen.at(-1)).toEqual({ pending: false, resuming: [] }) -}) - -it('holds nothing back when the launch asks instead of resuming', () => { - settings(false) - act(() => root.render()) - expect(seen.at(-1)).toEqual({ pending: false, resuming: [] }) -}) - -it('waits for settings when held chats enable the launch first, then resumes by default', async () => { - rpc.mockImplementation((_target, method) => - method === 'agentSession.restartResumable' - ? Promise.resolve({ sessions: offered }) - : new Promise(() => {}) - ) - await act(async () => root.render()) - expect(rpc).not.toHaveBeenCalledWith(expect.anything(), 'agentSession.restartResumable') - - settings(undefined) - await act(async () => root.render()) - - expect(seen.at(-1)).toEqual({ pending: false, resuming: ['a', 'b'] }) -}) diff --git a/src/renderer/src/components/native-chat/NativeChatAccountUsageDialog.test.tsx b/src/renderer/src/components/native-chat/NativeChatAccountUsageDialog.test.tsx index 36339f8a996..bc7be82a909 100644 --- a/src/renderer/src/components/native-chat/NativeChatAccountUsageDialog.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatAccountUsageDialog.test.tsx @@ -4,7 +4,6 @@ import { StrictMode } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentSessionAccountUsage } from '../../../../shared/agent-session/agent-session-account-usage' import NativeChatAccountUsageDialog from './NativeChatAccountUsageDialog' -import { openNativeChatAccountUsageModal } from './native-chat-account-usage-modal' const mocks = vi.hoisted(() => ({ read: vi.fn(), @@ -87,23 +86,6 @@ afterEach(() => { }) describe('/usage modal', () => { - it('opens for the chat it was typed in and closes from its Close button', async () => { - mocks.read.mockResolvedValue(result) - openNativeChatAccountUsageModal({ sessionId: 'chat', target }) - expect(mocks.openModal).toHaveBeenCalledWith('chat-account-usage', { - sessionId: 'chat', - target - }) - render() - expect(screen.getByRole('dialog', { name: 'Codex usage' })).toBeTruthy() - await screen.findByText('chat@example.com') - expect(mocks.read).toHaveBeenCalledWith(target, 'agentSession.accountUsage', { - sessionId: 'chat' - }) - fireEvent.click(screen.getByRole('button', { name: 'Close' })) - expect(mocks.closeModal).toHaveBeenCalled() - }) - it('keeps the newer first read when a replayed mount’s older read fails late', async () => { let failFirst!: (error: Error) => void mocks.read @@ -146,22 +128,6 @@ describe('/usage modal', () => { await screen.findByText('Nothing to reset right now.') }) - it('keeps the reset outcome when the refresh after it fails, through Retry', async () => { - mocks.read - .mockResolvedValueOnce(result) - .mockRejectedValueOnce(new Error('connection lost')) - .mockResolvedValueOnce(result) - mocks.reset.mockResolvedValueOnce({ outcome: 'reset' }) - render() - await screen.findByText('chat@example.com') - fireEvent.click(screen.getByRole('button', { name: 'Reset now' })) - await screen.findByText('Codex usage couldn’t be loaded. Try again.') - expect(screen.getByText('Codex limits were reset.')).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: 'Retry' })) - await screen.findByText('chat@example.com') - expect(screen.getByText('Codex limits were reset.')).toBeTruthy() - }) - it('reads nothing when a reset confirmed before closing finishes after it', async () => { let finish!: (value: unknown) => void mocks.read.mockResolvedValue(result) @@ -179,22 +145,6 @@ describe('/usage modal', () => { expect(mocks.read).toHaveBeenCalledOnce() }) - it('re-reads the offer when the host’s Codex state lands after the modal opened', async () => { - const { reset: _offer, ...withoutOffer } = result - mocks.read.mockResolvedValueOnce(withoutOffer).mockResolvedValueOnce(result) - render() - await screen.findByText('chat@example.com') - expect(screen.queryByRole('button', { name: 'Reset now' })).toBeNull() - act(() => hostStream.snapshot?.(hostState('chat', 0, 'fetching'))) - expect(mocks.read).toHaveBeenCalledOnce() - act(() => hostStream.snapshot?.(hostState('chat', 1))) - await screen.findByRole('button', { name: 'Reset now' }) - expect(mocks.read).toHaveBeenCalledTimes(2) - // An unchanged host state reads nothing more. - act(() => hostStream.snapshot?.(hostState('chat', 1))) - expect(mocks.read).toHaveBeenCalledTimes(2) - }) - it('collapses host changes during a background read into one follow-up, and sends none after closing', async () => { const finishes: ((value: AgentSessionAccountUsage) => void)[] = [] mocks.read.mockResolvedValueOnce(result).mockImplementation( diff --git a/src/renderer/src/components/native-chat/NativeChatComposerPlanChip.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposerPlanChip.test.tsx deleted file mode 100644 index f891804d70c..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatComposerPlanChip.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// @vitest-environment happy-dom -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, expect, it, vi } from 'vitest' -import { TooltipProvider } from '@/components/ui/tooltip' -import { NativeChatComposerPlanChip, useNativeChatPlanMode } from './NativeChatComposerPlanChip' -import type { SessionOptionsSurface } from '../../../../shared/native-chat/input/native-chat-session-options' -import { structuredAgentSessionInteractionDescriptor } from '../../../../shared/agent-session/options/agent-session-interaction-mode' -import { createStructuredAgentSessionOptionState } from '../../../../shared/agent-session/options/structured-agent-session-options' - -vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) -vi.mock('@/i18n/use-ui-language-revision', () => ({ useUiLanguageRevision: () => 0 })) -vi.mock('sonner', () => ({ toast: { error: vi.fn() } })) -afterEach(cleanup) - -function PlanChip(props: Parameters[0]) { - return -} - -it('shows only active Plan, exits with an absolute Build choice, and retains Plan after a failed exit', async () => { - const descriptor = structuredAgentSessionInteractionDescriptor({ - ...createStructuredAgentSessionOptionState('codex', null), - interactionModeSupported: true - })! - const setOption = vi - .fn() - .mockRejectedValueOnce(new Error('Disconnected')) - .mockResolvedValue({ snapshot: [] }) - const surface: SessionOptionsSurface = { - getSnapshot: () => [descriptor], - setOption, - invokeAction: async () => ({ snapshot: [] }), - subscribe: () => () => {} - } - const focus = vi.fn() - const renderChip = (snapshot: (typeof descriptor)[]) => ( - - - - ) - const view = render(renderChip([descriptor])) - expect(screen.queryByRole('button')).toBeNull() - const plan = { - ...descriptor, - kind: { type: 'select' as const, currentValue: 'plan', choices: [] } - } - view.rerender(renderChip([plan])) - await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Exit plan mode' }))) - expect(setOption).toHaveBeenLastCalledWith('interactionMode', 'default') - expect(screen.getByRole('button', { name: 'Exit plan mode' })).toHaveProperty('disabled', false) - expect(focus).toHaveBeenCalledOnce() - await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Exit plan mode' }))) - expect(setOption).toHaveBeenLastCalledWith('interactionMode', 'default') - view.rerender(renderChip([descriptor])) - expect(screen.queryByRole('button')).toBeNull() - view.rerender(renderChip([])) - expect(screen.queryByRole('button')).toBeNull() -}) diff --git a/src/renderer/src/components/native-chat/NativeChatComposerToolbar.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposerToolbar.test.tsx index dd11122f827..e119fd29818 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposerToolbar.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposerToolbar.test.tsx @@ -5,7 +5,6 @@ import { Editor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { focusMountedNativeChatEditor } from './native-chat-mounted-editor-view' import { TooltipProvider } from '@/components/ui/tooltip' -import { NativeChatComposerToolbar } from './NativeChatComposerToolbar' import { NativeChatComposerActions } from './NativeChatComposerActions' import type { SessionOptionDescriptor, @@ -250,58 +249,3 @@ it('keeps current mode, permissions and Goal reachable through overflow, includi view.unmount() editor.destroy() }) - -it('limits a footer resize to its own controls with five and fifty mounted composers', () => { - const callbacks = new Map void>() - vi.stubGlobal( - 'ResizeObserver', - class { - constructor(private callback: ResizeObserverCallback) {} - observe(target: Element) { - callbacks.set(target, () => this.callback([], this)) - } - unobserve(target: Element) { - callbacks.delete(target) - } - disconnect() {} - } - ) - vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(120) - const measured: HTMLElement[] = [] - vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function ( - this: HTMLElement - ) { - if (this.hasAttribute('data-composer-control')) { - measured.push(this) - } - return new DOMRect(0, 0, 40, 24) - }) - for (const population of [5, 50]) { - const view = render( - <> - {Array.from({ length: population }, (_, index) => ( -
- Plan }]} - right={[{ id: 'model', content: }]} - primary={} - /> -
- ))} - - ) - const chat = screen.getByRole('region', { name: 'Chat 0' }) - const toolbar = chat.querySelector('[data-compact]') - if (!toolbar) { - throw new Error('Expected the mounted composer toolbar') - } - measured.length = 0 - act(() => callbacks.get(toolbar)?.()) - // The changed footer needs at most full and compact widths for its two controls. - expect(measured.length).toBeGreaterThan(0) - expect(measured.length).toBeLessThanOrEqual(4) - expect(measured.every((control) => chat.contains(control))).toBe(true) - view.unmount() - callbacks.clear() - } -}) diff --git a/src/renderer/src/components/native-chat/NativeChatComposerWorkspace.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposerWorkspace.test.tsx deleted file mode 100644 index b1d6b4be540..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatComposerWorkspace.test.tsx +++ /dev/null @@ -1,294 +0,0 @@ -// @vitest-environment happy-dom -import { Profiler } from 'react' -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { - makeWorktree, - makeFolderWorkspace, - TEST_REPO -} from '@/store/slices/worktrees-slice-test-fixtures' -import { makeTab } from '@/store/slices/store-session-test-harness' -import { TooltipProvider } from '@/components/ui/tooltip' -import { NativeChatComposerWorkspace } from './NativeChatComposerWorkspace' -import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture' - -vi.mock('@/store', async () => { - const { createTestStore } = await import('@/store/slices/store-test-helpers') - return { useAppStore: createTestStore() } -}) - -const clipboard = vi.fn(async () => {}) -const wt = makeWorktree({ - id: 'wt-1', - repoId: TEST_REPO.id, - branch: 'refs/heads/feature/composer', - hostId: 'local' -}) - -beforeEach(() => { - vi.stubGlobal('api', undefined) - Object.defineProperty(window, 'api', { - configurable: true, - value: { ui: { writeClipboardText: clipboard } } - }) - clipboard.mockClear() - useAppStore.setState({ - settings: createGlobalSettingsFixture({ machineName: 'My laptop' }), - runtimeEnvironments: [], - sshTargetLabels: new Map(), - repos: [TEST_REPO], - worktreesByRepo: { [TEST_REPO.id]: [wt] }, - detectedWorktreesByRepo: {}, - tabsByWorktree: { [wt.id]: [makeTab({ id: 'tab-1', worktreeId: wt.id })] }, - unifiedTabsByWorktree: {}, - folderWorkspaces: [], - structuredSessionLaunchDirectoryByTabId: {}, - activeWorktreeId: wt.id, - activeWorkspaceExecutionHostId: 'local' - }) -}) -afterEach(() => { - cleanup() - vi.unstubAllGlobals() -}) - -function Footer() { - return ( - - - - ) -} - -describe('chat composer workspace', () => { - it('shows the machine, sidebar checkout name and branch without a separate project label', async () => { - useAppStore.setState({ - settings: createGlobalSettingsFixture(), - worktreesByRepo: { - [TEST_REPO.id]: [{ ...wt, displayName: 'my-checkout', isMainWorktree: true }] - } - }) - const getStatus = vi.fn().mockResolvedValue({ machineName: 'Detected laptop' }) - Object.defineProperty(window.api, 'runtime', { - configurable: true, - value: { getStatus } - }) - render(