From 72104bfc64de3a2dc0454dc1dc2fc6661b338bdf Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 30 Oct 2026 22:55:00 -0700 Subject: [PATCH] test: remove low-value fixture and forwarding suites (#36600) --- .github/workflows/computer-e2e.yml | 1 - config/flaky-test-pattern-baseline.txt | 1 - config/scripts/ci-shard-timings.json | 21 -- config/scripts/pr-package-test-paths.mjs | 1 - .../codex-hooks-read-denial-followup.test.ts | 138 ---------- .../codex/codex-session-source-home.test.ts | 72 ----- .../desktop-script-provider-client.test.ts | 202 -------------- src/main/github/issue-mutation-source.test.ts | 174 ------------ .../work-item-details-issue-source.test.ts | 239 ---------------- src/main/hooks-issue-command.test.ts | 144 ---------- .../ipc/readdir-error-diagnostics.test.ts | 72 ----- .../wsl-move-rewrite-store-coverage.test.ts | 258 ------------------ .../legacy-terminal-windows-tombstone.test.ts | 224 --------------- .../agent-hooks-uninstall-preflight.test.ts | 204 -------------- .../startup/codex-hook-startup-wiring.test.ts | 132 --------- .../startup/dev-instance-identity.test.ts | 78 ------ src/main/startup/linux-dev-shm-policy.test.ts | 145 ---------- .../linux-secret-service-probe.test.ts | 59 ---- .../wsl-holder-uninstall-preflight.test.ts | 133 --------- src/main/wsl/wsl-w1-w3-contract.test.ts | 153 ----------- .../source-control-discard-dialog.test.tsx | 142 ---------- ...source-control-entry-context-menu.test.tsx | 241 ---------------- ...e-control-header-toolbar-identity.test.tsx | 178 ------------ .../source-control-split-open.test.ts | 72 ----- ...ktree-visibility-source-provenance.test.ts | 129 --------- .../skill-install-provider-groups.test.ts | 60 ---- .../ResourceUsageStatusSegment.rows.test.tsx | 177 ------------ ...page-task-source-host-availability.test.ts | 132 --------- 28 files changed, 3582 deletions(-) delete mode 100644 src/main/codex/codex-hooks-read-denial-followup.test.ts delete mode 100644 src/main/codex/codex-session-source-home.test.ts delete mode 100644 src/main/computer/desktop-script-provider-client.test.ts delete mode 100644 src/main/github/issue-mutation-source.test.ts delete mode 100644 src/main/github/work-item-details-issue-source.test.ts delete mode 100644 src/main/hooks-issue-command.test.ts delete mode 100644 src/main/ipc/readdir-error-diagnostics.test.ts delete mode 100644 src/main/persistence/moving-wsl-projects/wsl-move-rewrite-store-coverage.test.ts delete mode 100644 src/main/pty/legacy-terminal-windows-tombstone.test.ts delete mode 100644 src/main/startup/agent-hooks-uninstall-preflight.test.ts delete mode 100644 src/main/startup/codex-hook-startup-wiring.test.ts delete mode 100644 src/main/startup/dev-instance-identity.test.ts delete mode 100644 src/main/startup/linux-dev-shm-policy.test.ts delete mode 100644 src/main/startup/linux-secret-service-probe.test.ts delete mode 100644 src/main/startup/wsl-holder-uninstall-preflight.test.ts delete mode 100644 src/main/wsl/wsl-w1-w3-contract.test.ts delete mode 100644 src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx delete mode 100644 src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx delete mode 100644 src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx delete mode 100644 src/renderer/src/components/right-sidebar/source-control-split-open.test.ts delete mode 100644 src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts delete mode 100644 src/renderer/src/components/skills/skill-install-provider-groups.test.ts delete mode 100644 src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx delete mode 100644 src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml index 567a45b73cb..a46e6ecb628 100644 --- a/.github/workflows/computer-e2e.yml +++ b/.github/workflows/computer-e2e.yml @@ -100,7 +100,6 @@ jobs: src/main/computer/macos-computer-use-permissions.test.ts src/main/computer/macos-computer-use-permission-status.test.ts src/main/providers/windows-shell-preflight-runtime.windows.test.ts - src/main/computer/desktop-script-provider-client.test.ts src/main/computer/desktop-script-provider-cache.test.ts src/main/computer/desktop-script-provider-actions.test.ts src/main/computer/desktop-script-provider-cache-lifecycle.test.ts diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 76a5c0a5ef8..27898c35e8b 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -457,7 +457,6 @@ 2 test-no-real-time-wait:src/main/ssh/sftp-upload.test.ts 1 test-no-real-time-wait:src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts 2 test-no-real-time-wait:src/main/ssh/ssh-quit-abort-upload.test.ts -1 test-no-real-time-wait:src/main/startup/codex-hook-startup-wiring.test.ts 2 test-no-real-time-wait:src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts 1 test-no-real-time-wait:src/main/startup/main-process-ready-duplicate-pty-repair.test.ts 3 test-no-real-time-wait:src/main/startup/run-electron-vite-dev-web.test.ts diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 1e131ad987f..5b879e555a9 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -1776,7 +1776,6 @@ "src/main/codex/codex-hook-trust-failure-memo.test.ts": 242, "src/main/codex/codex-hook-trust-grant.test.ts": 681, "src/main/codex/codex-hook-trust-memo.test.ts": 165, - "src/main/codex/codex-hooks-read-denial-followup.test.ts": 324, "src/main/codex/codex-index-heal-binary-contract.test.ts": 121, "src/main/codex/codex-interrupt-hook-trust.test.ts": 104, "src/main/codex/codex-legacy-session-resume.test.ts": 188, @@ -1812,7 +1811,6 @@ "src/main/codex/codex-session-resume-preparation.test.ts": 75, "src/main/codex/codex-session-resume-windows.test.ts": 57, "src/main/codex/codex-session-resume-wrong-account.test.ts": 65, - "src/main/codex/codex-session-source-home.test.ts": 37, "src/main/codex/codex-shared-server-fix.test.ts": 63, "src/main/codex/codex-shared-server-pane.test.ts": 66, "src/main/codex/codex-shared-server-probe.test.ts": 120, @@ -1947,7 +1945,6 @@ "src/main/computer/desktop-script-provider-bridge.test.ts": 59, "src/main/computer/desktop-script-provider-cache-lifecycle.test.ts": 70, "src/main/computer/desktop-script-provider-cache.test.ts": 109, - "src/main/computer/desktop-script-provider-client.test.ts": 237, "src/main/computer/desktop-script-provider-errors.test.ts": 279, "src/main/computer/desktop-script-provider-paste-validation.test.ts": 93, "src/main/computer/desktop-script-provider-paths.test.ts": 69, @@ -2620,7 +2617,6 @@ "src/main/github/github-repository-identity.gh-account.test.ts": 623, "src/main/github/github-repository-identity.signed-cache.test.ts": 526, "src/main/github/github-repository-identity.ssh-host-alias.test.ts": 490, - "src/main/github/issue-mutation-source.test.ts": 553, "src/main/github/issues.test.ts": 511, "src/main/github/pr-head-tracking-ref.test.ts": 59, "src/main/github/pr-mutation-review-invalidation.test.ts": 2171, @@ -2654,7 +2650,6 @@ "src/main/github/work-item-details-config-change.test.ts": 563, "src/main/github/work-item-details-enterprise-host.test.ts": 372, "src/main/github/work-item-details-file-viewed.test.ts": 487, - "src/main/github/work-item-details-issue-source.test.ts": 76, "src/main/github/work-item-details-pr-files.test.ts": 535, "src/main/github/work-item-details.test.ts": 428, "src/main/github/work-item-search-fallback-environment.test.ts": 463, @@ -2707,7 +2702,6 @@ "src/main/hook-termination-real-process.test.ts": 3176, "src/main/hooks-archive-exit-observation.test.ts": 229, "src/main/hooks-effective-hook-resolution.test.ts": 249, - "src/main/hooks-issue-command.test.ts": 307, "src/main/hooks-orca-yaml-parsing.test.ts": 366, "src/main/hooks-orca-yaml-unc-drive.test.ts": 316, "src/main/hooks-runner.test.ts": 193, @@ -3044,7 +3038,6 @@ "src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts": 642, "src/main/ipc/qoder-desktop-execution-host.test.ts": 3315, "src/main/ipc/qoder-independent-renderer-spawn-review.test.ts": 3959, - "src/main/ipc/readdir-error-diagnostics.test.ts": 29, "src/main/ipc/rebinding-file-watch-noise.test.ts": 1071, "src/main/ipc/rebinding-file-watch.test.ts": 4132, "src/main/ipc/register-core-handlers/register-core-handlers.test.ts": 706, @@ -4515,7 +4508,6 @@ "src/main/pty/codex-shell-no-daemon.test.ts": 8257, "src/main/pty/conda-activation-env.test.ts": 44, "src/main/pty/legacy-terminal-shim-dir.test.ts": 183, - "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 49, "src/main/pty/managed-ssh-host-login-shell.test.ts": 40, "src/main/pty/node-pty-master-fd-retirement.test.ts": 2376, "src/main/pty/overlay-mirror.test.ts": 73, @@ -6302,7 +6294,6 @@ "src/main/startup/cli-launch-redirect.test.ts": 120, "src/main/startup/cloud-session-shutdown-flush.test.ts": 77, "src/main/startup/codex-account-restart-resume.test.ts": 329, - "src/main/startup/codex-hook-startup-wiring.test.ts": 1174, "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 144, "src/main/startup/codex-session-resume-daemon-guard.test.ts": 117, "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 95, @@ -6310,7 +6301,6 @@ "src/main/startup/configure-process.test.ts": 178, "src/main/startup/desktop-profile-instance-lock.test.ts": 167, "src/main/startup/dev-education-suppression.test.ts": 110, - "src/main/startup/dev-instance-identity.test.ts": 73, "src/main/startup/ensure-virtual-display.test.ts": 70, "src/main/startup/first-window-startup-services.test.ts": 34, "src/main/startup/gpu-fallback-marker.test.ts": 48, @@ -6321,9 +6311,7 @@ "src/main/startup/hydrate-shell-path.test.ts": 471, "src/main/startup/hydrate-shell-path.windows.test.ts": 434, "src/main/startup/legacy-worker-renderer-recovery.test.ts": 51, - "src/main/startup/linux-dev-shm-policy.test.ts": 48, "src/main/startup/linux-keyring-backend-selection.test.ts": 39, - "src/main/startup/linux-secret-service-probe.test.ts": 41, "src/main/startup/login-shell-environment-cache.test.ts": 111, "src/main/startup/login-shell-environment.test.ts": 80, "src/main/startup/main-process-error-guards.test.ts": 73, @@ -6681,7 +6669,6 @@ "src/main/wsl/wsl-runner.wsl.test.ts": 64, "src/main/wsl/wsl-unc-drive-file-content.test.ts": 33, "src/main/wsl/wsl-unc-drive-fs-path.test.ts": 40, - "src/main/wsl/wsl-w1-w3-contract.test.ts": 54, "src/main/zcode/hook-service.test.ts": 107, "src/main/zcode/interactive-capability.live.test.ts": 55, "src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts": 32, @@ -8248,12 +8235,9 @@ "src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts": 148, "src/renderer/src/components/right-sidebar/source-control-created-review-link.test.ts": 57, "src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts": 118, - "src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx": 196, "src/renderer/src/components/right-sidebar/source-control-dropdown-items.create-pr-intent.test.ts": 163, "src/renderer/src/components/right-sidebar/source-control-dropdown-items.test.ts": 256, - "src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx": 367, "src/renderer/src/components/right-sidebar/source-control-file-filter.test.ts": 46, - "src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx": 346, "src/renderer/src/components/right-sidebar/source-control-hosted-review-creation-eligibility-snapshot.test.ts": 87, "src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.test.ts": 52, "src/renderer/src/components/right-sidebar/source-control-huge-repo-warning-dismissals.test.ts": 150, @@ -8264,7 +8248,6 @@ "src/renderer/src/components/right-sidebar/source-control-push-recovery.test.ts": 35, "src/renderer/src/components/right-sidebar/source-control-recovery-admission.test.ts": 131, "src/renderer/src/components/right-sidebar/source-control-section-order.test.ts": 69, - "src/renderer/src/components/right-sidebar/source-control-split-open.test.ts": 52, "src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts": 46, "src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx": 632, "src/shared/source-control-tree.test.ts": 57, @@ -8883,7 +8866,6 @@ "src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts": 119, "src/renderer/src/components/sidebar/worktree-unambiguous-id-index.test.ts": 19, "src/renderer/src/components/sidebar/worktree-unnest.test.ts": 94, - "src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts": 45, "src/renderer/src/components/sidebar/wsl-host-row-status.test.ts": 282, "src/renderer/src/components/skills/SkillFreshnessNudge.test.tsx": 292, "src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx": 2390, @@ -8902,7 +8884,6 @@ "src/renderer/src/components/skills/skill-delete-copy.test.ts": 146, "src/renderer/src/components/skills/skill-description-length.test.ts": 34, "src/renderer/src/components/skills/skill-install-progress-state.test.tsx": 258, - "src/renderer/src/components/skills/skill-install-provider-groups.test.ts": 32, "src/renderer/src/components/skills/skill-install-workspace-choices.test.ts": 55, "src/renderer/src/components/skills/skill-share-link.test.ts": 38, "src/renderer/src/components/skills/skill-share-package-selection.test.ts": 48, @@ -8922,7 +8903,6 @@ "src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx": 2041, "src/renderer/src/components/status-bar/PortsStatusSegment.host-routing.test.tsx": 712, "src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx": 1289, - "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx": 1780, "src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx": 381, "src/renderer/src/components/status-bar/SkillUpdateStatusSegment.test.tsx": 436, "src/renderer/src/components/status-bar/SshStatusSegment.large-host-list.test.tsx": 1552, @@ -9123,7 +9103,6 @@ "src/renderer/src/components/task-page/task-page-list-chrome-visibility.test.ts": 33, "src/renderer/src/components/task-page/task-page-mutation-page-allocation.test.ts": 128, "src/renderer/src/components/task-page/task-page-new-issue-draft.test.ts": 76, - "src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts": 401, "src/renderer/src/components/task-page/task-page-work-item-pagination.test.ts": 56, "src/renderer/src/components/task-page/github/rows-twin-host-routing.test.tsx": 1521, "src/renderer/src/components/task-page/github/task-page-github-dialog-state-authority.test.ts": 71, diff --git a/config/scripts/pr-package-test-paths.mjs b/config/scripts/pr-package-test-paths.mjs index 7b2b4280d73..4c0cba56430 100644 --- a/config/scripts/pr-package-test-paths.mjs +++ b/config/scripts/pr-package-test-paths.mjs @@ -88,7 +88,6 @@ export const WINDOWS_BOUNDARY_TESTS = [ 'src/main/wsl/wsl-guest-environment.test.ts', 'src/main/wsl/wsl-executable-path.win32.test.ts', 'src/main/claude-accounts/claude-profile-history-windows.test.ts', - 'src/main/wsl/wsl-w1-w3-contract.test.ts', 'src/shared/source-scan/source-tree-scan.test.ts', 'src/main/cli/wsl-cli-powershell-boundary.test.ts', 'src/main/computer/desktop-script-runtime-host.win32.test.ts', diff --git a/src/main/codex/codex-hooks-read-denial-followup.test.ts b/src/main/codex/codex-hooks-read-denial-followup.test.ts deleted file mode 100644 index 57066cf5306..00000000000 --- a/src/main/codex/codex-hooks-read-denial-followup.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as NodeFs from 'node:fs' -import type * as NodeOs from 'node:os' -import type * as CodexManagedTrustReconciliation from './codex-managed-trust-reconciliation' -import { tmpdir } from 'node:os' -import { join } from 'node:path' - -// Measured chmod/icacls denials leave exists/stat successful but fail content reads -// (macOS EACCES -13, Windows EPERM -4048); do not fake absence. - -const denials = vi.hoisted(() => { - const state = { - paths: new Set(), - deny(path: string): void { - state.paths.add(path) - }, - reset(): void { - state.paths.clear() - }, - check(target: unknown, syscall: string): void { - if (typeof target !== 'string' || !state.paths.has(target)) { - return - } - const error: NodeJS.ErrnoException = new Error( - `EACCES: permission denied, ${syscall} '${target}'` - ) - error.code = 'EACCES' - error.errno = -13 - error.syscall = syscall - error.path = target - throw error - } - } - return state -}) - -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal() - const guardRead = (fn: unknown, syscall: string): unknown => { - const original = fn as (...args: unknown[]) => unknown - const wrapped = (...args: unknown[]): unknown => { - denials.check(args[0], syscall) - return original(...args) - } - return Object.assign(wrapped, original) - } - const patched: Record = { - ...actual, - readFileSync: guardRead(actual.readFileSync, 'read'), - openSync: Object.assign((...args: unknown[]): unknown => { - const flags = args[1] - if (flags === undefined || (typeof flags === 'string' && flags.startsWith('r'))) { - denials.check(args[0], 'open') - } - return (actual.openSync as (...a: unknown[]) => unknown)(...args) - }, actual.openSync) - } - return { ...patched, default: patched } -}) - -const { getPathMock, homedirMock } = vi.hoisted(() => ({ - getPathMock: vi.fn<(name: string) => string>(), - homedirMock: vi.fn<() => string>() -})) - -vi.mock('electron', () => ({ app: { getPath: getPathMock } })) -vi.mock('node:os', async () => { - const actual = await vi.importActual('node:os') - return { ...actual, homedir: homedirMock } -}) - -const realFs = await vi.importActual('node:fs') - -let fakeHomeDir: string -let userDataDir: string -let previousUserDataPath: string | undefined - -beforeEach(() => { - denials.reset() - fakeHomeDir = realFs.mkdtempSync(join(tmpdir(), 'orca-hooksread-home-')) - userDataDir = realFs.mkdtempSync(join(tmpdir(), 'orca-hooksread-data-')) - previousUserDataPath = process.env.ORCA_USER_DATA_PATH - process.env.ORCA_USER_DATA_PATH = userDataDir - homedirMock.mockReturnValue(fakeHomeDir) - getPathMock.mockImplementation((name: string) => { - if (name === 'userData') { - return userDataDir - } - throw new Error(`unexpected app.getPath(${name})`) - }) - realFs.mkdirSync(join(fakeHomeDir, '.codex'), { recursive: true }) -}) - -afterEach(() => { - denials.reset() - realFs.rmSync(fakeHomeDir, { recursive: true, force: true }) - realFs.rmSync(userDataDir, { recursive: true, force: true }) - if (previousUserDataPath === undefined) { - delete process.env.ORCA_USER_DATA_PATH - } else { - process.env.ORCA_USER_DATA_PATH = previousUserDataPath - } - vi.clearAllMocks() -}) - -describe('an unreadable legacy hooks.json must not clear managed trust or its ledger', () => { - it('returns before the removal branch instead of discarding approvals', async () => { - const legacyHooksPath = join(fakeHomeDir, '.codex', 'hooks.json') - realFs.writeFileSync(legacyHooksPath, JSON.stringify({ hooks: { Stop: [] } }), 'utf-8') - - const removeSpy = vi.fn() - vi.resetModules() - vi.doMock('./codex-managed-trust-reconciliation', async (importOriginal) => { - const actual = await importOriginal() - return { ...actual, removeCodexManagedHookTrustEntries: removeSpy } - }) - try { - // Seed a grant so accidental removal would be observable. - const ledger = await import('./codex-trust-grant-ledger') - ledger.writeCodexTrustGrantLedgerHome( - join(fakeHomeDir, '.codex'), - { entries: { 'seed:1': { trustedHash: 'sha256:seed' } } } as never, - ledger.getCodexTrustGrantLedgerPath() - ) - - const hookService = await import('./hook-service') - denials.deny(legacyHooksPath) - - hookService._internals.cleanupLegacySystemManagedHooks() - - // Unreadable hooks must preserve existing trust and its ownership ledger. - expect(removeSpy).not.toHaveBeenCalled() - } finally { - vi.doUnmock('./codex-managed-trust-reconciliation') - vi.resetModules() - } - }) -}) diff --git a/src/main/codex/codex-session-source-home.test.ts b/src/main/codex/codex-session-source-home.test.ts deleted file mode 100644 index fa29650c495..00000000000 --- a/src/main/codex/codex-session-source-home.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - resolveHostCodexSessionSourceHome, - resolveWslCodexSessionSourceHome -} from './codex-session-source-home' - -describe('resolveHostCodexSessionSourceHome', () => { - it('returns undefined when no override is configured', () => { - expect(resolveHostCodexSessionSourceHome({})).toBeUndefined() - expect(resolveHostCodexSessionSourceHome({ codexSessionSourceHome: {} })).toBeUndefined() - }) - - it('returns undefined for blank/whitespace overrides so the default is kept', () => { - expect( - resolveHostCodexSessionSourceHome({ codexSessionSourceHome: { host: ' ' } }) - ).toBeUndefined() - }) - - it('returns the trimmed host override path', () => { - expect( - resolveHostCodexSessionSourceHome({ codexSessionSourceHome: { host: ' /custom/codex ' } }) - ).toBe('/custom/codex') - }) -}) - -describe('resolveWslCodexSessionSourceHome', () => { - it('returns undefined when no per-distro override exists', () => { - expect(resolveWslCodexSessionSourceHome({}, 'Ubuntu')).toBeUndefined() - expect( - resolveWslCodexSessionSourceHome( - { codexSessionSourceHome: { wsl: { Debian: '/home/me/.codex' } } }, - 'Ubuntu' - ) - ).toBeUndefined() - }) - - it('resolves a per-distro override for the matching distro', () => { - expect( - resolveWslCodexSessionSourceHome( - { codexSessionSourceHome: { wsl: { Ubuntu: '/home/me/.config/codex' } } }, - 'Ubuntu' - ) - ).toBe('/home/me/.config/codex') - }) - - it('matches distro names case-insensitively, mirroring WSL', () => { - expect( - resolveWslCodexSessionSourceHome( - { codexSessionSourceHome: { wsl: { Ubuntu: '/home/me/.config/codex' } } }, - 'ubuntu' - ) - ).toBe('/home/me/.config/codex') - }) - - it('ignores blank per-distro overrides so the default is kept', () => { - expect( - resolveWslCodexSessionSourceHome( - { codexSessionSourceHome: { wsl: { Ubuntu: ' ' } } }, - 'Ubuntu' - ) - ).toBeUndefined() - }) - - it('does not leak the host override into WSL resolution', () => { - expect( - resolveWslCodexSessionSourceHome( - { codexSessionSourceHome: { host: '/custom/codex' } }, - 'Ubuntu' - ) - ).toBeUndefined() - }) -}) diff --git a/src/main/computer/desktop-script-provider-client.test.ts b/src/main/computer/desktop-script-provider-client.test.ts deleted file mode 100644 index 2bdefe5763d..00000000000 --- a/src/main/computer/desktop-script-provider-client.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest' -import { - createDesktopScriptProviderClient, - mockBridgeResponse, - publicSnapshotKeys, - resetDesktopScriptProviderTestHarness, - sampleBridgeSnapshot, - sampleCapabilities -} from './desktop-script-provider-test-harness' - -describe('DesktopScriptProviderClient snapshots', () => { - afterEach(resetDesktopScriptProviderTestHarness) - - it('normalizes list-apps responses', async () => { - mockBridgeResponse({ - ok: true, - apps: [{ name: 'Notepad', bundleIdentifier: 'notepad', pid: 42 }] - }) - - const client = await createDesktopScriptProviderClient('windows', '/tmp/runtime.ps1') - - await expect(client.listApps()).resolves.toEqual({ - apps: [ - { - name: 'Notepad', - bundleId: 'notepad', - pid: 42, - isRunning: true, - lastUsedAt: null, - useCount: null - } - ] - }) - }) - - it('normalizes snapshots and remembers elements for follow-up actions', async () => { - mockBridgeResponse({ - ok: true, - snapshot: sampleBridgeSnapshot('Text Editor', 'initial') - }) - mockBridgeResponse({ - ok: true, - capabilities: sampleCapabilities() - }) - mockBridgeResponse({ - ok: true, - snapshot: sampleBridgeSnapshot('Text Editor', 'changed') - }) - - const client = await createDesktopScriptProviderClient('linux', '/tmp/runtime.py') - - const initial = await client.snapshot({ app: 'Text Editor' }) - expect(initial.snapshot.elementCount).toBe(1) - expect(initial.snapshot.window).toMatchObject({ - title: 'Text Editor', - id: 99, - x: 10, - y: 20, - width: 300, - height: 200 - }) - expect(initial.screenshotStatus).toEqual({ - state: 'captured', - metadata: { engine: 'unknown', windowId: 99 } - }) - expect(initial.snapshot.treeText).toContain('initial') - expect(publicSnapshotKeys(initial.snapshot)).toEqual([ - 'app', - 'coordinateSpace', - 'elementCount', - 'focusedElementId', - 'id', - 'treeText', - 'truncation', - 'window' - ]) - - await client.action('setValue', { - app: 'Text Editor', - elementIndex: 0, - value: 'changed', - noScreenshot: true - }) - }) - - it('uses bridge screenshot dimensions when the native payload is downscaled', async () => { - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'initial'), - screenshotWidth: 150, - screenshotHeight: 100, - screenshotScale: 0.5 - } - }) - - const client = await createDesktopScriptProviderClient('linux', '/tmp/runtime.py') - - const result = await client.snapshot({ app: 'Text Editor' }) - - expect(result.screenshot).toEqual({ - data: 'iVBORw0KGgo=', - format: 'png', - width: 150, - height: 100, - scale: 0.5 - }) - expect(result.snapshot.window).toMatchObject({ - width: 300, - height: 200 - }) - }) - - it('preserves provider window indexes when the snapshot has no stable window id', async () => { - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'initial'), - windowId: null, - windowIndex: 3 - } - }) - - const client = await createDesktopScriptProviderClient('linux', '/tmp/runtime.py') - - const result = await client.snapshot({ app: 'Text Editor', windowIndex: 3 }) - - expect(result.snapshot.window).toMatchObject({ - id: null, - index: 3, - title: 'Text Editor' - }) - }) - - it('uses window indexes in fallback snapshot IDs for ID-less provider windows', async () => { - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'first'), - snapshotId: undefined, - windowId: null, - windowIndex: 1 - } - }) - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'second'), - snapshotId: undefined, - windowId: null, - windowIndex: 2 - } - }) - - const client = await createDesktopScriptProviderClient('linux', '/tmp/runtime.py') - - const first = await client.snapshot({ app: 'Text Editor', windowIndex: 1 }) - const second = await client.snapshot({ app: 'Text Editor', windowIndex: 2 }) - - expect(first.snapshot.id).toContain('window-index:1') - expect(second.snapshot.id).toContain('window-index:2') - expect(first.snapshot.id).not.toBe(second.snapshot.id) - }) - - it('adds target window index metadata for ID-less action results', async () => { - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'initial'), - windowId: null, - windowIndex: 3 - } - }) - mockBridgeResponse({ - ok: true, - capabilities: sampleCapabilities() - }) - mockBridgeResponse({ - ok: true, - snapshot: { - ...sampleBridgeSnapshot('Text Editor', 'changed'), - windowId: null, - windowIndex: 3 - } - }) - - const client = await createDesktopScriptProviderClient('linux', '/tmp/runtime.py') - - await client.snapshot({ app: 'Text Editor', windowIndex: 3 }) - const result = await client.action('click', { - app: 'Text Editor', - windowIndex: 3, - elementIndex: 0, - noScreenshot: true - }) - - expect(result.action).toMatchObject({ - targetWindowId: null, - targetWindowIndex: 3 - }) - }) -}) diff --git a/src/main/github/issue-mutation-source.test.ts b/src/main/github/issue-mutation-source.test.ts deleted file mode 100644 index 0c416f561f0..00000000000 --- a/src/main/github/issue-mutation-source.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as GithubApiRepositoryModule from './github-api-repository' -import type * as GhUtils from './gh-utils' - -const { ghExecFileAsyncMock, resolveIssueSourceMock, acquireMock, releaseMock } = vi.hoisted( - () => ({ - ghExecFileAsyncMock: vi.fn(), - resolveIssueSourceMock: vi.fn(), - acquireMock: vi.fn(), - releaseMock: vi.fn() - }) -) - -vi.mock('./gh-utils', async () => { - const actual = await vi.importActual('./gh-utils') - return { - ...actual, - ghExecFileAsync: ghExecFileAsyncMock, - acquire: acquireMock, - release: releaseMock - } -}) - -vi.mock('./github-api-repository', async (importOriginal) => ({ - ...(await importOriginal()), - resolveIssueGitHubApiRepositorySource: resolveIssueSourceMock -})) - -import { addIssueComment } from './issue-comment' -import { updateIssue } from './issue-update' - -const ORIGIN = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' } -const UPSTREAM = { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' } - -describe('issue mutations follow the issue source preference', () => { - beforeEach(() => { - ghExecFileAsyncMock.mockReset() - acquireMock.mockReset() - acquireMock.mockResolvedValue(undefined) - releaseMock.mockReset() - resolveIssueSourceMock.mockReset() - resolveIssueSourceMock.mockImplementation(async (_repoPath: string, preference: unknown) => ({ - source: preference === 'origin' ? ORIGIN : UPSTREAM, - fellBack: false - })) - }) - - it('posts issue comments to origin when origin is selected', async () => { - ghExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ id: 1, user: { login: 'octo', avatar_url: '' }, body: 'hi' }) - }) - - await expect( - addIssueComment('/repo-root', 5, 'hi', null, null, {}, 'origin') - ).resolves.toMatchObject({ ok: true }) - - expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {}) - expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain( - 'repos/fork-owner/widgets/issues/5/comments' - ) - }) - - it('keeps an explicit owner/repo override ahead of the preference', async () => { - ghExecFileAsyncMock.mockResolvedValueOnce({ - stdout: JSON.stringify({ id: 2, user: { login: 'octo', avatar_url: '' }, body: 'hi' }) - }) - - await addIssueComment('/repo-root', 7, 'hi', null, UPSTREAM, {}, 'origin') - - expect(resolveIssueSourceMock).not.toHaveBeenCalled() - expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain( - 'repos/upstream-owner/widgets/issues/7/comments' - ) - }) - - it('edits issues on origin when origin is selected', async () => { - ghExecFileAsyncMock.mockResolvedValue({ stdout: '' }) - - await expect( - updateIssue('/repo-root', 5, { body: 'Updated', addAssignees: ['octo'] }, null, {}, 'origin') - ).resolves.toEqual({ ok: true }) - - expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {}) - expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain('repos/fork-owner/widgets/issues/5') - expect(ghExecFileAsyncMock.mock.calls[1][0]).toEqual([ - 'issue', - 'edit', - '5', - '--repo', - 'fork-owner/widgets', - '--add-assignee', - 'octo' - ]) - }) - - it('keeps the upstream-first default when no preference is set', async () => { - ghExecFileAsyncMock.mockResolvedValue({ stdout: '' }) - - await updateIssue('/repo-root', 5, { body: 'Updated' }) - - expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', undefined, undefined, {}) - expect(ghExecFileAsyncMock.mock.calls[0][0]).toContain('repos/upstream-owner/widgets/issues/5') - }) - - it('keeps edits on the opened issue when the saved preference changes', async () => { - ghExecFileAsyncMock.mockResolvedValue({ stdout: '' }) - - await updateIssue( - '/repo-root', - 5, - { body: 'Origin edit', state: 'closed', addLabels: ['bug'], addAssignees: ['octo'] }, - null, - {}, - 'upstream', - ORIGIN - ) - - expect(resolveIssueSourceMock).not.toHaveBeenCalled() - expect(ghExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([ - ['issue', 'close', '5', '--repo', 'fork-owner/widgets'], - ['api', '-X', 'PATCH', 'repos/fork-owner/widgets/issues/5', '--input', '-'], - [ - 'issue', - 'edit', - '5', - '--repo', - 'fork-owner/widgets', - '--add-label', - 'bug', - '--add-assignee', - 'octo' - ] - ]) - expect(ghExecFileAsyncMock.mock.calls[1][1]?.stdin).toBe( - JSON.stringify({ body: 'Origin edit' }) - ) - }) - - it('preserves local WSL execution with an explicit issue repository', async () => { - ghExecFileAsyncMock.mockResolvedValue({ stdout: '' }) - - await updateIssue( - '/home/fixture/widgets', - 5, - { body: 'Origin edit' }, - null, - { wslDistro: 'Ubuntu' }, - 'upstream', - ORIGIN - ) - - expect(ghExecFileAsyncMock).toHaveBeenCalledWith( - expect.arrayContaining(['repos/fork-owner/widgets/issues/5']), - expect.objectContaining({ - cwd: '/home/fixture/widgets', - wslDistro: 'Ubuntu', - host: 'github.com' - }) - ) - }) - - it('refuses unresolved Origin mutations instead of using ambient gh', async () => { - resolveIssueSourceMock.mockResolvedValue({ source: null, fellBack: false }) - - await expect( - updateIssue('/repo-root', 5, { body: 'Origin edit' }, null, {}, 'origin') - ).resolves.toMatchObject({ ok: false }) - await expect( - addIssueComment('/repo-root', 5, 'Origin reply', null, null, {}, 'origin') - ).resolves.toMatchObject({ ok: false }) - - expect(ghExecFileAsyncMock).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/github/work-item-details-issue-source.test.ts b/src/main/github/work-item-details-issue-source.test.ts deleted file mode 100644 index e52de876d18..00000000000 --- a/src/main/github/work-item-details-issue-source.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type * as WorkItemDetailsTestLookup from './work-item-details-test-lookup' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const { - ghExecFileAsyncMock, - getOwnerRepoMock, - getOwnerRepoForRemoteMock, - getWorkItemMock, - getWorkItemByOwnerRepoMock, - acquireMock, - releaseMock -} = vi.hoisted(() => ({ - ghExecFileAsyncMock: vi.fn(), - getOwnerRepoMock: vi.fn(), - getOwnerRepoForRemoteMock: vi.fn(), - getWorkItemMock: vi.fn(), - getWorkItemByOwnerRepoMock: vi.fn(), - acquireMock: vi.fn(), - releaseMock: vi.fn() -})) - -vi.mock('./gh-utils', () => ({ - ghExecFileAsync: ghExecFileAsyncMock, - getOwnerRepo: getOwnerRepoMock, - getOwnerRepoForRemote: getOwnerRepoForRemoteMock, - ghRepoExecOptions: vi.fn((context) => ({ cwd: context.repoPath })), - githubRepoContext: vi.fn((repoPath, connectionId, localGitOptions) => ({ - repoPath, - connectionId: connectionId ?? null, - ...localGitOptions - })), - acquire: acquireMock, - release: releaseMock -})) - -vi.mock('./client', async () => ({ - getWorkItem: getWorkItemMock, - getWorkItemWithRepository: ( - await vi.importActual('./work-item-details-test-lookup') - ).makeWorkItemDetailsLookupMock(getWorkItemMock), - getWorkItemByOwnerRepo: getWorkItemByOwnerRepoMock, - getPRChecks: vi.fn(), - getPRComments: vi.fn() -})) - -vi.mock('./github-enterprise-repository', () => ({ - getEnterpriseGitHubRepoSlug: vi.fn().mockResolvedValue(null), - getEnterpriseGitHubRepoSlugForRemote: vi.fn().mockResolvedValue(null), - isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true) -})) - -vi.mock('./rate-limit', () => ({ - repositoryRateLimitGuard: vi.fn(() => ({ blocked: false })), - noteRepositoryRateLimitSpend: vi.fn() -})) - -vi.mock('../git/remote-name-listing', () => ({ - shouldProbeGitRemote: vi.fn().mockResolvedValue(true) -})) - -import { getWorkItemDetails } from './work-item-details' -import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository' - -const REMOTES: Record = { - origin: { owner: 'fork-owner', repo: 'widgets' }, - upstream: { owner: 'upstream-owner', repo: 'widgets' } -} - -function graphQLIssueResponse(assignee: string): { stdout: string } { - return { - stdout: JSON.stringify({ - data: { - repository: { - issue: { - body: `${assignee} body`, - assignees: { nodes: [{ login: assignee }] }, - participants: { nodes: [] }, - comments: { nodes: [] } - } - } - } - }) - } -} - -function graphQLOwnerArg(): string | undefined { - const args: string[] = ghExecFileAsyncMock.mock.calls[0][0] - return args.find((arg) => arg.startsWith('owner=')) -} - -describe('getWorkItemDetails issue source', () => { - beforeEach(() => { - _resetOriginGitHubApiRepositoryCache() - ghExecFileAsyncMock.mockReset() - getOwnerRepoMock.mockReset() - getOwnerRepoMock.mockResolvedValue(REMOTES.origin) - getOwnerRepoForRemoteMock.mockReset() - getOwnerRepoForRemoteMock.mockImplementation( - async (_repoPath: string, remoteName: string) => REMOTES[remoteName] ?? null - ) - getWorkItemMock.mockReset() - getWorkItemByOwnerRepoMock.mockReset() - getWorkItemMock.mockResolvedValue({ - id: 'issue:5', - type: 'issue', - number: 5, - title: 'Origin issue', - state: 'open', - url: 'https://github.com/fork-owner/widgets/issues/5', - labels: [], - updatedAt: '2026-04-01T00:00:00Z', - author: 'fork-author' - }) - acquireMock.mockReset() - acquireMock.mockResolvedValue(undefined) - releaseMock.mockReset() - }) - - it('reads body, assignees and comments from origin when origin is selected', async () => { - ghExecFileAsyncMock - .mockResolvedValueOnce(graphQLIssueResponse('fork-assignee')) - .mockResolvedValueOnce({ stdout: '' }) - - const details = await getWorkItemDetails('/repo-root', 5, 'issue', null, {}, 'origin') - - expect(getWorkItemMock).toHaveBeenCalledWith('/repo-root', 5, 'issue', null, {}, 'origin') - expect(graphQLOwnerArg()).toBe('owner=fork-owner') - expect(ghExecFileAsyncMock.mock.calls[1][0]).toContain( - 'repos/fork-owner/widgets/issues/5/timeline?per_page=100&page=1' - ) - expect(details?.assignees).toEqual(['fork-assignee']) - }) - - it('keeps the upstream-first default when no preference is set', async () => { - ghExecFileAsyncMock - .mockResolvedValueOnce(graphQLIssueResponse('upstream-assignee')) - .mockResolvedValueOnce({ stdout: '' }) - - const details = await getWorkItemDetails('/repo-root', 5, 'issue', null, {}) - - expect(graphQLOwnerArg()).toBe('owner=upstream-owner') - expect(details?.assignees).toEqual(['upstream-assignee']) - }) - - it('keeps a Project row on upstream even when Origin is selected', async () => { - const upstream = { ...REMOTES.upstream, host: 'github.com' } - getWorkItemByOwnerRepoMock.mockResolvedValue({ - id: 'issue:5', - type: 'issue', - number: 5, - title: 'Upstream project issue', - url: 'https://github.com/upstream-owner/widgets/issues/5', - state: 'open', - labels: [], - updatedAt: '', - author: 'upstream-author' - }) - ghExecFileAsyncMock - .mockResolvedValueOnce(graphQLIssueResponse('upstream-assignee')) - .mockResolvedValueOnce({ stdout: '' }) - - const details = await getWorkItemDetails('/repo-root', 5, 'issue', null, {}, 'origin', upstream) - - expect(getWorkItemMock).not.toHaveBeenCalled() - expect(getWorkItemByOwnerRepoMock).toHaveBeenCalledWith( - '/repo-root', - upstream, - 5, - 'issue', - null, - {} - ) - expect(graphQLOwnerArg()).toBe('owner=upstream-owner') - expect(details?.item.title).toBe('Upstream project issue') - expect(details?.assignees).toEqual(['upstream-assignee']) - }) - - it('does not fall back when the selected issue repository is unavailable', async () => { - getWorkItemByOwnerRepoMock.mockResolvedValue(null) - - await expect( - getWorkItemDetails('/repo-root', 5, 'issue', null, {}, 'upstream', { - ...REMOTES.origin, - host: 'github.com' - }) - ).resolves.toBeNull() - - expect(getWorkItemMock).not.toHaveBeenCalled() - expect(ghExecFileAsyncMock).not.toHaveBeenCalled() - }) - - it('rejects an invalid runtime issue host before calling the repository lookup', async () => { - await expect( - getWorkItemDetails('/repo-root', 5, 'issue', null, {}, 'upstream', { - ...REMOTES.origin, - host: 42 - }) - ).resolves.toBeNull() - - expect(getWorkItemByOwnerRepoMock).not.toHaveBeenCalled() - expect(getWorkItemMock).not.toHaveBeenCalled() - expect(ghExecFileAsyncMock).not.toHaveBeenCalled() - }) - - it('ignores an issue repository override on an untyped lookup', async () => { - ghExecFileAsyncMock - .mockResolvedValueOnce(graphQLIssueResponse('fork-assignee')) - .mockResolvedValueOnce({ stdout: '' }) - - await getWorkItemDetails('/repo-root', 5, undefined, null, {}, 'origin', { - owner: 'untrusted', - repo: 'other', - host: 'untrusted.example' - }) - - expect(getWorkItemByOwnerRepoMock).not.toHaveBeenCalled() - expect(graphQLOwnerArg()).toBe('owner=fork-owner') - expect(ghExecFileAsyncMock.mock.calls[0][1]).toMatchObject({ host: 'github.com' }) - }) - - it('normalizes host-less issue targets before loading the conversation', async () => { - getWorkItemByOwnerRepoMock.mockResolvedValue({ - type: 'issue', - number: 5, - title: 'Origin issue', - url: 'https://github.com/fork-owner/widgets/issues/5', - author: null - }) - ghExecFileAsyncMock - .mockResolvedValueOnce(graphQLIssueResponse('fork-assignee')) - .mockResolvedValueOnce({ stdout: '' }) - - await getWorkItemDetails('/repo-root', 5, 'issue', null, {}, 'upstream', REMOTES.origin) - - expect(graphQLOwnerArg()).toBe('owner=fork-owner') - expect(ghExecFileAsyncMock.mock.calls[0][1]).toMatchObject({ host: 'github.com' }) - expect(ghExecFileAsyncMock.mock.calls[1][1]).toMatchObject({ host: 'github.com' }) - }) -}) diff --git a/src/main/hooks-issue-command.test.ts b/src/main/hooks-issue-command.test.ts deleted file mode 100644 index 42686241930..00000000000 --- a/src/main/hooks-issue-command.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type * as GitRunner from './git/runner' - -import { join } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import { - TEST_GITIGNORE_PATH, - TEST_ISSUE_COMMAND_PATH, - TEST_REPO_ORCA_YAML_PATH, - TEST_REPO_PATH -} from './hooks-test-fixtures' - -vi.mock('fs', () => ({ - readFileSync: vi.fn(), - existsSync: vi.fn(), - mkdirSync: vi.fn(), - writeFileSync: vi.fn(), - rmSync: vi.fn(), - chmodSync: vi.fn() -})) - -const { gitExecFileSyncMock } = vi.hoisted(() => ({ - gitExecFileSyncMock: vi.fn() -})) - -vi.mock('child_process', () => ({ - exec: vi.fn(), - execFile: vi.fn(), - execFileSync: vi.fn(), - spawn: vi.fn() -})) - -vi.mock('./git/runner', async () => ({ - ...(await vi.importActual('./git/runner')), - gitExecFileSync: gitExecFileSyncMock -})) - -vi.mock('./git/check-ignored-paths', () => ({ - checkIgnoredPaths: vi.fn().mockResolvedValue([]) -})) - -describe('readIssueCommand', () => { - it('prefers the local override over the shared orca.yaml command', async () => { - const fs = await import('node:fs') - vi.mocked(fs.existsSync).mockImplementation( - (path) => path === TEST_ISSUE_COMMAND_PATH || path === TEST_REPO_ORCA_YAML_PATH - ) - vi.mocked(fs.readFileSync).mockImplementation((path) => { - if (path === TEST_ISSUE_COMMAND_PATH) { - return 'local command\n' - } - if (path === TEST_REPO_ORCA_YAML_PATH) { - return 'issueCommand: |\n shared command\n' - } - return '' - }) - - const { readIssueCommand } = await import('./issue-command-file') - expect(readIssueCommand(TEST_REPO_PATH)).toEqual({ - localContent: 'local command', - sharedContent: 'shared command', - effectiveContent: 'local command', - localFilePath: TEST_ISSUE_COMMAND_PATH, - source: 'local' - }) - }) - - it('falls back to the shared orca.yaml command when no local override exists', async () => { - const fs = await import('node:fs') - vi.mocked(fs.existsSync).mockImplementation((path) => path === TEST_REPO_ORCA_YAML_PATH) - vi.mocked(fs.readFileSync).mockImplementation((path) => { - if (path === TEST_REPO_ORCA_YAML_PATH) { - return 'issueCommand: |\n shared command\n' - } - return '' - }) - - const { readIssueCommand } = await import('./issue-command-file') - expect(readIssueCommand(TEST_REPO_PATH)).toEqual({ - localContent: null, - sharedContent: 'shared command', - effectiveContent: 'shared command', - localFilePath: TEST_ISSUE_COMMAND_PATH, - source: 'shared' - }) - }) -}) - -describe('writeIssueCommand', () => { - it('checks file ignore rules in the selected WSL distro', async () => { - const { writeIssueCommand } = await import('./issue-command-file') - const { checkIgnoredPaths } = await import('./git/check-ignored-paths') - const fs = await import('node:fs') - vi.mocked(checkIgnoredPaths).mockResolvedValueOnce(['.orca/issue-command']) - vi.mocked(fs.writeFileSync).mockClear() - - await writeIssueCommand(TEST_REPO_PATH, 'local command', { wslDistro: 'Ubuntu' }) - - expect(checkIgnoredPaths).toHaveBeenLastCalledWith(TEST_REPO_PATH, ['.orca/issue-command'], { - wslDistro: 'Ubuntu' - }) - expect(fs.writeFileSync).toHaveBeenCalledExactlyOnceWith( - TEST_ISSUE_COMMAND_PATH, - 'local command\n', - 'utf-8' - ) - }) - - it('writes only the local override file and keeps .orca ignored locally', async () => { - const fs = await import('node:fs') - vi.mocked(fs.existsSync).mockImplementation( - (path) => path === TEST_GITIGNORE_PATH || path === join(TEST_REPO_PATH, '.orca') - ) - vi.mocked(fs.readFileSync).mockImplementation((path) => { - if (path === TEST_GITIGNORE_PATH) { - return 'node_modules/\n' - } - return '' - }) - - const { writeIssueCommand } = await import('./issue-command-file') - await writeIssueCommand(TEST_REPO_PATH, 'local command') - - expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith( - TEST_GITIGNORE_PATH, - 'node_modules/\n.orca\n', - 'utf-8' - ) - expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith( - TEST_ISSUE_COMMAND_PATH, - 'local command\n', - 'utf-8' - ) - }) - - it('deletes the local override when the override is cleared', async () => { - const { writeIssueCommand } = await import('./issue-command-file') - const fs = await import('node:fs') - await writeIssueCommand(TEST_REPO_PATH, ' ') - - expect(vi.mocked(fs.rmSync)).toHaveBeenCalledWith(TEST_ISSUE_COMMAND_PATH, { - force: true - }) - }) -}) diff --git a/src/main/ipc/readdir-error-diagnostics.test.ts b/src/main/ipc/readdir-error-diagnostics.test.ts deleted file mode 100644 index c1c7041a662..00000000000 --- a/src/main/ipc/readdir-error-diagnostics.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { buildReadDirErrorBreadcrumb, classifyReadDirPath } from './readdir-error-diagnostics' - -describe('classifyReadDirPath', () => { - it('classifies a WSL UNC path without leaking it', () => { - const classification = classifyReadDirPath( - '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', - undefined - ) - expect(classification).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true }) - }) - - it('classifies the legacy \\\\wsl$ root as WSL', () => { - expect(classifyReadDirPath('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true) - }) - - it('classifies a plain network UNC share as UNC but not WSL', () => { - const classification = classifyReadDirPath('\\\\fileserver\\share\\dir', undefined) - expect(classification).toMatchObject({ isUNC: true, isWsl: false }) - expect(classification.driveLetter).toBeUndefined() - }) - - it('extracts an uppercased drive letter for mapped drives', () => { - expect(classifyReadDirPath('z:\\projects\\repo', undefined)).toEqual({ - hasConnectionId: false, - isUNC: false, - isWsl: false, - driveLetter: 'Z' - }) - }) - - it('flags the SSH connection without recording it', () => { - const classification = classifyReadDirPath('/remote/repo', 'ssh-1') - expect(classification).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false }) - }) - - it('never includes the raw path in the classification', () => { - const classification = classifyReadDirPath('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9') - expect(JSON.stringify(classification)).not.toContain('secret') - }) -}) - -describe('buildReadDirErrorBreadcrumb', () => { - it('captures throw site, error code/name, and path classification', () => { - const breadcrumb = buildReadDirErrorBreadcrumb({ - dirPath: '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', - connectionId: undefined, - throwSite: 'readdir', - error: Object.assign(new Error('EIO: i/o error'), { code: 'EIO' }) - }) - expect(breadcrumb).toEqual({ - throwSite: 'readdir', - errorName: 'Error', - errorCode: 'EIO', - hasConnectionId: false, - isUNC: true, - isWsl: true - }) - }) - - it('omits errorCode when the error has none', () => { - const breadcrumb = buildReadDirErrorBreadcrumb({ - dirPath: '/remote/repo', - connectionId: 'ssh-1', - throwSite: 'ssh-provider', - error: new Error('Remote connection dropped.') - }) - expect(breadcrumb).toMatchObject({ throwSite: 'ssh-provider', errorName: 'Error' }) - expect(breadcrumb.errorCode).toBeUndefined() - expect(breadcrumb.hasConnectionId).toBe(true) - }) -}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-rewrite-store-coverage.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-rewrite-store-coverage.test.ts deleted file mode 100644 index f1d22a01d3b..00000000000 --- a/src/main/persistence/moving-wsl-projects/wsl-move-rewrite-store-coverage.test.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { toPersistedAutomationHostFilter } from '../../../shared/automation-host-filter' -import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../shared/constants' -import { toRuntimeExecutionHostId } from '../../../shared/execution-host' -import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' -import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' -import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' -import { WSL_MOVE_BROWSER_COOKIES_STORE_ID } from '../../../shared/wsl-move/wsl-move-browser-cookie-copy' -import { - PERSISTED_STATE_DISPOSITIONS, - WORKSPACE_SESSION_DISPOSITIONS, - type WslMoveFieldDisposition -} from '../../../shared/wsl-move/wsl-move-dispositions' -import { OUT_OF_STORE_DISPOSITIONS } from '../../../shared/wsl-move/wsl-move-out-of-store-dispositions' -import type { WslMoveRendererRewriteStoreId } from '../../../shared/wsl-move/wsl-move-renderer-rewrite-types' -import { - LOCAL_STORAGE_MODULE_DISPOSITIONS, - SQLITE_TABLE_DISPOSITIONS -} from '../../../shared/wsl-move/wsl-move-storage-dispositions' -import { UI_STATE_DISPOSITIONS } from '../../../shared/wsl-move/wsl-move-ui-dispositions' -import { registerWslMoveDesktopRewriteRegistry } from './wsl-move-desktop-rewrite-registry' -import { - isWslMoveDesktopRewriteRegistryComplete, - listWslMoveDesktopRewriteStores, - resetWslMoveDesktopRewriteStoresForTests, - WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE, - type WslMoveDesktopRewriteContext -} from './wsl-move-desktop-rewrite-stores' -import { registerWslMoveMainRewriteStores } from './wsl-move-main-rewrite-stores' -import { createUncToLinuxPathRewriter } from './wsl-move-path-rewriter' -import { wslMoveCutoverFixture } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' -import { wslMoverProfile } from '../__fixtures__/wsl-mover/wsl-mover-profiles' -import { WSL_MOVE_HOST_AUTOMATIONS_STORE_ID } from './wsl-move-host-automations-rewrite' -import { WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID } from './wsl-move-orchestration-history-rewrite' - -vi.mock('../../browser/browser-partition-cookie-copy', () => ({ - copyBrowserPartitionCookies: vi.fn(), - flushBrowserPartitionCookies: vi.fn() -})) -vi.mock('../../browser/browser-session-registry', () => ({ browserSessionRegistry: {} })) -vi.mock('../../browser/browser-session-startup', () => ({ browserSessionsInitialized: vi.fn() })) -vi.mock('../../browser/browser-route-partition-binding-runtime', () => ({ - activeBrowserRoutePartitionOrcaProfileId: vi.fn(), - currentBrowserRoutePartitionBindingStore: vi.fn() -})) -vi.mock('../../browser/browser-route-partition-storage-dependencies', () => ({ - releaseEvictedBrowserRoutePartitionStorage: vi.fn() -})) - -const UI_ROUTING = 'ui-routing' -const SESSION_GLOBALS = 'session-globals' -const HOST_SESSION_SEED = 'host-session-seed' -const renderer = (id: WslMoveRendererRewriteStoreId): string => id -// Rows whose rewrite shows in another field: the scalar scope stays, the host list it resolves to grows. -const WRITTEN_FIELD: Record = { 'ui.workspaceHostScope': 'visibleWorkspaceHostIds' } - -/** - * The store that carries out each disposition row it names. Keys are `.` after the - * disposition tables; every row here must exist, and every registered store must appear. - */ -const STORE_BY_ROW: Record = { - 'persisted.workspaceSessionsByHostId': HOST_SESSION_SEED, - // A move row, not a desktop rewrite: the host turns back on what the manifest carried disabled. - 'persisted.automations': WSL_MOVE_HOST_AUTOMATIONS_STORE_ID, - 'session.activeWorktreeId': SESSION_GLOBALS, - 'session.activeWorkspaceExecutionHostId': SESSION_GLOBALS, - 'session.activeWorkspaceKey': SESSION_GLOBALS, - // This desktop's copy, laid over the host partition the seed writes. - 'session.markdownFrontmatterVisible': HOST_SESSION_SEED, - 'session.closedTerminalTabTombstonesByTabId': HOST_SESSION_SEED, - 'ui.lastActiveWorktreeId': UI_ROUTING, - 'ui.manualRepoOrder': UI_ROUTING, - 'ui.explorerDisplayRootByWorktree': UI_ROUTING, - 'ui.showDotfilesByWorktree': UI_ROUTING, - 'ui.workspaceCleanup': UI_ROUTING, - 'ui.collapsedGroups': UI_ROUTING, - 'ui.workspaceHostScope': UI_ROUTING, - 'ui.workspaceHostOrder': UI_ROUTING, - 'ui.visibleWorkspaceHostIds': UI_ROUTING, - 'ui.agentsVisibleHostIds': UI_ROUTING, - 'ui.automationHostFilter': UI_ROUTING, - 'localStorage.src/renderer/src/lib/quick-open-file-history.ts': renderer('quick-open-history'), - 'localStorage.src/renderer/src/components/editor/ipynb-kernel-store.ts': - renderer('notebook-environments'), - 'localStorage.src/renderer/src/components/editor/file-view-preference-storage.ts': - renderer('file-view-preferences'), - 'sqlite.orchestration.worker_dispatches': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID, - 'sqlite.orchestration.worker_terminal_resources': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID, - 'outOfStore.orchestration.db': WSL_MOVE_ORCHESTRATION_HISTORY_STORE_ID, - 'outOfStore.browser partition (cookies, site storage)': WSL_MOVE_BROWSER_COOKIES_STORE_ID -} - -const prefixed = ( - prefix: string, - table: Record -): [string, WslMoveFieldDisposition][] => - Object.entries(table).map(([key, row]) => [`${prefix}.${key}`, row]) - -const ROWS = new Map([ - ...prefixed('persisted', PERSISTED_STATE_DISPOSITIONS), - ...prefixed('session', WORKSPACE_SESSION_DISPOSITIONS), - ...prefixed('ui', UI_STATE_DISPOSITIONS), - ...prefixed('localStorage', LOCAL_STORAGE_MODULE_DISPOSITIONS), - ...prefixed('sqlite', SQLITE_TABLE_DISPOSITIONS), - ...OUT_OF_STORE_DISPOSITIONS.map((row): [string, WslMoveFieldDisposition] => [ - `outOfStore.${row.store}`, - row - ]) -]) - -/** The stores startup registers, through the same entry point. */ -function registeredStoreIds(): string[] { - registerWslMoveDesktopRewriteRegistry({ - profile: { - getUI: vi.fn(), - updateUI: vi.fn(), - getWorkspaceSession: vi.fn(), - patchWorkspaceSession: vi.fn(), - setWorkspaceSession: vi.fn(), - flushPendingOrThrowAsync: vi.fn() - }, - orchestrationDatabasePath: '/nowhere/orchestration.db', - hostAutomations: { enable: vi.fn() }, - browserCookies: { - userDataPath: '/nowhere', - sessionsReady: () => false, - orcaProfileId: () => null, - listEnvironments: () => [], - listBrowserProfiles: () => [], - resolveHostStorage: vi.fn(), - bindHostPartition: vi.fn(), - copyCookies: vi.fn(), - flushCookies: vi.fn() - } - }) - return listWslMoveDesktopRewriteStores().map((store) => store.id) -} - -afterEach(() => { - resetWslMoveDesktopRewriteStoresForTests() -}) - -describe('WSL move desktop rewrite registry coverage', () => { - it('gives every desktop-rewrite disposition row a store', () => { - const rewriteRows = [...ROWS] - .filter(([, row]) => row.disposition === 'rewrite-desktop' || row.rewriteDesktopCopy) - .map(([key]) => key) - expect(rewriteRows.length).toBeGreaterThan(0) - expect(rewriteRows.filter((key) => !Object.hasOwn(STORE_BY_ROW, key))).toEqual([]) - }) - - it('names only rows the disposition tables have', () => { - expect(Object.keys(STORE_BY_ROW).filter((key) => !ROWS.has(key))).toEqual([]) - }) - - it('maps every registered store to a row, and registers every mapped store', () => { - const registered = registeredStoreIds() - const mapped = new Set(Object.values(STORE_BY_ROW)) - expect(registered.filter((id) => !mapped.has(id))).toEqual([]) - expect([...mapped].filter((id) => !registered.includes(id))).toEqual([]) - }) - - it('is marked complete', () => { - expect(WSL_MOVE_DESKTOP_REWRITE_REGISTRY_COMPLETE).toBe(true) - expect(isWslMoveDesktopRewriteRegistryComplete()).toBe(true) - }) - - it("has the profile's stores write every UI and session row they are mapped to", async () => { - const written = new Set() - const context: WslMoveDesktopRewriteContext = { - cutover: wslMoveCutoverFixture( - 'move-1', - 'destination-committed', - {}, - { repoIds: ['repo-a'] }, - { - version: 1, - worktreeMeta: [], - worktreeLineage: [], - workspaceLineage: [], - sparsePresets: [], - retiredWorktreeNames: [], - retiredWorktreeNamespaces: [], - automations: wslMoverProfile('folders-and-automations', 'destination').state.automations - } - ), - rewritePath: createUncToLinuxPathRewriter('Ubuntu'), - destinationHostId: HOST, - recordSeededHostWorkspaceIds: () => {} - } - registerWslMoveMainRewriteStores({ - getUI: () => ({ ...getDefaultPersistedState('/nowhere').ui, ...movedUi() }), - updateUI: (updates) => { - Object.keys(updates).forEach((key) => written.add(`ui.${key}`)) - }, - getWorkspaceSession: () => ({ ...getDefaultWorkspaceSession(), ...movedSession() }), - patchWorkspaceSession: (patch) => { - Object.keys(patch).forEach((key) => written.add(`session.${key}`)) - }, - setWorkspaceSession: vi.fn(), - flushPendingOrThrowAsync: vi.fn() - }) - for (const store of listWslMoveDesktopRewriteStores()) { - if (store.side === 'main' && (store.id === UI_ROUTING || store.id === SESSION_GLOBALS)) { - await store.run(context) - } - } - const missing = Object.entries(STORE_BY_ROW) - .filter(([, storeId]) => storeId === UI_ROUTING || storeId === SESSION_GLOBALS) - .map(([row]) => { - const field = WRITTEN_FIELD[row] - return field ? `${row.slice(0, row.indexOf('.'))}.${field}` : row - }) - .filter((row) => !written.has(row)) - expect(missing).toEqual([]) - }) -}) - -const HOST = toRuntimeExecutionHostId('wsl:Ubuntu') -const MOVED = 'repo-a::\\\\wsl.localhost\\Ubuntu\\home\\dev\\app' - -/** Every UI row the move rewrites, naming the moved project on this computer. */ -function movedUi(): Partial { - return { - lastActiveWorktreeId: MOVED, - manualRepoOrder: [{ hostId: 'local', repoId: 'repo-a' }], - explorerDisplayRootByWorktree: { [MOVED]: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\app\\src' }, - showDotfilesByWorktree: { [MOVED]: true }, - workspaceCleanup: { - dismissals: { - [`local\0${MOVED}`]: { - worktreeId: MOVED, - executionHostId: 'local', - dismissedAt: 1, - fingerprint: 'f', - classifierVersion: 1 - } - } - }, - collapsedGroups: [`lineage:local|${MOVED}`], - workspaceHostScope: 'local', - visibleWorkspaceHostIds: null, - workspaceHostOrder: ['local'], - agentsVisibleHostIds: ['local'], - automationHostFilter: toPersistedAutomationHostFilter({ - kind: 'host', - host: { authority: { kind: 'desktop' }, selector: { kind: 'self' } } - }) - } -} - -function movedSession(): Partial { - return { - activeWorktreeId: MOVED, - activeWorkspaceKey: worktreeWorkspaceKey(MOVED), - activeWorkspaceExecutionHostId: 'local' - } -} diff --git a/src/main/pty/legacy-terminal-windows-tombstone.test.ts b/src/main/pty/legacy-terminal-windows-tombstone.test.ts deleted file mode 100644 index fbd993bdbd1..00000000000 --- a/src/main/pty/legacy-terminal-windows-tombstone.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { resolvePosixTombstoneInterpreter } from './legacy-terminal-posix-tombstone' -import { - __resetLegacyTerminalShimNeutralizationForTests, - neutralizeLegacyTerminalShimDir -} from './legacy-terminal-shim-dir' - -// Missing indexOf operands return -1 and can falsely satisfy ordering. -function expectOrdered(text: string, first: string, second: string): void { - expect(text, `missing: ${first}`).toContain(first) - expect(text, `missing: ${second}`).toContain(second) - expect(text.indexOf(first)).toBeLessThan(text.indexOf(second)) -} - -describe('legacy terminal Windows tombstone text', () => { - const tempRoots: string[] = [] - - const makeUserDataDir = (): string => { - const userData = mkdtempSync(join(tmpdir(), 'orca-legacy-shim-win-')) - tempRoots.push(userData) - return userData - } - - beforeEach(() => { - __resetLegacyTerminalShimNeutralizationForTests() - }) - - afterEach(() => { - for (const tempRoot of tempRoots.splice(0)) { - rmSync(tempRoot, { recursive: true, force: true }) - } - }) - - it('rejects stale Windows real-command paths inside the wrapper directory', () => { - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - const cmd = readFileSync(join(win32Dir, 'git.cmd'), 'utf8') - expect(cmd).toContain( - 'if defined orca_real for %%G in ("%orca_real%") do if /I "%%~dpG"=="%~dp0" set "orca_real="' - ) - // A stale captured path must fall back instead of invoking a missing binary. - expect(cmd).toContain('if defined orca_real if not exist "%orca_real%" set "orca_real="') - expectOrdered(cmd, 'if not exist "%orca_real%"', ':orca_try_candidate') - const powershell = readFileSync(join(win32Dir, 'git-wrapper.ps1'), 'utf8') - expect(powershell).toContain('[StringComparison]::OrdinalIgnoreCase') - expect(powershell).toContain('$realCommand = $null') - expectOrdered( - powershell, - '[StringComparison]::OrdinalIgnoreCase', - 'Test-Path -LiteralPath $realCommand' - ) - }) - - it('emits no percent expression inside a cmd rem comment', () => { - // cmd expands variables even in rem; separators behave differently inside blocks. - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - for (const command of ['git', 'gh'] as const) { - const offending = readFileSync(join(win32Dir, `${command}.cmd`), 'utf8') - .split('\r\n') - .filter((line) => /^\s*rem\b/i.test(line) && line.includes('%')) - expect(offending, `rem comments with percent expressions: ${offending.join(' | ')}`).toEqual( - [] - ) - } - }) - - it('keeps the Windows wrappers ASCII-only', () => { - // Multibyte comments shifted cmd byte positions and broke following lines on Windows 11. - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - for (const name of ['git.cmd', 'gh.cmd', 'git-wrapper.ps1', 'gh-wrapper.ps1']) { - const contents = readFileSync(join(win32Dir, name), 'utf8') - const offending = [...contents].find((character) => character.charCodeAt(0) > 0x7f) - expect(offending, `${name} contains non-ASCII ${JSON.stringify(offending)}`).toBeUndefined() - expect(Buffer.byteLength(contents, 'utf8')).toBe(contents.length) - } - }) - - it('resolves Windows fallbacks against PATH only, never the current directory', () => { - // where.exe searches cwd first and could execute repository-local git/gh (STA-4169). - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - for (const command of ['git', 'gh'] as const) { - const cmd = readFileSync(join(win32Dir, `${command}.cmd`), 'utf8') - expect(cmd).not.toContain('where.exe') - expect(cmd).toContain('for %%P in ("%orca_clean_path:;=" "%") do (') - expect(cmd).toContain(`if exist "%orca_candidate_dir%\\${command}.exe"`) - // Normalize trailing separators to avoid self-exec; a variable avoids cmd's literal-backslash parser trap. - expect(cmd).toContain('set "orca_sep=\\"') - // Inherited /V:ON expands ! in PATH; measured cases executed cwd-local git or lost legitimate entries. - expect(cmd).toContain('setlocal DisableDelayedExpansion') - expect(cmd).not.toContain('\r\nsetlocal\r\n') - // Relative captured commands resolve from cwd and must be rejected. - expect(cmd).toContain('if defined orca_real call :orca_check_rooted') - expect(cmd).toContain('if defined orca_real if not defined orca_rooted set "orca_real="') - // Exported PATH reaches child tools; pin the exact subroutine because matching earlier call sites hid its missing guard. - expect(cmd).toContain(':orca_append_path\r\nif not defined orca_entry exit /b\r\n') - expect(cmd).toContain( - 'set "orca_probe=%orca_entry%"\r\ncall :orca_check_rooted\r\nif not defined orca_rooted exit /b\r\nfor %%G in ("%orca_entry%") do set "orca_path_entry_dir=%%~fG"' - ) - // CALL re-expands path arguments: literal percent variables could become cwd and execute planted git. Pass data through variables. - expect(cmd).not.toContain('call :orca_check_rooted "') - expect(cmd).not.toContain('call :orca_try_candidate "') - expect(cmd).not.toContain('call :orca_append_path "') - expect(cmd).not.toContain('%~1') - expect(cmd).toContain('set "orca_probe=%orca_entry%"\r\ncall :orca_check_rooted\r\n') - // Define the separator before any normalization; otherwise stripping silently no-ops. - expectOrdered(cmd, 'set "orca_sep=', 'if "%orca_legacy_norm:~-1%."') - // Compare dot-suffixed values to avoid expanding the separator into a literal-backslash parser trap. - expect(cmd).not.toContain('=="%orca_sep%"') - // Pin rejection too; accept-only assertions stayed green when the cwd-hijack guard was removed. - expectOrdered( - cmd, - 'call :orca_check_rooted\r\nif not defined orca_rooted exit /b', - 'for %%G in ("%orca_entry%") do set "orca_candidate_dir=' - ) - expect(cmd).toContain('if not defined orca_rooted exit /b') - expect(cmd).toContain('if "%orca_candidate_dir:~-1%."=="%orca_sep%."') - expect(cmd).not.toContain(':\\#=#%') - // CALL rebinds %~dp0; self-exclusion must use the cached wrapper directory. - expect(cmd).toContain('if /I "%orca_candidate_dir%\\"=="%orca_wrapper_dir%" exit /b') - // External tools in the rooted-path check would reintroduce cwd lookup. - expect(cmd).not.toContain('findstr') - expect(cmd).toContain('if "%orca_probe:~1,2%"==":\\" set "orca_rooted=1"') - expect(cmd).toContain('if "%orca_probe:~0,2%"=="\\\\" set "orca_rooted=1"') - // Both empty-element guards need pins; removing either otherwise left the suite green. - expect(cmd).toContain('if not defined orca_entry exit /b') - - const powershell = readFileSync(join(win32Dir, `${command}-wrapper.ps1`), 'utf8') - expect(powershell).not.toContain('Get-Command') - expect(powershell).toContain("($env:PATH -split ';')") - // IsPathRooted accepts drive-relative C:foo; require an absolute prefix. - expect(powershell).not.toContain('[IO.Path]::IsPathRooted(') - expect(powershell).toContain("$pathEntry -match '^([A-Za-z]:[\\\\/]|\\\\\\\\)'") - expect(powershell).toContain("$realCommand -notmatch '^([A-Za-z]:[\\\\/]|\\\\\\\\)'") - // A shorter prefix would also accept drive-relative C:foo. - expect(powershell).toContain("-notmatch '^([A-Za-z]:[\\\\/]|\\\\\\\\)'") - // Mutation showed each trailing-separator normalization could disappear without other failures. - expect(powershell).toContain("ForEach-Object { $_.TrimEnd('\\', '/') }") - expect(powershell).toContain("$pathEntry.TrimEnd('\\', '/')") - expect(powershell).toContain("$dir.TrimEnd('\\', '/')") - expect(powershell).toContain("$capturedDir.TrimEnd('\\', '/')") - expect(powershell).toContain( - "$dir.TrimEnd('\\', '/'), [StringComparison]::OrdinalIgnoreCase) }) { continue }" - ) - // Both separators are accepted in rooted paths and must be normalized for self-exclusion. - expect(powershell).not.toContain("TrimEnd('\\')") - expect(cmd).toContain('if "%orca_probe:~1,2%"==":/" set "orca_rooted=1"') - expect(cmd).toContain('if "%orca_legacy_norm:~-1%."=="%orca_sep%."') - // cmd expands whole lines before if defined; defer unset-variable substring operations into a CALL body. - expect(cmd).toContain('if defined orca_legacy_wrapper_dir call :orca_normalize_legacy_dir') - expect(cmd).not.toContain('if defined orca_legacy_wrapper_dir for ') - expect(powershell).toContain('if (-not $dir) { continue }') - expect(powershell).toContain('Test-Path -LiteralPath $candidate -PathType Leaf') - } - }) - - it('emits Windows wrappers with CRLF line endings', () => { - // cmd label lookup failed for longer LF-only wrappers on Windows; require CRLF. - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - for (const file of ['git.cmd', 'gh.cmd', 'git-wrapper.ps1', 'gh-wrapper.ps1']) { - const body = readFileSync(join(win32Dir, file), 'utf8') - expect(body, file).toContain('\r\n') - expect(body.replaceAll('\r\n', ''), file).not.toContain('\n') - } - }) - - it('guards both cmd PATH walks so an empty variable cannot break parsing', () => { - // Expanding an empty PATH into quoted elements leaves unbalanced quotes and breaks cmd parsing. - const userData = makeUserDataDir() - const win32Dir = join(userData, 'orca-terminal-attribution', 'win32') - mkdirSync(win32Dir, { recursive: true }) - - neutralizeLegacyTerminalShimDir(userData) - - for (const command of ['git', 'gh'] as const) { - const cmd = readFileSync(join(win32Dir, `${command}.cmd`), 'utf8') - expect(cmd).toContain('if not defined PATH goto :orca_path_walked') - expect(cmd).toContain('if not defined orca_clean_path goto :orca_candidates_walked') - for (const [guard, loop] of [ - ['if not defined PATH goto :orca_path_walked', 'for %%P in ("%PATH:;='], - [ - 'if not defined orca_clean_path goto :orca_candidates_walked', - 'for %%P in ("%orca_clean_path:;=' - ] - ] as const) { - expectOrdered(cmd, guard, loop) - } - } - }) - - it('never bakes an ambient interpreter lookup on Windows', () => { - // Windows cannot verify guest paths; both Git Bash and WSL map /bin/bash without ambient lookup. - expect(resolvePosixTombstoneInterpreter(undefined, [], 'win32')).toBe('/bin/bash') - expect(resolvePosixTombstoneInterpreter('C:\\Git\\bin;C:\\Windows', [], 'win32')).toBe( - '/bin/bash' - ) - }) -}) diff --git a/src/main/startup/agent-hooks-uninstall-preflight.test.ts b/src/main/startup/agent-hooks-uninstall-preflight.test.ts deleted file mode 100644 index babd72e3a95..00000000000 --- a/src/main/startup/agent-hooks-uninstall-preflight.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { readFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' -import { getBundledLauncherPath } from '../cli/bundled-cli-launcher-path' - -const electron = vi.hoisted(() => ({ - exit: vi.fn(), - requestSingleInstanceLock: vi.fn(), - BrowserWindow: vi.fn(), - handle: vi.fn() -})) -const appData = vi.hoisted(() => ({ ensure: vi.fn() })) -const hooks = vi.hoisted(() => ({ - remove: vi.fn<() => Promise>(async () => []) -})) - -vi.mock('electron', () => ({ - app: { - exit: electron.exit, - getPath: () => '/user-data', - setPath: vi.fn(), - requestSingleInstanceLock: electron.requestSingleInstanceLock - }, - BrowserWindow: electron.BrowserWindow, - ipcMain: { handle: electron.handle } -})) -vi.mock('./windows-app-data-path', () => ({ ensureWindowsAppDataPath: appData.ensure })) -vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ - removeManagedAgentHooks: hooks.remove -})) - -import { - AGENT_HOOKS_UNINSTALL_FLAG, - removeAgentHooksForUninstall, - runAgentHooksUninstallPreflight -} from './agent-hooks-uninstall-preflight' - -function status(agent: 'claude' | 'codex', state: AgentHookInstallStatus['state']) { - return { agent, state, configPath: '', managedHooksPresent: false, detail: null } -} - -const nsis = (name: string): string => - readFileSync(new URL(`../../../config/nsis/${name}`, import.meta.url), 'utf8') - -function realUninstallBranch(): string { - const unInstall = nsis('orca-installer-hooks.nsh').slice( - nsis('orca-installer-hooks.nsh').indexOf('!macro customUnInstall') - ) - // The real-uninstall branch ends with the relocated daemon host's cleanup. - return unInstall.slice( - unInstall.indexOf('${ifNot} ${isUpdated}'), - unInstall.indexOf('daemon-host-profiles') - ) -} - -describe('managed agent hook removal on uninstall', () => { - it('succeeds only when no agent reports an error', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - await expect( - removeAgentHooksForUninstall(async () => [ - status('claude', 'not_installed'), - status('codex', 'not_installed') - ]) - ).resolves.toBe(0) - await expect( - removeAgentHooksForUninstall(async () => [ - status('claude', 'not_installed'), - status('codex', 'error') - ]) - ).resolves.toBe(1) - await expect( - removeAgentHooksForUninstall(async () => { - throw new Error('settings unreadable') - }) - ).resolves.toBe(1) - }) - - it('ignores ordinary launches', () => { - expect(runAgentHooksUninstallPreflight(['Orca.exe'])).toBe(false) - expect(hooks.remove).not.toHaveBeenCalled() - }) - - it('removes every agent and exits, opening no window, lock or IPC', async () => { - electron.exit.mockClear() - hooks.remove.mockResolvedValueOnce([status('claude', 'not_installed')]) - - expect(runAgentHooksUninstallPreflight(['Orca.exe', AGENT_HOOKS_UNINSTALL_FLAG])).toBe(true) - - await vi.waitFor(() => expect(electron.exit).toHaveBeenCalledWith(0)) - expect(hooks.remove).toHaveBeenCalledExactlyOnceWith() - expect(electron.BrowserWindow).not.toHaveBeenCalled() - expect(electron.requestSingleInstanceLock).not.toHaveBeenCalled() - expect(electron.handle).not.toHaveBeenCalled() - }) - - it('exits 1 rather than leaving an error box when setup throws', () => { - vi.useFakeTimers() - electron.exit.mockClear() - appData.ensure.mockImplementationOnce(() => { - throw new Error('Orca could not find the Windows roaming AppData folder') - }) - try { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - expect(runAgentHooksUninstallPreflight(['Orca.exe', AGENT_HOOKS_UNINSTALL_FLAG])).toBe(true) - expect(electron.exit).toHaveBeenCalledExactlyOnceWith(1) - // The timer armed before setup is cleared, so it can't exit a second time. - vi.runAllTimers() - expect(electron.exit).toHaveBeenCalledOnce() - } finally { - vi.useRealTimers() - } - }) -}) - -describe('the NSIS uninstaller', () => { - it('removes agent hooks on a real uninstall only, after the app is gone, never blocking', () => { - const branch = realUninstallBranch() - const launch = branch.indexOf( - `nsExec::Exec '"$INSTDIR\\\${APP_EXECUTABLE_FILENAME}" ${AGENT_HOOKS_UNINSTALL_FLAG}'` - ) - expect(launch).toBeGreaterThan(-1) - // Killed first, so a running Orca cannot reinstall the hooks behind the removal. - expect(launch).toBeGreaterThan(branch.indexOf('orca-terminal-daemon.exe')) - // The exit code is popped and never checked. - expect(branch.slice(launch).split('\n')[1].trim()).toBe('Pop $0') - expect(nsis('orca-installer-hooks.nsh').split(AGENT_HOOKS_UNINSTALL_FLAG)).toHaveLength(2) - }) - - it('keeps the hooks while another Orca install for this user still uses them', () => { - const branch = realUninstallBranch() - const launch = branch.indexOf(AGENT_HOOKS_UNINSTALL_FLAG) - const check = branch.indexOf('!insertmacro ORCA_OTHER_INSTALL_STATE $1') - expect(check).toBeGreaterThan(-1) - // Only a definite "no other install" runs the removal; "other" and "unknown" skip it. - const gate = branch.lastIndexOf('${if} $1 == "none"', launch) - expect(gate).toBeGreaterThan(check) - expect(branch.slice(gate, launch)).not.toContain('${endIf}') - expect(nsis('orca-installer-hooks.nsh')).toContain( - '!include "${__FILEDIR__}\\orca-other-install-check.nsh"' - ) - - const detect = nsis('orca-other-install-check.nsh') - // Both registrations a second install can have, by the key the installer writes. - expect(detect).toContain( - '!insertmacro ORCA_CHECK_ONE_REGISTRATION ${ORCA_OTHER_INSTALL_HKCU} HKCU ${OUT}' - ) - expect(detect).toContain( - '!insertmacro ORCA_CHECK_ONE_REGISTRATION ${ORCA_OTHER_INSTALL_HKLM} HKLM ${OUT}' - ) - expect(detect).toContain('ReadRegStr $R5 ${HIVE} "${INSTALL_REGISTRY_KEY}" InstallLocation') - // Our own registration is excluded, and the other one must still have the app. - expect(detect).toContain('${elseIf} $R5 != $INSTDIR') - expect(detect).toContain('${andIf} ${FileExists} "$R5\\${APP_EXECUTABLE_FILENAME}"') - // A read failure other than "no such key" reports unknown, which skips the removal. - expect(detect).toMatch( - /\$\{elseIf\} \$R7 != \$\{ORCA_ERROR_FILE_NOT_FOUND\}\s+StrCpy \$\{OUT\} "unknown"/ - ) - expect(detect).not.toMatch(/nsExec|ExecWait|powershell/i) - }) - - it('keeps the hooks when an Orca registration names no install location', () => { - const detect = nsis('orca-other-install-check.nsh') - // An opened key whose InstallLocation reads empty is doubtful, never "none". - expect(detect).toMatch( - /ReadRegStr \$R5 \$\{HIVE\} "\$\{INSTALL_REGISTRY_KEY\}" InstallLocation[\s\S]*?\$\{if\} \$R5 == ""\s+(?:;[^\n]*\n\s+)?StrCpy \$\{OUT\} "unknown"\s+\$\{elseIf\} \$R5 != \$INSTDIR/ - ) - }) - - it("removes the `orca` command's PATH entry on a real uninstall only", () => { - const hooksFile = nsis('orca-installer-hooks.nsh') - expect(hooksFile).toContain('!include "${__FILEDIR__}\\orca-user-path-cleanup.nsh"') - const call = '!insertmacro ORCA_REMOVE_USER_PATH_ENTRY "$INSTDIR\\resources\\bin"' - expect(realUninstallBranch()).toContain(call) - expect(hooksFile.split(call)).toHaveLength(2) - // The directory the app registers: the packaged launcher's folder. - const launcher = getBundledLauncherPath('win32', join('INSTDIR', 'resources')) - expect( - dirname(launcher ?? '') - .split(/[\\/]/) - .slice(-2) - ).toEqual(['resources', 'bin']) - }) - - it('edits PATH natively, keeps its type, and never writes back a truncated read', () => { - const cleanup = nsis('orca-user-path-cleanup.nsh') - const code = cleanup - .split('\n') - .filter((line) => !line.trim().startsWith(';')) - .join('\n') - expect(code).not.toMatch(/nsExec|ExecWait|powershell|reg\.exe|reg add/i) - expect(cleanup).toContain('IntOp $R2 ${NSIS_MAX_STRLEN} - 1') - expect(cleanup).toMatch(/\$\{andIf\} \$R1 < \$R2/) - expect(cleanup).toContain('WriteRegExpandStr HKCU "Environment" "Path" $R3') - expect(cleanup).toContain('WriteRegStr HKCU "Environment" "Path" $R3') - // Only a removal rewrites PATH, and only with a value type it actually read. - expect(cleanup).toMatch(/\$\{if\} \$R7 == 1\s+\$\{andIf\} \$R8 != ""/) - expect(cleanup.indexOf('${if} $R7 == 1')).toBeLessThan(cleanup.indexOf('WriteRegExpandStr')) - expect(cleanup).toContain('StrCpy $R8 ""') - expect(cleanup).toMatch(/\$\{if\} \$R7 == 0\s+StrCpy \$R8 \$R4/) - expect(cleanup).toContain('${elseIf} $R8 == ${ORCA_REG_SZ}') - expect(cleanup).not.toMatch(/\$\{else\}\s+WriteRegStr/) - }) -}) diff --git a/src/main/startup/codex-hook-startup-wiring.test.ts b/src/main/startup/codex-hook-startup-wiring.test.ts deleted file mode 100644 index 09bdab5d8a2..00000000000 --- a/src/main/startup/codex-hook-startup-wiring.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/global-settings-types' - -const fixture = vi.hoisted(() => { - const settings: Partial = { agentStatusHooksEnabled: true } - return { - app: { isPackaged: true, on: vi.fn() }, - settings, - pathReady: Promise.resolve(), - startCodexHooks: vi.fn(), - installFirstWorkRename: vi.fn() - } -}) - -vi.mock('electron', () => ({ app: fixture.app, nativeTheme: {} })) -vi.mock('../codex/codex-hook-reconcile', () => ({ startCodexHooks: fixture.startCodexHooks })) -vi.mock('../agent-hooks/local-agent-cli-presence', () => ({ - hydrateAgentCliShellPath: () => fixture.pathReady -})) -// Use the real opt-out predicate to keep startup aligned with managed launches. -vi.mock( - '../agent-hooks/managed-agent-hook-controls', - async () => await import('../../shared/agent-status-hooks-setting') -) -vi.mock('./main-process-state', () => ({ - mainProcessState: { store: { getSettings: () => fixture.settings } } -})) -vi.mock('./main-process-runtime-service', () => ({ - initializeMainProcessRuntime: () => ({ - setAgentBrowserBridge: vi.fn(), - setEmulatorBridge: vi.fn() - }), - configureRuntimeServices: vi.fn() -})) -vi.mock('../star-nag/service', () => ({ - StarNagService: class { - start = vi.fn() - registerIpcHandlers = vi.fn() - } -})) -vi.mock('../browser/agent-browser-bridge', () => ({ - AgentBrowserBridge: class { - sweepOrphanedSessions = vi.fn() - } -})) -vi.mock('../emulator/emulator-bridge', () => ({ EmulatorBridge: class {} })) -vi.mock('../runtime/rpc/dispatcher', () => ({ RpcDispatcher: class {} })) -vi.mock('../browser/browser-manager', () => ({ browserManager: {} })) -vi.mock('../browser/browser-client-page-automation-runtime', () => ({ - configureBrowserClientPageAutomationRuntime: vi.fn() -})) -vi.mock('../crash-reporting/process-gone-diagnostics', () => ({ - startPreGoneCrashSampling: vi.fn() -})) -vi.mock('./main-window-lifecycle-flags', () => ({ recordProcessGoneCrash: vi.fn() })) -vi.mock('./gpu-lifecycle', () => ({ handleGpuChildCrash: vi.fn() })) -vi.mock('./configure-process', () => ({ shouldInstallManagedHooks: () => false })) -vi.mock('../agent-hooks/install-telemetry', () => ({ recordManagedHookInstallFailure: vi.fn() })) -vi.mock('./main-process-observers', () => ({ initializeMainProcessObservers: vi.fn() })) -vi.mock('./branch-rename-hook', () => ({ - installFirstWorkRenameHookSubscription: fixture.installFirstWorkRename -})) -vi.mock('./main-process-account-services', () => ({ - initializeMainProcessAccountServices: vi.fn() -})) -vi.mock('./main-process-automations', () => ({ initializeMainProcessAutomations: vi.fn() })) -vi.mock('./main-process-plugins', () => ({ initializeMainProcessPlugins: vi.fn() })) -vi.mock('../worktree-trash', () => ({ - collectWorktreeTrashSweepRoots: vi.fn(), - sweepStaleWorktreeTrash: vi.fn() -})) -vi.mock('./worktree-removal-records-load', () => ({ loadWorktreeRemovalRecordsForStore: vi.fn() })) -vi.mock('./first-window-deferral', () => ({ runAfterFirstWindowShown: vi.fn() })) -vi.mock('./startup-diagnostics', () => ({ logStartupMilestone: vi.fn() })) -vi.mock('../opencode/opencode-status-plugin-startup-refresh', () => ({ - refreshInstalledOpenCodeStatusPlugins: vi.fn() -})) -vi.mock('../orcad/orcad-artifact-cache-retention', () => ({ - pruneDesktopOrcadArtifactCache: vi.fn() -})) - -import { initializeReadyRuntimeServices } from './main-process-ready-runtime' - -// Start reconciles ~/.codex; awaiting shell PATH avoids launchd failing to find Codex. - -beforeEach(() => { - fixture.startCodexHooks.mockClear() - fixture.settings.disabledTuiAgents = [] -}) - -describe('Codex hook startup', () => { - it('starts once in app readiness, after the shell PATH is hydrated', async () => { - let hydrate: () => void = () => {} - fixture.pathReady = new Promise((resolve) => { - hydrate = resolve - }) - - await initializeReadyRuntimeServices() - - expect(fixture.startCodexHooks).toHaveBeenCalledTimes(1) - let ready = false - const { pathReady } = fixture.startCodexHooks.mock.calls[0]![0] - void pathReady.then(() => { - ready = true - }) - await new Promise((resolve) => setTimeout(resolve, 10)) - expect(ready).toBe(false) - hydrate() - await pathReady - expect(ready).toBe(true) - }) - - it("reads Codex's per-agent hook setting each time it is asked", async () => { - await initializeReadyRuntimeServices() - const { isEnabled } = fixture.startCodexHooks.mock.calls[0]![0] - - expect(isEnabled()).toBe(true) - fixture.settings.disabledTuiAgents = ['codex'] - expect(isEnabled()).toBe(false) - }) -}) - -// Headless serve never opens a window, so app readiness must arm rename (STA-5871). -describe('First-work rename startup', () => { - it('arms the hook subscription during app readiness, without a window', async () => { - fixture.installFirstWorkRename.mockClear() - - await initializeReadyRuntimeServices() - - expect(fixture.installFirstWorkRename).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/main/startup/dev-instance-identity.test.ts b/src/main/startup/dev-instance-identity.test.ts deleted file mode 100644 index 18d67d961bc..00000000000 --- a/src/main/startup/dev-instance-identity.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getDevInstanceIdentity, shouldApplyPreReadyAppName } from './dev-instance-identity' - -describe('dev-instance-identity', () => { - it('keeps packaged identity stable', () => { - expect(getDevInstanceIdentity(false, {})).toMatchObject({ - name: 'Orca', - appName: 'Orca', - isDev: false, - devLabel: null, - dockBadgeLabel: null, - appUserModelId: 'com.stablyai.orca' - }) - }) - - it('pins a stable dev appName across branches so the safeStorage key does not churn', () => { - const a = getDevInstanceIdentity(true, { ORCA_DEV_BRANCH: 'feature/a' }) - const b = getDevInstanceIdentity(true, { ORCA_DEV_BRANCH: 'feature/b' }) - - expect(a.name).not.toBe(b.name) - expect(a.appName).toBe('Orca Dev') - expect(b.appName).toBe('Orca Dev') - expect(a.appName).not.toBe('Orca') - }) - - it('never renames a packaged build before ready', () => { - // Packaged forks must retain their CFBundleName-derived safeStorage key or encrypted secrets become orphaned. - expect(shouldApplyPreReadyAppName(getDevInstanceIdentity(false, {}))).toBe(false) - expect(shouldApplyPreReadyAppName({ isDev: false })).toBe(false) - }) - - it('applies the dev name before ready so safeStorage sees it', () => { - expect(shouldApplyPreReadyAppName(getDevInstanceIdentity(true, {}))).toBe(true) - }) - - it('derives a readable dev label from worktree and branch env', () => { - const identity = getDevInstanceIdentity(true, { - ORCA_DEV_REPO_ROOT: '/repo/worktrees/dev-indicator', - ORCA_DEV_WORKTREE_NAME: 'dev-indicator', - ORCA_DEV_BRANCH: 'nwparker/dev-indicator' - }) - - expect(identity).toMatchObject({ - isDev: true, - devLabel: 'dev-indicator', - devBranch: 'nwparker/dev-indicator', - devWorktreeName: 'dev-indicator', - devRepoRoot: '/repo/worktrees/dev-indicator' - }) - expect(identity.name).toBe('Orca: nwparker/dev-indicator') - expect(identity.dockBadgeLabel).toBeNull() - expect(identity.appUserModelId).toMatch(/^com\.stablyai\.orca\.dev\.[a-f0-9]{10}$/) - }) - - it('includes the branch when it differs from the worktree basename', () => { - const identity = getDevInstanceIdentity(true, { - ORCA_DEV_REPO_ROOT: '/repo/worktrees/payment-ui', - ORCA_DEV_WORKTREE_NAME: 'payment-ui', - ORCA_DEV_BRANCH: 'feature/billing-shell' - }) - - expect(identity.devLabel).toBe('payment-ui @ feature/billing-shell') - expect(identity.name).toBe('Orca: feature/billing-shell') - expect(identity.dockBadgeLabel).toBeNull() - }) - - it('allows an explicit label override', () => { - const identity = getDevInstanceIdentity(true, { - ORCA_DEV_INSTANCE_LABEL: 'manual label', - ORCA_DEV_WORKTREE_NAME: 'dev-indicator', - ORCA_DEV_BRANCH: 'feature/other' - }) - - expect(identity.devLabel).toBe('manual label') - expect(identity.name).toBe('Orca: feature/other') - expect(identity.dockBadgeLabel).toBeNull() - }) -}) diff --git a/src/main/startup/linux-dev-shm-policy.test.ts b/src/main/startup/linux-dev-shm-policy.test.ts deleted file mode 100644 index 9f95882a06b..00000000000 --- a/src/main/startup/linux-dev-shm-policy.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const { appMock, statfsSyncMock, breadcrumbMock } = vi.hoisted(() => ({ - appMock: { - commandLine: { - appendSwitch: vi.fn(), - hasSwitch: vi.fn(() => false) - } - }, - statfsSyncMock: vi.fn(), - breadcrumbMock: vi.fn() -})) - -vi.mock('electron', () => ({ app: appMock })) -vi.mock('node:fs', async (importOriginal) => ({ - ...(await importOriginal>()), - statfsSync: statfsSyncMock -})) -vi.mock('../crash-reporting/crash-breadcrumb-store', () => ({ - recordCrashBreadcrumb: breadcrumbMock -})) - -const MIB = 1024 * 1024 -const originalPlatform = process.platform -const originalOverride = process.env.ORCA_DEV_SHM - -function setPlatform(platform: NodeJS.Platform): void { - Object.defineProperty(process, 'platform', { value: platform, configurable: true }) -} - -function mockDevShm(totalMib: number, freeMib: number): void { - // statfs reports sizes in blocks; use 4 KiB blocks like tmpfs. - const bsize = 4096 - statfsSyncMock.mockReturnValue({ - bsize, - blocks: (totalMib * MIB) / bsize, - bavail: (freeMib * MIB) / bsize - }) -} - -beforeEach(() => { - vi.resetModules() - appMock.commandLine.appendSwitch.mockReset() - appMock.commandLine.hasSwitch.mockReset() - appMock.commandLine.hasSwitch.mockReturnValue(false) - statfsSyncMock.mockReset() - breadcrumbMock.mockReset() - delete process.env.ORCA_DEV_SHM -}) - -afterEach(() => { - setPlatform(originalPlatform) - if (originalOverride === undefined) { - delete process.env.ORCA_DEV_SHM - } else { - process.env.ORCA_DEV_SHM = originalOverride - } -}) - -describe('configureLinuxDevShmUsage', () => { - it('moves Chromium shared memory off a Docker-default 64 MB /dev/shm', async () => { - setPlatform('linux') - mockDevShm(64, 60) - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - - configureLinuxDevShmUsage() - - expect(statfsSyncMock).toHaveBeenCalledWith('/dev/shm') - expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') - expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { - devShmTotalMB: 64, - devShmFreeMB: 60, - devShmUsageDisabled: true, - reason: 'small' - }) - }) - - it('leaves a normally sized /dev/shm on the fast shared-memory path', async () => { - setPlatform('linux') - mockDevShm(8192, 8000) - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - - configureLinuxDevShmUsage() - - expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() - expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { - devShmTotalMB: 8192, - devShmFreeMB: 8000, - devShmUsageDisabled: false, - reason: 'ok' - }) - }) - - it('disables /dev/shm usage when the mount is missing', async () => { - setPlatform('linux') - statfsSyncMock.mockImplementation(() => { - throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) - }) - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - - configureLinuxDevShmUsage() - - expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') - expect(breadcrumbMock).toHaveBeenCalledWith('dev_shm_policy', { - devShmUsageDisabled: true, - reason: 'unreadable' - }) - }) - - it('honors ORCA_DEV_SHM=off on a small mount and =force on a large one', async () => { - setPlatform('linux') - mockDevShm(64, 60) - process.env.ORCA_DEV_SHM = 'off' - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - configureLinuxDevShmUsage() - expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() - - mockDevShm(8192, 8000) - process.env.ORCA_DEV_SHM = 'force' - configureLinuxDevShmUsage() - expect(appMock.commandLine.appendSwitch).toHaveBeenCalledWith('disable-dev-shm-usage') - }) - - it('does not append the switch twice when it is already on the command line', async () => { - setPlatform('linux') - mockDevShm(64, 60) - appMock.commandLine.hasSwitch.mockReturnValue(true) - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - - configureLinuxDevShmUsage() - - expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() - }) - - it('is a no-op off Linux', async () => { - setPlatform('darwin') - const { configureLinuxDevShmUsage } = await import('./linux-dev-shm-policy') - - configureLinuxDevShmUsage() - - expect(statfsSyncMock).not.toHaveBeenCalled() - expect(appMock.commandLine.appendSwitch).not.toHaveBeenCalled() - expect(breadcrumbMock).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/startup/linux-secret-service-probe.test.ts b/src/main/startup/linux-secret-service-probe.test.ts deleted file mode 100644 index 57046f0cf0e..00000000000 --- a/src/main/startup/linux-secret-service-probe.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const runProcessSync = vi.hoisted(() => vi.fn()) -vi.mock('@orca/process-host', () => ({ runProcessSync })) - -const { probeSecretServiceCollection } = await import('./linux-secret-service-probe') - -function answers(overrides: Partial<{ code: number | null; stdout: string; timedOut: boolean }>) { - runProcessSync.mockReturnValue({ code: 0, stdout: '', stderr: '', timedOut: false, ...overrides }) -} - -describe('probeSecretServiceCollection', () => { - beforeEach(() => vi.clearAllMocks()) - - it('reports unlocked for the gdbus false variant', () => { - answers({ stdout: '(,)\n' }) - expect(probeSecretServiceCollection()).toBe('unlocked') - }) - - it('reports locked for the gdbus true variant', () => { - answers({ stdout: '(,)\n' }) - expect(probeSecretServiceCollection()).toBe('locked') - }) - - it('reports unavailable when no name owns org.freedesktop.secrets', () => { - answers({ code: 1, stdout: '' }) - expect(probeSecretServiceCollection()).toBe('unavailable') - }) - - // Timeout proves no collection state; report unavailable rather than invent a locked verdict. - it('reports unavailable when the probe is killed on its timeout', () => { - answers({ code: null, stdout: '', timedOut: true }) - expect(probeSecretServiceCollection()).toBe('unavailable') - }) - - it('reports unavailable when gdbus is not installed', () => { - runProcessSync.mockImplementation(() => { - throw Object.assign(new Error('spawnSync gdbus ENOENT'), { code: 'ENOENT' }) - }) - expect(probeSecretServiceCollection()).toBe('unavailable') - }) - - it('reports unavailable for output it does not recognise rather than assuming', () => { - answers({ stdout: '(<@ms "something else">,)' }) - expect(probeSecretServiceCollection()).toBe('unavailable') - }) - - // Require a kill deadline and a non-prompting property read so the probe cannot reproduce the stall. - it('bounds the child with a timeout and reads a property that cannot prompt', () => { - answers({ stdout: '(,)' }) - probeSecretServiceCollection() - const spec = runProcessSync.mock.calls[0]![0] - expect(spec.timeoutMs).toBeGreaterThan(0) - expect(spec.program).toBe('gdbus') - expect(spec.args).toContain('org.freedesktop.DBus.Properties.Get') - expect(spec.args).toContain('Locked') - expect(spec.args).not.toContain('Unlock') - }) -}) diff --git a/src/main/startup/wsl-holder-uninstall-preflight.test.ts b/src/main/startup/wsl-holder-uninstall-preflight.test.ts deleted file mode 100644 index d5ea35eac66..00000000000 --- a/src/main/startup/wsl-holder-uninstall-preflight.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import type { WslDistroHolder, WslDistroHolderAdoption } from '../wsl/wsl-distro-holder' - -const electron = vi.hoisted(() => ({ - userDataPath: '/user-data', - exit: vi.fn(), - requestSingleInstanceLock: vi.fn(), - BrowserWindow: vi.fn(), - handle: vi.fn() -})) -const appData = vi.hoisted(() => ({ ensure: vi.fn() })) - -vi.mock('electron', () => ({ - app: { - exit: electron.exit, - getPath: () => electron.userDataPath, - setPath: vi.fn(), - requestSingleInstanceLock: electron.requestSingleInstanceLock - }, - BrowserWindow: electron.BrowserWindow, - ipcMain: { handle: electron.handle } -})) -vi.mock('./windows-app-data-path', () => ({ ensureWindowsAppDataPath: appData.ensure })) - -import { - releaseWslHoldersForUninstall, - runWslHolderUninstallPreflight, - WSL_HOLDER_UNINSTALL_FLAG -} from './wsl-holder-uninstall-preflight' - -function holder(release: () => Promise): WslDistroHolder { - return { - ensureHeld: vi.fn(), - adoptHolders: vi.fn(), - release: vi.fn(), - releaseAll: vi.fn(release), - holderState: vi.fn() - } -} - -describe('WSL holder release on uninstall', () => { - it('succeeds only when every recorded holder is proven gone', async () => { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - const released = holder(async () => [{ distro: 'ubuntu', state: 'exited' }]) - await expect(releaseWslHoldersForUninstall(released)).resolves.toBe(0) - // Uninstall trusts a matching holder command line; nothing later could release it. - expect(released.releaseAll).toHaveBeenCalledWith({ forUninstall: true }) - await expect(releaseWslHoldersForUninstall(holder(async () => []))).resolves.toBe(0) - await expect( - releaseWslHoldersForUninstall( - holder(async () => [ - { distro: 'ubuntu', state: 'exited' }, - { distro: 'debian', state: 'unverifiable' } - ]) - ) - ).resolves.toBe(1) - await expect( - releaseWslHoldersForUninstall( - holder(async () => { - throw new Error('table unreadable') - }) - ) - ).resolves.toBe(1) - }) - - it('runs from the uninstaller only, after the app and its daemon are gone', () => { - const hooks = readFileSync( - new URL('../../../config/nsis/orca-installer-hooks.nsh', import.meta.url), - 'utf8' - ) - const unInstall = hooks.slice(hooks.indexOf('!macro customUnInstall')) - const realUninstall = unInstall.slice( - unInstall.indexOf('${ifNot} ${isUpdated}'), - // The real-uninstall branch ends with the relocated daemon host's cleanup. - unInstall.indexOf('daemon-host-profiles') - ) - const release = realUninstall.indexOf( - `nsExec::Exec '"$INSTDIR\\\${APP_EXECUTABLE_FILENAME}" ${WSL_HOLDER_UNINSTALL_FLAG}'` - ) - expect(release).toBeGreaterThan(-1) - // Killed first, so no running Orca can respawn a holder behind the release. - expect(release).toBeGreaterThan(realUninstall.indexOf('orca-terminal-daemon.exe')) - expect(hooks.split(WSL_HOLDER_UNINSTALL_FLAG)).toHaveLength(2) - }) - - it('exits 0 with no holder records, and opens no window, lock or IPC', async () => { - electron.userDataPath = mkdtempSync(join(tmpdir(), 'orca-uninstall-no-holders-')) - electron.exit.mockClear() - try { - expect(runWslHolderUninstallPreflight(['Orca.exe', WSL_HOLDER_UNINSTALL_FLAG])).toBe(true) - await vi.waitFor(() => expect(electron.exit).toHaveBeenCalledWith(0)) - expect(electron.BrowserWindow).not.toHaveBeenCalled() - expect(electron.requestSingleInstanceLock).not.toHaveBeenCalled() - expect(electron.handle).not.toHaveBeenCalled() - } finally { - rmSync(electron.userDataPath, { recursive: true, force: true }) - } - }) - - it('exits 1 rather than leaving an error box when setup throws', () => { - vi.useFakeTimers() - electron.exit.mockClear() - appData.ensure.mockImplementationOnce(() => { - throw new Error('Orca could not find the Windows roaming AppData folder') - }) - try { - vi.spyOn(console, 'warn').mockImplementation(() => {}) - expect(runWslHolderUninstallPreflight(['Orca.exe', WSL_HOLDER_UNINSTALL_FLAG])).toBe(true) - expect(electron.exit).toHaveBeenCalledExactlyOnceWith(1) - // The timer armed before setup is cleared, so it can't exit a second time. - vi.runAllTimers() - expect(electron.exit).toHaveBeenCalledOnce() - } finally { - vi.useRealTimers() - } - }) - - it('launches from the uninstaller only for a userData that has holder records', () => { - const hooks = readFileSync( - new URL('../../../config/nsis/orca-installer-hooks.nsh', import.meta.url), - 'utf8' - ) - const launch = hooks.indexOf(`--orca-uninstall-release-wsl-holders'`) - const gate = hooks.lastIndexOf('${if} $1 == "1"', launch) - expect(gate).toBeGreaterThan(-1) - expect(hooks.slice(0, gate)).toMatch( - /\$\{FileExists\} "\$APPDATA\\\$\{APP_PACKAGE_NAME\}\\wsl-distro-holders\\\*\.json"/ - ) - }) -}) diff --git a/src/main/wsl/wsl-w1-w3-contract.test.ts b/src/main/wsl/wsl-w1-w3-contract.test.ts deleted file mode 100644 index 074cca8d8ed..00000000000 --- a/src/main/wsl/wsl-w1-w3-contract.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest' - -const runProcessMock = vi.hoisted(() => vi.fn()) -vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock })) -vi.mock('./wsl-executable-path', () => ({ - resolveWslExecutablePath: () => 'C:\\Windows\\System32\\wsl.exe' -})) - -import { runWslProcess } from './wsl-runner' -import { - invalidateWslGuestEnvironment, - seedWslGuestEnvironmentForTests -} from './wsl-guest-environment' - -/** W1 spawn, W2 Windows ownership and W3 WSL runner compose one contract; keep regressions together across all three layers. */ -const ENV = { path: '/home/u/.nvm/bin:/usr/bin', home: '/home/u', envBinary: '/usr/bin/env' } - -function spawnSpec(): Record { - return runProcessMock.mock.calls.at(-1)?.[0] as Record -} - -beforeEach(() => { - runProcessMock.mockReset() - runProcessMock.mockResolvedValue({ - code: 0, - signal: null, - stdout: '', - stderr: '', - timedOut: false - }) - invalidateWslGuestEnvironment(undefined, true) - seedWslGuestEnvironmentForTests(undefined, ENV) - seedWslGuestEnvironmentForTests('Ubuntu', ENV) -}) - -describe('W1: every WSL call inherits the spawn chokepoint', () => { - it('resolves wsl.exe absolutely, never by bare name', async () => { - // Bare executable resolution is vulnerable to Group Policy/stripped Electron PATH (#15749). - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', program: '/bin/true' }) - expect(spawnSpec().program).toBe('C:\\Windows\\System32\\wsl.exe') - }) - - it('passes argv as an array, never a command string', async () => { - // Pass argv rather than return quoting responsibility to callers. - await runWslProcess({ - loginPath: 'preferred', - distro: 'Ubuntu', - program: '/bin/echo', - args: ['a b'] - }) - expect(Array.isArray(spawnSpec().args)).toBe(true) - expect(spawnSpec().args).toContain('a b') - }) - - it('always bounds the call', async () => { - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', program: '/bin/true' }) - expect(spawnSpec().timeoutMs).toBeGreaterThan(0) - }) -}) - -describe('W3: the five per-call decisions are made once', () => { - it('never uses the -- separator, which expands $name host-side', async () => { - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', script: 'echo "$HOME"' }) - const argv = spawnSpec().args as string[] - // Assert the wsl.exe separator position; later -- belongs to the guest shell. - const separator = argv.findIndex((a) => a === '--' || a === '--exec') - expect(argv[separator]).toBe('--exec') - }) - - it('keeps a script byte-identical instead of encoding it', async () => { - // --exec preserves exact script argv and stdin without base64/eval quote repair (#14292). - const script = `case "$x" in a) echo 'it'\\''s fine';; esac` - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', script }) - expect(spawnSpec().args).toContain(script) - expect(spawnSpec().input).toBeUndefined() - }) - - it('propagates env only through WSLENV', async () => { - await runWslProcess({ - loginPath: 'preferred', - distro: 'Ubuntu', - program: '/bin/true', - env: { GITLAB_HOST: 'git.example.com' } - }) - const env = spawnSpec().env as NodeJS.ProcessEnv - expect(env.WSLENV).toContain('GITLAB_HOST') - }) - - it('runs no shell on the probe lane, so ~/.profile cannot stall it', async () => { - // #14288: blocking profiles must not consume the command timeout. - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', program: 'codex' }) - expect(JSON.stringify(spawnSpec().args)).not.toContain('_orca_wsl_shell') - }) - - it('applies the user login PATH even with no shell in the loop', async () => { - // Keep nvm-installed tools visible through login PATH (#9725/#7563/#8366). - await runWslProcess({ loginPath: 'preferred', distro: 'Ubuntu', program: 'codex' }) - expect(spawnSpec().args).toContain('PATH=/home/u/.nvm/bin:/usr/bin') - }) -}) - -describe('failure modes stay distinguishable', () => { - it('a non-zero exit is data, not an exception', async () => { - // Nonzero process exits resolve; callers must receive the code. - runProcessMock.mockResolvedValue({ - code: 3, - signal: null, - stdout: 'partial', - stderr: '', - timedOut: false - }) - const result = await runWslProcess({ - loginPath: 'preferred', - distro: 'Ubuntu', - program: '/bin/false' - }) - expect(result.code).toBe(3) - expect(result.timedOut).toBe(false) - }) - - it('a timeout is reported, not disguised as an empty answer', async () => { - runProcessMock.mockResolvedValue({ - code: null, - signal: null, - stdout: '', - stderr: '', - timedOut: true - }) - const result = await runWslProcess({ - loginPath: 'preferred', - distro: 'Ubuntu', - program: '/bin/true' - }) - expect(result.timedOut).toBe(true) - }) - - it('an unresolved login PATH is reported, never fatal', async () => { - invalidateWslGuestEnvironment(undefined, true) - runProcessMock.mockResolvedValue({ - code: 1, - signal: null, - stdout: '', - stderr: 'stopped', - timedOut: false - }) - const result = await runWslProcess({ - loginPath: 'preferred', - distro: 'Ubuntu', - program: 'codex' - }) - expect(result.environmentResolved).toBe(false) - }) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx deleted file mode 100644 index fce999ae9d4..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import type { ButtonHTMLAttributes, ReactNode } from 'react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { i18n } from '@/i18n/i18n' -import es from '@/i18n/locales/es.json' -import type { GitStatusEntry } from '../../../../shared/git-status-types' - -type CapturedButtonProps = ButtonHTMLAttributes & { - children?: ReactNode - variant?: string -} - -const mocks = vi.hoisted(() => ({ - buttons: [] as CapturedButtonProps[], - dialogContentProps: [] as Record[] -})) - -vi.mock('@/components/ui/dialog', () => ({ - Dialog: ({ open, children }: { open: boolean; children: ReactNode }) => - open ?
{children}
: null, - DialogContent: ({ - children, - ...props - }: { - children: ReactNode - onOpenAutoFocus?: (event: Event) => void - }) => { - mocks.dialogContentProps.push(props) - return
{children}
- }, - DialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, - DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, - DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

-})) - -vi.mock('@/components/ui/button', async () => { - const ReactModule = await import('react') - return { - Button: ReactModule.forwardRef(function Button( - { children, variant: _variant, ...props }, - ref - ) { - mocks.buttons.push({ ...props, variant: _variant, children }) - return ( - - ) - }) - } -}) - -function entry(partial: Partial & { path: string }): GitStatusEntry { - return { - area: 'unstaged', - status: 'modified', - ...partial - } -} - -function textContent(node: ReactNode): string { - if (node == null || typeof node === 'boolean') { - return '' - } - if (typeof node === 'string' || typeof node === 'number') { - return String(node) - } - if (Array.isArray(node)) { - return node.map(textContent).join('') - } - if (typeof node === 'object' && 'props' in node) { - return textContent((node as { props?: { children?: ReactNode } }).props?.children) - } - return '' -} - -afterEach(async () => { - await i18n.changeLanguage('en') -}) - -describe('SourceControlDiscardDialog', () => { - beforeEach(() => { - mocks.buttons = [] - mocks.dialogContentProps = [] - }) - - it('keeps the delete icon when its label is translated', async () => { - i18n.addResourceBundle('es', 'translation', es) - await i18n.changeLanguage('es') - const { SourceControlDiscardDialog } = await import('./source-control/commit/discard-dialog') - const markup = renderToStaticMarkup( - - ) - expect(markup).toContain('lucide-trash') - expect(markup).not.toContain('lucide-undo-2') - expect(markup).toContain(es.sourceControl.discard.deleteCount.replace('{{count}}', '2')) - }) - - it('makes the discard button the dialog default action', async () => { - const { SourceControlDiscardDialog } = await import('./source-control/commit/discard-dialog') - - renderToStaticMarkup( - - ) - - const cancelButton = mocks.buttons.find((button) => textContent(button.children) === 'Cancel') - const discardButton = mocks.buttons.find((button) => - textContent(button.children).includes('Discard') - ) - - expect(cancelButton?.autoFocus).not.toBe(true) - expect(discardButton?.variant).toBe('destructive') - expect(discardButton?.autoFocus).toBe(true) - expect(mocks.dialogContentProps[0]?.onOpenAutoFocus).toEqual(expect.any(Function)) - }) -}) - -describe('focusDiscardDialogConfirmButton', () => { - it('prevents Radix from focusing the first tabbable button', async () => { - const { focusDiscardDialogConfirmButton } = - await import('./source-control/commit/discard-dialog') - const event = { preventDefault: vi.fn() } as unknown as Event - const confirmButton = { focus: vi.fn() } as unknown as HTMLButtonElement - - focusDiscardDialogConfirmButton(event, confirmButton) - - expect(event.preventDefault).toHaveBeenCalledTimes(1) - expect(confirmButton.focus).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx b/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx deleted file mode 100644 index 7d1a729a122..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React from 'react' -import { renderToStaticMarkup } from 'react-dom/server' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { OpenInApplication } from '../../../../shared/ui-chrome-types' -import { SourceControlEntryContextMenu } from './source-control/listing/entry-context-menu' - -type ItemProps = { onSelect?: () => void; disabled?: boolean; children?: React.ReactNode } - -const items = vi.hoisted(() => ({ list: [] as ItemProps[] })) -const storeState = vi.hoisted( - (): { - settings: { - openInApplications: OpenInApplication[] - activeRuntimeEnvironmentId: string | null - } - worktreesByRepo: Record - } => ({ - settings: { openInApplications: [], activeRuntimeEnvironmentId: null }, - worktreesByRepo: { 'repo-1': [{ id: 'worktree-1', repoId: 'repo-1', hostId: 'local' }] } - }) -) -const ownerRuntime = vi.hoisted((): { environmentId: string | null } => ({ environmentId: null })) -const revealInFileManager = vi.hoisted(() => vi.fn()) -const openWorktreePath = vi.hoisted(() => vi.fn()) - -vi.mock('@/components/ui/context-menu', async () => { - const React_ = await import('react') - const passthrough = ({ children }: { children?: React.ReactNode }) => - React_.createElement(React_.Fragment, null, children) - - return { - ContextMenu: passthrough, - ContextMenuContent: passthrough, - ContextMenuItem: (props: ItemProps) => { - items.list.push(props) - return React_.createElement(React_.Fragment, null, props.children) - }, - ContextMenuSeparator: () => null, - ContextMenuSub: passthrough, - ContextMenuSubContent: passthrough, - ContextMenuSubTrigger: passthrough, - ContextMenuTrigger: passthrough - } -}) - -vi.mock('@/store', () => ({ - useAppStore: (selector: (state: typeof storeState) => unknown) => selector(storeState) -})) - -vi.mock('@/lib/worktree-runtime-owner', () => ({ - runtimeEnvironmentIdOrLocalUnlessTwin: () => ownerRuntime.environmentId -})) - -vi.mock(import('@/lib/reveal-in-file-manager'), async (importOriginal) => ({ - ...(await importOriginal()), - getRevealInFileManagerLabel: () => 'Reveal in Finder', - revealInFileManager -})) - -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, fallback: string) => fallback -})) - -vi.mock('@/lib/open-in-app-catalog', () => ({ - OpenInApplicationIcon: () => null -})) - -vi.mock('@/components/sidebar/WorktreeOpenInMenu', async () => { - const { getExternalEditorOpenCapability } = await import('@/lib/external-editor-open-capability') - return { - getOpenInEntryAvailability: ( - entry: { command?: string }, - connectionId?: string | null, - runtimeEnvironmentId?: string | null - ) => ({ - disabled: !getExternalEditorOpenCapability({ - command: entry.command, - connectionId, - runtimeEnvironmentId - }).allowed - }), - openOpenInAppsSettings: vi.fn(), - openWorktreePath - } -}) - -function childrenText(children: React.ReactNode): string { - return React.Children.toArray(children) - .map((child) => { - if (typeof child === 'string') { - return child - } - return React.isValidElement<{ children?: React.ReactNode }>(child) - ? childrenText(child.props.children) - : '' - }) - .join('') -} - -function showsLocalOnlyHint(item: ItemProps | undefined): boolean { - return renderToStaticMarkup(<>{item?.children}).includes('Local only') -} - -function renderMenu(props: { connectionId?: string; hasWorkingTreeFile?: boolean } = {}): void { - renderToStaticMarkup( - -
- - ) -} - -function renderRevealItem(props?: Parameters[0]): ItemProps | undefined { - renderMenu(props) - return items.list.find((item) => childrenText(item.children) === 'Reveal in Finder') -} - -describe('SourceControlEntryContextMenu', () => { - const writeClipboardText = vi.fn() - - beforeEach(() => { - items.list = [] - storeState.settings.activeRuntimeEnvironmentId = null - storeState.settings.openInApplications = [] - storeState.worktreesByRepo = { - 'repo-1': [{ id: 'worktree-1', repoId: 'repo-1', hostId: 'local' }] - } - ownerRuntime.environmentId = null - revealInFileManager.mockReset() - openWorktreePath.mockReset() - writeClipboardText.mockReset() - vi.stubGlobal('window', { - api: { ui: { writeClipboardText } } - }) - }) - - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('copies the supplied relative path', () => { - renderMenu() - - const copyRelativePathItem = items.list.find( - (item) => childrenText(item.children) === 'Copy Relative Path' - ) - - expect(copyRelativePathItem).toBeDefined() - copyRelativePathItem?.onSelect?.() - expect(writeClipboardText).toHaveBeenCalledWith('src/example.ts') - }) - - it('reveals the changed file in the OS file manager (issue #24003)', () => { - const revealItem = renderRevealItem() - - expect(revealItem?.disabled).toBe(false) - expect(showsLocalOnlyHint(revealItem)).toBe(false) - revealItem?.onSelect?.() - expect(revealInFileManager).toHaveBeenCalledWith('/repo/src/example.ts', 'local') - }) - - it('reveals a local repo file while a remote server is focused', () => { - storeState.settings.activeRuntimeEnvironmentId = 'env-1' - - expect(renderRevealItem()?.disabled).toBe(false) - }) - - it('offers the file manager once, outside the "Open in" apps', () => { - storeState.settings.openInApplications = [{ id: 'zed', label: 'Zed', command: 'zed' }] - - renderMenu() - - const labels = items.list.map((item) => childrenText(item.children)) - expect(labels).toContain('Zed') - expect(labels).not.toContain('Finder') - expect(labels.filter((label) => label === 'Reveal in Finder')).toHaveLength(1) - }) - - it('uses the target owner for editor availability and click dispatch while focus is local', () => { - ownerRuntime.environmentId = 'env-2' - storeState.settings.openInApplications = [{ id: 'zed', label: 'Zed', command: 'zed' }] - renderMenu() - - const editorItem = items.list.find((item) => childrenText(item.children) === 'Zed') - expect(editorItem?.disabled).toBe(true) - editorItem?.onSelect?.() - expect(openWorktreePath).toHaveBeenCalledWith({ - target: 'external-editor', - worktreePath: '/repo/src/example.ts', - connectionId: null, - runtimeEnvironmentId: 'env-2', - ownerUnresolved: false, - workspace: { workspaceId: 'worktree-1' }, - command: 'zed' - }) - }) - - it('routes a repo the catalog places on an SSH host there, even with no connection prop', () => { - storeState.worktreesByRepo = { - 'repo-1': [{ id: 'worktree-1', repoId: 'repo-1', hostId: 'ssh:ssh-9' }] - } - storeState.settings.activeRuntimeEnvironmentId = 'env-1' - storeState.settings.openInApplications = [{ id: 'code', label: 'Code', command: 'code' }] - - const revealItem = renderRevealItem() - expect(revealItem?.disabled).toBe(true) - items.list.find((item) => childrenText(item.children) === 'Code')?.onSelect?.() - expect(openWorktreePath).toHaveBeenCalledWith( - expect.objectContaining({ connectionId: 'ssh-9', runtimeEnvironmentId: null }) - ) - }) - - it('disables reveal, with no reason, for a deleted file', () => { - const revealItem = renderRevealItem({ hasWorkingTreeFile: false }) - - expect(revealItem?.disabled).toBe(true) - expect(showsLocalOnlyHint(revealItem)).toBe(false) - }) - - it('disables reveal as local-only for a repo on an SSH host', () => { - const revealItem = renderRevealItem({ connectionId: 'ssh-1' }) - - expect(revealItem?.disabled).toBe(true) - expect(showsLocalOnlyHint(revealItem)).toBe(true) - }) - - it('disables reveal as local-only for a repo owned by a runtime that is not the focused one', () => { - ownerRuntime.environmentId = 'env-2' - - const revealItem = renderRevealItem() - - expect(revealItem?.disabled).toBe(true) - expect(showsLocalOnlyHint(revealItem)).toBe(true) - }) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx deleted file mode 100644 index 1b267af4c9f..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import type { ReactNode } from 'react' -import { describe, expect, it, vi } from 'vitest' -import { SourceControlHeaderToolbar } from './source-control/panel/header-toolbar' -import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' -import type { GitBranchLineTotal } from '../../../../shared/git-status-types' -import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { WorktreeGitIdentityDisplay } from '../../../../shared/worktree/git-identity-display' -import type { PrimaryAction } from './source-control-primary-action' - -vi.mock('@/components/ui/tooltip', () => ({ - Tooltip: ({ children }: { children: ReactNode }) => <>{children}, - TooltipContent: ({ children }: { children: ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} -})) - -vi.mock('./source-control/panel/header-overflow-menu', () => ({ - SourceControlHeaderOverflowMenu: () => -})) - -const CREATE_PR_ACTION: PrimaryAction = { - kind: 'create_pr', - label: 'Create PR', - title: 'Create a pull request', - disabled: false -} - -const readySummary: GitBranchCompareSummary = { - baseRef: 'origin/main', - baseOid: 'base', - compareRef: 'feature', - headOid: 'head', - mergeBase: 'base', - changedFiles: 0, - commitsAhead: 1, - status: 'ready' -} - -function renderToolbar(options?: { - headDisplay?: WorktreeGitIdentityDisplay | null - branchSummary?: GitBranchCompareSummary | null - compareBaseRef?: string | null - branchLineTotal?: GitBranchLineTotal | null - visibleCreatePrHeaderAction?: PrimaryAction | null - hostedReview?: HostedReviewInfo | null - suppressedGitHubPRNumber?: number | null -}): string { - return renderToStaticMarkup( - - ) -} - -describe('SourceControlHeaderToolbar branch identity', () => { - it('shows the relink recovery instead of blank chrome for a matched unlinked PR', () => { - const markup = renderToolbar({ - visibleCreatePrHeaderAction: null, - suppressedGitHubPRNumber: 42 - }) - - expect(markup).toContain('PR #42 unlinked') - expect(markup).toContain('Link PR #42') - expect(markup).not.toContain('Create PR') - }) - - it('keeps the relink recovery ahead of transient Create PR loading chrome', () => { - const markup = renderToolbar({ - visibleCreatePrHeaderAction: { - ...CREATE_PR_ACTION, - title: 'Checking whether this branch can create a pull request…', - disabled: true - }, - suppressedGitHubPRNumber: 42 - }) - - expect(markup).toContain('Link PR #42') - expect(markup).not.toContain('Create PR') - expect(markup).not.toContain('Checking whether') - }) - - it('keeps Create PR while stacking head above base in the context row', () => { - const markup = renderToolbar() - const branchIndex = markup.indexOf('brennanb2025/source-control-branch-name') - const createPrIndex = markup.indexOf('Create PR') - - expect(branchIndex).toBeGreaterThan(-1) - expect(createPrIndex).toBeGreaterThan(-1) - expect(markup).toContain('aria-label="brennanb2025/source-control-branch-name → origin/main"') - expect(markup).toContain('aria-label="Current branch: brennanb2025/source-control-branch-name"') - expect(markup).toContain('data-testid="source-control-head-identity"') - }) - - it('shows head-only identity with Create PR when no compare base is configured', () => { - const markup = renderToolbar({ - branchSummary: null, - compareBaseRef: null, - headDisplay: { kind: 'branch', branchName: 'local-only-branch' } - }) - - expect(markup).toContain('Create PR') - expect(markup).toContain('data-testid="source-control-head-identity"') - expect(markup).toContain('local-only-branch') - expect(markup).not.toContain('→') - }) - - it('renders detached HEAD with Create PR when compare base is present', () => { - const markup = renderToolbar({ - headDisplay: { - kind: 'detached', - shortHead: '8cec248', - sidebarLabel: 'Detached HEAD @ 8cec248', - sourceControlLabel: 'Detached HEAD · 8cec248', - tooltip: 'Detached HEAD at 8cec248. You are viewing a commit, not a branch.' - } - }) - - expect(markup).toContain('Detached HEAD · 8cec248') - expect(markup).toContain('tabindex="0"') - expect(markup).toContain('Create PR') - expect(markup).toContain('aria-label="Detached HEAD · 8cec248 → origin/main"') - }) - - it('omits identity chrome when there is no git identity and no base', () => { - const markup = renderToolbar({ - headDisplay: null, - branchSummary: null, - compareBaseRef: null - }) - - expect(markup).not.toContain('data-testid="source-control-head-identity"') - expect(markup).not.toContain('→') - expect(markup).toContain('Create PR') - }) - - it('threads the branch line total down to the base-ref line', () => { - const markup = renderToolbar({ branchLineTotal: { added: 24, removed: 3, mergeBase: 'base' } }) - - expect(markup).toContain('aria-label="24 lines added, 3 lines deleted"') - expect(markup).toContain('+24') - expect(markup).toContain('-3') - }) - - it('renders exactly as before when no branch line total is supplied', () => { - expect(renderToolbar()).not.toContain('data-testid="source-control-branch-line-total"') - }) -}) diff --git a/src/renderer/src/components/right-sidebar/source-control-split-open.test.ts b/src/renderer/src/components/right-sidebar/source-control-split-open.test.ts deleted file mode 100644 index 15f6f3e90e9..00000000000 --- a/src/renderer/src/components/right-sidebar/source-control-split-open.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - isSourceControlSplitOpenModifier, - shouldOpenSourceControlRowAsPreview, - toPermanentSourceControlRowOpenEvent, - type SourceControlRowOpenEvent -} from './source-control/listing/split-open' - -function event(overrides: Partial = {}): SourceControlRowOpenEvent { - return { - altKey: false, - ctrlKey: false, - metaKey: false, - shiftKey: false, - ...overrides - } -} - -describe('isSourceControlSplitOpenModifier', () => { - it('uses Cmd on macOS and Ctrl elsewhere as the platform primary modifier', () => { - expect(isSourceControlSplitOpenModifier(event({ metaKey: true }), true)).toBe(true) - expect(isSourceControlSplitOpenModifier(event({ ctrlKey: true }), true)).toBe(false) - - expect(isSourceControlSplitOpenModifier(event({ ctrlKey: true }), false)).toBe(true) - expect(isSourceControlSplitOpenModifier(event({ metaKey: true }), false)).toBe(false) - }) - - it('treats Shift and Alt/Option as split-open modifiers', () => { - expect(isSourceControlSplitOpenModifier(event({ shiftKey: true }), true)).toBe(true) - expect(isSourceControlSplitOpenModifier(event({ altKey: true }), false)).toBe(true) - }) - - it('ignores a plain click', () => { - expect(isSourceControlSplitOpenModifier(event(), true)).toBe(false) - expect(isSourceControlSplitOpenModifier(event(), false)).toBe(false) - }) -}) - -describe('shouldOpenSourceControlRowAsPreview', () => { - it('uses preview for plain row opens in the current group', () => { - expect(shouldOpenSourceControlRowAsPreview(event(), undefined)).toBe(true) - }) - - it('does not preview when opening into a split group', () => { - expect(shouldOpenSourceControlRowAsPreview(event(), 'group-2')).toBe(false) - }) - - it('does not preview when the row requests a permanent open', () => { - expect(shouldOpenSourceControlRowAsPreview(event({ openAsPermanent: true }), undefined)).toBe( - false - ) - }) -}) - -describe('toPermanentSourceControlRowOpenEvent', () => { - it('preserves modifier keys and marks the row open as permanent', () => { - expect( - toPermanentSourceControlRowOpenEvent( - event({ - altKey: true, - metaKey: true - }) - ) - ).toEqual({ - altKey: true, - ctrlKey: false, - metaKey: true, - shiftKey: false, - openAsPermanent: true - }) - }) -}) diff --git a/src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts b/src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts deleted file mode 100644 index ad22d49f74d..00000000000 --- a/src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { WorktreeVisibilityDefaults } from '../../../../shared/global-settings-types' -import type { Repo } from '../../../../shared/repo-types' - -vi.mock('@/i18n/i18n', () => ({ - translate: (_key: string, fallback: string, values?: Record) => - values - ? fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => String(values[name] ?? '')) - : fallback -})) - -const { - getWorktreeVisibilityOverrideNotice, - getWorktreeVisibilitySourceNote, - getWorktreeVisibilitySourceProvenance, - listInheritedWorktreeVisibilitySources -} = await import('./worktree-visibility-source-provenance') - -function repo(overrides: Partial = {}): Repo { - return { - id: 'repo-1', - path: '/repo', - displayName: 'orca', - badgeColor: '#000000', - addedAt: 0, - externalWorktreeVisibility: undefined, - ...overrides - } -} - -const shownClaude: WorktreeVisibilityDefaults = { - external: 'hide', - sourcePreferences: { builtIn: { claude: 'show' } } -} - -const NO_REPO_SOURCES: ReadonlySet = new Set() - -function provenanceFor(target: Repo, defaults: WorktreeVisibilityDefaults) { - return getWorktreeVisibilitySourceProvenance( - target, - { kind: 'built-in', id: 'claude' }, - defaults, - NO_REPO_SOURCES - ) -} - -describe('listInheritedWorktreeVisibilitySources', () => { - it('reports what each inheritable source is set to globally', () => { - expect(listInheritedWorktreeVisibilitySources(repo(), shownClaude)).toEqual([ - { source: { kind: 'built-in', id: 'claude' }, globalVisibility: 'show' }, - { source: { kind: 'built-in', id: 'gsd' }, globalVisibility: 'hide' }, - { source: { kind: 'other' }, globalVisibility: 'hide' } - ]) - }) - - it('keeps a source the project has overridden, since global is what it overrode', () => { - const overridden = listInheritedWorktreeVisibilitySources( - repo({ worktreeVisibilitySourcePreferences: { builtIn: { claude: 'hide' } } }), - shownClaude - ) - expect(overridden[0]).toEqual({ - source: { kind: 'built-in', id: 'claude' }, - globalVisibility: 'show' - }) - }) - - it('lists a global custom source but not one the project added itself', () => { - const listed = listInheritedWorktreeVisibilitySources( - repo({ customWorktreeVisibilitySources: [{ id: 'local', rootPath: '/srv/local' }] }), - { - external: 'hide', - customSources: [{ id: 'team', rootPath: '/srv/team' }], - sourcePreferences: { custom: { team: 'show' } } - } - ) - expect(listed.map(({ source }) => source)).toContainEqual({ - kind: 'custom', - source: { id: 'team', rootPath: '/srv/team' } - }) - expect( - listed.some(({ source }) => source.kind === 'custom' && source.source.id === 'local') - ).toBe(false) - }) -}) - -describe('getWorktreeVisibilityOverrideNotice', () => { - it('names the global value a project override is ignoring', () => { - const provenance = provenanceFor( - repo({ worktreeVisibilitySourcePreferences: { builtIn: { claude: 'hide' } } }), - shownClaude - ) - expect(getWorktreeVisibilityOverrideNotice(provenance, 'hide')).toBe( - 'Overriding global setting: Show' - ) - }) - - it('stays quiet for an override that agrees with global settings', () => { - // Why: a legacy repo materializes an override on every source, most of which change nothing. - const provenance = provenanceFor(repo({ agentWorktreeVisibility: 'show' }), shownClaude) - expect(provenance?.kind).toBe('project-override') - expect(getWorktreeVisibilityOverrideNotice(provenance, 'show')).toBeNull() - }) - - it('stays quiet for a source that is still following global settings', () => { - expect( - getWorktreeVisibilityOverrideNotice(provenanceFor(repo(), shownClaude), 'show') - ).toBeNull() - }) - - it('stays quiet outside a project scope', () => { - expect(getWorktreeVisibilityOverrideNotice(null, 'show')).toBeNull() - }) -}) - -describe('getWorktreeVisibilitySourceNote', () => { - it('marks a source the project added itself', () => { - const provenance = getWorktreeVisibilitySourceProvenance( - repo({ customWorktreeVisibilitySources: [{ id: 'local', rootPath: '/srv/local' }] }), - { kind: 'custom', source: { id: 'local', rootPath: '/srv/local' } }, - { external: 'hide' }, - new Set(['local']) - ) - expect(getWorktreeVisibilitySourceNote(provenance)).toBe('Added in this project only.') - }) - - it('says nothing for an inherited source', () => { - expect(getWorktreeVisibilitySourceNote(provenanceFor(repo(), shownClaude))).toBeNull() - }) -}) diff --git a/src/renderer/src/components/skills/skill-install-provider-groups.test.ts b/src/renderer/src/components/skills/skill-install-provider-groups.test.ts deleted file mode 100644 index be1487e0ba7..00000000000 --- a/src/renderer/src/components/skills/skill-install-provider-groups.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - defaultSelectedSkillProviders, - groupSkillInstallProviders, - skillProviderDirectoryLabel, - toggledSkillProviderSelection -} from './skill-install-provider-groups' - -describe('groupSkillInstallProviders', () => { - it('separates agents that read the canonical root from agents with their own', () => { - const workspace = groupSkillInstallProviders('workspace') - const global = groupSkillInstallProviders('global') - expect(workspace.canonical.map((provider) => provider.id)).toContain('codex') - expect(global.canonical.map((provider) => provider.id)).toContain('codex') - expect(workspace.selectable.map((choice) => choice.provider.id)).toContain('claude') - expect(workspace.selectable.map((choice) => choice.provider.id)).not.toContain('codex') - expect(global.selectable.map((choice) => choice.provider.id)).not.toContain('codex') - }) - - it('moves an agent between the groups when the scope changes', () => { - expect(groupSkillInstallProviders('workspace').canonical.map((p) => p.id)).toContain('cursor') - expect(groupSkillInstallProviders('global').selectable.map((c) => c.provider.id)).toContain( - 'cursor' - ) - }) - - it('labels a directory relative to the scope it is shown under', () => { - expect(skillProviderDirectoryLabel(['.claude', 'skills'], 'workspace')).toBe('.claude/skills') - expect(skillProviderDirectoryLabel(['.claude', 'skills'], 'global')).toBe('~/.claude/skills') - }) -}) - -describe('defaultSelectedSkillProviders', () => { - it('starts with the agents the machine actually has', () => { - const selected = defaultSelectedSkillProviders(['claude', 'codex', 'unrelated-tool']) - expect([...selected]).toEqual(['codex', 'claude']) - }) - - // Unavailable detection is unknown, not proof that no agents are installed. - it('falls back to every agent when detection is unavailable', () => { - const selected = defaultSelectedSkillProviders(null) - expect(selected.has('claude')).toBe(true) - expect(selected.has('aug')).toBe(true) - }) - - it('selects nothing extra when the machine has no known agent', () => { - expect([...defaultSelectedSkillProviders([])]).toEqual([]) - }) - - it('adds and removes one agent at a time', () => { - const withoutClaude = toggledSkillProviderSelection( - defaultSelectedSkillProviders(null), - 'claude', - false - ) - expect(withoutClaude.has('claude')).toBe(false) - expect(withoutClaude.has('codex')).toBe(true) - expect(toggledSkillProviderSelection(withoutClaude, 'claude', true).has('claude')).toBe(true) - }) -}) diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx deleted file mode 100644 index 82dd17915ff..00000000000 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -// @vitest-environment happy-dom - -import React, { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { ORPHAN_WORKTREE_ID } from '../../../../shared/constants' -import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' -import type { UnifiedSessionRow, UnifiedWorktreeRow } from './resource-usage-merge-types' - -vi.mock('@/store', () => { - const storeState = {} - const useAppStore = Object.assign( - (selector: (state: typeof storeState) => unknown) => selector(storeState), - { getState: () => storeState } - ) - return { useAppStore } -}) - -vi.mock('@/components/ui/tooltip', () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children} -})) - -vi.mock('@/i18n/i18n', () => ({ - i18n: { language: 'en' }, - translate: (_key: string, fallback: string, values?: Record) => - values - ? Object.entries(values).reduce( - (text, [token, value]) => text.replace(`{{${token}}}`, value), - fallback - ) - : fallback -})) - -vi.mock('sonner', () => ({ - toast: { error: vi.fn(), success: vi.fn() } -})) - -import { WorktreeRow } from './ResourceUsageStatusSegment' - -function makeSession(overrides: Partial): UnifiedSessionRow { - return { - sessionId: 'sess-1', - paneKey: null, - pid: 100, - label: 'zsh', - bound: true, - agentOwnership: 'absent' as const, - tabId: 'tab-1', - cpu: 1, - memory: 100, - hasLocalSamples: true, - ...overrides - } -} - -function makeWorktree(overrides: Partial): UnifiedWorktreeRow { - return { - worktreeId: 'wt-1', - worktreeName: 'feature-branch', - repoId: 'repo-1', - repoName: 'repo', - cpu: 1, - memory: 100, - history: [], - hasLocalSamples: true, - isRemote: false, - sessions: [], - browsers: [], - ...overrides - } -} - -describe('resource manager row presentation', () => { - let container: HTMLDivElement - let root: Root - - afterEach(() => { - act(() => { - root.unmount() - }) - container.remove() - }) - - function renderWorktreeRow(worktree: UnifiedWorktreeRow): void { - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - act(() => { - root.render( - {}} - onNavigate={() => {}} - onDelete={() => {}} - onKillSession={() => {}} - navigateToTab={() => {}} - /> - ) - }) - } - - it('keeps the remote chip and kill affordances on SSH-backed rows', () => { - renderWorktreeRow( - makeWorktree({ - isRemote: true, - cpu: null, - memory: null, - sessions: [ - makeSession({ sessionId: 'ssh-a', bound: true }), - makeSession({ sessionId: 'ssh-b', bound: false, tabId: null, cpu: null, memory: null }) - ] - }) - ) - - expect(container.textContent).toContain('· remote') - const killButtons = container.querySelectorAll('button[aria-label^="Kill session"]') - expect(killButtons).toHaveLength(2) - expect(container.querySelector('button[aria-label="Kill session ssh-a"]')).not.toBeNull() - expect(container.querySelector('button[aria-label="Kill session ssh-b"]')).not.toBeNull() - }) - - it("names the server and offers no kill on a paired server's own samples", () => { - renderWorktreeRow( - makeWorktree({ - isRemote: true, - hostLabel: 'Lab box', - sessions: [makeSession({ sessionId: 'server-a', bound: false, readOnly: true })] - }) - ) - - expect(container.textContent).toContain('· Lab box') - expect(container.textContent).not.toContain('· remote') - expect(container.querySelector('button[aria-label^="Kill session"]')).toBeNull() - }) - - it('keeps kill affordances on the orphan bucket rows', () => { - renderWorktreeRow( - makeWorktree({ - worktreeId: ORPHAN_WORKTREE_ID, - worktreeName: 'Orphaned terminals', - sessions: [makeSession({ sessionId: 'orphan-a', bound: false, tabId: null })] - }) - ) - - expect(container.querySelector('button[aria-label="Kill session orphan-a"]')).not.toBeNull() - }) - - it('shows browsers as read-only workspace resources', () => { - renderWorktreeRow( - makeWorktree({ - browsers: [ - { - id: 'browser-1', - worktreeId: 'wt-1', - title: 'Orca docs', - url: 'https://docs.orca.dev', - loading: false, - faviconUrl: null, - canGoBack: false, - canGoForward: false, - loadError: null, - createdAt: 1 - } as BrowserWorkspace - ] - }) - ) - - expect(container.textContent).toContain('Orca docs') - expect(container.querySelector('.lucide-globe')).not.toBeNull() - expect(container.querySelector('button[aria-label^="Open browser"]')).toBeNull() - }) -}) diff --git a/src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts b/src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts deleted file mode 100644 index 19f8199cb61..00000000000 --- a/src/renderer/src/components/task-page/task-page-task-source-host-availability.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it, afterEach, vi } from 'vitest' -import { TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' -import type { ExecutionHostRegistryEntry } from '../../../../shared/execution-host-registry' -import { - getGitHubProjectSourceSummary, - getTaskSourceHostAvailabilityForHost -} from './task-page-source-context' - -function hostEntry( - overrides: Partial = {} -): ExecutionHostRegistryEntry { - return { - id: 'local', - kind: 'local', - label: 'This Mac', - detail: 'local', - health: 'local', - ...overrides - } -} - -describe('getTaskSourceHostAvailabilityForHost', () => { - it('returns null when there is no host', () => { - expect(getTaskSourceHostAvailabilityForHost(null, 'local')).toBeNull() - }) - - it('reports a runtime still checking capabilities', () => { - const host = hostEntry({ - id: 'runtime:env-1', - kind: 'runtime', - label: 'Env', - health: 'available' - }) - expect(getTaskSourceHostAvailabilityForHost(host, 'runtime:env-1')).toEqual({ - hostId: 'runtime:env-1', - reason: 'checking-task-source-capability' - }) - }) - - it('reports a runtime missing the task-source capability', () => { - const host = hostEntry({ - id: 'runtime:env-1', - kind: 'runtime', - label: 'Env', - health: 'available', - capabilities: [] - }) - expect(getTaskSourceHostAvailabilityForHost(host, 'runtime:env-1')).toEqual({ - hostId: 'runtime:env-1', - reason: 'missing-task-source-capability' - }) - }) - - it('returns null for a healthy runtime that declares the capability', () => { - const host = hostEntry({ - id: 'runtime:env-1', - kind: 'runtime', - label: 'Env', - health: 'available', - capabilities: [TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY] - }) - expect(getTaskSourceHostAvailabilityForHost(host, 'runtime:env-1')).toBeNull() - }) - - it('returns null for a healthy local host', () => { - expect(getTaskSourceHostAvailabilityForHost(hostEntry(), 'local')).toBeNull() - }) - - it('passes through unhealthy host status', () => { - const host = hostEntry({ - id: 'ssh:box', - kind: 'ssh', - label: 'Box', - health: 'disconnected', - connectionStatus: 'disconnected', - capabilities: [TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY] - }) - expect(getTaskSourceHostAvailabilityForHost(host, 'ssh:box')).toEqual({ - hostId: 'ssh:box', - health: 'disconnected', - status: 'disconnected' - }) - }) -}) - -describe('getGitHubProjectSourceSummary', () => { - const runtimeHost = (overrides: Partial) => - hostEntry({ id: 'runtime:env-1', kind: 'runtime', label: 'Build server', ...overrides }) - - it('names the board host, which a capability check alone never marks unavailable', () => { - const summary = getGitHubProjectSourceSummary({ - providerLabel: 'GitHub', - hostId: 'runtime:env-1', - host: runtimeHost({ health: 'available' }), - hostLabelById: new Map([['runtime:env-1', 'Build server']]) - }) - expect(summary.label).toBe('GitHub · Build server · Current account') - }) - - it('says when the board host is unreachable', () => { - const summary = getGitHubProjectSourceSummary({ - providerLabel: 'GitHub', - hostId: 'runtime:env-1', - host: runtimeHost({ - health: 'disconnected', - capabilities: [TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY] - }), - hostLabelById: new Map([['runtime:env-1', 'Build server']]) - }) - expect(summary.label).toContain('Build server · ') - expect(summary.label).not.toBe('GitHub · Build server · Current account') - }) -}) - -const { rejectAppStoreAccess } = vi.hoisted(() => ({ - rejectAppStoreAccess: vi.fn(() => { - throw new Error('Pure helper tests must not access the app store') - }) -})) - -vi.mock('@/store', () => ({ - useAppStore: Object.assign(rejectAppStoreAccess, { - getState: rejectAppStoreAccess, - getInitialState: rejectAppStoreAccess, - setState: rejectAppStoreAccess, - subscribe: rejectAppStoreAccess - }) -})) - -afterEach(() => { - expect(rejectAppStoreAccess).not.toHaveBeenCalled() -})