From da8396395c8e7faa6db96bca35a77acd54ca548a Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 9 Nov 2026 09:04:22 -0800 Subject: [PATCH] test: remove low-value browser, hook and SSH cases (#40423) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/legacy-relay-compat-tags-baseline.txt | 1 - .../scripts/ci/sharding/ci-shard-timings.json | 3 - .../server-claude-helper-recovery.test.ts | 33 --- .../server-interrupt-inference-guards.test.ts | 222 --------------- .../server-hook-root-acquisition.test.ts | 33 --- .../presence/server-hook-root-replay.test.ts | 24 -- .../grab/browser-manager-grab-capture.test.ts | 99 ------- .../browser-manager-grab-selection.test.ts | 65 ----- .../browser-manager-auth-user-agent.test.ts | 254 ------------------ ...rowser-manager-load-failure-replay.test.ts | 54 ---- .../browser-manager-popup-child.test.ts | 207 +------------- .../browser-manager-destroyed-guest.test.ts | 51 ---- ...browser-manager-viewport-ownership.test.ts | 229 ---------------- .../ssh-multi-key-authentication.test.ts | 78 +----- .../sftp/sftp-stream-late-error.test.ts | 25 -- .../host-keys/ssh-known-hosts-source.test.ts | 21 +- .../ssh-host-server-on-connect.test.ts | 140 ---------- .../ssh-remote-node-resolution.test.ts | 194 ++----------- 18 files changed, 28 insertions(+), 1705 deletions(-) delete mode 100644 src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-capture.test.ts delete mode 100644 src/main/browser/guest-lifecycle/manager/navigation/browser-manager-auth-user-agent.test.ts delete mode 100644 src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-ownership.test.ts diff --git a/config/legacy-relay-compat-tags-baseline.txt b/config/legacy-relay-compat-tags-baseline.txt index c168b52ead7..3ed5dc0a23e 100644 --- a/config/legacy-relay-compat-tags-baseline.txt +++ b/config/legacy-relay-compat-tags-baseline.txt @@ -15,7 +15,6 @@ src/main/managed-server/migration/cutover/orcad-migration-delta-move.ts src/main/managed-server/migration/linking/orcad-migration-repository-link.test.ts src/main/ssh/migration-source/orcad-migration-terminal-gate.test.ts src/main/ssh/migration-source/orcad-migration-terminal-gate.ts -src/main/ssh/host-server/ssh-host-server-on-connect.test.ts src/main/ssh/host-server/ssh-host-server-on-connect.ts src/main/ssh/targets/ssh-target-orcad-preflight.ts src/preload/api-types.ts diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index f9e502b4944..da0472d6b16 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -927,12 +927,10 @@ "src/main/browser/identity/mode/browser-identity-mode-store.test.ts": 95, "src/main/browser/guest-lifecycle/prompts/browser-javascript-dialogs.test.ts": 54, "src/main/browser/guest-lifecycle/manager/viewport/browser-manager-annotation-bridge.test.ts": 603, - "src/main/browser/guest-lifecycle/manager/navigation/browser-manager-auth-user-agent.test.ts": 555, "src/main/browser/guest-lifecycle/manager/downloads/browser-manager-client-hosted-downloads.test.ts": 670, "src/main/browser/guest-lifecycle/manager/registration/browser-manager-destroyed-guest.test.ts": 633, "src/main/browser/guest-lifecycle/manager/devtools/browser-manager-devtools-retirement.test.ts": 551, "src/main/browser/guest-lifecycle/manager/downloads/browser-manager-downloads.test.ts": 592, - "src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-capture.test.ts": 516, "src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-mode.test.ts": 567, "src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-selection.test.ts": 584, "src/main/browser/guest-lifecycle/manager/browser-manager-guest-visibility.test.ts": 543, @@ -942,7 +940,6 @@ "src/main/browser/guest-lifecycle/manager/popups/browser-manager-popup-routing.test.ts": 575, "src/main/browser/guest-lifecycle/manager/viewport/browser-manager-tab-identity.test.ts": 619, "src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-override.test.ts": 597, - "src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-ownership.test.ts": 550, "src/main/browser/network/execution-routes/browser-network-deferred-socket.test.ts": 101, "src/main/browser/network/execution-routes/browser-network-execution-route.test.ts": 272, "src/main/browser/network/tunnels/browser-network-tunnel-client.test.ts": 127, diff --git a/src/main/agent-hooks/server/testing/interruptions/server-claude-helper-recovery.test.ts b/src/main/agent-hooks/server/testing/interruptions/server-claude-helper-recovery.test.ts index cada292132b..a72dcf5b80f 100644 --- a/src/main/agent-hooks/server/testing/interruptions/server-claude-helper-recovery.test.ts +++ b/src/main/agent-hooks/server/testing/interruptions/server-claude-helper-recovery.test.ts @@ -9,7 +9,6 @@ import { AgentHookServer } from '../../agent-hook-server' import { buildBody, PANE, postHookEvent } from '../server.test-fixtures' import { ClaudeHelperKillPoller } from '../../../../../shared/agent-hook-listener/providers/claude-helper-kill-poller' import '../../../../../shared/agent-hook-listener/providers/claude-transcript-decoder' -import * as cursor from '../../../../../shared/agent-hook-listener/providers/claude-transcript-cursor' import '../../../../../shared/agent-hook-listener/transcripts/agent-transcript-poll-scheduler' // Complete captured lifecycle records, with runtime replay seeded by real persisted HTTP state. @@ -369,38 +368,6 @@ it('a real failed bind parks without resources and explicit retry delivers its f expect(p.row()?.workingMode).toBe('monitoring') }) -it.each(['relay-spool', 'desktop-spool'])( - 'stop during %s recovery invalidates its parked owner', - async (kind) => { - const read = vi.spyOn(cursor, 'readClaudeTranscriptChunk') - const p = await setup(kind, 'accepted-before-bind', { - stopBeforeBind: true, - beforeStart: () => read.mockClear() - }) - expect(p.entries.size).toBe(0) - expect(read).not.toHaveBeenCalled() - expect(p.poller['scheduler']['entries'].size).toBe(0) - } -) - -it.each(['desktop-http', 'relay-http', 'desktop-spool', 'relay-spool'])( - 'refused %s admission creates no observation or wake', - async (kind) => { - const capture = vi.spyOn(ClaudeHelperKillPoller.prototype, 'schedule') - const p = await setup(kind, 'before-start', { - retired: true, - beforeStart: () => capture.mockClear() - }) - expect(p.entries.size).toBe(0) - expect(capture).not.toHaveBeenCalled() - if (kind === 'desktop-spool') { - expect(p.row()?.restoredUnconfirmed).toBe(true) - } else { - expect(p.row()).toBeUndefined() - } - } -) - it('a held local child permission cannot reset the admitted owner', async () => { const p = await setup('desktop-http', 'after-start') await p.post({ diff --git a/src/main/agent-hooks/server/testing/interruptions/server-interrupt-inference-guards.test.ts b/src/main/agent-hooks/server/testing/interruptions/server-interrupt-inference-guards.test.ts index 80d23e8188a..e69358a1ab7 100644 --- a/src/main/agent-hooks/server/testing/interruptions/server-interrupt-inference-guards.test.ts +++ b/src/main/agent-hooks/server/testing/interruptions/server-interrupt-inference-guards.test.ts @@ -27,105 +27,6 @@ afterEach(() => { }) describe('AgentHookServer listener replay', () => { - it('does not infer an interrupt while a subagent child is still working', () => { - vi.useFakeTimers() - vi.setSystemTime(1_000) - try { - const server = new AgentHookServer() - server.ingestRemote( - { - paneKey: PANE, - tabId: 'tab-1', - worktreeId: 'wt-1', - payload: { - state: 'working', - prompt: 'review loop', - agentType: 'claude', - // Child-driven working status cannot prove Ctrl+C ended the lead's turn. - subagents: [{ id: 'a1', state: 'working', startedAt: 900 }] - } - }, - 'conn-1' - ) - const baseline = server.getStatusSnapshot()[0] - - vi.setSystemTime(1_500) - const applied = server.inferInterrupt({ - paneKey: PANE, - baselineUpdatedAt: baseline.receivedAt, - baselineStateStartedAt: baseline.stateStartedAt, - baselinePrompt: 'review loop', - baselineAgentType: 'claude', - intent: 'ctrl-c' - }) - - expect(applied).toBe(false) - expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working' }) - } finally { - vi.useRealTimers() - } - }) - - it('does not infer an interrupt while Claude reports a background shell and the row carries no main agent fact', () => { - // Legacy hosts omit mainAgent, so working may mean an idle lead held open by live shell work. - vi.useFakeTimers() - vi.setSystemTime(1_000) - try { - const server = new AgentHookServer() - server.ingestRemote( - { - paneKey: PANE, - tabId: 'tab-1', - worktreeId: 'wt-1', - claudeRunningNonAgentTask: true, - payload: { state: 'working', prompt: 'run in background', agentType: 'claude' } - }, - 'conn-1' - ) - let baseline = server.getStatusSnapshot()[0] - - vi.setSystemTime(1_500) - expect( - server.inferInterrupt({ - paneKey: PANE, - baselineUpdatedAt: baseline.receivedAt, - baselineStateStartedAt: baseline.stateStartedAt, - baselinePrompt: 'run in background', - baselineAgentType: 'claude', - intent: 'ctrl-c' - }) - ).toBe(false) - expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working' }) - - vi.setSystemTime(2_000) - server.ingestRemote( - { - paneKey: PANE, - tabId: 'tab-1', - worktreeId: 'wt-1', - claudeRunningNonAgentTask: false, - payload: { state: 'working', prompt: 'run in background', agentType: 'claude' } - }, - 'conn-1' - ) - baseline = server.getStatusSnapshot()[0] - - vi.setSystemTime(2_500) - expect( - server.inferInterrupt({ - paneKey: PANE, - baselineUpdatedAt: baseline.receivedAt, - baselineStateStartedAt: baseline.stateStartedAt, - baselinePrompt: 'run in background', - baselineAgentType: 'claude', - intent: 'ctrl-c' - }) - ).toBe(true) - } finally { - vi.useRealTimers() - } - }) - it('infers a cancel of a live Claude main agent turn and keeps the shell it leaves running as monitoring', () => { vi.useFakeTimers() vi.setSystemTime(1_000) @@ -443,43 +344,6 @@ describe('AgentHookServer listener replay', () => { } }) - it('does not apply Claude background metadata from a rejected remote status', () => { - const server = new AgentHookServer() - server.ingestRemote( - { - paneKey: PANE, - hookEventName: 'PermissionRequest', - claudeRunningNonAgentTask: true, - payload: { - state: 'waiting', - prompt: 'approve shell', - agentType: 'claude', - toolName: 'Bash' - } - }, - 'conn-1' - ) - const waiting = server.getStatusSnapshot()[0] - - server.ingestRemote( - { - paneKey: PANE, - hookEventName: 'PreToolUse', - claudeRunningNonAgentTask: false, - payload: { - state: 'working', - prompt: 'approve shell', - agentType: 'claude', - toolName: 'OtherTool' - } - }, - 'conn-1' - ) - - expect(server.getStatusSnapshot()[0]).toEqual(waiting) - expect(server._getStateForTests().claudeRunningNonAgentTaskPaneKeys.has(PANE)).toBe(true) - }) - it('preserves an inferred interrupted row when OpenCode immediately reports SessionIdle', () => { vi.useFakeTimers() vi.setSystemTime(1_000) @@ -543,90 +407,4 @@ describe('AgentHookServer listener replay', () => { vi.useRealTimers() } }) - - it.each(['opencode', 'opencode2', 'copilot'] as const)( - 'rejects single plain Escape inference for %s', - (agentType) => { - vi.useFakeTimers() - vi.setSystemTime(1_000) - try { - const server = new AgentHookServer() - server.ingestRemote( - { - paneKey: PANE, - tabId: 'tab-1', - worktreeId: 'wt-1', - payload: { state: 'working', prompt: 'long task', agentType } - }, - 'conn-1' - ) - const baseline = server.getStatusSnapshot()[0] - - vi.setSystemTime(1_500) - const applied = server.inferInterrupt({ - paneKey: PANE, - baselineUpdatedAt: baseline.receivedAt, - baselineStateStartedAt: baseline.stateStartedAt, - baselinePrompt: 'long task', - baselineAgentType: agentType, - intent: 'plain-escape' - }) - - expect(applied).toBe(false) - expect(server.getStatusSnapshot()).toEqual([ - expect.objectContaining({ - state: 'working', - prompt: 'long task', - agentType - }) - ]) - } finally { - vi.useRealTimers() - } - } - ) - - it.each(['opencode', 'opencode2', 'copilot'] as const)( - 'accepts double plain Escape inference for %s', - (agentType) => { - vi.useFakeTimers() - vi.setSystemTime(1_000) - try { - const server = new AgentHookServer() - server.ingestRemote( - { - paneKey: PANE, - tabId: 'tab-1', - worktreeId: 'wt-1', - payload: { state: 'working', prompt: 'long task', agentType } - }, - 'conn-1' - ) - const baseline = server.getStatusSnapshot()[0] - - vi.setSystemTime(1_500) - const applied = server.inferInterrupt({ - paneKey: PANE, - baselineUpdatedAt: baseline.receivedAt, - baselineStateStartedAt: baseline.stateStartedAt, - baselinePrompt: 'long task', - baselineAgentType: agentType, - intent: 'plain-escape', - inputCount: 2 - }) - - expect(applied).toBe(true) - expect(server.getStatusSnapshot()).toEqual([ - expect.objectContaining({ - state: 'done', - prompt: 'long task', - agentType, - interrupted: true - }) - ]) - } finally { - vi.useRealTimers() - } - } - ) }) diff --git a/src/main/agent-hooks/server/testing/presence/server-hook-root-acquisition.test.ts b/src/main/agent-hooks/server/testing/presence/server-hook-root-acquisition.test.ts index 982b2de7ba6..89b9c569c5e 100644 --- a/src/main/agent-hooks/server/testing/presence/server-hook-root-acquisition.test.ts +++ b/src/main/agent-hooks/server/testing/presence/server-hook-root-acquisition.test.ts @@ -85,20 +85,6 @@ describe('a pane without a proven owner', () => { expect(attempts()).toBe(1) }) - it('acquires a Claude started through a shell script named claude', async () => { - const server = await createServer() - runningPane( - 49960, - [49960, 101, 49960, 'bash /Users/u/bin/claude', 'S+'], - [49969, 49960, 49960, 'node /opt/homebrew/bin/claude', 'S+'] - ) - observe(server, 'waiting') - await hook(server, 'SessionStart', { session: 'session-a' }) - await hook(server, 'UserPromptSubmit', { session: 'session-a', prompt: 'Wrapped task' }) - expect(server.row()).toMatchObject({ state: 'working', prompt: 'Wrapped task' }) - expect(server.presence()).toMatchObject({ process: capture(49969), session: 'session-a' }) - }) - it.each([['parent'], [undefined]])( 'keeps a detached nested run out of an unproven owner in session %s, through its exit', async (session) => { @@ -259,25 +245,6 @@ describe('terminal reports and Orca restatements', () => { expect(server.row()).toMatchObject({ agentType: 'codex', prompt: 'Next task' }) expect(server.presence()).toEqual({ agent: 'codex', session: 'cx' }) }) - - it('keeps a suspended owner through a terminal report', async () => { - const server = await createServer() - runningPane(49969, CLAUDE) - await hook(server, 'SessionStart', { session: 'session-a' }) - probe.mockImplementationOnce(async (_identity, _read, _platform, onSuspension) => { - onSuspension?.(true) - return 'unverifiable' - }) - await server.checkAgentPresence(PANE) - observe(server, 'working') - expect(server.presence()).toEqual({ - agent: 'claude', - process: capture(49969), - session: 'session-a', - sessionOwner: true, - suspended: true - }) - }) }) describe('a retired owner', () => { diff --git a/src/main/agent-hooks/server/testing/presence/server-hook-root-replay.test.ts b/src/main/agent-hooks/server/testing/presence/server-hook-root-replay.test.ts index ea3922d640d..e5d7592bf79 100644 --- a/src/main/agent-hooks/server/testing/presence/server-hook-root-replay.test.ts +++ b/src/main/agent-hooks/server/testing/presence/server-hook-root-replay.test.ts @@ -249,30 +249,6 @@ describe('startup spool replay of panes without a proven owner', () => { release() } ) - - it('answers past 64 unfinished reads as unknown, and reads that pane on its next turn', async () => { - const panes = [...Array.from({ length: 69 }, (_, index) => paneOf(index)), PANE] - const userDataPath = spoolDir( - panes.map((paneKey) => - spooled(paneKey, 49969, { hook_event_name: 'UserPromptSubmit', prompt: 'task' }) - ) - ) - runningPane(49969, CLAUDE) - const { release, requested } = rootAnswers(() => true) - const { server, started } = launchServer(userDataPath) - await requested - await vi.advanceTimersByTimeAsync(500) - await started - expect(attempts()).toBe(64) - vi.useRealTimers() - release() - await settled(64) - expect(server.presence(panes[0])?.fallback).toBeUndefined() - expect(server.presence()).toMatchObject({ process: capture(49969), fallback: true }) - await hook(server, 'UserPromptSubmit', { prompt: 'next' }) - expect(attempts()).toBe(65) - expect(server.presence()?.fallback).toBeUndefined() - }) }) describe('WSL relay startup spool replay', () => { diff --git a/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-capture.test.ts b/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-capture.test.ts deleted file mode 100644 index 6afd8aa4888..00000000000 --- a/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-capture.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { GRAB_BUDGET } from '../../../../../shared/browser/element-grab/browser-grab-types' - -const grabMocks = vi.hoisted(() => ({ - webContentsFromIdMock: vi.fn(), - guestOnMock: vi.fn(), - guestOffMock: vi.fn(), - guestSetBackgroundThrottlingMock: vi.fn(), - guestSetWindowOpenHandlerMock: vi.fn(), - guestExecuteJavaScriptMock: vi.fn(), - guestExecuteJavaScriptInIsolatedWorldMock: vi.fn(), - guestIsDestroyedMock: vi.fn(() => false), - guestGetZoomFactorMock: vi.fn(() => 1), - guestCapturePageMock: vi.fn(), - menuBuildFromTemplateMock: vi.fn(), - rendererSendMock: vi.fn(), - rendererIsDestroyedMock: vi.fn(() => false) -})) - -vi.mock('electron', () => ({ - clipboard: { writeText: vi.fn() }, - shell: { openExternal: vi.fn() }, - Menu: { buildFromTemplate: grabMocks.menuBuildFromTemplateMock }, - webContents: { fromId: grabMocks.webContentsFromIdMock } -})) - -import { browserManager } from '../browser-manager' -import { resetGrabTestEnvironment } from './browser-manager-grab-test-harness' - -const { guestCapturePageMock, guestExecuteJavaScriptMock } = grabMocks - -describe('browserManager grab operations', () => { - let guest: Electron.WebContents - - beforeEach(() => { - guest = resetGrabTestEnvironment(grabMocks) - }) - - describe('captureSelectionScreenshot', () => { - it('captures, crops, and converts screenshot dimensions back to CSS pixels', async () => { - const cropMock = vi.fn(() => ({ - toPNG: vi.fn(() => Buffer.from('png-data')) - })) - guestCapturePageMock.mockResolvedValue({ - isEmpty: vi.fn(() => false), - getSize: vi.fn(() => ({ width: 2000, height: 1000 })), - crop: cropMock - }) - guestExecuteJavaScriptMock.mockImplementation(async (script: string) => - script === 'window.innerWidth' ? 1000 : undefined - ) - - const screenshot = await browserManager.captureSelectionScreenshot( - 'tab-1', - { x: 10, y: 20, width: 100, height: 50 }, - guest - ) - - expect(cropMock).toHaveBeenCalledWith({ x: 20, y: 40, width: 200, height: 100 }) - expect(screenshot).toEqual({ - mimeType: 'image/png', - dataUrl: `data:image/png;base64,${Buffer.from('png-data').toString('base64')}`, - width: 100, - height: 50 - }) - expect(guestExecuteJavaScriptMock).toHaveBeenNthCalledWith( - 1, - expect.stringContaining('__orcaGrab') - ) - expect(guestExecuteJavaScriptMock).toHaveBeenNthCalledWith( - 2, - expect.stringContaining('__orcaGrab') - ) - expect(guestExecuteJavaScriptMock).toHaveBeenNthCalledWith(3, 'window.innerWidth') - }) - - it('omits screenshots that exceed the byte budget', async () => { - const oversizedBuffer = Buffer.alloc(GRAB_BUDGET.screenshotMaxBytes + 1) - guestCapturePageMock.mockResolvedValue({ - isEmpty: vi.fn(() => false), - getSize: vi.fn(() => ({ width: 1000, height: 500 })), - crop: vi.fn(() => ({ - toPNG: vi.fn(() => oversizedBuffer) - })) - }) - guestExecuteJavaScriptMock.mockImplementation(async (script: string) => - script === 'window.innerWidth' ? 1000 : undefined - ) - - const screenshot = await browserManager.captureSelectionScreenshot( - 'tab-1', - { x: 0, y: 0, width: 100, height: 50 }, - guest - ) - - expect(screenshot).toBeNull() - }) - }) -}) diff --git a/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-selection.test.ts b/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-selection.test.ts index f991c4bfb3b..3c2ead1142e 100644 --- a/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-selection.test.ts +++ b/src/main/browser/guest-lifecycle/manager/grab/browser-manager-grab-selection.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserGrabPayload } from '../../../../../shared/browser/element-grab/browser-grab-types' const grabMocks = vi.hoisted(() => ({ webContentsFromIdMock: vi.fn(), @@ -35,57 +34,6 @@ import { const { guestExecuteJavaScriptMock } = grabMocks -function makeValidGrabPayload(): BrowserGrabPayload { - return { - page: { - sanitizedUrl: 'https://example.com/', - title: 'Example', - viewportWidth: 1280, - viewportHeight: 720, - scrollX: 0, - scrollY: 0, - devicePixelRatio: 2, - capturedAt: '2026-04-10T00:00:00.000Z' - }, - target: { - tagName: 'button', - selector: 'button', - textSnippet: 'Click me', - htmlSnippet: '', - attributes: {}, - accessibility: { - role: 'button', - accessibleName: 'Click me', - ariaLabel: null, - ariaLabelledBy: null - }, - rectViewport: { x: 0, y: 0, width: 100, height: 40 }, - rectPage: { x: 0, y: 0, width: 100, height: 40 }, - computedStyles: { - display: 'block', - position: 'static', - width: '100px', - height: '40px', - margin: '0', - padding: '0', - color: '#000', - backgroundColor: '#fff', - border: 'none', - borderRadius: '0', - fontFamily: 'sans-serif', - fontSize: '14px', - fontWeight: '400', - lineHeight: '20px', - textAlign: 'left', - zIndex: 'auto' - } - }, - nearbyText: [], - ancestorPath: ['div', 'body'], - screenshot: null - } -} - describe('browserManager grab operations', () => { let guest: Electron.WebContents @@ -94,19 +42,6 @@ describe('browserManager grab operations', () => { }) describe('awaitGrabSelection', () => { - it('does not treat a valid payload message field as cancellation', async () => { - guestExecuteJavaScriptMock.mockResolvedValueOnce({ - ...makeValidGrabPayload(), - message: 'cancelled' - }) - - const result = await browserManager.awaitGrabSelection('tab-1', 'op-1', guest) - expect(result.kind).toBe('selected') - if (result.kind === 'selected') { - expect(result.payload.target.tagName).toBe('button') - } - }) - it('main-side clamp redacts secret-bearing attribute values', async () => { const mockPayload = { page: { diff --git a/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-auth-user-agent.test.ts b/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-auth-user-agent.test.ts deleted file mode 100644 index 5c533dbb08f..00000000000 --- a/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-auth-user-agent.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const browserMocks = vi.hoisted(() => ({ - appGetPathMock: vi.fn(() => '/downloads'), - shellOpenExternalMock: vi.fn(), - browserWindowFromWebContentsMock: vi.fn(), - menuBuildFromTemplateMock: vi.fn(), - guestOffMock: vi.fn(), - guestOnMock: vi.fn(), - guestSetBackgroundThrottlingMock: vi.fn(), - guestSetWindowOpenHandlerMock: vi.fn(), - guestOpenDevToolsMock: vi.fn(), - webContentsFromIdMock: vi.fn(), - screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })), - openPopupWithOriginBarMock: vi.fn(), - processUserAgentMode: 'clean', - processUserAgent: 'Mozilla/5.0 (Test) Chrome/140.0.0.0' -})) - -vi.mock('electron', () => ({ - app: { - getPath: browserMocks.appGetPathMock - }, - BrowserWindow: { - fromWebContents: browserMocks.browserWindowFromWebContentsMock - }, - clipboard: { writeText: vi.fn() }, - shell: { openExternal: browserMocks.shellOpenExternalMock }, - Menu: { - buildFromTemplate: browserMocks.menuBuildFromTemplateMock - }, - screen: { - getCursorScreenPoint: browserMocks.screenGetCursorScreenPointMock - }, - webContents: { - fromId: browserMocks.webContentsFromIdMock - } -})) - -vi.mock('../../popups/presentation/popup-origin-bar-window', () => ({ - openPopupWithOriginBar: browserMocks.openPopupWithOriginBarMock -})) - -vi.mock('../../../identity/browser-process-user-agent', () => ({ - getBrowserProcessUserAgentIdentity: () => ({ - mode: browserMocks.processUserAgentMode, - userAgent: browserMocks.processUserAgent, - electronUserAgent: - browserMocks.processUserAgentMode === 'native' - ? browserMocks.processUserAgent - : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/134.0.0.0 Electron/30.0.0 Safari/537.36' - }) -})) - -vi.mock('../../../identity/browser-tab-identity', async (importOriginal) => - ( - await import('../viewport/browser-manager-desktop-ua-metadata-test-fixture') - ).withPinnedDesktopUserAgentMetadata(await importOriginal()) -) - -import { browserManager } from '../browser-manager' -import { googleAuthUserAgent } from '../../../identity/browser-google-auth-ua' -import { - guestBaseUserAgent, - rendererWebContentsId, - resetBrowserManagerMocks, - resetBrowserManagerState -} from '../browser-manager-test-harness' -import { pinnedDesktopUserAgentMetadata as desktopMetadata } from '../viewport/browser-manager-desktop-ua-metadata-test-fixture' - -const { - guestOffMock, - guestOnMock, - guestSetBackgroundThrottlingMock, - guestSetWindowOpenHandlerMock, - guestOpenDevToolsMock, - webContentsFromIdMock -} = browserMocks - -describe('browserManager', () => { - beforeEach(() => { - resetBrowserManagerMocks(browserMocks) - resetBrowserManagerState() - browserMocks.processUserAgentMode = 'clean' - browserMocks.processUserAgent = guestBaseUserAgent - }) - - afterEach(() => { - vi.useRealTimers() - }) - - // setUserAgent during will-redirect can abort and replay the request, breaking sign-in POST flows. - it('retargets the auth-host UA over CDP during a redirect instead of cancelling it', async () => { - const debuggerHandlers = new Map void>() - let holdNextUserAgentOverride = false - let releaseHeldUserAgentOverride = (): void => {} - let rejectNextUserAgentOverride = false - const sendCommand = vi.fn((method: string) => { - if (method === 'Emulation.setUserAgentOverride' && holdNextUserAgentOverride) { - holdNextUserAgentOverride = false - return new Promise((resolve) => { - releaseHeldUserAgentOverride = resolve - }) - } - if (method === 'Emulation.setUserAgentOverride' && rejectNextUserAgentOverride) { - rejectNextUserAgentOverride = false - return Promise.reject(new Error('debugger detached')) - } - return Promise.resolve(undefined) - }) - const guest = { - id: 418, - isDestroyed: vi.fn(() => false), - isCurrentlyAudible: vi.fn(() => false), - send: vi.fn(), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'https://mail.google.com/'), - getUserAgent: vi.fn(() => guestBaseUserAgent), - setUserAgent: vi.fn(), - debugger: { - isAttached: vi.fn(() => true), - sendCommand, - on: vi.fn((event: string, handler: () => void) => debuggerHandlers.set(event, handler)), - off: vi.fn((event: string) => debuggerHandlers.delete(event)) - }, - session: { getUserAgent: vi.fn(() => guestBaseUserAgent) } - } - webContentsFromIdMock.mockReturnValue(guest) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'browser-redirect-ua', - webContentsId: guest.id, - rendererWebContentsId - }) - const willRedirect = guestOnMock.mock.calls.find( - ([event]) => event === 'will-redirect' - )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - sendCommand.mockClear() - - rejectNextUserAgentOverride = true - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - await expect(sendCommand.mock.results.at(-1)?.value).rejects.toThrow('debugger detached') - - // Failed writes must remain retryable. - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - await expect(sendCommand.mock.results.at(-1)?.value).resolves.toBeUndefined() - - expect(guest.setUserAgent).not.toHaveBeenCalled() - expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent(), - userAgentMetadata: desktopMetadata - }) - - // Detaching clears Chromium's CDP overrides and invalidates the confirmed identity. - sendCommand.mockClear() - debuggerHandlers.get('detach')?.() - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent(), - userAgentMetadata: desktopMetadata - }) - - // getUserAgent omits the CDP override; clear it on exit to restore UA and client hints. - sendCommand.mockClear() - const didStartNavigation = guestOnMock.mock.calls.find( - ([event]) => event === 'did-start-navigation' - )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - rejectNextUserAgentOverride = true - didStartNavigation(null, 'https://example.com/', false, true) - await expect(sendCommand.mock.results.at(-1)?.value).rejects.toThrow('debugger detached') - didStartNavigation(null, 'https://example.com/', false, true) - - expect(guest.setUserAgent).not.toHaveBeenCalled() - expect(sendCommand.mock.calls).toEqual([ - ['Emulation.setUserAgentOverride', { userAgent: '' }], - ['Emulation.setUserAgentOverride', { userAgent: '' }] - ]) - - // Newer confirmed writes outrank older pending writes. - debuggerHandlers.get('detach')?.() - holdNextUserAgentOverride = true - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - const olderHeldWrite = sendCommand.mock.results.at(-1)?.value as Promise - didStartNavigation(null, 'https://example.com/', false, true) - await expect(sendCommand.mock.results.at(-1)?.value).resolves.toBeUndefined() - - sendCommand.mockClear() - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { - userAgent: googleAuthUserAgent(), - userAgentMetadata: desktopMetadata - }) - releaseHeldUserAgentOverride() - await olderHeldWrite - - // A newer failure must preserve older pending writes. - debuggerHandlers.get('detach')?.() - holdNextUserAgentOverride = true - willRedirect( - { preventDefault: vi.fn() }, - 'https://accounts.google.com/v3/signin/identifier', - false, - true - ) - const heldWrite = sendCommand.mock.results.at(-1)?.value as Promise - rejectNextUserAgentOverride = true - didStartNavigation(null, 'https://example.com/', false, true) - await expect(sendCommand.mock.results.at(-1)?.value).rejects.toThrow('debugger detached') - releaseHeldUserAgentOverride() - await heldWrite - - sendCommand.mockClear() - didStartNavigation(null, 'https://example.com/', false, true) - expect(sendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', { userAgent: '' }) - browserManager.unregisterGuest('browser-redirect-ua') - }) - - // Prefer a stale UA to aborting the redirect when CDP is unavailable. - - // Direct navigation permits setUserAgent before request dispatch. - - // Keep the process UA override until direct navigation can replace the stale auth base UA. -}) diff --git a/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-load-failure-replay.test.ts b/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-load-failure-replay.test.ts index b18c3456761..cc171520ee3 100644 --- a/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-load-failure-replay.test.ts +++ b/src/main/browser/guest-lifecycle/manager/navigation/browser-manager-load-failure-replay.test.ts @@ -73,60 +73,6 @@ describe('browserManager', () => { vi.useRealTimers() }) - it('drops a queued failure when a replacement navigation starts before registration', () => { - const rendererSendMock = vi.fn() - const guest = { - id: 407, - isDestroyed: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - isCurrentlyAudible: vi.fn(() => false), - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'https://example.com/'), - ...guestUaMethods() - } - webContentsFromIdMock.mockImplementation((id: number) => - id === guest.id - ? guest - : id === rendererWebContentsId - ? { isDestroyed: vi.fn(() => false), send: rendererSendMock } - : null - ) - - browserManager.attachGuestPolicies(guest as never) - const didFailLoad = guestOnMock.mock.calls.find( - ([event]) => event === 'did-fail-load' - )?.[1] as ( - event: unknown, - errorCode: number, - errorDescription: string, - validatedUrl: string, - isMainFrame: boolean - ) => void - const didStartNavigation = guestOnMock.mock.calls.find( - ([event]) => event === 'did-start-navigation' - )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void - - didFailLoad(null, -202, 'Certificate authority invalid', 'https://localhost:3443/', true) - didStartNavigation(null, 'https://example.com/', false, true) - expect( - browserManager.registerGuest({ - browserPageId: 'browser-late-registration', - webContentsId: guest.id, - rendererWebContentsId - }) - ).toBe(true) - - expect(rendererSendMock).not.toHaveBeenCalledWith( - 'browser:guest-load-failed', - expect.anything() - ) - expect(browserManager.getBrowserPageLoadError('browser-late-registration')).toBeNull() - }) - it('restores a certificate failure when a retry navigation aborts before committing', () => { const guest = { id: 406, diff --git a/src/main/browser/guest-lifecycle/manager/popups/browser-manager-popup-child.test.ts b/src/main/browser/guest-lifecycle/manager/popups/browser-manager-popup-child.test.ts index ad6a10d0c9c..f8ca2dc42dc 100644 --- a/src/main/browser/guest-lifecycle/manager/popups/browser-manager-popup-child.test.ts +++ b/src/main/browser/guest-lifecycle/manager/popups/browser-manager-popup-child.test.ts @@ -32,21 +32,12 @@ vi.mock('../../popups/presentation/popup-origin-bar-window', () => ({ import { browserManager } from '../browser-manager' import { createDownloadItem, - getDownloadItemEventHandler, rendererWebContentsId, resetBrowserManagerMocks, resetBrowserManagerState } from '../browser-manager-test-harness' -const { - guestOffMock, - guestOnMock, - guestSetBackgroundThrottlingMock, - guestSetWindowOpenHandlerMock, - guestOpenDevToolsMock, - webContentsFromIdMock, - shellOpenExternalMock -} = browserMocks +const { webContentsFromIdMock } = browserMocks let didCreateWindow: (window: { webContents: unknown }) => void = () => {} const fakeGuest = (id: number) => ({ @@ -76,202 +67,6 @@ describe('browserManager popup child policies', () => { vi.useRealTimers() }) - it('attaches guest policies to created popup child windows', () => { - const rendererSendMock = vi.fn() - const childSetBackgroundThrottlingMock = vi.fn() - const childSetWindowOpenHandlerMock = vi.fn() - const childOnMock = vi.fn() - const childOffMock = vi.fn() - const childOpenDevToolsMock = vi.fn() - const childGuest = { - id: 4040, - isDestroyed: vi.fn(() => false), - isCurrentlyAudible: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: childSetBackgroundThrottlingMock, - setWindowOpenHandler: childSetWindowOpenHandlerMock, - on: childOnMock, - off: childOffMock, - openDevTools: childOpenDevToolsMock, - getURL: vi.fn(() => 'https://identity.example.com/login') - } - const guest = { - id: 404, - isDestroyed: vi.fn(() => false), - isCurrentlyAudible: vi.fn(() => false), - getType: vi.fn(() => 'webview'), - setBackgroundThrottling: guestSetBackgroundThrottlingMock, - setWindowOpenHandler: guestSetWindowOpenHandlerMock, - on: guestOnMock, - off: guestOffMock, - openDevTools: guestOpenDevToolsMock - } - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === rendererWebContentsId) { - return { isDestroyed: vi.fn(() => false), send: rendererSendMock } - } - return null - }) - - browserManager.attachGuestPolicies(guest as never) - browserManager.registerGuest({ - browserPageId: 'browser-1', - webContentsId: guest.id, - rendererWebContentsId - }) - - const didCreateWindowHandler = guestOnMock.mock.calls.find( - ([event]) => event === 'did-create-window' - )?.[1] as ((window: { webContents: typeof childGuest }) => void) | undefined - expect(didCreateWindowHandler).toBeTypeOf('function') - - didCreateWindowHandler?.({ webContents: childGuest }) - - expect(childSetBackgroundThrottlingMock).not.toHaveBeenCalled() - expect(childSetWindowOpenHandlerMock).toHaveBeenCalledTimes(1) - expect(childOnMock.mock.calls.filter(([event]) => event === 'did-create-window')).toHaveLength( - 1 - ) - expect(childOnMock.mock.calls.filter(([event]) => event === 'will-navigate')).toHaveLength(1) - expect(childOnMock.mock.calls.filter(([event]) => event === 'will-redirect')).toHaveLength(1) - - const childWindowOpenHandler = childSetWindowOpenHandlerMock.mock.calls[0][0] as (details: { - url: string - }) => { action: 'allow' | 'deny' } - expect(childWindowOpenHandler({ url: 'https://identity.example.com/login' })).toMatchObject({ - action: 'allow' - }) - expect(childWindowOpenHandler({ url: 'file:///etc/passwd' })).toEqual({ action: 'deny' }) - expect(rendererSendMock).toHaveBeenCalledWith('browser:popup', { - browserPageId: 'browser-1', - origin: 'null', - action: 'blocked' - }) - browserManager.notifyPermissionDenied({ - guestWebContentsId: childGuest.id, - permission: 'notifications', - rawUrl: 'https://identity.example.com/login' - }) - expect(rendererSendMock).toHaveBeenCalledWith('browser:permission-denied', { - browserPageId: 'browser-1', - permission: 'notifications', - origin: 'https://identity.example.com' - }) - - const childDidFailLoadHandler = childOnMock.mock.calls.find( - ([event]) => event === 'did-fail-load' - )?.[1] as - | (( - event: Electron.Event, - errorCode: number, - errorDescription: string, - validatedURL: string, - isMainFrame: boolean - ) => void) - | undefined - childDidFailLoadHandler?.( - {} as Electron.Event, - -105, - 'Name not resolved', - 'https://identity.example.com/unavailable', - true - ) - expect(rendererSendMock).not.toHaveBeenCalledWith( - 'browser:guest-load-failed', - expect.anything() - ) - - const childDownloadItem = createDownloadItem() - browserManager.handleGuestWillDownload({ - guestWebContentsId: childGuest.id, - item: childDownloadItem - }) - expect(rendererSendMock).toHaveBeenCalledWith( - 'browser:download-requested', - expect.objectContaining({ browserPageId: 'browser-1' }) - ) - const childDownloadDoneHandler = getDownloadItemEventHandler(childDownloadItem, 'once', 'done') - childDownloadDoneHandler?.({} as Electron.Event, 'completed') - - const managerState = browserManager as unknown as { - popupOwnerContextByGuestId: Map - } - expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(true) - - const cleanupChildOnMock = vi.fn() - const cleanupChildGuest = { - ...childGuest, - id: 4041, - on: cleanupChildOnMock, - off: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWindowOpenHandler: vi.fn() - } - const childDidCreateWindowHandler = childOnMock.mock.calls.find( - ([event]) => event === 'did-create-window' - )?.[1] as ((window: { webContents: typeof cleanupChildGuest }) => void) | undefined - childDidCreateWindowHandler?.({ webContents: cleanupChildGuest }) - expect(managerState.popupOwnerContextByGuestId.has(cleanupChildGuest.id)).toBe(true) - const cleanupChildWindowOpenHandler = cleanupChildGuest.setWindowOpenHandler.mock - .calls[0][0] as (details: { url: string }) => { action: 'allow' | 'deny' } - expect( - cleanupChildWindowOpenHandler({ url: 'https://identity.example.com/continue' }) - ).toMatchObject({ action: 'allow' }) - const cleanupChildDestroyedHandler = cleanupChildOnMock.mock.calls.find( - ([event]) => event === 'destroyed' - )?.[1] as (() => void) | undefined - cleanupChildDestroyedHandler?.() - expect(managerState.popupOwnerContextByGuestId.has(cleanupChildGuest.id)).toBe(false) - - const replacementGuest = { - ...guest, - id: 405, - on: vi.fn(), - off: vi.fn(), - setBackgroundThrottling: vi.fn(), - setWindowOpenHandler: vi.fn() - } - webContentsFromIdMock.mockImplementation((id: number) => { - if (id === guest.id) { - return guest - } - if (id === replacementGuest.id) { - return replacementGuest - } - if (id === rendererWebContentsId) { - return { isDestroyed: vi.fn(() => false), send: rendererSendMock } - } - return null - }) - browserManager.attachGuestPolicies(replacementGuest as never) - browserManager.registerGuest({ - browserPageId: 'browser-1', - webContentsId: replacementGuest.id, - rendererWebContentsId - }) - - expect(childWindowOpenHandler({ url: 'https://identity.example.com/next' })).toEqual({ - action: 'deny' - }) - expect(shellOpenExternalMock).toHaveBeenCalledWith('https://identity.example.com/next') - expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(false) - - const childDestroyedHandler = childOnMock.mock.calls.find( - ([event]) => event === 'destroyed' - )?.[1] as (() => void) | undefined - childDestroyedHandler?.() - expect(managerState.popupOwnerContextByGuestId.has(childGuest.id)).toBe(false) - - browserManager.unregisterAll() - - expect(childOffMock).toHaveBeenCalledWith('did-create-window', expect.any(Function)) - expect(childOffMock).toHaveBeenCalledWith('will-navigate', expect.any(Function)) - expect(childOffMock).toHaveBeenCalledWith('will-redirect', expect.any(Function)) - }) - it("saves a popup page's agent download to its path and refuses a second claim on the tab", async () => { const opener = fakeGuest(406) const popup = fakeGuest(4060) diff --git a/src/main/browser/guest-lifecycle/manager/registration/browser-manager-destroyed-guest.test.ts b/src/main/browser/guest-lifecycle/manager/registration/browser-manager-destroyed-guest.test.ts index 0d9fc8b44a5..06ec03a0215 100644 --- a/src/main/browser/guest-lifecycle/manager/registration/browser-manager-destroyed-guest.test.ts +++ b/src/main/browser/guest-lifecycle/manager/registration/browser-manager-destroyed-guest.test.ts @@ -67,21 +67,6 @@ describe('browser guest destruction ownership', () => { mocks.guests.clear() }) - it('releases registered callbacks and ownership after 1000 distinct guest destructions', () => { - for (let index = 0; index < 1000; index++) { - register(`retired-${index}`).destroy() - } - expectRetainedCount(0) - }) - - it('keeps explicit unregister before destruction idempotent', () => { - const guest = register('explicit') - manager.unregisterGuest('explicit') - guest.destroy() - manager.unregisterGuest('explicit') - expectRetainedCount(0) - }) - it('does not let a captured old destroyed callback retire a replacement guest', () => { const old = register('replacement') const [oldDestroyed] = old.listeners('destroyed') @@ -93,42 +78,6 @@ describe('browser guest destruction ownership', () => { expectRetainedCount(1) }) - it('cleans a popup without retiring its live primary page', () => { - const parent = register('popup-parent') - const popup = createGuest() - manager.attachGuestPolicies(popup.asWebContents(), { - rootGuestWebContentsId: parent.id, - browserTabId: 'popup-parent' - }) - popup.destroy() - expect(manager.getGuestWebContentsId('popup-parent')).toBe(parent.id) - expectRetainedCount(1) - }) - - it('cleans policies for a guest destroyed before registration', () => { - const guest = createGuest() - manager.attachGuestPolicies(guest.asWebContents()) - guest.destroy() - expectRetainedCount(0) - }) - - it('preserves live guest ownership when its renderer process needs reload recovery', () => { - const guest = register('renderer-recovery') - guest.emit('render-process-gone', {}, { reason: 'crashed', exitCode: 1 }) - expect(manager.getGuestWebContentsId('renderer-recovery')).toBe(guest.id) - expect(manager.getSessionProfileIdForTab('renderer-recovery')).toBe('profile-1') - expectRetainedCount(1) - }) - - it('rebuilds ownership when a restored page registers its fresh guest', () => { - register('fresh-owner').destroy() - const replacement = register('fresh-owner') - expect(manager.getGuestWebContentsId('fresh-owner')).toBe(replacement.id) - expect(manager.getWorktreeIdForTab('fresh-owner')).toBe('worktree-1') - expect(manager.getSessionProfileIdForTab('fresh-owner')).toBe('profile-1') - expectRetainedCount(1) - }) - it('preserves a sibling page using the same browser session profile', () => { const retiring = register('retiring') const sibling = register('sibling') diff --git a/src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-ownership.test.ts b/src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-ownership.test.ts deleted file mode 100644 index 60d21990759..00000000000 --- a/src/main/browser/guest-lifecycle/manager/viewport/browser-manager-viewport-ownership.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' - -const mocks = vi.hoisted(() => ({ - appGetPathMock: vi.fn(() => '/downloads'), - shellOpenExternalMock: vi.fn(), - browserWindowFromWebContentsMock: vi.fn(), - menuBuildFromTemplateMock: vi.fn(), - guestOffMock: vi.fn(), - guestOnMock: vi.fn(), - guestSetBackgroundThrottlingMock: vi.fn(), - guestSetWindowOpenHandlerMock: vi.fn(), - guestOpenDevToolsMock: vi.fn(), - webContentsFromIdMock: vi.fn(), - screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })), - openPopupWithOriginBarMock: vi.fn(), - processUserAgentMode: 'clean', - processUserAgent: '' -})) - -vi.mock('electron', () => ({ - app: { getPath: mocks.appGetPathMock }, - BrowserWindow: { fromWebContents: mocks.browserWindowFromWebContentsMock }, - clipboard: { writeText: vi.fn() }, - shell: { openExternal: mocks.shellOpenExternalMock }, - Menu: { buildFromTemplate: mocks.menuBuildFromTemplateMock }, - screen: { getCursorScreenPoint: mocks.screenGetCursorScreenPointMock }, - webContents: { fromId: mocks.webContentsFromIdMock } -})) -vi.mock('../../popups/presentation/popup-origin-bar-window', () => ({ - openPopupWithOriginBar: mocks.openPopupWithOriginBarMock -})) - -vi.mock('../../../identity/browser-process-user-agent', () => ({ - getBrowserProcessUserAgentIdentity: () => ({ - mode: mocks.processUserAgentMode, - userAgent: mocks.processUserAgent, - electronUserAgent: - mocks.processUserAgentMode === 'native' - ? mocks.processUserAgent - : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/134.0.0.0 Electron/30.0.0 Safari/537.36' - }) -})) - -import { browserManager } from '../browser-manager' -import { resetBrowserManagerMocks, resetBrowserManagerState } from '../browser-manager-test-harness' -import { - createViewportGuestFactory, - GUEST_CLEAN_UA -} from './browser-manager-viewport-test-fixtures' - -const makeGuest = createViewportGuestFactory(mocks) -const mobile = { width: 375, height: 667, deviceScaleFactor: 2, mobile: true } -const desktop = { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false } -const guests = new Map>() -const registeredGuests = readViewportStateMap('webContentsIdByTabId') -const presetIntents = readViewportStateMap('viewportPresetByTabId') -const pendingOperations = readViewportStateMap('viewportOpsByTabId') -const cdpUserAgentOverrides = readViewportStateMap('cdpUserAgentOverrideStateByGuestId') - -function readViewportStateMap( - name: - | 'webContentsIdByTabId' - | 'viewportPresetByTabId' - | 'viewportOpsByTabId' - | 'cdpUserAgentOverrideStateByGuestId' -): Map { - const value: unknown = browserManager[name] - if (!(value instanceof Map)) { - throw new Error(`Expected manager state map: ${name}`) - } - return value -} - -function register(tab: string, id: number) { - const handle = makeGuest(id) - guests.set(id, handle.guest) - expect( - browserManager.registerOffscreenGuest({ - browserPageId: tab, - webContentsId: id - }) - ).toBe(true) - return handle -} - -function pause(handle: ReturnType, method: string) { - const entered = Promise.withResolvers() - const gate = Promise.withResolvers() - let blocked = false - handle.debuggerSendCommand.mockImplementation((next) => { - if (!blocked && next === method) { - blocked = true - entered.resolve() - return gate.promise - } - return Promise.resolve() - }) - return { entered: entered.promise, ...gate } -} - -describe('browser viewport operation ownership', () => { - beforeEach(() => { - expect(process.env.ORCA_BACKGROUND_LAUNCH).toBe('1') - resetBrowserManagerMocks(mocks) - resetBrowserManagerState() - mocks.processUserAgentMode = 'clean' - mocks.processUserAgent = GUEST_CLEAN_UA - guests.clear() - mocks.webContentsFromIdMock.mockImplementation((id) => guests.get(id)) - }) - afterEach(() => { - browserManager.unregisterAll() - vi.restoreAllMocks() - }) - - it('does not recreate closed-tab UA intent after a late touch completion', async () => { - const handle = register('closed', 100) - const gate = pause(handle, 'Emulation.setTouchEmulationEnabled') - const result = browserManager.setViewportOverride('closed', mobile) - await gate.entered - browserManager.unregisterGuest('closed') - expect(presetIntents.size).toBe(0) - const isDestroyed = handle.guest.isDestroyed - expect(vi.isMockFunction(isDestroyed)).toBe(true) - if (vi.isMockFunction(isDestroyed)) { - isDestroyed.mockReturnValue(true) - } - gate.resolve() - await expect(result).resolves.toBe(false) - expect(registeredGuests.size).toBe(0) - expect(presetIntents.size).toBe(0) - expect(cdpUserAgentOverrides.size).toBe(0) - }) - - it('preserves replacement desktop intent after an old clear fails', async () => { - const handle = register('replacement', 102) - await browserManager.setViewportOverride('replacement', mobile) - const gate = pause(handle, 'Emulation.setUserAgentOverride') - const result = browserManager.setViewportOverride('replacement', null) - await gate.entered - browserManager.unregisterGuest('replacement') - register('replacement', 103) - await expect(browserManager.setViewportOverride('replacement', desktop)).resolves.toBe(true) - gate.reject(new Error('Old target closed')) - await expect(result).resolves.toBe(false) - expect(registeredGuests.get('replacement')).toBe(103) - expect(presetIntents.get('replacement')).toEqual({ - guestWebContentsId: 103, - requested: desktop, - applied: desktop - }) - }) - - // Requested and applied metrics may clear while the separate UA override remains tracked. - it('same-owner clear failure keeps the standing mobile override tracked', async () => { - const handle = register('same-owner', 106) - await browserManager.setViewportOverride('same-owner', mobile) - const gate = pause(handle, 'Emulation.setUserAgentOverride') - const result = browserManager.setViewportOverride('same-owner', null) - await gate.entered - gate.reject(new Error('Protocol error')) - await expect(result).resolves.toBe(false) - expect(presetIntents.get('same-owner')).toEqual({ - guestWebContentsId: 106, - requested: null, - applied: null - }) - expect(handle.presentedUserAgent()).toContain('iPhone') - expect(cdpUserAgentOverrides.has(106)).toBe(true) - }) - - it('old queued operations cannot remove or join a replacement promise tail', async () => { - const old = register('queued-replacement', 114) - const oldGate = pause(old, 'Emulation.setTouchEmulationEnabled') - const first = browserManager.setViewportOverride('queued-replacement', mobile) - const second = browserManager.setViewportOverride('queued-replacement', null) - await oldGate.entered - browserManager.unregisterGuest('queued-replacement') - const replacement = register('queued-replacement', 115) - const newGate = pause(replacement, 'Emulation.setTouchEmulationEnabled') - const replacementFirst = browserManager.setViewportOverride('queued-replacement', desktop) - const replacementSecond = browserManager.setViewportOverride('queued-replacement', mobile) - await newGate.entered - const tail = pendingOperations.get('queued-replacement') - oldGate.resolve() - await expect(first).resolves.toBe(false) - await expect(second).resolves.toBe(false) - expect(pendingOperations.get('queued-replacement')).toBe(tail) - newGate.resolve() - await expect(replacementFirst).resolves.toBe(true) - await expect(replacementSecond).resolves.toBe(true) - expect(pendingOperations.size).toBe(0) - expect(presetIntents.get('queued-replacement')).toEqual({ - guestWebContentsId: 115, - requested: mobile, - applied: mobile - }) - expect(replacement.presentedUserAgent()).toContain('iPhone') - }) - - it('normal same-owner toggles preserve last-requested order and remove the promise tail', async () => { - const handle = register('serialized', 116) - const gate = pause(handle, 'Emulation.setTouchEmulationEnabled') - const first = browserManager.setViewportOverride('serialized', mobile) - await gate.entered - const second = browserManager.setViewportOverride('serialized', desktop) - const third = browserManager.setViewportOverride('serialized', null) - gate.resolve() - expect(await Promise.all([first, second, third])).toEqual([true, true, true]) - // Clear mobile identity when desktop metrics replace the mobile viewport. - expect(handle.debuggerSendCommand.mock.calls.map(([method]) => method)).toEqual([ - 'Emulation.setDeviceMetricsOverride', - 'Emulation.setTouchEmulationEnabled', - 'Emulation.setUserAgentOverride', - 'Emulation.setDeviceMetricsOverride', - 'Emulation.setTouchEmulationEnabled', - 'Emulation.setUserAgentOverride', - 'Emulation.clearDeviceMetricsOverride', - 'Emulation.setTouchEmulationEnabled' - ]) - expect(handle.presentedUserAgent()).toBe(GUEST_CLEAN_UA) - expect(pendingOperations.size).toBe(0) - expect(presetIntents.get('serialized')).toEqual({ - guestWebContentsId: 116, - requested: null, - applied: null - }) - }) -}) diff --git a/src/main/ssh/authentication/ssh-multi-key-authentication.test.ts b/src/main/ssh/authentication/ssh-multi-key-authentication.test.ts index d4c8289439d..ab4b2e4de05 100644 --- a/src/main/ssh/authentication/ssh-multi-key-authentication.test.ts +++ b/src/main/ssh/authentication/ssh-multi-key-authentication.test.ts @@ -5,8 +5,7 @@ import { type AnyAuthMethod, type AuthenticationType, type AuthHandlerMiddleware, - type ConnectConfig, - type ParsedKey + type ConnectConfig } from 'ssh2' vi.mock('os', () => ({ @@ -31,7 +30,6 @@ vi.mock('./ssh-identity-file-read', () => ({ })) import { buildConnectConfig } from '../connection/ssh-connect-config' -import { getPassphrasePrivateKeyPath } from './ssh-private-key-authentication' import '../system-transport/system-ssh-args' import { walkInitialAuthLadder } from '../connection/testing/ssh-connection-test-fixtures' import type { SshTarget } from '../../../shared/ssh-types' @@ -144,33 +142,6 @@ describe('ordered SSH private-key authentication', () => { expect(nextAuth(config, false)).toBe('keyboard-interactive') }) - it('keeps explicit manual keys and unresolved imported keys as singular overrides', () => { - const manual = buildConnectConfig( - makeTarget({ - source: 'manual', - configHost: 'manual.example.com', - host: 'manual.example.com', - identityFile: '/keys/manual' - }), - makeResolved(), - { includeAgent: false, includePrivateKey: true } - ) - const unresolvedImport = buildConnectConfig(makeTarget(), null, { - includeAgent: false, - includePrivateKey: true - }) - - expect(manual.privateKey).toEqual(Buffer.from('/keys/manual')) - expect(unresolvedImport.privateKey).toEqual(Buffer.from('/keys/stale-imported')) - // Single-key targets also need per-stage keyboard-interactive restart for MFA. - expect(nextAuth(manual, true)).toMatchObject({ type: 'none' }) - expect(nextAuth(manual, false)).toMatchObject({ - type: 'publickey', - key: Buffer.from('/keys/manual') - }) - expect(nextAuth(manual, false)).toBe('keyboard-interactive') - }) - it('keeps the password challenge on an agent attempt that carries a key, because no key retry follows it', () => { vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock') // Real key bytes, so '/keys/explicit' counts as unencrypted and IdentitiesOnly builds its agent. @@ -216,35 +187,11 @@ describe('ordered SSH private-key authentication', () => { expect(partialSuccessAuth(config, ['keyboard-interactive'])).toBe(false) }) - it('offers every resolved key for a manually owned config-picker target', () => { - const config = buildConnectConfig( - makeTarget({ - source: 'manual', - configHost: 'prod', - host: 'prod.internal', - identityFile: undefined - }), - makeResolved(), - { includeAgent: false, includePrivateKey: true } - ) - - expect(nextAuth(config, true)).toMatchObject({ type: 'none' }) - expect(nextAuth(config, false)).toMatchObject({ - type: 'publickey', - key: Buffer.from('/keys/unauthorized-first') - }) - expect(nextAuth(config, false)).toMatchObject({ - type: 'publickey', - key: Buffer.from('/keys/authorized-second') - }) - }) - - it('resets ordered authentication for credential retries without extra key reads', () => { + it('resets ordered authentication for credential retries', () => { const config = buildConnectConfig(makeTarget(), makeResolved(), { includeAgent: false, includePrivateKey: true }) - const readsAfterResolution = mockReadFileSync.mock.calls.length expect(nextAuth(config, true)).toMatchObject({ type: 'none' }) config.password = 'retry-password' @@ -257,26 +204,5 @@ describe('ordered SSH private-key authentication', () => { type: 'publickey', key: Buffer.from('/keys/unauthorized-first') }) - expect(mockReadFileSync).toHaveBeenCalledTimes(readsAfterResolution) - }) - - it('keeps encrypted-key prompts tied to the matching fresh identity path', () => { - vi.spyOn(utils, 'parseKey').mockImplementation((key) => { - if ( - Buffer.from(key as Buffer) - .toString() - .includes('encrypted-second') - ) { - return new Error('Encrypted private OpenSSH key detected, but no passphrase given') - } - return { isPrivateKey: () => true } as ParsedKey - }) - const config = buildConnectConfig( - makeTarget(), - makeResolved({ identityFile: ['/keys/first', '/keys/encrypted-second'] }), - { includeAgent: false, includePrivateKey: true } - ) - - expect(getPassphrasePrivateKeyPath(config)).toBe('/keys/encrypted-second') }) }) diff --git a/src/main/ssh/file-transfer/sftp/sftp-stream-late-error.test.ts b/src/main/ssh/file-transfer/sftp/sftp-stream-late-error.test.ts index 60ab686b110..209a6d575f3 100644 --- a/src/main/ssh/file-transfer/sftp/sftp-stream-late-error.test.ts +++ b/src/main/ssh/file-transfer/sftp/sftp-stream-late-error.test.ts @@ -7,9 +7,6 @@ import type { SFTPWrapper } from 'ssh2' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { uploadBuffer, writeStringViaSftp } from './sftp-upload' import { uploadFile } from './sftp-file-upload' -import { writeRemoteInstallFile } from '../../../managed-server/install/remote-install-transfers' -import type { SshConnection } from '../../connection/ssh-connection' -import { getRemoteHostPlatform } from '../../remote-host/ssh-remote-platform' function sftpNoSuchFileError(): Error { return Object.assign(new Error('file does not exist'), { code: 2 }) @@ -78,25 +75,3 @@ describe('late SFTP stream errors', () => { ) }) }) - -describe('sandboxed SFTP subsystem diagnosis', () => { - it('leaves a permission refusal as itself rather than blaming a chroot', async () => { - // Permission denial names visible paths; mislabeling it as sandboxing sends users to fix ProxyJump for chmod. - const conn = { - writeFile: () => Promise.reject(Object.assign(new Error('permission denied'), { code: 3 })) - } as unknown as SshConnection - - const failure: unknown = await writeRemoteInstallFile( - conn, - getRemoteHostPlatform('linux-x64'), - '/home/user/.orca-remote/relay-1/.version', - 'v1' - ).then( - () => null, - (err: unknown) => err - ) - - expect((failure as Error).message).toBe('permission denied') - expect(failure).not.toHaveProperty('sandboxedSftpNamespace') - }) -}) diff --git a/src/main/ssh/host-keys/ssh-known-hosts-source.test.ts b/src/main/ssh/host-keys/ssh-known-hosts-source.test.ts index 176ed1feae4..48223e2627a 100644 --- a/src/main/ssh/host-keys/ssh-known-hosts-source.test.ts +++ b/src/main/ssh/host-keys/ssh-known-hosts-source.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import '../config/ssh-g-config-resolution' @@ -62,16 +62,6 @@ describe('the union of known_hosts entries', () => { expect(verdict(entries, 'shared.example', ED_C)).toBe('mismatch') }) - it('skips a missing file and keeps the rest', async () => { - const root = await createRoot() - const present = join(root, 'known_hosts') - await writeFile(present, hostLine('alpha.example', ED_A)) - - const entries = await loadKnownHostsEntries([join(root, 'absent'), present]) - - expect(verdict(entries, 'alpha.example', ED_A)).toBe('match') - }) - it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)( 'skips an unreadable file and keeps the rest', async () => { @@ -118,15 +108,6 @@ describe('loadKnownHostsEvidence', () => { expect(evidence.unreadableFileCount).toBe(1) } ) - - it('counts a path that is a directory', async () => { - const root = await createRoot() - await mkdir(join(root, 'a_directory')) - - const evidence = await loadKnownHostsEvidence([join(root, 'a_directory')]) - - expect(evidence.unreadableFileCount).toBe(1) - }) }) /** OpenSSH10.2p1 prints spaced trust paths unquoted; splitting can hide existing changed-key evidence as absent files. */ diff --git a/src/main/ssh/host-server/ssh-host-server-on-connect.test.ts b/src/main/ssh/host-server/ssh-host-server-on-connect.test.ts index 1010b5b15dd..d10c4eed181 100644 --- a/src/main/ssh/host-server/ssh-host-server-on-connect.test.ts +++ b/src/main/ssh/host-server/ssh-host-server-on-connect.test.ts @@ -31,76 +31,6 @@ describe('which server an SSH host runs on connect', () => { expect(d.autoUpdate).not.toHaveBeenCalled() }) - it('checks for an update right after pairing with a server another version runs', async () => { - const d = deps({ - isEmptyHost: () => true, - deploy: vi.fn(async () => ({ - outcome: 'already-current' as const, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the orchestrator reads only the environment id. - environment: { id: 'env-1' } as never, - activeVersion: '0.9' - })), - autoUpdate: vi.fn(async () => ({ - outcome: 'deferred' as const, - code: 'orcad_update_terminals_running', - reason: '3 terminals are running' - })) - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'managed', - environmentId: 'env-1', - update: { state: 'deferred', detail: '3 terminals are running' } - }) - expect(d.autoUpdate).toHaveBeenCalledWith('env-1', expect.anything()) - expect(d.abandonDeploy).not.toHaveBeenCalled() - }) - - // COMPAT(legacy-relay-bridge): the census only reports; converting leaves relay terminals running. - it('converts anyway when the bridge takes the live or unproven relay terminals', async () => { - for (const verdict of ['live', 'unverifiable'] as const) { - const proof = { verdict, count: 2, bridged: true as const } - const d = deps({ relayTerminals: async () => proof }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toMatchObject({ - route: 'managed' - }) - expect(d.convert).toHaveBeenCalledWith(target, proof) - } - }) - - it('surfaces a refusal for any other reason, naming it', async () => { - const d = deps({ - convert: async () => ({ - outcome: 'refused', - verdict: 'live', - code: 'orcad_migration_preflight_blocked', - reason: 'An automation still runs on this host.' - }) - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'unserved', - reason: 'refused', - detail: 'An automation still runs on this host.' - }) - }) - - it('offers "Move anyway" when only saved state refuses', async () => { - const d = deps({ - convert: async () => ({ - outcome: 'refused', - verdict: 'live', - code: 'orcad_migration_preflight_blocked', - reason: 'This SSH host cannot move yet: saved tabs and panes (2).', - canLeaveBehind: true - }) - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'unserved', - reason: 'refused', - detail: 'This SSH host cannot move yet: saved tabs and panes (2).', - canLeaveBehind: true - }) - }) - it('falls back to the relay ladder for good when orcad cannot run, and remembers why', async () => { const d = deps({ isEmptyHost: () => true, @@ -168,32 +98,6 @@ describe('which server an SSH host runs on connect', () => { warn.mockRestore() }) - it('touches no host when this build carries no orcad template', async () => { - const d = deps({ hasTemplate: () => false, isEmptyHost: () => true }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'unserved', - reason: 'orcad_unavailable', - detail: 'artifacts_unavailable' - }) - expect(d.deploy).not.toHaveBeenCalled() - }) - - it('retries a transient failure on the next connect without recording it', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const d = deps({ - convert: async () => { - throw new Error('Connection lost') - } - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'unserved', - reason: 'failed', - detail: 'Connection lost' - }) - expect(d.recordUnavailable).not.toHaveBeenCalled() - warn.mockRestore() - }) - it('names a conflict the server will keep refusing, and the way out, instead of a retry', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const d = deps({ @@ -241,19 +145,6 @@ describe('which server an SSH host runs on connect', () => { warn.mockRestore() }) - it('keeps a staged claim of another desktop’s move a retry, since it expires', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const d = deps({ - convert: async () => { - throw new Error('orcad_migration_staged_claim_conflict:repository-path:/root/migrepo') - } - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toMatchObject({ - reason: 'failed' - }) - warn.mockRestore() - }) - it('records why when even the stdio bridge cannot reach the deployed server', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const d = deps({ @@ -341,36 +232,5 @@ describe('which server an SSH host runs on connect', () => { }) expect(order).toEqual(['start', 'update']) }) - - it('keeps the old version serving while terminals run, and retries on a later connect', async () => { - const reason = '2 terminals are running on this host.' - const d = managed({ - autoUpdate: async () => ({ - outcome: 'deferred', - code: 'orcad_update_terminals_running', - reason - }) - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'managed', - environmentId: 'env-9', - update: { state: 'deferred', detail: reason } - }) - expect(d.recordUpdateFailure).not.toHaveBeenCalled() - }) - - it('still connects when the update check itself throws', async () => { - const d = managed({ - autoUpdate: async () => { - throw new Error('ssh dropped') - } - }) - await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'managed', - environmentId: 'env-9', - update: { state: 'check-failed', detail: 'ssh dropped' } - }) - expect(d.recordUpdateFailure).not.toHaveBeenCalled() - }) }) }) diff --git a/src/main/ssh/remote-host/node-toolchain/ssh-remote-node-resolution.test.ts b/src/main/ssh/remote-host/node-toolchain/ssh-remote-node-resolution.test.ts index d8e23894f67..37ccfbeb69c 100644 --- a/src/main/ssh/remote-host/node-toolchain/ssh-remote-node-resolution.test.ts +++ b/src/main/ssh/remote-host/node-toolchain/ssh-remote-node-resolution.test.ts @@ -38,14 +38,6 @@ function loginShellProbe(shell: string, mode: string) { ) } -function decodePowerShellCommand(command: string): string { - const match = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)/) - if (!match) { - throw new Error(`No encoded PowerShell command found in: ${command}`) - } - return Buffer.from(match[1]!, 'base64').toString('utf16le') -} - describe('resolveRemoteNodePath', () => { beforeEach(() => { execCommandMock.mockReset() @@ -60,46 +52,36 @@ describe('resolveRemoteNodePath', () => { await expect(resolveRemoteNodePath(conn)).resolves.toBe( '/home/u/.nvm/versions/node/v22.22.0/bin/node' ) - - expect(execCommandMock.mock.calls[1]![1]).toContain("PATH='/usr/bin':$PATH npm --version") - expect(execCommandMock.mock.calls[2]![1]).toContain( - "PATH='/home/u/.nvm/versions/node/v22.22.0/bin':$PATH npm --version" - ) }) - it.runIf(process.platform !== 'win32')( - 'accepts npm elsewhere on PATH without probing another Node candidate', - async () => { - const root = mkdtempSync(path.join(os.tmpdir(), 'orca-split-node-npm-')) - try { - const nodePath = path.join(root, 'selected node', 'bin', 'node') - const npmBinDir = path.join(root, 'npm elsewhere', 'bin') - mkdirSync(path.dirname(nodePath), { recursive: true }) - mkdirSync(npmBinDir, { recursive: true }) - writeFileSync(nodePath, '#!/bin/sh\nprintf "v22.22.0\\n"\n') - writeFileSync(path.join(npmBinDir, 'npm'), '#!/bin/sh\nprintf "11.13.0\\n"\n') - chmodSync(nodePath, 0o755) - chmodSync(path.join(npmBinDir, 'npm'), 0o755) + it.runIf(process.platform !== 'win32')('accepts npm elsewhere on PATH', async () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'orca-split-node-npm-')) + try { + const nodePath = path.join(root, 'selected node', 'bin', 'node') + const npmBinDir = path.join(root, 'npm elsewhere', 'bin') + mkdirSync(path.dirname(nodePath), { recursive: true }) + mkdirSync(npmBinDir, { recursive: true }) + writeFileSync(nodePath, '#!/bin/sh\nprintf "v22.22.0\\n"\n') + writeFileSync(path.join(npmBinDir, 'npm'), '#!/bin/sh\nprintf "11.13.0\\n"\n') + chmodSync(nodePath, 0o755) + chmodSync(path.join(npmBinDir, 'npm'), 0o755) - execCommandMock - .mockResolvedValueOnce(`${nodePath}\n${path.join(root, 'fallback', 'bin', 'node')}\n`) - .mockImplementationOnce((_conn: SshConnection, command: string) => - Promise.resolve( - execFileSync('/bin/sh', ['-c', command], { - encoding: 'utf8', - env: { HOME: root, PATH: npmBinDir } - }) - ) + execCommandMock + .mockResolvedValueOnce(`${nodePath}\n${path.join(root, 'fallback', 'bin', 'node')}\n`) + .mockImplementationOnce((_conn: SshConnection, command: string) => + Promise.resolve( + execFileSync('/bin/sh', ['-c', command], { + encoding: 'utf8', + env: { HOME: root, PATH: npmBinDir } + }) ) + ) - await expect(resolveRemoteNodePath(conn)).resolves.toBe(nodePath) - // One inventory exec plus one candidate probe keeps SSH startup work bounded. - expect(execCommandMock).toHaveBeenCalledTimes(2) - } finally { - rmSync(root, { recursive: true, force: true }) - } + await expect(resolveRemoteNodePath(conn)).resolves.toBe(nodePath) + } finally { + rmSync(root, { recursive: true, force: true }) } - ) + }) it('finds node under a MISE_DATA_DIR exported from a shell dotfile', async () => { execCommandMock @@ -290,14 +272,6 @@ describe('resolveRemoteNodePath', () => { ) }) - it('accepts Node 18 (the exact minimum) as valid', async () => { - execCommandMock - .mockResolvedValueOnce('/usr/local/bin/node\n') - .mockResolvedValueOnce('v18.0.0\n') - - await expect(resolveRemoteNodePath(conn)).resolves.toBe('/usr/local/bin/node') - }) - it('falls back to the login shell when path probes find nothing', async () => { execCommandMock .mockResolvedValueOnce('\n') @@ -328,37 +302,6 @@ describe('resolveRemoteNodePath', () => { ) }) - it('respects a non-default $SHELL instead of hardcoding bash', async () => { - execCommandMock - .mockResolvedValueOnce('\n') - .mockResolvedValueOnce('/usr/bin/fish') - .mockImplementationOnce(loginShellAnswer('/opt/homebrew/bin/node\n')) - .mockResolvedValueOnce('v22.0.0\n') - - await resolveRemoteNodePath(conn) - - expect(execCommandMock).toHaveBeenNthCalledWith( - 3, - conn, - loginShellProbe('/usr/bin/fish', '-lc'), - { wrapCommand: false, timeoutMs: 8_000 } - ) - }) - - it('uses /bin/sh when the remote shell expansion falls back to it', async () => { - execCommandMock - .mockResolvedValueOnce('\n') - .mockResolvedValueOnce('/bin/sh\n') - .mockImplementationOnce(loginShellAnswer('/usr/local/bin/node\n')) - .mockResolvedValueOnce('v20.0.0\n') - - await expect(resolveRemoteNodePath(conn)).resolves.toBe('/usr/local/bin/node') - expect(execCommandMock).toHaveBeenNthCalledWith(3, conn, loginShellProbe('/bin/sh', '-c'), { - wrapCommand: false, - timeoutMs: 8_000 - }) - }) - it('uses -c (not -lc) for a csh login shell, which rejects combined -lc', async () => { // csh/tcsh require plain -c; -lc/-l can fail with Bad modifier (#8701). execCommandMock @@ -374,25 +317,6 @@ describe('resolveRemoteNodePath', () => { }) }) - it('uses -c (not -lc) for a tcsh login shell', async () => { - execCommandMock - .mockResolvedValueOnce('\n') - .mockResolvedValueOnce('/usr/bin/tcsh\n') - .mockImplementationOnce(loginShellAnswer('/usr/local/bin/node\n')) - .mockResolvedValueOnce('v20.0.0\n') - - await expect(resolveRemoteNodePath(conn)).resolves.toBe('/usr/local/bin/node') - expect(execCommandMock).toHaveBeenNthCalledWith( - 3, - conn, - loginShellProbe('/usr/bin/tcsh', '-c'), - { - wrapCommand: false, - timeoutMs: 8_000 - } - ) - }) - it('keeps the default path-probe fallback for SSH session-limit-shaped errors', async () => { const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 4 @@ -416,73 +340,6 @@ describe('resolveRemoteNodePath', () => { ).rejects.toBe(sessionLimitError) }) - it('rethrows SSH session-limit errors from candidate version checks in strict mode', async () => { - const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { - reason: 4 - }) - execCommandMock - .mockResolvedValueOnce('/usr/local/bin/node\n') - .mockRejectedValueOnce(sessionLimitError) - - await expect( - resolveRemoteNodePath(conn, undefined, { rethrowSessionLimitErrors: true }) - ).rejects.toBe(sessionLimitError) - }) - - it('rethrows SSH session-limit errors from login-shell resolution in strict mode', async () => { - const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { - reason: 4 - }) - execCommandMock.mockResolvedValueOnce('\n').mockRejectedValueOnce(sessionLimitError) - - await expect( - resolveRemoteNodePath(conn, undefined, { rethrowSessionLimitErrors: true }) - ).rejects.toBe(sessionLimitError) - }) - - it('rethrows SSH session-limit errors from package-manager hint probing in strict mode', async () => { - const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { - reason: 4 - }) - execCommandMock - .mockResolvedValueOnce('\n') - .mockResolvedValueOnce('/bin/bash') - .mockResolvedValueOnce('\n') - .mockRejectedValueOnce(sessionLimitError) - - await expect( - resolveRemoteNodePath(conn, undefined, { rethrowSessionLimitErrors: true }) - ).rejects.toBe(sessionLimitError) - }) - - it('rethrows SSH session-limit errors from Windows node resolution in strict mode', async () => { - const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { - reason: 4 - }) - execCommandMock.mockRejectedValueOnce(sessionLimitError) - - await expect( - resolveRemoteNodePath(conn, getRemoteHostPlatform('win32-x64'), { - rethrowSessionLimitErrors: true - }) - ).rejects.toBe(sessionLimitError) - }) - - it('rethrows SSH session-limit errors from Windows version checks in strict mode', async () => { - const sessionLimitError = Object.assign(new Error('(SSH) Channel open failure: open failed'), { - reason: 4 - }) - execCommandMock - .mockResolvedValueOnce('C:\\Program Files\\nodejs\\node.exe\n') - .mockRejectedValueOnce(sessionLimitError) - - await expect( - resolveRemoteNodePath(conn, getRemoteHostPlatform('win32-x64'), { - rethrowSessionLimitErrors: true - }) - ).rejects.toBe(sessionLimitError) - }) - it('surfaces an AbortError instead of not-found when the shared signal is aborted', async () => { // Shared bootstrap abort must remain cancellation; swallowing it as Node-not-found defeats sequential fallback. const controller = new AbortController() @@ -508,9 +365,6 @@ describe('resolveRemoteNodePath', () => { await expect(resolveRemoteNodePath(conn, getRemoteHostPlatform('win32-x64'))).resolves.toBe( 'C:/Program Files/nodejs/node.exe' ) - - const discoveryScript = decodePowerShellCommand(execCommandMock.mock.calls[0]![1] as string) - expect(discoveryScript).not.toMatch(/Write-Output \$path\s+exit 0/) }) describe('strict resolution, which settles rung D only on an answered "no Node"', () => {