test: remove 259 low-value tests (#39701)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 19:53:07 -08:00
committed by GitHub
co-authored by OrcaWin
parent b69a0cd0a9
commit 331864e249
37 changed files with 92 additions and 5009 deletions
@@ -47,7 +47,6 @@ jobs:
src/main/ipc/preflight-runnable-local-cli.test.ts
src/main/ipc/preflight-wsl-agent-detection.test.ts
src/main/ipc/preflight-windows-path-refresh.repro.test.ts
src/main/startup/hydrate-shell-path.test.ts
src/main/startup/hydrate-shell-path.windows.test.ts
src/main/startup/login-shell-environment.test.ts
src/packages/process-host/src/process-tree-termination.test.ts
+2 -2
View File
@@ -658,7 +658,7 @@
2 test-no-wall-clock:src/main/runtime/workspace-layout-live-titles.test.ts
1 test-no-wall-clock:src/main/runtime/workspaces/process-listing/runtime-worktree-structured-agent-rows-liveness.test.ts
6 test-no-wall-clock:src/main/skills/skill-bundle-process-termination.integration.test.ts
7 test-no-wall-clock:src/main/skills/skill-install-lock.test.ts
6 test-no-wall-clock:src/main/skills/skill-install-lock.test.ts
1 test-no-wall-clock:src/main/skills/skill-install-recovery.test.ts
1 test-no-wall-clock:src/main/skills/skill-install-request-service.test.ts
1 test-no-wall-clock:src/main/skills/skill-install-service.test.ts
@@ -859,7 +859,7 @@
1 test-timeout-needs-reason:src/main/windows/windows-host-job.win32.test.ts
1 test-timeout-needs-reason:src/main/windows/windows-msys-job.win32.test.ts
2 test-timeout-needs-reason:src/main/windows/windows-pty-job.win32.test.ts
8 test-timeout-needs-reason:src/main/worker-thread-child-process/worker-thread-child-process.test.ts
5 test-timeout-needs-reason:src/main/worker-thread-child-process/worker-thread-child-process.test.ts
2 test-timeout-needs-reason:src/main/workspace-hooks/hook-archive-timeout-observation.test.ts
2 test-timeout-needs-reason:src/main/workspace-hooks/hook-termination-real-process.test.ts
2 test-timeout-needs-reason:src/main/workspaces/removal/local-worktree-filesystem-wsl-banner.wsl.test.ts
File diff suppressed because one or more lines are too long
@@ -4363,7 +4363,6 @@
"src/main/ssh/legacy-relay/ssh-host-relay-windows-inventory.test.ts": 178,
"src/main/ssh/legacy-relay/ssh-host-relay-windows-pipe-access.test.ts": 211,
"src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 1546,
"src/main/ssh/legacy-relay/ssh-relay-endpoint-incumbent.test.ts": 96,
"src/main/ssh/legacy-relay/ssh-relay-endpoint-runtime.test.ts": 49,
"src/main/ssh/legacy-relay/ssh-relay-incumbent-process.test.ts": 5357,
"src/main/ssh/legacy-relay/ssh-relay-sentinel-copy-budget.test.ts": 74,
@@ -4506,7 +4505,6 @@
"src/main/startup/gpu-fallback-marker.test.ts": 61,
"src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts": 641,
"src/main/startup/headless-serve-x11-reexec.test.ts": 76,
"src/main/startup/hydrate-shell-path.test.ts": 462,
"src/main/startup/hydrate-shell-path.windows.test.ts": 426,
"src/main/startup/legacy-worker-renderer-recovery.test.ts": 47,
"src/main/startup/linux-keyring-backend-selection.test.ts": 62,
@@ -4586,7 +4584,6 @@
"src/main/text-generation/commit-message-text-generation-remote-execution.test.ts": 469,
"src/main/text-generation/conversation-name-generation-request.test.ts": 507,
"src/main/text-generation/jira-issue-summary-requests.test.ts": 276,
"src/main/text-generation/jira-issue-summary-text-generation.test.ts": 543,
"src/main/text-generation/kimi-generation-failure.test.ts": 151,
"src/main/text-generation/pull-request-context.test.ts": 97,
"src/main/text-generation/source-control-agent-cli-path.test.ts": 615,
@@ -4665,7 +4662,6 @@
"src/main/window/createMainWindow-recovery-reload-watchdog.test.ts": 1251,
"src/main/window/createMainWindow-renderer-crash-recovery.test.ts": 1613,
"src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts": 2628,
"src/main/window/createMainWindow-tray-minimize-close.test.ts": 1140,
"src/main/window/createMainWindow-user-quit-close.test.ts": 1592,
"src/main/window/createMainWindow.test.ts": 1603,
"src/main/window/darwin-user-temp-dir.test.ts": 51,
@@ -4733,7 +4729,6 @@
"src/main/workspaces/creation/preparation/worktree-create-preparation-claim.test.ts": 52,
"src/main/workspaces/creation/preparation/worktree-create-preparation-contention.test.ts": 426,
"src/main/workspaces/creation/preparation/worktree-create-preparation-stale-cleanup.test.ts": 373,
"src/main/workspaces/creation/preparation/worktree-create-preparation-timing.test.ts": 357,
"src/main/workspaces/creation/preparation/worktree-create-preparation-wsl-root.test.ts": 402,
"src/main/workspaces/creation/preparation/worktree-create-preparation.test.ts": 393,
"src/main/workspaces/removal/worktree-failed-removal.test.ts": 749,
@@ -4747,7 +4742,6 @@
"src/main/workspaces/removal/retirement/worktree-retirement-backfill-scan.test.ts": 55,
"src/main/workspaces/removal/retirement/worktree-retirement-backfill-stall.test.ts": 365,
"src/main/workspaces/removal/retirement/worktree-retirement-discovery-wsl.test.ts": 355,
"src/main/workspaces/removal/retirement/worktree-retirement-namespace.test.ts": 235,
"src/main/workspaces/creation/worktree-root-preparation.test.ts": 180,
"src/main/workspaces/removal/worktree-trash.test.ts": 126,
"src/main/persistence/moving-wsl-projects/wsl-approved-root-race.wsl.test.ts": 102,
@@ -4784,7 +4778,6 @@
"src/main/wsl/execution/wsl-guest-pane-processes.test.ts": 48,
"src/main/wsl/execution/wsl-guest-process-identity.test.ts": 54,
"src/main/wsl/host-lifecycle/wsl-host-automation-wake.test.ts": 288,
"src/main/wsl/host-lifecycle/wsl-host-gate.test.ts": 156,
"src/main/wsl/host-lifecycle/wsl-host-kept-for-peers.test.ts": 159,
"src/main/wsl/host-lifecycle/wsl-host-pending-removal.test.ts": 955,
"src/main/wsl/host-lifecycle/wsl-host-server-stop.test.ts": 407,
-15
View File
@@ -1,15 +0,0 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { resolveBranchSwitchTarget } from './branch-switch-target'
import { gitExecFileAsync } from './runner'
vi.mock('./runner', () => ({ gitExecFileAsync: vi.fn() }))
beforeEach(() => vi.mocked(gitExecFileAsync).mockReset())
it('explains how to recover when remote settings cannot be read', async () => {
vi.mocked(gitExecFileAsync)
.mockResolvedValueOnce({ stdout: 'refs/remotes/origin/topic\t\t\n', stderr: '' })
.mockRejectedValueOnce(new Error('raw remote command failure'))
await expect(
resolveBranchSwitchTarget('/repo', { kind: 'remote', name: 'origin/topic' }, {})
).rejects.toThrow(
"Orca couldn't read this repository's remotes. Check its remote settings in a terminal, then try again."
)
})
-58
View File
@@ -1,58 +0,0 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { gitExecFileAsync } from './runner'
import { listLocalBranches } from './checkout'
vi.mock('./runner', () => ({ gitExecFileAsync: vi.fn() }))
vi.mock('./source-control/git-read-cache-invalidation', () => ({
runWithGitReadCacheInvalidation: vi.fn()
}))
beforeEach(() => vi.mocked(gitExecFileAsync).mockReset())
it.each(['unknown field: worktreepath', 'format failed on this host'])(
'retries the original listing once after %s',
async (message) => {
vi.mocked(gitExecFileAsync)
.mockRejectedValueOnce(new Error(message))
.mockResolvedValueOnce({ stdout: '*\tmain\n \tfeature\n', stderr: '' })
expect(await listLocalBranches('/repo')).toEqual({
current: 'main',
branches: ['main', 'feature']
})
expect(vi.mocked(gitExecFileAsync).mock.calls[1]?.[0]).toEqual([
'for-each-ref',
'--format=%(HEAD)%09%(refname:lstrip=2)',
'refs/heads/'
])
expect(gitExecFileAsync).toHaveBeenCalledTimes(2)
}
)
it('propagates the retry error without a third attempt', async () => {
const failure = new Error('repository unreadable')
vi.mocked(gitExecFileAsync)
.mockRejectedValueOnce(new Error('first'))
.mockRejectedValueOnce(failure)
await expect(listLocalBranches('/repo')).rejects.toBe(failure)
expect(gitExecFileAsync).toHaveBeenCalledTimes(2)
})
it('does not retry cancellation', async () => {
const abort = new Error('cancelled')
abort.name = 'AbortError'
vi.mocked(gitExecFileAsync).mockRejectedValueOnce(abort)
await expect(listLocalBranches('/repo')).rejects.toBe(abort)
expect(gitExecFileAsync).toHaveBeenCalledOnce()
})
it('translates occupied WSL branches into the owning workspace path namespace', async () => {
const platform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
try {
vi.mocked(gitExecFileAsync).mockResolvedValueOnce({
stdout: ' \tfeature\t1700000000\t/home/alice/feature\n',
stderr: ''
})
const result = await listLocalBranches(String.raw`\\wsl.localhost\Ubuntu\home\alice\repo`)
expect(result.worktreePaths).toEqual({
feature: String.raw`\\wsl.localhost\Ubuntu\home\alice\feature`
})
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
}
})
@@ -1,60 +0,0 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { buildSearchBaseRefsArgv } from './repo-ref-search-command'
import { searchRemoteRefPage } from './repo-remote-ref-search'
import { gitExecFileAsync } from './runner'
import { clearGitCapabilityStateForTests } from './command-runner/git-capability-state'
vi.mock('./runner', () => ({ gitExecFileAsync: vi.fn() }))
beforeEach(() => {
vi.mocked(gitExecFileAsync).mockReset()
clearGitCapabilityStateForTests()
})
it('applies the remote namespace before both modern and legacy candidate caps', () => {
for (const excludeRemoteHead of [true, false]) {
const args = buildSearchBaseRefsArgv('topic', 51, { namespace: 'remote', excludeRemoteHead })
expect(args.filter((arg) => arg.startsWith('refs/'))).toEqual([
'refs/remotes/**/*topic*',
'refs/remotes/**/*topic*/**'
])
expect(args).toContain(excludeRemoteHead ? '--count=204' : '--count=304')
}
})
it.each([0, 1])(
'propagates failure from read %i rather than returning empty discovery',
async (read) => {
const failure = new Error('read failed')
if (read === 1) {
vi.mocked(gitExecFileAsync).mockResolvedValueOnce({ stdout: 'origin\n', stderr: '' })
}
vi.mocked(gitExecFileAsync).mockRejectedValueOnce(failure)
await expect(searchRemoteRefPage('/repo', 'topic', 50)).rejects.toBe(failure)
}
)
it('uses a sentinel and reports a saturated scan even after HEAD filtering', async () => {
vi.mocked(gitExecFileAsync)
.mockResolvedValueOnce({ stdout: 'origin\n', stderr: '' })
.mockResolvedValueOnce({
stdout: Array.from({ length: 204 }, () => 'refs/remotes/origin/HEAD\0origin/HEAD').join('\n'),
stderr: ''
})
expect(await searchRemoteRefPage('/repo', '', 50)).toEqual({ refDetails: [], truncated: true })
})
it.each([100, 1000])(
'retains only the requested 50 remote rows from %i candidates',
async (size) => {
vi.mocked(gitExecFileAsync)
.mockResolvedValueOnce({ stdout: 'origin\n', stderr: '' })
.mockResolvedValueOnce({
stdout: Array.from(
{ length: size },
(_, n) => `refs/remotes/origin/topic-${n}\0origin/topic-${n}`
).join('\n'),
stderr: ''
})
const page = await searchRemoteRefPage('/repo', 'topic', 50)
expect(page.refDetails).toHaveLength(50)
expect(page.truncated).toBe(true)
// The caller needs 50 choices plus a sentinel; Git bounds candidates before returning them.
expect(vi.mocked(gitExecFileAsync).mock.calls[1]?.[0]).toContain('--count=204')
expect(gitExecFileAsync).toHaveBeenCalledTimes(2)
}
)
@@ -205,68 +205,6 @@ describe('pending PR actions', () => {
expect(count('pr_chat_actions')).toBe(0)
})
it('starts each due row once when an expired row ends inside the pass', async () => {
const journal = await openJournal()
const call = (ordinal: number, pr: number) =>
journal.appendItem(
{ provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal },
shellCall('gh pr create --fill', `https://github.com/acme/repo/pull/${pr}\n`),
{ fence: 0, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
await call(0, 7)
clock += 25 * 60 * 60_000
await call(1, 8)
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
try {
// A, past its expiry, ends at once; recent B's workspace is not resolvable yet.
const chatWorkspace = vi.fn(async () => null)
const resume = resumeAfterRestart({ chatWorkspace })
resume.drain()
await vi.waitFor(() => expect(count('pr_pending_actions')).toBe(1))
for (let pass = 0; pass < 5; pass++) {
resume.drain()
}
await Promise.resolve()
expect(chatWorkspace).toHaveBeenCalledTimes(1)
expect(vi.getTimerCount()).toBe(1)
resume.clear()
} finally {
vi.useRealTimers()
}
})
it('backs off a row whose end cannot be saved instead of rerunning it at once', async () => {
const journal = await openJournal()
await journal.appendItem(
{ provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 },
shellCall('gh pr create --fill', 'https://github.com/acme/repo/pull/7\n'),
{ fence: 0, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
clock += 25 * 60 * 60_000
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
try {
// The expired row ends at once, but the disk is full: its end commits nothing. A spinning
// drain is stopped after a few turns so this fails instead of hanging.
let resume: PRPendingActionResume | undefined
const settle = vi.fn(() => {
if (settle.mock.calls.length > 10) {
resume?.clear()
}
throw new Error('SQLITE_FULL')
})
resume = resumeAfterRestart({ settle })
resume.drain()
expect(settle).toHaveBeenCalledTimes(2)
settle.mockClear()
clock += 60_000
vi.advanceTimersByTime(60_000)
expect(settle).toHaveBeenCalled()
resume.clear()
} finally {
vi.useRealTimers()
}
})
it('captures nothing from an imported transcript', async () => {
const journal = await openJournal()
await journal.replaceEpochItems('legacy_import', 0, [
@@ -1,16 +1,10 @@
import { describe, expect, it, vi } from 'vitest'
import './runtime-test-module-registration.spec'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import '../../../shared/constants'
import { OrcaRuntimeService } from '../orca-runtime'
import {
electronMocks,
scanLocalRepoWorktreesForResolutionMock
} from '../orca-runtime-test-mocks/setup.spec'
import { scanLocalRepoWorktreesForResolutionMock } from '../orca-runtime-test-mocks/setup.spec'
import {
HEADLESS_LEAF_ID,
TEST_WINDOW_ID,
TEST_WORKTREE_ID,
createRuntime,
makeHeadlessTerminalLayout,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal,
@@ -19,111 +13,6 @@ import {
import type { RuntimeWorktreeScanResult } from '../workspaces/catalog/repo-worktree-resolution-scan'
describe('OrcaRuntimeService', () => {
it('starts unavailable with no authoritative window', () => {
const runtime = createRuntime()
expect(runtime.getStatus()).toMatchObject({
graphStatus: 'unavailable',
authoritativeWindowId: null,
desktopWindowStatus: 'openable',
rendererGraphEpoch: 0
})
expect(runtime.getRuntimeId()).toBeTruthy()
})
it('reports the configured Windows terminal shell on status', () => {
const runtime = new OrcaRuntimeService({
...store,
getSettings: () => ({
...store.getSettings(),
terminalWindowsShell: 'wsl.exe'
})
} as never)
expect(runtime.getStatus().terminalWindowsShell).toBe('wsl.exe')
})
it('reports floating workspace availability from settings on status', () => {
expect(createRuntime().getStatus().floatingWorkspaceEnabled).toBe(true)
const disabledRuntime = new OrcaRuntimeService({
...store,
getSettings: () => ({
...store.getSettings(),
floatingTerminalEnabled: false
})
} as never)
expect(disabledRuntime.getStatus().floatingWorkspaceEnabled).toBe(false)
})
it('polls floating tabs with targeted PTY liveness and no repo/provider inventory', async () => {
const getRepos = vi.fn(store.getRepos)
const listProcesses = vi.fn().mockResolvedValue([])
const floatingPtyId = `${FLOATING_TERMINAL_WORKTREE_ID}@@pty-1`
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
activeRepoId: null,
activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeTabIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-tab' },
tabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: 'floating-tab',
ptyId: floatingPtyId,
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
title: 'Floating Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'floating-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: floatingPtyId })
}
})
)
const runtime = new OrcaRuntimeService({ ...runtimeStore, getRepos } as never)
const ptyController = {
livePtyIds: new Set([floatingPtyId]),
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
hasPty(this: { livePtyIds: Set<string> }, ptyId: string) {
return this.livePtyIds.has(ptyId)
},
listProcesses
}
const hasPty = vi.spyOn(ptyController, 'hasPty')
runtime.setPtyController(ptyController)
const tabs = await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
const terminals = await runtime.listTerminals(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
await runtime.listTerminals(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
expect(tabs.tabs).toEqual([
expect.objectContaining({
type: 'terminal',
parentTabId: 'floating-tab',
status: 'ready',
terminal: expect.any(String)
})
])
expect(terminals.terminals).toEqual([
expect.objectContaining({
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
connected: true
})
])
expect(hasPty).toHaveBeenCalledTimes(4)
expect(hasPty).toHaveBeenCalledWith(floatingPtyId)
expect(listProcesses).not.toHaveBeenCalled()
// Floating tabs have no repoId and must not trigger repository hydration (#9343).
expect(getRepos).not.toHaveBeenCalled()
})
it('does not block a targeted mobile session tab list on an unrelated worktree scan', async () => {
const remoteWorktreeId = 'repo-ssh::/remote/worktree'
const remotePtyId = 'ssh:ssh-target@@remote-pty'
@@ -214,142 +103,4 @@ describe('OrcaRuntimeService', () => {
}
}
})
it('hydrates persisted tabs when the store cannot report repos', async () => {
// An unavailable repo inventory must not masquerade as an empty one and prune every tab (#9343).
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
)
const runtime = new OrcaRuntimeService({
...runtimeStore,
getRepos: () => undefined
} as never)
const tabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(tabs.tabs).toEqual([
expect.objectContaining({ type: 'terminal', parentTabId: 'host-tab' })
])
})
it('advertises browser screencast only when a renderer window is available', () => {
const runtime = createRuntime()
electronMocks.BrowserWindow.fromId.mockReturnValue({ isDestroyed: () => false } as never)
runtime.attachWindow(TEST_WINDOW_ID)
expect(runtime.getStatus().capabilities).toContain('browser.screencast.v1')
})
it('routes mobile Codex reset consumption through the account mutation coordinator', async () => {
const runtime = createRuntime()
const expectedScope = {
target: { runtime: 'host' as const, wslDistro: null },
accountId: 'codex-account',
accountRevision: 42,
offerRevision: 'v1:offer'
}
const capturedCodex = {
accounts: [],
activeAccountId: expectedScope.accountId,
activeAccountIdsByRuntime: { host: expectedScope.accountId, wsl: {} }
}
const capturedRateLimits = {
codexTarget: expectedScope.target,
marker: 'captured-before-queue-advanced'
}
const codexAccounts = {
consumeRateLimitResetCredit: vi.fn().mockResolvedValue({
outcome: 'reset',
scope: expectedScope,
codex: capturedCodex,
rateLimits: capturedRateLimits
}),
listAccounts: vi.fn(() => ({
accounts: [],
activeAccountId: 'queued-next-account',
activeAccountIdsByRuntime: { host: 'queued-next-account', wsl: {} }
}))
}
const rateLimits = {
consumeCodexRateLimitResetCredit: vi.fn(),
getState: vi.fn(() => ({
codexTarget: expectedScope.target,
marker: 'after-queue-advanced'
}))
}
runtime.setAccountServices({
claudeAccounts: {
listAccounts: vi.fn(() => ({ accounts: [], activeAccountId: null }))
},
codexAccounts,
rateLimits
} as never)
const result = await runtime.consumeCodexRateLimitResetCredit(
'11111111-1111-4111-8111-111111111111',
expectedScope
)
expect(result).toMatchObject({
outcome: 'reset',
scope: expectedScope,
snapshot: { codex: capturedCodex, rateLimits: capturedRateLimits }
})
expect(codexAccounts.listAccounts).not.toHaveBeenCalled()
expect(rateLimits.getState).not.toHaveBeenCalled()
expect(codexAccounts.consumeRateLimitResetCredit).toHaveBeenCalledWith(
'11111111-1111-4111-8111-111111111111',
expectedScope
)
expect(rateLimits.consumeCodexRateLimitResetCredit).not.toHaveBeenCalled()
})
it('maps a definite pre-provider rejection into an authoritative current snapshot', async () => {
const runtime = createRuntime()
const expectedScope = {
target: { runtime: 'host' as const, wslDistro: null },
accountId: 'codex-account',
accountRevision: 42,
offerRevision: 'v1:stale'
}
const codex = {
accounts: [],
activeAccountId: null,
activeAccountIdsByRuntime: { host: null, wsl: {} }
}
const rateLimitState = {
codexTarget: expectedScope.target,
marker: 'current-after-rejection'
}
runtime.setAccountServices({
claudeAccounts: {
listAccounts: vi.fn(() => ({ accounts: [], activeAccountId: null }))
},
codexAccounts: {
consumeRateLimitResetCredit: vi.fn().mockResolvedValue({
status: 'rejectedBeforeProvider',
retryDisposition: 'discardAttempt',
reason: 'offerChanged',
scope: expectedScope,
codex,
rateLimits: rateLimitState
})
},
rateLimits: {}
} as never)
await expect(
runtime.consumeCodexRateLimitResetCredit(
'11111111-1111-4111-8111-111111111111',
expectedScope
)
).resolves.toMatchObject({
status: 'rejectedBeforeProvider',
retryDisposition: 'discardAttempt',
reason: 'offerChanged',
scope: expectedScope,
snapshot: { codex, rateLimits: rateLimitState }
})
})
})
@@ -5,13 +5,11 @@ import {
TEST_WINDOW_ID,
TEST_WORKTREE_ID,
createRuntime,
createRuntimeWithSshLease,
store
createRuntimeWithSshLease
} from './orca-runtime-test-fixtures.spec'
import './runtime-test-module-registration.spec'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { OrcaRuntimeService } from '../orca-runtime'
import type { SshRemotePtyLease } from '../../../shared/ssh-types'
import '../orca-runtime'
describe('OrcaRuntimeService', () => {
it('keeps a no-incarnation handle across an in-graph pane remint', async () => {
@@ -150,39 +148,6 @@ describe('OrcaRuntimeService', () => {
await waiting
expect(settled).toBe('rejected')
})
it('recovers a moved SSH pane through the tab it now sits in, not the one its lease froze', async () => {
// A moved pane's current coordinates override the lease's frozen old tabId.
const leaseTabId = 'tab-before-move'
const currentTabId = 'tab-after-move'
const appPtyId = 'ssh:ssh-target@@pty-8'
const runtime = createRuntimeWithSshLease(appPtyId, leaseTabId)
const paneKey = makePaneKey(currentTabId, HEADLESS_LEAF_ID)
registerBoundPty(runtime, appPtyId, TEST_WORKTREE_ID, 'ssh-target', {
tabId: currentTabId,
leafId: HEADLESS_LEAF_ID
})
const handle = runtime.resolveTerminalPane(paneKey, TEST_WORKTREE_ID).handle
runtime.onPtyExit(appPtyId, -1, undefined, { hostExitConfirmed: true })
const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
handle: 'term-replacement',
tabId: currentTabId,
paneKey,
ptyId: 'pty-replacement',
worktreeId: TEST_WORKTREE_ID,
title: null,
surface: 'background'
})
await expect(
runtime.recoverTerminalPane(paneKey, TEST_WORKTREE_ID, handle)
).resolves.toMatchObject({ handle: 'term-replacement' })
expect(createTerminal).toHaveBeenCalledWith(`id:${TEST_WORKTREE_ID}`, {
tabId: currentTabId,
leafId: HEADLESS_LEAF_ID,
focus: false,
reopensPane: true
})
})
it('refuses a moved SSH pane addressed through the tab it left', async () => {
// Old-tab coordinates must refuse the moved leaf; otherwise one PTY binds into two tabs.
@@ -224,128 +189,3 @@ describe('OrcaRuntimeService', () => {
expect(createTerminal).not.toHaveBeenCalled()
})
})
// Several leases can share pane coordinates; select an eligible orphan rather than the first match.
const TARGET = 'ssh-target'
const TAB_ID = 'tab-candidacy'
type LeaseReader = {
workspaceSessionWorktreeHasRuntimeOwnedPtyCandidate: (
session: { terminalLayoutsByTabId?: Record<string, unknown> },
worktreeId: string,
tabs: { id: string; ptyId: string | null }[]
) => boolean
collectRecentExpiredSshLeaseTabIds: (worktreeId: string) => ReadonlySet<string>
getRecentExpiredSshLease: (
worktreeId: string,
tabId: string,
leafId: string | undefined,
ptyId?: string
) => SshRemotePtyLease | null
hasRecentExpiredSshLeasePane: (
worktreeId: string,
tab: { parentTabId: string; leafId: string }
) => boolean
}
function leaseFor(ptyId: string, marks: Partial<SshRemotePtyLease> = {}): SshRemotePtyLease {
const now = Date.now()
return {
targetId: TARGET,
ptyId,
worktreeId: TEST_WORKTREE_ID,
tabId: TAB_ID,
leafId: HEADLESS_LEAF_ID,
state: 'expired',
createdAt: now,
updatedAt: now,
...marks
}
}
function readerWithLeases(leases: SshRemotePtyLease[]): LeaseReader {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The runtime implements these protected lease readers.
return new OrcaRuntimeService({
...store,
getSshRemotePtyLeases: () => leases
}) as unknown as LeaseReader
}
const pane = { parentTabId: TAB_ID, leafId: HEADLESS_LEAF_ID }
describe('recent expired SSH lease candidacy', () => {
it('reports a plain expired orphan', () => {
const reader = readerWithLeases([leaseFor('pty-1')])
expect(reader.hasRecentExpiredSshLeasePane(TEST_WORKTREE_ID, pane)).toBe(true)
})
it('does not report a pane whose only recent lease was superseded', () => {
// A superseded lease no longer routes to its original shell and cannot offer recovery.
const reader = readerWithLeases([leaseFor('pty-1', { supersededBy: 'pty-2' })])
expect(reader.hasRecentExpiredSshLeasePane(TEST_WORKTREE_ID, pane)).toBe(false)
})
it('does not report a pane whose only recent lease had its relay id recycled', () => {
// A recycled relay id belongs to another shell and must never be adopted.
const reader = readerWithLeases([leaseFor('pty-1', { relayIdRecycled: true })])
expect(reader.hasRecentExpiredSshLeasePane(TEST_WORKTREE_ID, pane)).toBe(false)
})
it('picks the eligible orphan over a superseded predecessor that matches first', () => {
// Store the superseded lease first to catch first-match recovery.
const reader = readerWithLeases([
leaseFor('pty-1', { supersededBy: 'pty-2' }),
leaseFor('pty-2')
])
expect(reader.getRecentExpiredSshLease(TEST_WORKTREE_ID, TAB_ID, HEADLESS_LEAF_ID)?.ptyId).toBe(
'pty-2'
)
expect(reader.hasRecentExpiredSshLeasePane(TEST_WORKTREE_ID, pane)).toBe(true)
})
it('collects the same tabs the per-tab reader reports, in one sweep of the leases', () => {
const leases = [leaseFor('pty-1', { supersededBy: 'pty-2' }), leaseFor('pty-2')]
let sweeps = 0
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The runtime implements these protected lease readers.
const reader = new OrcaRuntimeService({
...store,
getSshRemotePtyLeases: () => {
sweeps += 1
return leases
}
}) as unknown as LeaseReader
const tabs = Array.from({ length: 8 }, (_, index) => ({
id: index === 7 ? TAB_ID : `tab-${index}`,
ptyId: null
}))
expect(
reader.workspaceSessionWorktreeHasRuntimeOwnedPtyCandidate(
{ terminalLayoutsByTabId: {} },
TEST_WORKTREE_ID,
tabs
)
).toBe(true)
expect(sweeps).toBe(1)
expect([...reader.collectRecentExpiredSshLeaseTabIds(TEST_WORKTREE_ID)]).toEqual([TAB_ID])
})
it('reports no candidate when no lease names any of the worktree tabs', () => {
const reader = readerWithLeases([
leaseFor('pty-1', { tabId: 'somewhere-else', leafId: undefined })
])
expect(
reader.workspaceSessionWorktreeHasRuntimeOwnedPtyCandidate(
{ terminalLayoutsByTabId: {} },
TEST_WORKTREE_ID,
[{ id: TAB_ID, ptyId: null }]
)
).toBe(false)
})
})
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SkillInstallResult } from '../../shared/skill-install-contract'
import type { SkillDiscoveryResult } from '../../shared/skills'
import type { WslResult, WslSpec } from '../wsl/execution/wsl-runner'
import type { discoverSkills } from './discovery'
@@ -11,15 +11,12 @@ const transport = vi.hoisted(() => ({
vi.mock('../wsl/execution/wsl-runner', () => ({ runWslProcess: transport.run }))
vi.mock('./discovery', () => ({ discoverSkills: transport.native }))
import { discoverSkillsInWsl } from './skill-discovery-wsl'
import './skill-discovery-wsl'
import { verifySkillInstallDiscovery } from './skill-install-discovery-verification'
const home = '/home/alice'
const workspace = '/work/project'
const homeAgents = `${home}/.agents/skills`
const homeClaude = `${home}/.claude/skills`
const repoAgents = `${workspace}/.agents/skills`
const repoClaude = `${workspace}/.claude/skills`
const pluginRoot = `${home}/.claude/plugins/cache/example/1/skills`
type ScanRow = { root: string; directory: string; canonical: string; name: string }
@@ -94,14 +91,6 @@ function interceptScan(rows: ScanRow[]): void {
})
}
function scanScript(): string {
const script = transport.run.mock.calls.at(-1)?.[0].script
if (!script) {
throw new Error('Expected a mandatory scan')
}
return script
}
const input = {
scope: 'global',
homeDirectory: home,
@@ -123,81 +112,6 @@ beforeEach(() => {
})
describe('WSL install verification at the discovery transport boundary', () => {
it.each(['global', 'workspace'] as const)(
'uses one fresh scan instead of two processes for a %s install with co-owners',
async (scope) => {
const canonicalRoot = scope === 'global' ? homeAgents : repoAgents
const aliasRoot = scope === 'global' ? homeClaude : repoClaude
const coownerRoot = scope === 'global' ? repoAgents : homeClaude
const canonical = `${canonicalRoot}/review`
const result = installResult([canonical, `${aliasRoot}/review`])
interceptScan([
skillRow(canonicalRoot),
skillRow(aliasRoot, canonical),
skillRow(coownerRoot, canonical),
skillRow(pluginRoot, canonical)
])
const args = { ...input, scope, workspaceDirectory: workspace, result }
const broad = await verifySkillInstallDiscovery({
...args,
discover: () => discoverSkillsInWsl({ distro: 'Ubuntu', homeDir: home, cwd: workspace })
})
expect(broad).toEqual(result)
expect(transport.run).toHaveBeenCalledTimes(2)
expect(scanScript()).toContain(`'${pluginRoot}'`)
transport.run.mockClear()
expect(await verifySkillInstallDiscovery(args)).toEqual(broad)
expect(transport.run).toHaveBeenCalledTimes(1)
expect(transport.native).not.toHaveBeenCalled()
expect(transport.run).toHaveBeenCalledWith(
expect.objectContaining({
distro: 'Ubuntu',
shell: 'bash',
loginPath: 'none',
timeoutMs: 10_000,
maxOutputBytes: 128 * 1024 * 1024
})
)
expect(scanScript()).toContain(`'${homeAgents}'`)
expect(scanScript()).toContain(`'${repoAgents}'`)
expect(scanScript()).toContain("'review') return 0 ;;")
expect(scanScript()).not.toContain('read_metadata()')
expect(scanScript()).not.toContain(`'${pluginRoot}'`)
expect(scanScript()).not.toContain(`'${home}/.codex/plugins/cache'`)
}
)
it('keeps an overridden provider root even when it is also an enabled plugin root', async () => {
const canonical = `${homeAgents}/review`
const result = installResult([canonical, `${pluginRoot}/review`])
interceptScan([skillRow(homeAgents), skillRow(pluginRoot, canonical)])
const args = {
...input,
result,
providerRootOverrides: {
claude:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.claude\\plugins\\cache\\example\\1\\skills'
}
}
const broad = await verifySkillInstallDiscovery({
...args,
discover: () =>
discoverSkillsInWsl({
distro: 'Ubuntu',
homeDir: home,
providerRootOverrides: { claude: pluginRoot }
})
})
expect(broad).toEqual(result)
expect(transport.run).toHaveBeenCalledTimes(2)
transport.run.mockClear()
expect(await verifySkillInstallDiscovery(args)).toEqual(broad)
expect(transport.run).toHaveBeenCalledTimes(1)
expect(scanScript()).toContain(`'${pluginRoot}'`)
expect(scanScript()).not.toContain('read_metadata()')
})
it('reads again for each item and fails if its canonical placement disappears', async () => {
const result = installResult([`${homeAgents}/review`])
interceptScan([skillRow(homeAgents)])
@@ -232,20 +146,6 @@ describe('WSL install verification at the discovery transport boundary', () => {
expect(transport.run).toHaveBeenCalledTimes(1)
})
it.each(['review', 'Review', 'réview'])(
'verifies frontmatter name %s in a different directory without accepting unrelated siblings',
async (name) => {
const result = installResult([`${homeAgents}/review`], name)
interceptScan([
{ ...skillRow(homeAgents, `${homeAgents}/renamed`, 'renamed'), name },
{ ...skillRow(homeAgents, `${homeAgents}/other`, 'other'), name: 'other' }
])
expect(await verifySkillInstallDiscovery({ ...input, result })).toEqual(result)
expect(transport.run).toHaveBeenCalledTimes(1)
expect(scanScript()).toContain('matches_requested_name()')
}
)
it.each([
{ ...wslResult(''), code: 1 },
{ ...wslResult(''), timedOut: true },
@@ -265,50 +165,4 @@ describe('WSL install verification at the discovery transport boundary', () => {
})
expect(transport.run).toHaveBeenCalledTimes(1)
})
it('leaves native fresh discovery arguments and answered co-owner proof unchanged', async () => {
const result = installResult([`${homeAgents}/review`, `${homeClaude}/review`])
const discovery: SkillDiscoveryResult = {
scannedAt: 1,
skills: [
{
id: 'review',
name: 'review',
description: null,
providers: ['agent-skills', 'claude'],
sourceKind: 'home',
sourceLabel: 'Home',
rootPath: homeAgents,
rootPaths: [homeAgents, homeClaude],
directoryPath: `${homeAgents}/review`,
skillFilePath: `${homeAgents}/review/SKILL.md`,
installed: true,
updatedAt: null
}
],
sources: [
{
id: 'home-agents',
label: 'Home',
path: homeAgents,
sourceKind: 'home',
providers: ['agent-skills'],
owner: null,
exists: true,
skippedReason: 'unavailable'
}
]
}
transport.native.mockResolvedValue(discovery)
expect(
await verifySkillInstallDiscovery({ result, scope: 'global', homeDirectory: home })
).toEqual(result)
expect(transport.native).toHaveBeenCalledExactlyOnceWith({
homeDir: home,
repos: [],
refresh: true,
includeCwd: false
})
expect(transport.run).not.toHaveBeenCalled()
})
})
+1 -36
View File
@@ -7,7 +7,7 @@ import {
reclaimDeadSkillInstallLocks,
skillInstallLockPath
} from './skill-install-lock'
import { readSkillInstallLockOwner } from './skill-install-lock-owner'
import './skill-install-lock-owner'
const roots: string[] = []
@@ -24,15 +24,6 @@ async function readPublishedOwner(lockPath: string): Promise<Record<string, unkn
}
describe('skill install lock', () => {
it.each([0, -1])('rejects a non-positive owner pid (%s)', async (pid) => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-lock-test-'))
roots.push(root)
const ownerPath = join(root, 'owner.json')
await writeFile(ownerPath, JSON.stringify({ token: 'invalid-pid', pid, createdAt: Date.now() }))
await expect(readSkillInstallLockOwner(ownerPath)).resolves.toBeNull()
})
it('reclaims a fresh legacy lock whose process was killed', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-lock-test-'))
roots.push(root)
@@ -147,32 +138,6 @@ describe('skill install lock', () => {
await release()
})
it('retries when the current owner releases after a contended rename', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-lock-test-'))
roots.push(root)
const lockPath = skillInstallLockPath(join(root, 'state'), join(root, 'skills', 'alpha'))
const firstRelease = await acquireSkillInstallLock({ path: lockPath })
let attempts = 0
const secondRelease = await acquireSkillInstallLock({
path: lockPath,
timeoutMs: 500,
publishLock: async (candidatePath, targetPath) => {
attempts += 1
if (attempts === 1) {
await firstRelease()
const error = new Error('injected-contention') as NodeJS.ErrnoException
error.code = 'ENOTEMPTY'
throw error
}
await rename(candidatePath, targetPath)
}
})
expect(attempts).toBe(2)
await secondRelease()
})
it('reclaims abandoned legacy owner files at startup', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-lock-test-'))
roots.push(root)
@@ -55,7 +55,7 @@ import {
resetSshCredentialDeclinesForTests
} from '../ssh-credential-decline'
import { isAuthError } from '../ssh-reconnect-error-classification'
import { isSshSessionStartFailure } from '../ssh-session-start-failure'
import '../ssh-session-start-failure'
import { recordSshUserDisconnected, resetSshUserDisconnectsForTests } from '../ssh-user-disconnect'
const target = {
@@ -84,30 +84,6 @@ describe('SshConnectionManager', () => {
await rejected
})
it('tears a failed startup down quietly, so its error is not replaced by a disconnect', async () => {
mockState.connectResults.push(Promise.reject(new Error('auth failed')))
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
await expect(manager.connect(target)).rejects.toThrow('auth failed')
expect(mockState.instances.at(-1)?.disconnect).toHaveBeenCalledWith({ quiet: true })
expect(manager.getConnection(target.id)).toBeUndefined()
})
it('marks a dial that never reached the host, so setup reports SSH’s reason', async () => {
const refused = new Error('connect ECONNREFUSED 10.0.0.1:22')
mockState.connectResults.push(Promise.reject(refused))
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
await expect(manager.connect(target)).rejects.toBe(refused)
expect(isSshSessionStartFailure(new Error('Deploy failed', { cause: refused }))).toBe(true)
})
it('removes the registration when bulk teardown fails', async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
await manager.connect(target)
mockState.instances[0].disconnect.mockRejectedValueOnce(new Error('close unconfirmed'))
await manager.disconnectAll()
expect(manager.getConnection(target.id)).toBeUndefined()
})
beforeEach(() => {
mockState.connectResults.length = 0
mockState.instances.length = 0
@@ -203,32 +179,6 @@ describe('SshConnectionManager', () => {
expect(mockState.instances).toHaveLength(1)
expect(manager.getConnection(target.id)).toBeUndefined()
})
it('leaves a connection whose own reconnect is asking the user alone', async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
const conn = await manager.connect(target)
const opened = mockState.instances.at(-1)!
const openedCount = mockState.instances.length
// Its reconnect after a drop has the credential dialog open.
opened.status = 'connecting'
opened.awaitingCredential = true
await expect(manager.connect(target)).rejects.toThrow('already in progress')
expect(opened.disconnect).not.toHaveBeenCalled()
expect(mockState.instances).toHaveLength(openedCount)
expect(manager.getConnection(target.id)).toBe(conn)
})
it('clears the pool entry when the closed connection is still the registered one', async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
const conn = await manager.connect(target)
await manager.disconnectConnection(target.id, conn)
expect(mockState.instances[0].disconnect).toHaveBeenCalledOnce()
expect(manager.getConnection(target.id)).toBeUndefined()
})
})
describe('SshConnectionManager after a cancelled sign-in', () => {
@@ -248,14 +198,6 @@ describe('SshConnectionManager after a cancelled sign-in', () => {
expect(mockState.instances).toHaveLength(0)
})
it('still reuses a connection that is already up', async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
const conn = await manager.connect(target)
recordSshCredentialDeclined(target.id)
await expect(manager.connect(target)).resolves.toBe(conn)
})
it('refuses a background dial to a host awaiting its first sign-in, saying so', async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
recordSshSignInAwaited(target.id)
@@ -311,11 +253,4 @@ describe('SshConnectionManager after a user Disconnect', () => {
await expect(manager.connect(target)).rejects.toThrow('was disconnected')
})
it("dials a user's connection test", async () => {
const manager = new SshConnectionManager({ onStateChange: vi.fn() })
recordSshUserDisconnected(target.id)
await expect(manager.connect(target, { probe: true })).resolves.toBeDefined()
})
})
@@ -129,17 +129,6 @@ async function run(
}
describe('the relay-era orca CLI on migrated legacy orchestration records', () => {
it('attests the bridge-verified worker instead of answering read-only', async () => {
const { runtime, database } = legacyRuntime()
expect(database.getLegacyAdoption()).toBeTruthy()
const check = await run(runtime, ['orchestration', 'check', '--json'])
expect(check.exitCode).toBe(0)
expect(check.body).toMatchObject({
ok: true,
result: { legacyCompatibility: { readOnly: false } }
})
})
it('never lets a host pane adopt the unbound legacy coordinator by naming its handle', async () => {
const { runtime, database, taskId, dispatchId } = legacyRuntime()
const escalation = await run(runtime, escalationArgv(taskId, dispatchId))
@@ -266,26 +255,4 @@ describe('the relay-era orca CLI on migrated legacy orchestration records', () =
]
await expect(run(runtime, fresh)).rejects.toThrow(LEGACY_RELAY_CLI_UNVERIFIED)
})
it('vouches only for the exact terminal its attachment carries', () => {
const runtime = new OrcaRuntimeService()
const terminal = { handle: WORKER, paneKey: WORKER_PANE, processIncarnation: `${PTY_ID}:inc-1` }
const host = runtime.registerOrchestrationCompatibilitySshAttachment(TARGET, 'c-1', terminal)
const evidence = { terminalHandle: WORKER, paneKey: WORKER_PANE, launchToken: 't', host }
expect(runtime.verifyOrchestrationCompatibilityCaller(evidence)).toMatchObject({
terminalHandle: WORKER,
processIncarnation: `${PTY_ID}:inc-1`,
hostScope: { kind: 'ssh', targetId: TARGET }
})
for (const forged of [
{ ...evidence, terminalHandle: 'term_other' },
{ ...evidence, paneKey: 'tab_x:55555555-5555-4555-8555-555555555555' },
{ ...evidence, host: { ...host, targetId: 'other-target' } },
{ ...evidence, host: { ...host, connectionIncarnation: 'c-2' } }
]) {
expect(runtime.verifyOrchestrationCompatibilityCaller(forged)).toBeNull()
}
runtime.releaseOrchestrationCompatibilitySshAttachment(host.attachmentId)
expect(runtime.verifyOrchestrationCompatibilityCaller(evidence)).toBeNull()
})
})
@@ -4,7 +4,7 @@ import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CONNECTOR_REPLY_UNAVAILABLE } from '../../../shared/rpc-contract/connector-params'
import { toAppSshPtyId } from '../../../shared/ssh-pty-id'
import { isRemoteCliControlAllowed } from '../../../shared/ssh-remote-cli-control'
import '../../../shared/ssh-remote-cli-control'
import { OrcaRuntimeService } from '../../runtime/orca-runtime'
import { OrchestrationDb } from '../../runtime/orchestration/db/orchestration-db'
import '../../runtime/orchestration/db/contract-constants'
@@ -143,20 +143,6 @@ describe('the relay-era orca CLI on the desktop runtime', () => {
expect(db.getTask(ids.taskId)?.status).toBe('dispatched')
})
it.each([
['no saved value', {}, true],
['an explicit true', { allowRemoteCliControl: true }, true],
['an explicit false', { allowRemoteCliControl: false }, false]
])('admits per the saved switch: %s', async (_label, saved, admitted) => {
allowed = isRemoteCliControlAllowed(saved)
const check = run(['orchestration', 'check', '--json'])
if (admitted) {
expect((await check).exitCode).toBe(0)
} else {
await expect(check).rejects.toThrow(LEGACY_RELAY_CLI_CONTROL_OFF)
}
})
it('refuses commands outside orchestration', async () => {
await expect(run(['worktree', 'list'])).rejects.toThrow(LEGACY_RELAY_CLI_UNSUPPORTED)
await expect(run(['terminal', 'send', '--terminal', COORDINATOR])).rejects.toThrow(
@@ -238,25 +224,6 @@ describe('the relay-era orca CLI on the desktop runtime', () => {
expect(db.getInbox(100)).toHaveLength(1)
})
it('sends to its own coordinator', async () => {
const sent = await run([
'orchestration',
'send',
'--to',
COORDINATOR,
'--subject',
'progress',
'--json'
])
expect(json(sent)).toMatchObject({ ok: true })
})
it('refuses a malformed post-output action', async () => {
await expect(
server().postOutput({ env: ENV, postOutput: { kind: 'other' } }, new AbortController().signal)
).rejects.toThrow('Invalid SSH CLI post-output action.')
})
it('replays a completed worker_done after its Dispatch settled, and nothing new', async () => {
const retry = ['--retry-request', '11111111-1111-4111-8111-111111111111']
const first = await run([...workerDone(), ...retry])
@@ -269,23 +236,6 @@ describe('the relay-era orca CLI on the desktop runtime', () => {
).rejects.toThrow(LEGACY_RELAY_CLI_UNVERIFIED)
})
it('answers --help without running the command', async () => {
for (const flag of ['--help', '-h']) {
expect(await run([...workerDone(), flag])).toMatchObject({ exitCode: 0, stderr: '' })
}
expect(db.getTask(ids.taskId)?.status).toBe('dispatched')
})
it('refuses an emptied --retry-request instead of starting a new mutation', async () => {
for (const argv of [
[...workerDone(), '--retry-request='],
[...workerDone(), '--retry-request']
]) {
await expect(run(argv)).rejects.toThrow(/--retry-request/)
}
expect(db.getTask(ids.taskId)?.status).toBe('dispatched')
})
it('replays a completed worker_done after the terminal took a new assignment', async () => {
const retry = ['--retry-request', '44444444-4444-4444-8444-444444444444']
const first = await run([...workerDone(), ...retry])
@@ -1,310 +0,0 @@
import type * as ExecCommandModule from '../remote-exec/remote-exec-command'
import { describe, expect, it, vi } from 'vitest'
import { RELAY_LSOF_PROBE_JS } from '@orca/process-host/posix-lsof-probe'
const execCommand = vi.fn()
vi.mock('../remote-exec/remote-exec-command', async (importOriginal) => ({
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: (...args: unknown[]) => execCommand(...args),
isUnconfirmedSshCommandTermination: (error: unknown) =>
(error as { sshChannelCloseConfirmed?: boolean } | null)?.sshChannelCloseConfirmed === false
}))
import {
isReapableRelayHusk,
parseRelayEndpointIncumbentProbe,
probeRelayEndpointIncumbent,
relayEndpointIncumbentProbeCommand
} from './ssh-relay-endpoint-incumbent'
import { getRemoteHostPlatform } from '../ssh-remote-platform'
import { unusedSshConnection } from '../unused-host-transport.test-fixture'
const connection = unusedSshConnection()
const SOCK = '/home/u/.orca-remote/relay-0.1.0+aaaa/relay-deadbeef.sock'
const POSIX_HOST = getRemoteHostPlatform('linux-x64')
const WINDOWS_HOST = getRemoteHostPlatform('win32-x64')
function probeOutput(lines: string[]): string {
return ['ORCA-INCUMBENT-BEGIN', ...lines, 'ORCA-INCUMBENT-END'].join('\n')
}
describe('parseRelayEndpointIncumbentProbe', () => {
it('reports live when the socket accepted a connection', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput([
'PRESENT=yes',
'LISTEN=accepted',
'HOLDERS_SOURCE=lsof',
'HOLDER=4242 yes 13 11'
])
)
expect(incumbent.verdict).toBe('live')
expect(incumbent.evidence).toBe('accepted-connection')
expect(incumbent.holders).toEqual([
{ pid: 4242, matchesRelayArgv: true, childCount: 13, unrecognizedChildCount: 11 }
])
})
it('reports live when a process still holds an inode that refuses connections', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=refused', 'HOLDERS_SOURCE=lsof', 'HOLDER=91 yes 2 2'])
)
expect(incumbent.verdict).toBe('live')
expect(incumbent.evidence).toBe('holder-process')
})
it('reports exited only when the connect was refused AND nothing holds the socket', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput([
'PRESENT=yes',
'LISTEN=refused',
'FS_TYPE=linux ef53 ef53',
'HOLDERS_SOURCE=lsof'
])
)
expect(incumbent.verdict).toBe('exited')
expect(incumbent.evidence).toBe('no-holder')
expect(incumbent.socketPresent).toBe(true)
})
it('reports exited for a refused socket on a macOS boot-volume filesystem or Linux tmpfs', () => {
for (const fsType of ['darwin 1a 1a', 'Linux 1021994 ef53', 'darwin-mount apfs']) {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=refused', `FS_TYPE=${fsType}`, 'HOLDERS_SOURCE=lsof'])
)
expect(incumbent.verdict).toBe('exited')
}
})
// Shared-home sockets may be held on another kernel; local refusal/no-holder evidence cannot prove exit.
it('reports unverifiable for a refused socket that another machine may hold', () => {
for (const fsType of [
'linux 6969 ef53', // nfs
'linux ff534d42 ef53', // cifs
'linux 65735546 ef53', // fuse (sshfs)
'darwin 1c 1a', // macOS nfs/smbfs
'darwin-mount nfs',
'darwin-mount smbfs',
'darwin-mount ', // no mount line matched
'linux ', // statfs unavailable
'freebsd 35 35'
]) {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=refused', `FS_TYPE=${fsType}`, 'HOLDERS_SOURCE=lsof'])
)
expect(incumbent.verdict).toBe('unverifiable')
}
const withoutType = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=refused', 'HOLDERS_SOURCE=lsof'])
)
expect(withoutType.verdict).toBe('unverifiable')
})
it('still reports exited for a socket path that no longer exists, on any filesystem', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=no', 'LISTEN=absent', 'FS_TYPE=linux 6969 ef53', 'HOLDERS_SOURCE=lsof'])
)
expect(incumbent.verdict).toBe('exited')
})
it('reports unverifiable when the host cannot enumerate socket holders', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=refused', 'HOLDERS_SOURCE=unavailable'])
)
expect(incumbent.verdict).toBe('unverifiable')
expect(incumbent.holdersEnumerable).toBe(false)
})
it('reports unverifiable when the connect probe timed out', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=unknown', 'HOLDERS_SOURCE=lsof'])
)
expect(incumbent.verdict).toBe('unverifiable')
})
it('reports unverifiable for truncated or garbled probe output', () => {
expect(parseRelayEndpointIncumbentProbe(SOCK, 'PRESENT=yes\nLISTEN=refused').verdict).toBe(
'unverifiable'
)
expect(parseRelayEndpointIncumbentProbe(SOCK, '').verdict).toBe('unverifiable')
})
it('drops holder lines that do not carry a usable pid', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput([
'PRESENT=yes',
'LISTEN=refused',
'FS_TYPE=linux ef53 ef53',
'HOLDERS_SOURCE=lsof',
'HOLDER=- no unknown unknown'
])
)
expect(incumbent.holders).toEqual([])
expect(incumbent.verdict).toBe('exited')
})
it('keeps an unreadable child count as null rather than zero', () => {
const [holder] = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput([
'PRESENT=yes',
'LISTEN=accepted',
'HOLDERS_SOURCE=lsof',
'HOLDER=7 yes unknown unknown'
])
).holders
expect(holder.childCount).toBeNull()
expect(holder.unrecognizedChildCount).toBeNull()
})
it('keeps a holder line with no unrecognized-child field unreapable', () => {
const incumbent = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=accepted', 'HOLDERS_SOURCE=lsof', 'HOLDER=7 yes 0'])
)
expect(incumbent.holders[0].unrecognizedChildCount).toBeNull()
expect(isReapableRelayHusk(incumbent)).toBe(false)
})
})
describe('probeRelayEndpointIncumbent', () => {
it('keeps the whole probe alive long enough to return a bounded lsof result', async () => {
execCommand.mockResolvedValueOnce(
probeOutput(['PRESENT=yes', 'LISTEN=refused', 'HOLDERS_SOURCE=unavailable'])
)
await probeRelayEndpointIncumbent(connection, POSIX_HOST, '/usr/bin/node', SOCK)
expect(execCommand).toHaveBeenCalledWith(expect.anything(), expect.any(String), {
wrapCommand: true,
signal: undefined
})
})
it('keeps a confirmed timeout or rejection unverifiable and unenumerable', async () => {
execCommand.mockRejectedValueOnce(
Object.assign(new Error('lsof timed out after 5s'), { sshChannelCloseConfirmed: true })
)
const incumbent = await probeRelayEndpointIncumbent(
connection,
POSIX_HOST,
'/usr/bin/node',
SOCK
)
expect(incumbent.verdict).toBe('unverifiable')
expect(incumbent.holdersEnumerable).toBe(false)
expect(incumbent.holders).toEqual([])
})
it('rethrows an unconfirmed termination instead of masking it as unverifiable', async () => {
const unconfirmed = Object.assign(new Error('remote channel close was not confirmed'), {
sshChannelCloseConfirmed: false
})
execCommand.mockRejectedValueOnce(unconfirmed)
await expect(
probeRelayEndpointIncumbent(connection, POSIX_HOST, '/usr/bin/node', SOCK)
).rejects.toBe(unconfirmed)
})
it('does not shell out on Windows hosts, where the endpoint is a named pipe', async () => {
execCommand.mockClear()
const incumbent = await probeRelayEndpointIncumbent(connection, WINDOWS_HOST, 'node.exe', SOCK)
expect(execCommand).not.toHaveBeenCalled()
expect(incumbent.verdict).toBe('unverifiable')
})
})
describe('relayEndpointIncumbentProbeCommand', () => {
it('ANDs the lsof selectors so it cannot match unrelated unix-socket holders', () => {
expect(RELAY_LSOF_PROBE_JS).toContain("['-t', '-a', '-U', process.argv[1]]")
})
it('never unlinks the relay endpoint', () => {
const command = relayEndpointIncumbentProbeCommand('/usr/bin/node', SOCK)
expect(command).not.toMatch(/\brm\b/)
})
it('bounds only lsof and keeps the connect-probe output available', () => {
const command = relayEndpointIncumbentProbeCommand('/usr/bin/node', SOCK)
expect(RELAY_LSOF_PROBE_JS).toContain("spawn('lsof'")
expect(command).toContain('}, 5000)')
expect(command).toContain("printf 'HOLDERS_SOURCE=unavailable\\n'")
expect(command.indexOf("printf 'LISTEN=%s\\n'")).toBeLessThan(command.indexOf('child = spawn('))
})
})
describe('isReapableRelayHusk', () => {
const husk = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=accepted', 'HOLDERS_SOURCE=lsof', 'HOLDER=500 yes 0 0'])
)
it('accepts a single proven relay holder with no unaccounted-for children', () => {
expect(isReapableRelayHusk(husk)).toBe(true)
})
it('accepts a relay whose only children are its own service processes (#13614)', () => {
const withServices = parseRelayEndpointIncumbentProbe(
SOCK,
probeOutput(['PRESENT=yes', 'LISTEN=accepted', 'HOLDERS_SOURCE=lsof', 'HOLDER=500 yes 2 0'])
)
expect(withServices.holders[0].childCount).toBe(2)
expect(isReapableRelayHusk(withServices)).toBe(true)
})
it('refuses a relay that still holds children it could not account for', () => {
expect(
isReapableRelayHusk({
...husk,
holders: [{ pid: 500, matchesRelayArgv: true, childCount: 3, unrecognizedChildCount: 1 }]
})
).toBe(false)
})
it('refuses a holder whose unrecognized-child count could not be read', () => {
expect(
isReapableRelayHusk({
...husk,
holders: [{ pid: 500, matchesRelayArgv: true, childCount: 0, unrecognizedChildCount: null }]
})
).toBe(false)
})
it('refuses a holder whose argv is not this relay at this socket', () => {
expect(
isReapableRelayHusk({
...husk,
holders: [{ pid: 500, matchesRelayArgv: false, childCount: 0, unrecognizedChildCount: 0 }]
})
).toBe(false)
})
it('refuses when more than one process holds the socket', () => {
expect(
isReapableRelayHusk({
...husk,
holders: [
{ pid: 500, matchesRelayArgv: true, childCount: 0, unrecognizedChildCount: 0 },
{ pid: 501, matchesRelayArgv: true, childCount: 0, unrecognizedChildCount: 0 }
]
})
).toBe(false)
})
it('refuses an unverifiable endpoint however empty it looks', () => {
expect(isReapableRelayHusk({ ...husk, verdict: 'unverifiable' })).toBe(false)
expect(isReapableRelayHusk({ ...husk, holdersEnumerable: false })).toBe(false)
})
})
-361
View File
@@ -1,361 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EventEmitter } from 'node:events'
import { delimiter } from 'node:path'
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import {
_resetHydrateShellPathCache,
hydrateShellPath,
mergePathSegments,
_setLaunchPathForTests,
type HydrationResult
} from './hydrate-shell-path'
const { spawnMock } = vi.hoisted(() => ({
spawnMock: vi.fn()
}))
vi.mock('child_process', () => ({
spawn: spawnMock
}))
type HydrationSpawner = (shell: string) => Promise<HydrationResult>
function createMockShellProcess(): ChildProcessWithoutNullStreams {
const proc = new EventEmitter() as ChildProcessWithoutNullStreams
Object.assign(proc, {
stdout: new EventEmitter(),
stderr: new EventEmitter(),
stdin: new EventEmitter(),
kill: vi.fn()
})
return proc
}
describe('hydrateShellPath', () => {
const originalPath = process.env.PATH
beforeEach(() => {
_resetHydrateShellPathCache()
spawnMock.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
if (originalPath === undefined) {
delete process.env.PATH
} else {
process.env.PATH = originalPath
}
})
it('invokes the provided shell with a custom spawner and returns its segments', async () => {
let capturedShell = ''
const result = await hydrateShellPath({
shellOverride: '/bin/zsh',
spawner: async (shell) => {
capturedShell = shell
return {
segments: ['/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin'],
ok: true,
failureReason: 'none'
}
}
})
expect(capturedShell).toBe('/bin/zsh')
expect(result.ok).toBe(true)
expect(result.segments).toEqual(['/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin'])
expect(result.failureReason).toBe('none')
})
it('caches the hydration result so repeated calls do not re-spawn', async () => {
let spawnCount = 0
const spawner: HydrationSpawner = async () => {
spawnCount += 1
return { segments: ['/a'], ok: true, failureReason: 'none' }
}
await hydrateShellPath({ shellOverride: '/bin/zsh', spawner })
await hydrateShellPath({ shellOverride: '/bin/zsh', spawner })
await hydrateShellPath({ shellOverride: '/bin/zsh', spawner })
expect(spawnCount).toBe(1)
})
it('re-spawns when force:true is passed — matches the Refresh button contract', async () => {
let spawnCount = 0
const spawner: HydrationSpawner = async () => {
spawnCount += 1
return { segments: ['/a'], ok: true, failureReason: 'none' }
}
await hydrateShellPath({ shellOverride: '/bin/zsh', spawner })
await hydrateShellPath({ shellOverride: '/bin/zsh', spawner, force: true })
expect(spawnCount).toBe(2)
})
it('continues the probe queue after a rejected hydration', async () => {
const rejectedSpawner = vi.fn<HydrationSpawner>(async () => {
throw new Error('profile failed')
})
const successfulSpawner = vi.fn<HydrationSpawner>(async () => ({
segments: ['/current'],
ok: true,
failureReason: 'none'
}))
await expect(
hydrateShellPath({ shellOverride: '/bin/zsh', spawner: rejectedSpawner, force: true })
).rejects.toThrow('profile failed')
await expect(
hydrateShellPath({ shellOverride: '/bin/bash', spawner: successfulSpawner, force: true })
).resolves.toEqual({ segments: ['/current'], ok: true, failureReason: 'none' })
expect(successfulSpawner).toHaveBeenCalledWith('/bin/bash')
})
it('re-probes after a rejected hydration instead of serving the cached rejection', async () => {
const spawner = vi.fn<HydrationSpawner>()
spawner
.mockRejectedValueOnce(new Error('profile failed'))
.mockResolvedValueOnce({ segments: ['/retried'], ok: true, failureReason: 'none' })
await expect(hydrateShellPath({ shellOverride: '/bin/zsh', spawner })).rejects.toThrow(
'profile failed'
)
await expect(hydrateShellPath({ shellOverride: '/bin/zsh', spawner })).resolves.toEqual({
segments: ['/retried'],
ok: true,
failureReason: 'none'
})
expect(spawner).toHaveBeenCalledTimes(2)
})
it('reports spawn_error instead of rejecting when spawn throws synchronously', async () => {
spawnMock.mockImplementation(() => {
throw Object.assign(new Error('spawn EACCES'), { code: 'EACCES' })
})
await expect(hydrateShellPath({ shellOverride: '/bin/zsh', force: true })).resolves.toEqual({
segments: [],
ok: false,
failureReason: 'spawn_error'
})
})
it('returns failureReason:no_shell when no shell is available (Windows path)', async () => {
const result = await hydrateShellPath({
shellOverride: null,
spawner: async () => {
throw new Error('spawner must not run when shell is null')
}
})
expect(result).toEqual({ segments: [], ok: false, failureReason: 'no_shell' })
})
// Keep failure classifications distinct; the spawner override isolates propagation from real-shell classification.
it('propagates failureReason:timeout from the spawner', async () => {
const result = await hydrateShellPath({
shellOverride: '/bin/zsh',
spawner: async () => ({ segments: [], ok: false, failureReason: 'timeout' })
})
expect(result).toEqual({ segments: [], ok: false, failureReason: 'timeout' })
})
it('propagates failureReason:spawn_error from the spawner', async () => {
const result = await hydrateShellPath({
shellOverride: '/bin/zsh',
spawner: async () => ({ segments: [], ok: false, failureReason: 'spawn_error' })
})
expect(result).toEqual({ segments: [], ok: false, failureReason: 'spawn_error' })
})
it('propagates failureReason:empty_path from the spawner', async () => {
const result = await hydrateShellPath({
shellOverride: '/bin/zsh',
spawner: async () => ({ segments: [], ok: false, failureReason: 'empty_path' })
})
expect(result).toEqual({ segments: [], ok: false, failureReason: 'empty_path' })
})
it('cleans up shell listeners when hydration times out', async () => {
vi.useFakeTimers()
const proc = createMockShellProcess()
spawnMock.mockReturnValue(proc)
try {
const resultPromise = hydrateShellPath({ shellOverride: '/bin/zsh', force: true })
const assertion = expect(resultPromise).resolves.toEqual({
segments: [],
ok: false,
failureReason: 'timeout'
})
await vi.advanceTimersByTimeAsync(10_000)
await assertion
expect(proc.kill).toHaveBeenCalledWith('SIGKILL')
expect(proc.stdout.listenerCount('data')).toBe(0)
expect(proc.listenerCount('error')).toBe(0)
expect(proc.listenerCount('close')).toBe(0)
} finally {
vi.useRealTimers()
}
})
async function captureProbeEnv(shell = '/bin/bash'): Promise<NodeJS.ProcessEnv> {
const proc = createMockShellProcess()
spawnMock.mockReturnValue(proc)
const resultPromise = hydrateShellPath({ shellOverride: shell, force: true })
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalled())
proc.emit('close', 0)
await resultPromise
return spawnMock.mock.calls[0][2].env
}
it('probes with the launch PATH so seeded fallbacks cannot pin nvm', async () => {
process.env.PATH = `/home/u/.nvm/versions/node/v26.7.0/bin${delimiter}/usr/bin`
_setLaunchPathForTests('/usr/bin')
expect((await captureProbeEnv()).PATH).toBe('/usr/bin')
})
it('uses the PATH captured at module load, not a later mutation of process.env', async () => {
// The probe must retain launch PATH even after packaged seeding mutates process.env.
const seeded = `/seeded/newest-nvm/bin${delimiter}/usr/bin`
process.env.PATH = seeded
expect((await captureProbeEnv()).PATH).not.toBe(seeded)
})
it('sets a probe marker so rc files can take a fast path', async () => {
expect((await captureProbeEnv()).ORCA_SHELL_PATH_PROBE).toBe('1')
})
it.skipIf(process.platform === 'win32').each(['/bin/bash', '/bin/zsh'])(
'keeps the POSIX login probe for %s',
async (shell) => {
await captureProbeEnv(shell)
expect(spawnMock.mock.calls[0][1]).toEqual([
'-ilc',
`printf '%s' '__ORCA_SHELL_PATH__'; printf '%s' "$PATH"; printf '%s' '__ORCA_SHELL_PATH__'`
])
}
)
it.skipIf(process.platform === 'win32')(
'reads Nushell PATH while preserving interactive login configuration',
async () => {
const proc = createMockShellProcess()
spawnMock.mockReturnValue(proc)
const resultPromise = hydrateShellPath({
shellOverride: '/run/current-system/sw/bin/nu',
force: true
})
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalled())
expect(spawnMock.mock.calls[0][1]).toEqual([
'-ilc',
[
"print -n '__ORCA_SHELL_PATH__';",
'print -n ($env.PATH | str join (char esep));',
"print -n '__ORCA_SHELL_PATH__'"
].join(' ')
])
proc.stdout.emit(
'data',
Buffer.from('__ORCA_SHELL_PATH__/etc/profiles/per-user/me/bin:/usr/bin__ORCA_SHELL_PATH__')
)
proc.emit('close', 0)
await expect(resultPromise).resolves.toEqual({
segments: ['/etc/profiles/per-user/me/bin', '/usr/bin'],
ok: true,
failureReason: 'none'
})
}
)
it('overwrites the captured key in place so Windows never carries both Path and PATH', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const originalWindowsPath = process.env.Path
delete process.env.PATH
process.env.PATH = 'C:\\Users\\u\\.nvm\\versions\\node\\v22.9.0\\bin;C:\\Windows'
process.env.Path = 'C:\\Users\\u\\.nvm\\versions\\node\\v22.9.0\\bin;C:\\Windows'
_setLaunchPathForTests('C:\\Windows', 'Path')
try {
const env = await captureProbeEnv('C:/Git/bin/bash.exe')
expect(env.Path).toBe('C:\\Windows')
expect(env.PATH).toBeUndefined()
} finally {
if (originalWindowsPath === undefined) {
delete process.env.Path
} else {
process.env.Path = originalWindowsPath
}
}
})
})
describe('mergePathSegments', () => {
const originalPath = process.env.PATH
beforeEach(() => {
_resetHydrateShellPathCache()
})
afterEach(() => {
if (originalPath === undefined) {
delete process.env.PATH
} else {
process.env.PATH = originalPath
}
})
// Use the host delimiter so this suite also runs on Windows.
const joinPath = (...segments: string[]): string => segments.join(delimiter)
it('prepends new segments ahead of existing PATH entries', () => {
process.env.PATH = joinPath('/usr/bin', '/bin')
const added = mergePathSegments(['/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin'])
expect(added).toEqual(['/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin'])
expect(process.env.PATH).toBe(
joinPath('/Users/tester/.opencode/bin', '/Users/tester/.cargo/bin', '/usr/bin', '/bin')
)
})
it('promotes shell segments already on PATH so shell ordering wins', () => {
process.env.PATH = joinPath('/Users/tester/.cargo/bin', '/usr/bin')
const added = mergePathSegments(['/Users/tester/.cargo/bin', '/Users/tester/.opencode/bin'])
expect(added).toEqual(['/Users/tester/.opencode/bin'])
expect(process.env.PATH).toBe(
joinPath('/Users/tester/.cargo/bin', '/Users/tester/.opencode/bin', '/usr/bin')
)
})
it('moves user-local shell paths ahead of packaged Homebrew fallbacks', () => {
process.env.PATH = joinPath('/opt/homebrew/bin', '/Users/tester/.local/bin', '/usr/bin', '/bin')
const added = mergePathSegments(['/Users/tester/.local/bin', '/opt/homebrew/bin'])
expect(added).toEqual([])
expect(process.env.PATH).toBe(
joinPath('/Users/tester/.local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin')
)
})
it('returns [] and leaves PATH untouched when given nothing', () => {
process.env.PATH = joinPath('/usr/bin', '/bin')
expect(mergePathSegments([])).toEqual([])
expect(process.env.PATH).toBe(joinPath('/usr/bin', '/bin'))
})
})
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EventEmitter } from 'node:events'
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import 'node:events'
import {
_resetHydrateShellPathCache,
configureWindowsShellPathHydration,
@@ -32,17 +32,6 @@ function deferred<T>(): Deferred<T> {
return { promise, resolve }
}
function createMockShellProcess(): ChildProcessWithoutNullStreams {
const proc = new EventEmitter() as ChildProcessWithoutNullStreams
Object.assign(proc, {
stdout: new EventEmitter(),
stderr: new EventEmitter(),
stdin: new EventEmitter(),
kill: vi.fn()
})
return proc
}
describe('Windows shell PATH hydration', () => {
const originalPath = process.env.PATH
@@ -61,171 +50,6 @@ describe('Windows shell PATH hydration', () => {
}
})
it('hydrates the default PowerShell profile PATH on Windows (#13328)', async () => {
const spawner = vi.fn<HydrationSpawner>(async () => ({
segments: ['C:\\Users\\tester\\AppData\\Local\\fnm'],
ok: true,
failureReason: 'none'
}))
await expect(hydrateShellPath({ spawner })).resolves.toEqual({
segments: ['C:\\Users\\tester\\AppData\\Local\\fnm'],
ok: true,
failureReason: 'none'
})
expect(spawner).toHaveBeenCalledWith('powershell.exe')
})
it('uses the configured PowerShell executable', async () => {
const spawner = vi.fn<HydrationSpawner>(async () => ({
segments: ['C:\\tools'],
ok: true,
failureReason: 'none'
}))
configureWindowsShellPathHydration('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
await hydrateShellPath({ spawner })
expect(spawner).toHaveBeenCalledWith('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
})
it('falls back to Windows PowerShell when preferred pwsh cannot spawn', async () => {
const spawner = vi.fn<HydrationSpawner>()
spawner
.mockResolvedValueOnce({ segments: [], ok: false, failureReason: 'spawn_error' })
.mockResolvedValueOnce({
segments: ['C:\\WindowsPowerShell-profile'],
ok: true,
failureReason: 'none'
})
configureWindowsShellPathHydration('pwsh.exe', null, 'powershell.exe')
await expect(hydrateShellPath({ spawner })).resolves.toEqual({
segments: ['C:\\WindowsPowerShell-profile'],
ok: true,
failureReason: 'none'
})
expect(spawner.mock.calls).toEqual([['pwsh.exe'], ['powershell.exe']])
})
it('falls back to Windows PowerShell when pwsh throws synchronously at spawn', async () => {
const proc = createMockShellProcess()
spawnMock
.mockImplementationOnce(() => {
throw Object.assign(new Error('spawn EPERM'), { code: 'EPERM' })
})
.mockReturnValueOnce(proc)
configureWindowsShellPathHydration('pwsh.exe', null, 'powershell.exe')
const resultPromise = hydrateShellPath()
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledTimes(2))
proc.stdout.emit('data', Buffer.from('__ORCA_SHELL_PATH__C:\\inbox-ps__ORCA_SHELL_PATH__'))
proc.emit('close', 0, null)
await expect(resultPromise).resolves.toEqual({
segments: ['C:\\inbox-ps'],
ok: true,
failureReason: 'none'
})
expect(spawnMock.mock.calls.map((call) => call[0])).toEqual(['pwsh.exe', 'powershell.exe'])
})
it('reports spawn_error when every shell throws synchronously at spawn', async () => {
spawnMock.mockImplementation(() => {
throw Object.assign(new Error('spawn EPERM'), { code: 'EPERM' })
})
configureWindowsShellPathHydration('pwsh.exe', null, 'powershell.exe')
await expect(hydrateShellPath()).resolves.toEqual({
segments: [],
ok: false,
failureReason: 'spawn_error'
})
})
it('uses the resolved Git Bash executable for the configured sentinel', async () => {
const spawner = vi.fn<HydrationSpawner>(async () => ({
segments: ['C:\\tools'],
ok: true,
failureReason: 'none'
}))
configureWindowsShellPathHydration('git-bash', 'C:\\Program Files\\Git\\bin\\bash.exe')
await hydrateShellPath({ spawner })
expect(spawner).toHaveBeenCalledWith('C:\\Program Files\\Git\\bin\\bash.exe')
})
it('loads PowerShell profiles and parses their Windows PATH', async () => {
const proc = createMockShellProcess()
spawnMock.mockReturnValue(proc)
const resultPromise = hydrateShellPath({ force: true })
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledOnce())
proc.stdout.emit(
'data',
Buffer.from(
'profile banner\r\n__ORCA_SHELL_PATH__C:\\profile-node;C:\\Windows__ORCA_SHELL_PATH__'
)
)
proc.emit('close', 0, null)
await expect(resultPromise).resolves.toEqual({
segments: ['C:\\profile-node', 'C:\\Windows'],
ok: true,
failureReason: 'none'
})
expect(spawnMock).toHaveBeenCalledWith(
'powershell.exe',
['-NoLogo', '-Command', expect.stringContaining('$env:Path')],
expect.objectContaining({ windowsHide: true })
)
expect(spawnMock.mock.calls[0]?.[1]).not.toContain('-NoProfile')
})
it('converts a Git Bash PATH to Windows segments before parsing it', async () => {
const proc = createMockShellProcess()
spawnMock.mockReturnValue(proc)
const resultPromise = hydrateShellPath({
shellOverride: 'C:\\Program Files\\Git\\bin\\bash.exe',
force: true
})
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledOnce())
proc.stdout.emit(
'data',
Buffer.from('__ORCA_SHELL_PATH__C:\\git-node;C:\\Windows__ORCA_SHELL_PATH__')
)
proc.emit('close', 0, null)
await expect(resultPromise).resolves.toEqual({
segments: ['C:\\git-node', 'C:\\Windows'],
ok: true,
failureReason: 'none'
})
expect(spawnMock).toHaveBeenCalledWith(
'C:\\Program Files\\Git\\bin\\bash.exe',
['-ilc', expect.stringContaining('cygpath -wp "$PATH"')],
expect.objectContaining({ windowsHide: true })
)
})
it.each(['cmd.exe', 'wsl.exe', 'git-bash', 'C:\\Tools\\zsh.exe'])(
'does not merge %s guest or profile-less paths',
async (shell) => {
const spawner = vi.fn<HydrationSpawner>()
configureWindowsShellPathHydration(shell)
await expect(hydrateShellPath({ spawner })).resolves.toEqual({
segments: [],
ok: false,
failureReason: 'no_shell'
})
expect(spawner).not.toHaveBeenCalled()
}
)
it('serializes shell changes, discards stale results, and caches the newest request', async () => {
const powerShellResult = deferred<HydrationResult>()
const gitBashResult = deferred<HydrationResult>()
@@ -297,20 +121,6 @@ describe('Windows shell PATH hydration', () => {
expect(spawner).toHaveBeenCalledOnce()
})
it('deduplicates PATH entries case-insensitively', () => {
process.env.PATH = 'C:\\Tools;C:\\Windows'
expect(mergePathSegments(['c:\\tools', 'C:\\profile-node'])).toEqual(['C:\\profile-node'])
expect(process.env.PATH).toBe('c:\\tools;C:\\profile-node;C:\\Windows')
})
it('deduplicates PATH entries with trailing slashes', () => {
process.env.PATH = 'C:\\Windows\\;C:\\Tools'
expect(mergePathSegments(['C:\\Windows', 'C:\\profile-node\\'])).toEqual(['C:\\profile-node\\'])
expect(process.env.PATH).toBe('C:\\Windows;C:\\profile-node\\;C:\\Tools')
})
it('removes profile entries when switching to a shell without a profile', () => {
process.env.PATH = 'C:\\Inherited;C:\\Windows'
mergePathSegments(['C:\\PowerShell-profile', 'C:\\Inherited'])
@@ -1,7 +1,7 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it } from 'vitest'
import { prepareLocalCommitMessageAgentEnv } from './commit-message-agent-environment'
import { ManagedCodexHomeTemporarilyUnavailableError } from '../codex-accounts/host-codex-managed-home-ownership'
@@ -51,20 +51,6 @@ describe('prepareLocalCommitMessageAgentEnv', () => {
})
})
it('prefers the original OpenCode config root over inherited PTY overlays', async () => {
process.env.OPENCODE_CONFIG_DIR = '/tmp/orca-opencode-overlay'
process.env.ORCA_OPENCODE_SOURCE_CONFIG_DIR = '/Users/tester/company/opencode'
const result = await prepareLocalCommitMessageAgentEnv('opencode', undefined)
expect(result).toEqual({
ok: true,
env: expect.objectContaining({
OPENCODE_CONFIG_DIR: '/Users/tester/company/opencode'
})
})
})
it('hydrates Pi agent dir from shell startup files for headless generation', async () => {
const home = makeHome()
delete process.env.PI_CODING_AGENT_DIR
@@ -80,51 +66,6 @@ describe('prepareLocalCommitMessageAgentEnv', () => {
})
})
it('prefers the original Pi agent root over inherited PTY overlays', async () => {
process.env.PI_CODING_AGENT_DIR = '/tmp/orca-pi-overlay'
process.env.ORCA_PI_SOURCE_AGENT_DIR = '/Users/tester/.pi/agent'
const result = await prepareLocalCommitMessageAgentEnv('pi', undefined)
expect(result).toEqual({
ok: true,
env: expect.objectContaining({
PI_CODING_AGENT_DIR: '/Users/tester/.pi/agent'
})
})
})
it('does not synthesize env for agents without shell-scoped auth or config roots', async () => {
makeHome()
await expect(prepareLocalCommitMessageAgentEnv('cursor', undefined)).resolves.toEqual({
ok: true
})
})
it('falls back to inherited env when managed account resolvers are unavailable', async () => {
await expect(prepareLocalCommitMessageAgentEnv('codex', undefined)).resolves.toEqual({
ok: true
})
await expect(prepareLocalCommitMessageAgentEnv('claude', undefined)).resolves.toEqual({
ok: true
})
})
it('sets CODEX_HOME for host managed Codex accounts', async () => {
const result = await prepareLocalCommitMessageAgentEnv('codex', {
prepareForCodexLaunch: () =>
'C:\\Users\\tester\\AppData\\Roaming\\Orca\\codex-accounts\\a\\home'
})
expect(result).toEqual({
ok: true,
env: expect.objectContaining({
CODEX_HOME: 'C:\\Users\\tester\\AppData\\Roaming\\Orca\\codex-accounts\\a\\home'
})
})
})
// STA-4422: failed managed-home preparation must not fall through to the user's real account while UI shows a managed account.
it('fails commit generation instead of building a system-account env when the managed home is unreadable', async () => {
process.env.CODEX_HOME = '/home/me/.config/codex'
@@ -157,180 +98,4 @@ describe('prepareLocalCommitMessageAgentEnv', () => {
expect(env?.CODEX_HOME).toBeUndefined()
expect(env?.ORCA_CODEX_HOME).toBeUndefined()
})
it('preserves a user-owned CODEX_HOME when the launch resolves to the real home', async () => {
process.env.CODEX_HOME = '/home/me/.config/codex'
delete process.env.ORCA_CODEX_HOME
const result = await prepareLocalCommitMessageAgentEnv('codex', {
prepareForCodexLaunch: () => null
})
expect(result.ok).toBe(true)
const env = (result as { ok: true; env?: NodeJS.ProcessEnv }).env
expect(env?.CODEX_HOME).toBe('/home/me/.config/codex')
})
it('does not pass WSL managed Codex homes to host-local commit generation', async () => {
process.env.CODEX_HOME = 'C:\\Users\\tester\\.codex'
const result = await prepareLocalCommitMessageAgentEnv('codex', {
prepareForCodexLaunch: () =>
'\\\\wsl.localhost\\Ubuntu\\home\\tester\\.local\\share\\orca\\codex-accounts\\a\\home'
})
expect(result).toEqual({ ok: true })
})
it('passes WSL managed Codex homes as Linux paths for WSL-local commit generation', async () => {
const result = await prepareLocalCommitMessageAgentEnv(
'codex',
{
prepareForCodexLaunch: (target) => {
expect(target).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
return '\\\\wsl.localhost\\Ubuntu\\home\\tester\\.codex'
}
},
{ runtime: 'wsl', wslDistro: 'Ubuntu' }
)
expect(result).toEqual({
ok: true,
env: expect.objectContaining({
CODEX_HOME: '/home/tester/.codex'
})
})
})
it('does not hydrate host shell config roots for WSL-local commit generation', async () => {
process.env.OPENCODE_CONFIG_DIR = 'C:\\Users\\tester\\opencode'
const result = await prepareLocalCommitMessageAgentEnv('opencode', undefined, {
runtime: 'wsl',
wslDistro: 'Ubuntu'
})
expect(result).toEqual({ ok: true })
})
describe('login-shell base environment', () => {
const envOf = (
result: Awaited<ReturnType<typeof prepareLocalCommitMessageAgentEnv>>
): NodeJS.ProcessEnv | undefined => (result.ok ? result.env : undefined)
const shellBase = {
PATH: '/shell/bin',
ANTHROPIC_BASE_URL: 'https://proxy.example',
ANTHROPIC_AUTH_TOKEN: 'shell-token'
}
it('gives Claude the shell-exported variables Orca was launched without', async () => {
delete process.env.ANTHROPIC_BASE_URL
delete process.env.ANTHROPIC_AUTH_TOKEN
const result = await prepareLocalCommitMessageAgentEnv('claude', {
resolveBaseEnvironment: async () => shellBase,
prepareForClaudeLaunch: async () => ({
configDir: '/home/me/.claude',
envPatch: {},
provenance: 'system'
})
})
expect(result).toEqual({ ok: true, env: shellBase })
})
it('lets the selected Claude account set its folder and keeps the shell auth', async () => {
const result = await prepareLocalCommitMessageAgentEnv('claude', {
resolveBaseEnvironment: async () => shellBase,
prepareForClaudeLaunch: async () => ({
configDir: '/managed/claude',
envPatch: { CLAUDE_CONFIG_DIR: '/managed/claude' },
provenance: 'managed:account-1'
})
})
const env = envOf(result)
expect(env?.CLAUDE_CONFIG_DIR).toBe('/managed/claude')
expect(env?.ANTHROPIC_AUTH_TOKEN).toBe('shell-token')
expect(env?.PATH).toBe('/shell/bin')
})
it('lets the managed Codex home override a shell CODEX_HOME', async () => {
const result = await prepareLocalCommitMessageAgentEnv('codex', {
resolveBaseEnvironment: async () => ({ ...shellBase, CODEX_HOME: '/shell/codex' }),
prepareForCodexLaunch: () => '/managed/codex-home'
})
expect(result).toEqual({
ok: true,
env: { ...shellBase, CODEX_HOME: '/managed/codex-home' }
})
})
it('gives agents without an account resolver the shell environment', async () => {
makeHome()
delete process.env.OPENCODE_CONFIG_DIR
await expect(
prepareLocalCommitMessageAgentEnv('cursor', {
resolveBaseEnvironment: async () => shellBase
})
).resolves.toEqual({ ok: true, env: shellBase })
await expect(
prepareLocalCommitMessageAgentEnv('opencode', {
resolveBaseEnvironment: async () => shellBase
})
).resolves.toEqual({ ok: true, env: shellBase })
})
it("prefers the login shell's config dir over Orca's inherited one", async () => {
makeHome()
process.env.OPENCODE_CONFIG_DIR = '/old/opencode'
delete process.env.ORCA_OPENCODE_SOURCE_CONFIG_DIR
const result = await prepareLocalCommitMessageAgentEnv('opencode', {
resolveBaseEnvironment: async () => ({ ...shellBase, OPENCODE_CONFIG_DIR: '/new/opencode' })
})
expect(envOf(result)?.OPENCODE_CONFIG_DIR).toBe('/new/opencode')
})
it('keeps Orca env for WSL-local generation instead of the host shell', async () => {
delete process.env.ANTHROPIC_BASE_URL
const resolveBaseEnvironment = vi.fn(async () => shellBase)
const codex = await prepareLocalCommitMessageAgentEnv(
'codex',
{ resolveBaseEnvironment, prepareForCodexLaunch: () => null },
{ runtime: 'wsl', wslDistro: 'Ubuntu' }
)
const cursor = await prepareLocalCommitMessageAgentEnv(
'cursor',
{ resolveBaseEnvironment },
{ runtime: 'wsl', wslDistro: 'Ubuntu' }
)
expect(resolveBaseEnvironment).not.toHaveBeenCalled()
expect(envOf(codex)?.ANTHROPIC_BASE_URL).toBeUndefined()
expect(cursor).toEqual({ ok: true })
})
it('falls back to Orca env when the shell environment cannot be read', async () => {
process.env.ORCA_COMMIT_ENV_MARKER = 'process'
const result = await prepareLocalCommitMessageAgentEnv('codex', {
resolveBaseEnvironment: async () => {
throw new Error('settings unavailable')
},
prepareForCodexLaunch: () => '/managed/codex-home'
})
expect(envOf(result)).toEqual(
expect.objectContaining({
ORCA_COMMIT_ENV_MARKER: 'process',
CODEX_HOME: '/managed/codex-home'
})
)
})
})
})
@@ -1,327 +0,0 @@
import { sourceShapedKimiOutput } from '../../shared/__fixtures__/kimi-generation-source-shaped'
import { describe, expect, it } from 'vitest'
import { ChildProcess } from 'node:child_process'
import { PassThrough } from 'node:stream'
import { generateCommitMessage } from './source-control-text-generation-requests'
import type {
CommitMessageGenerationTarget,
SpawnSourceControlAgent
} from './source-control-text-generation-types'
import { getAgentModelProbeSpec } from '../../shared/agent-model-probe-spec'
import { planModelDiscovery } from './commit-message-model-discovery-policy'
import { planCommitMessageGeneration } from '../../shared/source-control/ai/generation/commit-message-plan'
import type { CommitMessagePlan } from '../../shared/source-control/ai/generation/commit-message-plan'
import { generateJiraIssueSummaryFromContext } from './jira-issue-summary-text-generation'
function remoteTarget(execute: (plan: CommitMessagePlan) => Promise<string>) {
return {
kind: 'remote' as const,
cwd: '/home/user',
missingBinaryLocation: 'remote PATH',
execute: async (plan: CommitMessagePlan) => ({
stdout: await execute(plan),
stderr: '',
exitCode: 0,
timedOut: false
})
}
}
describe('generateJiraIssueSummaryFromContext', () => {
it.each(['custom', 'kimi'] as const)('%s Jira summary', async (agentId) => {
const result = await generateJiraIssueSummaryFromContext(
{ description: 'Login crashes when retried offline.' },
{ agentId, model: agentId === 'kimi' ? 'default' : '', customAgentCommand: 'agent' },
remoteTarget(async () =>
agentId === 'kimi'
? sourceShapedKimiOutput('"Fix offline login retry crash"\nextra commentary')
: '"Fix offline login retry crash"\nextra commentary'
)
)
expect(result).toEqual({
success: true,
summary: 'Fix offline login retry crash',
agentLabel: agentId === 'kimi' ? 'Kimi' : 'agent'
})
})
it('sends its own Jira prompt and ignores source-control templates and instructions', async () => {
let prompt = ''
await generateJiraIssueSummaryFromContext(
{
description: 'Crash on save',
projectName: 'Mobile App',
issueTypeName: 'Bug'
},
{
agentId: 'custom',
model: '',
customAgentCommand: 'agent',
customPrompt: 'Prefer kebab-case names.',
commandInputTemplate: 'Prefer kebab-case names.\n\n{basePrompt}'
},
remoteTarget(async (plan) => {
prompt = plan.stdinPayload ?? ''
return 'Fix crash on save'
})
)
expect(prompt).toContain('Generate a concise Jira issue summary')
expect(prompt).toContain('Project: <project>Mobile App</project>')
expect(prompt).toContain('Issue type: <issue_type>Bug</issue_type>')
expect(prompt).toContain('Crash on save')
expect(prompt).not.toContain('kebab-case')
})
it('fails when the output sanitizes to an empty summary', async () => {
const result = await generateJiraIssueSummaryFromContext(
{ description: 'Crash on save' },
{ agentId: 'custom', model: '', customAgentCommand: 'agent' },
remoteTarget(async () => '""\n')
)
expect(result).toMatchObject({
success: false,
error: 'Generated summary was empty after sanitization.'
})
})
it('propagates agent failures', async () => {
const result = await generateJiraIssueSummaryFromContext(
{ description: 'Crash on save' },
{ agentId: 'custom', model: '', customAgentCommand: 'agent' },
{
kind: 'remote',
cwd: '/home/user',
missingBinaryLocation: 'remote PATH',
execute: async () => ({
stdout: '',
stderr: 'No API key found.',
exitCode: 1,
timedOut: false
})
}
)
expect(result).toMatchObject({
success: false,
error: 'agent CLI command failed with code 1: No API key found.'
})
})
})
// One owner must cover Jira and ordinary generation through both existing transports.
it.each(['local', 'remote'] as const)(
'retries the same Claude invocation for Jira and commits on %s',
async (kind) => {
for (const operation of ['jira-issue-summary', 'commit-message'] as const) {
const calls: { args: string[]; other: unknown }[] = []
const response = (args: string[]) => {
const refused = args.includes('--strict-mcp-config')
return {
stdout: refused ? '' : 'Fix crash',
stderr: refused
? 'You cannot use --strict-mcp-config when an enterprise MCP config is present'
: '',
exitCode: refused ? 1 : 0,
timedOut: false
}
}
const spawnAgent: SpawnSourceControlAgent = ({ args, ...other }) => {
calls.push({ args, other })
const child = new (class extends ChildProcess {
override stdin = new PassThrough()
override stdout = new PassThrough()
override stderr = new PassThrough()
})()
queueMicrotask(() => {
const output = response(args)
child.stdout.emit('data', Buffer.from(output.stdout))
child.stderr.emit('data', Buffer.from(output.stderr))
child.emit('close', output.exitCode)
})
return child
}
const target: CommitMessageGenerationTarget =
kind === 'local'
? { kind, cwd: '/repo' }
: {
kind,
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
execute: async ({ args, ...other }, cwd, timeout, op) => {
expect(op).toBe(operation)
calls.push({ args, other: { ...other, cwd, timeout, op } })
return response(args)
}
}
const params = { agentId: 'claude' as const, model: 'haiku' }
const result =
operation === 'jira-issue-summary'
? await generateJiraIssueSummaryFromContext(
{ description: 'Crash on save' },
params,
target,
spawnAgent
)
: await generateCommitMessage({
context: { branch: 'main', stagedSummary: 'M file', stagedPatch: '+fix' },
params,
target,
spawnAgent
})
expect(result.success).toBe(true)
expect(calls).toHaveLength(2)
// The retry loads managed MCP servers, so it denies their tools; nothing else changes.
const retryArgs = calls[0].args.filter((arg) => arg !== '--strict-mcp-config')
retryArgs.splice(retryArgs.indexOf('--permission-mode'), 0, '--disallowedTools', 'mcp__*')
expect(calls[1]).toEqual({ ...calls[0], args: retryArgs })
}
}
)
it('keeps the picked model and a non-plan mode on an old CLI under enterprise MCP', async () => {
const calls: CommitMessagePlan[] = []
const params = { agentId: 'claude' as const, model: 'haiku' }
const result = await generateCommitMessage({
context: { branch: 'main', stagedSummary: 'M file', stagedPatch: '+fix' },
params,
target: {
kind: 'remote',
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
// Answers like CLI 2.0.30 with a managed MCP config.
execute: async (plan) => {
calls.push(plan)
const stderr = plan.args.includes('dontAsk')
? "error: option '--permission-mode <mode>' argument 'dontAsk' is invalid. Allowed choices are acceptEdits, bypassPermissions, default, plan.\n"
: plan.args.includes('--strict-mcp-config')
? 'You cannot use --strict-mcp-config when an enterprise MCP config is present'
: ''
return {
stdout: stderr ? '' : 'Fix crash',
stderr,
exitCode: stderr ? 1 : 0,
timedOut: false
}
}
},
spawnAgent: () => {
throw new Error('A remote target spawns nothing locally')
}
})
expect(result).toMatchObject({ success: true, message: 'Fix crash' })
const prompt = calls[0].stdinPayload ?? ''
const expected = [
planCommitMessageGeneration(params, prompt),
planCommitMessageGeneration(params, prompt, { legacyToolPolicy: true }),
planCommitMessageGeneration(params, prompt, { legacyToolPolicy: true, strictMcp: false })
].map((planned) => (planned.ok ? planned.plan : null))
expect(calls).toEqual(expected)
for (const plan of calls) {
expect(plan.args[plan.args.indexOf('--model') + 1]).toBe('haiku')
expect(plan.args).not.toContain('plan')
}
expect(calls[2].args[calls[2].args.indexOf('--permission-mode') + 1]).toBe('default')
// Every rebuilt attempt keeps its read grant.
expect(calls.map((plan) => plan.args[plan.args.indexOf('--allowedTools') + 1])).toEqual([
'Read,Grep,Glob',
'Read(//**),Grep(//**),Glob(//**)',
'Read(//**),Grep(//**),Glob(//**)'
])
})
it('preserves Jira custom output and OpenCode one-attempt behavior', async () => {
expect(
await generateJiraIssueSummaryFromContext(
{ description: 'Crash' },
{ agentId: 'custom', model: '', customAgentCommand: 'agent' },
remoteTarget(async () => 'Draft thought</think>\nFix crash')
)
).toMatchObject({ summary: 'Draft thought</think>' })
let attempts = 0
const result = await generateJiraIssueSummaryFromContext(
{ description: 'Crash' },
{ agentId: 'opencode', model: 'provider/model', thinkingLevel: 'high' },
{
kind: 'remote',
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
execute: async () => {
attempts++
return { stdout: '', stderr: 'Unknown argument: variant', exitCode: 1, timedOut: false }
}
}
)
expect(result.success).toBe(false)
expect(attempts).toBe(1)
})
it.each(['\'{"model":"haiku"}\'', 'provider-settings.json'])(
'uses existing settings override for generation and discovery: %s',
(settings) => {
const override = `claude --settings ${settings}`
const spec = getAgentModelProbeSpec('claude')
if (!spec) {
throw new Error('Missing Claude spec')
}
const requests = [
planModelDiscovery(spec, override),
planCommitMessageGeneration(
{ agentId: 'claude', model: 'haiku', agentCommandOverride: override },
'prompt'
),
planCommitMessageGeneration(
{ agentId: 'claude', model: 'haiku', agentArgs: `--settings ${settings}` },
'prompt'
)
]
for (const planned of requests) {
if (!planned.ok) {
throw new Error(planned.error)
}
expect(planned.plan.args.filter((arg) => arg === '--settings')).toHaveLength(1)
expect(planned.plan.args[planned.plan.args.indexOf('--settings') + 1]).toBe(
settings.replaceAll("'", '')
)
expect(planned.plan.args).not.toContain('{"disableAllHooks":true}')
}
}
)
it('rebuilds without inspecting prompt-like, optional or user strict tokens', async () => {
for (const agentArgs of [
"--append-subagent-system-prompt '--strict-mcp-config'",
'--resume --strict-mcp-config saved-session',
'--mcp-config custom.json --strict-mcp-config'
]) {
const calls: CommitMessagePlan[] = []
const params = { agentId: 'claude' as const, model: 'haiku', agentArgs }
const result = await generateJiraIssueSummaryFromContext({ description: 'Crash' }, params, {
kind: 'remote',
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
execute: async (plan) => {
calls.push(plan)
return {
stdout: '',
stderr: 'You cannot use --strict-mcp-config when an enterprise MCP config is present',
exitCode: 1,
timedOut: false
}
}
})
expect(result.success).toBe(false)
expect(calls).toHaveLength(2)
const first = planCommitMessageGeneration(params, calls[0].stdinPayload ?? '')
const second = planCommitMessageGeneration(params, calls[0].stdinPayload ?? '', {
strictMcp: false
})
if (!first.ok || !second.ok) {
throw new Error('Invalid fixture')
}
expect(calls).toEqual([first.plan, second.plan])
expect(calls[1].args).toContain('--strict-mcp-config')
}
})
@@ -132,12 +132,6 @@ vi.mock('./linux-package/linux-update-package-type', () => ({
isExternallyManagedLinuxInstall: () => false
}))
const DEB_ELEVATION_ERROR =
'Error: Command failed: /usr/bin/pkexec --disable-internal-agent "/bin/bash" "-c" "dpkg -i \'/home/u/.cache/orca-updater/pending/orca-ide_1.4.163_amd64.deb\'"\npkexec must be setuid root'
const WINDOWS_SIGNATURE_MISMATCH_ERROR =
'New version 1.4.163 is not signed by the application owner: publisherNames: Orca, Inc.'
type CapturedSpan = {
readonly name: string
readonly exit: { readonly _tag: string; readonly cause?: string }
@@ -162,10 +156,6 @@ function capturingSink(): TracerModule.TracerSink {
}
}
function installSpan(): CapturedSpan | undefined {
return spans.find((span) => span.name === 'updater.install')
}
async function reachDownloaded(): Promise<typeof UpdaterModule> {
const mainWindow = updaterMainWindow(vi.fn())
autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined)
@@ -225,100 +215,6 @@ describe('quitAndInstall failure carries the updater cause', () => {
tracer = null
})
it('reports the underlying elevation failure rather than a generic restart message', async () => {
const { quitAndInstall, getUpdateStatus } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
autoUpdaterMock.emit('error', new Error(DEB_ELEVATION_ERROR))
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
})
const status = getUpdateStatus()
expect(status.state).toBe('error')
const message = status.state === 'error' ? status.message : ''
expect(message).toContain('pkexec')
expect(message).toContain('Could not start the update installer.')
})
it('keeps the durable breadcrumb to classification only', async () => {
const { quitAndInstall } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
autoUpdaterMock.emit('error', new Error(DEB_ELEVATION_ERROR))
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
})
expect(recordUpdaterLifecycleMock).toHaveBeenCalledWith(
'quit_and_install_failed_via_event',
{ errorType: 'Error' },
expect.anything()
)
})
it('exits the install span Failure with the cause', async () => {
const { quitAndInstall } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
autoUpdaterMock.emit('error', new Error(DEB_ELEVATION_ERROR))
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(installSpan()).toBeDefined()
})
expect(installSpan()?.exit._tag).toBe('Failure')
expect(installSpan()?.exit.cause).toContain('pkexec')
})
it('exits the install span Success when the installer takes over', async () => {
const { quitAndInstall } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
// The installer runs and the old process is left to exit; no 'error' comes back.
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(installSpan()).toBeDefined()
})
expect(installSpan()?.exit._tag).toBe('Success')
})
it('keeps a Windows signature verdict unprefixed so its own card still renders', async () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true
})
const { quitAndInstall, getUpdateStatus } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
autoUpdaterMock.emit('error', new Error(WINDOWS_SIGNATURE_MISMATCH_ERROR))
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
})
const status = getUpdateStatus()
expect(status.state === 'error' ? status.message : '').toBe(WINDOWS_SIGNATURE_MISMATCH_ERROR)
})
it('redacts the cause the same way the retained-package card does', async () => {
const { quitAndInstall, getUpdateStatus } = await reachDownloaded()
const home = os.homedir()
@@ -343,23 +239,4 @@ describe('quitAndInstall failure carries the updater cause', () => {
expect(message).not.toContain(escape)
expect(message).toContain("<home>/.cache/orca-updater/pending/orca.deb'")
})
it('carries the cause when quitAndInstall throws instead of dispatching an error event', async () => {
const { quitAndInstall, getUpdateStatus } = await reachDownloaded()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
throw new Error('Squirrel.framework is missing from the app bundle')
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
await vi.waitFor(() => {
expect(getUpdateStatus().state).toBe('error')
})
const status = getUpdateStatus()
const message = status.state === 'error' ? status.message : ''
expect(message).toContain('Squirrel.framework is missing from the app bundle')
expect(message).toContain('Could not start the update installer.')
})
})
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PostHog } from 'posthog-node'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { agentTokenUsageSchema } from '../../shared/telemetry-agent-token-usage-schema'
import '../../shared/telemetry-agent-token-usage-schema'
import {
_enableTransportForTests,
_setPostHogClientForTests,
@@ -112,31 +112,6 @@ describe('agent token usage', () => {
expect(captures()).toEqual([])
})
it('resolves every session identity in one batch', async () => {
const write = vi.spyOn(UsageCacheSnapshotWriter.prototype, 'write')
const second = '00000000-0000-4000-8000-000000000002'
identity.mockImplementation(async (ids) =>
ids.map((id) => (id === 'second-session' ? second : ID))
)
const instance = reporter()
await instance.report([
row,
{ ...row, providerSessionId: 'invalid-session', input_tokens: -1 },
{ ...row, providerSessionId: 'second-session', input_tokens: 7 }
])
expect(identity).toHaveBeenCalledOnce()
expect(identity).toHaveBeenCalledWith([row.providerSessionId, 'second-session'])
expect(captures()).toEqual([
expect.objectContaining({ analytics_session_id: ID, input_tokens: 100 }),
expect.objectContaining({ analytics_session_id: second, input_tokens: 7 })
])
expect(write).toHaveBeenCalledTimes(1)
write.mockClear()
await instance.report([])
expect(captures()).toHaveLength(2)
expect(write).not.toHaveBeenCalled()
})
it('does not publish a revision whose write failed and retries it', async () => {
const instance = reporter()
const write = vi
@@ -168,45 +143,6 @@ describe('agent token usage', () => {
}
)
it('deduplicates acceptance in memory when only part of a batch is accepted', async () => {
vi.spyOn(Date, 'now').mockReturnValue(1_800_000_000_000)
identity.mockImplementation(async (ids) =>
ids.map((_, index) => `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`)
)
const rows = Array.from({ length: 31 }, (_, index) => ({
...row,
providerSessionId: `session-${index}`
}))
const instance = reporter()
const write = vi.spyOn(UsageCacheSnapshotWriter.prototype, 'write')
await instance.report(rows)
expect(captures()).toHaveLength(30)
expect(write).toHaveBeenCalledTimes(1)
const persisted = readFileSync(file, 'utf8')
resetBurstCapsForSession()
await instance.report([])
expect(write).toHaveBeenCalledTimes(1)
expect(readFileSync(file, 'utf8')).toBe(persisted)
expect(captures()).toHaveLength(31)
expect(captures().at(-1)).toMatchObject({
analytics_session_id: '00000000-0000-4000-8000-000000000031',
revision: 1
})
})
it('retries a throwing capture without inventing a new revision', async () => {
telemetry.mock.capture.mockImplementationOnce(() => {
throw new Error('enqueue failed')
})
const instance = reporter()
await expect(instance.report([row])).rejects.toThrow('enqueue failed')
await instance.report([row])
expect(captures()).toHaveLength(2)
expect(captures()[1]).toMatchObject({ revision: 1 })
await instance.report([row])
expect(captures()).toHaveLength(2)
})
it.each(['offline send', 'exit before send'])(
'retries after %s with the installed SDK',
async (failure) => {
@@ -286,15 +222,4 @@ describe('agent token usage', () => {
expect(readFileSync(file, 'utf8')).toBe('{broken')
expect(captures()).toEqual([])
})
it('rejects invalid counts and unexpected content fields', async () => {
await reporter().report([{ ...row, input_tokens: -1 }])
const payload = { ...row, analytics_session_id: ID, revision: 1, provider: 'claude' }
expect(agentTokenUsageSchema.safeParse(payload).success).toBe(false)
const { providerSessionId: _, ...valid } = payload
expect(agentTokenUsageSchema.safeParse(valid).success).toBe(true)
for (const field of ['prompt', 'path', 'model', 'activity_timestamp', 'estimated_cost']) {
expect(agentTokenUsageSchema.safeParse({ ...valid, [field]: 'private' }).success).toBe(false)
}
})
})
@@ -1,328 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { recordAgentSessionRuntimeEndMock } = vi.hoisted(() => ({
recordAgentSessionRuntimeEndMock: vi.fn()
}))
vi.mock('electron', async () =>
(await import('./createMainWindow-test-harness')).electronModuleMock()
)
vi.mock('./macos-tahoe-release', async () =>
(await import('./createMainWindow-test-harness')).macosTahoeReleaseMock()
)
vi.mock('./desktop-integration/app-icon', async () =>
(await import('./createMainWindow-test-harness')).appIconMock()
)
vi.mock('../browser/browser-manager', async () =>
(await import('./createMainWindow-test-harness')).browserManagerMock()
)
vi.mock('../runtime/agent-session/records/agent-session-runtime-end-record', () => ({
recordAgentSessionRuntimeEnd: recordAgentSessionRuntimeEndMock
}))
import { createMainWindow } from './createMainWindow'
import { ipcMain } from 'electron'
import {
resetExpectedTeardownStateForTest,
resolveExpectedTeardownScope,
WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
} from '../crash-reporting/expected-teardown-state'
import {
clearCrashBreadcrumbsForTest,
getCrashBreadcrumbSnapshot
} from '../crash-reporting/crash-breadcrumb-store'
import {
browserWindowMock,
notificationMock,
notificationShowMock,
resetMainWindowMocks
} from './createMainWindow-test-harness'
describe('createMainWindow', () => {
beforeEach(() => {
resetMainWindowMocks()
resetExpectedTeardownStateForTest()
clearCrashBreadcrumbsForTest()
vi.useRealTimers()
})
describe('minimize to tray on close (win32)', () => {
const originalPlatform = process.platform
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
}
type CloseFixture = {
windowHandlers: Record<string, (...args: any[]) => void>
webContents: { send: ReturnType<typeof vi.fn> }
instance: { hide: ReturnType<typeof vi.fn>; isMinimized: ReturnType<typeof vi.fn> }
}
function setupCloseWindow(): CloseFixture {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
setZoomLevel: vi.fn(),
setBackgroundThrottling: vi.fn(),
setWebRTCIPHandlingPolicy: vi.fn(),
invalidate: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn(),
isCrashed: vi.fn(() => false),
id: 1
}
const instance = {
webContents,
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
isDestroyed: vi.fn(() => false),
isMaximized: vi.fn(() => false),
isFullScreen: vi.fn(() => false),
isMinimized: vi.fn(() => false),
getSize: vi.fn(() => [1200, 800]),
setSize: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
hide: vi.fn(),
loadFile: vi.fn(() => Promise.resolve()),
loadURL: vi.fn(() => Promise.resolve())
}
browserWindowMock.mockImplementation(function () {
return instance
})
return { windowHandlers, webContents, instance }
}
function makeStore(minimizeToTrayOnClose: boolean, trayMinimizeNoticeShown: boolean) {
return {
getUI: vi.fn(() => ({ trayMinimizeNoticeShown })),
getSettings: vi.fn(() => ({ windowBackgroundBlur: false, minimizeToTrayOnClose })),
updateUI: vi.fn()
}
}
afterEach(() => {
setPlatform(originalPlatform)
clearCrashBreadcrumbsForTest()
})
it('marks production teardown state on irrevocable Windows session end', () => {
setPlatform('win32')
resetExpectedTeardownStateForTest(() => 1_000)
const { windowHandlers } = setupCloseWindow()
createMainWindow(null)
windowHandlers['session-end']?.({} as never)
expect(
resolveExpectedTeardownScope({
isQuitting: false,
isQuittingForUpdate: false,
isExpectedRendererReload: false
})
).toBe('app-shutdown')
})
it('durably records the session-end reasons so bundles can identify OS shutdown', () => {
setPlatform('win32')
const { windowHandlers } = setupCloseWindow()
createMainWindow(null)
windowHandlers['session-end']?.({ reasons: ['shutdown', 'critical'] } as never)
windowHandlers['session-end']?.({} as never)
expect(getCrashBreadcrumbSnapshot()).toEqual([
expect.objectContaining({
name: 'system_session_end',
data: expect.objectContaining({ reasons: 'shutdown,critical' })
}),
expect.objectContaining({
name: 'system_session_end',
data: expect.objectContaining({ reasons: '' })
})
])
})
it("records the Orca runtime's end, since Windows emits no will-quit for it", () => {
setPlatform('win32')
recordAgentSessionRuntimeEndMock.mockClear()
const { windowHandlers } = setupCloseWindow()
createMainWindow(null)
windowHandlers['session-end']?.({ reasons: ['logoff'] })
expect(recordAgentSessionRuntimeEndMock).toHaveBeenCalledExactlyOnceWith('quit')
})
it.each(['darwin', 'linux'] as const)(
'does not mark session teardown state on %s',
(platform) => {
setPlatform(platform)
const { windowHandlers } = setupCloseWindow()
createMainWindow(null)
expect(windowHandlers['session-end']).toBeUndefined()
expect(
resolveExpectedTeardownScope({
isQuitting: false,
isQuittingForUpdate: false,
isExpectedRendererReload: false
})
).toBe('none')
}
)
it('still minimizes to tray after the session-end reporting window expires', () => {
setPlatform('win32')
let now = 1_000
resetExpectedTeardownStateForTest(() => now)
const { windowHandlers, webContents, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never)
windowHandlers['session-end']?.({} as never)
now += WINDOWS_SESSION_END_CRASH_SUPPRESSION_WINDOW_MS
const preventDefault = vi.fn()
windowHandlers.close({ preventDefault } as never)
expect(preventDefault).toHaveBeenCalledOnce()
expect(instance.hide).toHaveBeenCalledOnce()
expect(webContents.send).not.toHaveBeenCalledWith('window:close-requested', expect.anything())
})
it('hides to the tray instead of closing when the setting is on', () => {
setPlatform('win32')
const { windowHandlers, webContents, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never, { getIsQuitting: () => false })
const preventDefault = vi.fn()
windowHandlers.close({ preventDefault } as never)
expect(preventDefault).toHaveBeenCalled()
expect(instance.hide).toHaveBeenCalledTimes(1)
expect(webContents.send).not.toHaveBeenCalledWith('window:close-requested', expect.anything())
expect(notificationMock).not.toHaveBeenCalled()
})
it('keeps the normal close flow when the setting is off', () => {
setPlatform('win32')
const { windowHandlers, webContents, instance } = setupCloseWindow()
const store = makeStore(false, true)
createMainWindow(store as never, { getIsQuitting: () => false })
windowHandlers.close({ preventDefault: vi.fn() } as never)
expect(instance.hide).not.toHaveBeenCalled()
expect(webContents.send).toHaveBeenCalledWith('window:close-requested', {
isQuitting: false,
requestId: expect.any(Number)
})
})
it('does not hide on a real quit even with the setting on', () => {
setPlatform('win32')
const { windowHandlers, webContents, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never, { getIsQuitting: () => true })
windowHandlers.close({ preventDefault: vi.fn() } as never)
expect(instance.hide).not.toHaveBeenCalled()
expect(webContents.send).toHaveBeenCalledWith('window:close-requested', {
isQuitting: true,
requestId: expect.any(Number)
})
})
it('does not hide when the renderer process is gone', () => {
setPlatform('win32')
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const { windowHandlers, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never, { getIsQuitting: () => false })
windowHandlers['render-process-gone']?.(
{} as never,
{ reason: 'crashed', exitCode: 5 } as never
)
const preventDefault = vi.fn()
windowHandlers.close({ preventDefault } as never)
expect(instance.hide).not.toHaveBeenCalled()
expect(preventDefault).not.toHaveBeenCalled()
consoleError.mockRestore()
})
it('shows the first-run notification once and persists the flag', () => {
setPlatform('win32')
const { windowHandlers } = setupCloseWindow()
const store = makeStore(true, false)
createMainWindow(store as never, { getIsQuitting: () => false })
windowHandlers.close({ preventDefault: vi.fn() } as never)
expect(notificationMock).toHaveBeenCalledTimes(1)
expect(notificationShowMock).toHaveBeenCalledTimes(1)
expect(store.updateUI).toHaveBeenCalledWith({ trayMinimizeNoticeShown: true })
})
it('leaves the close handler unchanged off win32', () => {
setPlatform('darwin')
const { windowHandlers, webContents, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never, { getIsQuitting: () => false })
windowHandlers.close({ preventDefault: vi.fn() } as never)
expect(instance.hide).not.toHaveBeenCalled()
expect(webContents.send).toHaveBeenCalledWith('window:close-requested', {
isQuitting: false,
requestId: expect.any(Number)
})
})
// Windows custom X uses window:request-close; the native close guard alone would quit instead of hide.
function captureIpcHandlers(): Record<string, (...args: any[]) => void> {
const ipcHandlers: Record<string, (...args: any[]) => void> = {}
vi.mocked(ipcMain.on).mockImplementation((channel, handler) => {
ipcHandlers[channel] = handler as (...args: any[]) => void
return ipcMain
})
return ipcHandlers
}
it('hides to the tray when the renderer-drawn X requests close', () => {
setPlatform('win32')
const ipcHandlers = captureIpcHandlers()
const { webContents, instance } = setupCloseWindow()
const store = makeStore(true, true)
createMainWindow(store as never, { getIsQuitting: () => false })
ipcHandlers['window:request-close']?.()
expect(instance.hide).toHaveBeenCalledTimes(1)
expect(webContents.send).not.toHaveBeenCalledWith('window:close-requested', expect.anything())
})
it('forwards window:request-close to the renderer when the setting is off', () => {
setPlatform('win32')
const ipcHandlers = captureIpcHandlers()
const { webContents, instance } = setupCloseWindow()
const store = makeStore(false, true)
createMainWindow(store as never, { getIsQuitting: () => false })
ipcHandlers['window:request-close']?.()
expect(instance.hide).not.toHaveBeenCalled()
expect(webContents.send).toHaveBeenCalledWith('window:close-requested', {
isQuitting: false
})
})
})
})
@@ -4,9 +4,8 @@ import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { MessageChannel, Worker, type MessagePort } from 'node:worker_threads'
import { runProcess } from '@orca/process-host'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import {
IDLE_TEARDOWN_MS,
WorkerThreadChildProcessHost,
type WorkerThreadChildProcess
} from './worker-thread-child-process'
@@ -15,7 +14,6 @@ import { createWorkerThreadChildProcessTestEntry } from './worker-thread-child-p
// Use a real MessageChannel so protocol tests include structured-clone behavior.
const openPorts: MessagePort[] = []
const terminations: number[] = []
function makeConnectedHost(): WorkerThreadChildProcessHost {
return new WorkerThreadChildProcessHost(() => {
@@ -28,7 +26,6 @@ function makeConnectedHost(): WorkerThreadChildProcessHost {
postMessage: (message: unknown) => port1.postMessage(message),
unref: () => port1.unref(),
terminate: async () => {
terminations.push(Date.now())
port1.close()
return 0
}
@@ -81,9 +78,6 @@ process.stdin.on('end', () => { process.stderr.write('bye'); process.exit(7) })
const IDLE_SCRIPT = 'setInterval(() => {}, 1000)'
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
terminations.length = 0
for (const port of openPorts.splice(0)) {
port.close()
}
@@ -184,16 +178,6 @@ describe('WorkerThreadChildProcessHost over a message channel', () => {
await expect(waitForClose(child)).resolves.toEqual([expect.any(Number), null])
}, 20_000)
it('closes a child whose creation threw on the worker', async () => {
const child = makeConnectedHost().spawn({ program: '', args: [] })
const errors: NodeJS.ErrnoException[] = []
child.on('error', (error) => errors.push(error))
await expect(waitForClose(child)).resolves.toEqual([-1, null])
expect(errors).toMatchObject([{ code: 'ERR_INVALID_ARG_VALUE' }])
expect(child.exitCode).toBe(-1)
}, 20_000)
it('kills a running child on request', async () => {
const child = makeConnectedHost().spawn({
program: process.execPath,
@@ -207,62 +191,6 @@ describe('WorkerThreadChildProcessHost over a message channel', () => {
const [code, signal] = await closed
expect(code !== 0 || signal !== null).toBe(true)
}, 20_000)
it('fails closed, after listeners can attach, when no worker can be started', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
const host = new WorkerThreadChildProcessHost(() => {
throw new Error('entry missing')
})
const child = host.spawn({ program: process.execPath, args: [] })
const events: unknown[][] = []
child.on('error', (error: Error) => events.push(['error', error.message]))
child.on('exit', (code) => events.push(['exit', code]))
child.on('close', (code) => events.push(['close', code]))
expect(events).toEqual([])
await new Promise((resolve) => process.nextTick(resolve))
expect(events).toEqual([
['error', 'Child process worker is unavailable'],
['exit', -1],
['close', -1]
])
expect(child.kill()).toBe(false)
})
it('tears the idle worker down only after the last child closes', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const host = makeConnectedHost()
const first = host.spawn({ program: process.execPath, args: ['-e', ''] })
const second = host.spawn({ program: process.execPath, args: ['-e', IDLE_SCRIPT] })
await waitForSpawn(second)
await waitForClose(first)
await vi.advanceTimersByTimeAsync(IDLE_TEARDOWN_MS)
expect(terminations).toHaveLength(0)
const secondClosed = waitForClose(second)
second.kill()
await secondClosed
await vi.advanceTimersByTimeAsync(IDLE_TEARDOWN_MS - 1)
expect(terminations).toHaveLength(0)
await vi.advanceTimersByTimeAsync(1)
expect(terminations).toHaveLength(1)
}, 20_000)
it('cancels a pending idle teardown when a new child spawns', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const host = makeConnectedHost()
await waitForClose(host.spawn({ program: process.execPath, args: ['-e', ''] }))
await vi.advanceTimersByTimeAsync(IDLE_TEARDOWN_MS - 1)
const next = host.spawn({ program: process.execPath, args: ['-e', IDLE_SCRIPT] })
await vi.advanceTimersByTimeAsync(IDLE_TEARDOWN_MS)
expect(terminations).toHaveLength(0)
const closed = waitForClose(next)
next.kill()
await closed
}, 20_000)
})
// Real thread death closes child pipes; abandoned probes depend on that graceful stop instead of PID-addressed killing.
@@ -1,334 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AddWorktreeResult } from '../../../git/worktrees/worktree-operation-options'
const mocks = vi.hoisted(() => ({
mkdir: vi.fn(),
prepare: vi.fn(),
finalize: vi.fn(),
discard: vi.fn(),
listWorktreeGraph: vi.fn(),
resolveBaseRef: vi.fn()
}))
vi.mock('node:fs/promises', () => ({ mkdir: mocks.mkdir }))
vi.mock('../../../git/worktrees/catalog/worktree-scan-cache', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
listWorktreeGraph: mocks.listWorktreeGraph
}))
vi.mock('../../../git/worktrees/creation/worktree-create-preparation', () => ({
prepareWorktreeCreateCheckout: mocks.prepare,
finalizePreparedWorktree: mocks.finalize,
discardPreparedWorktree: mocks.discard,
unlockPreparedWorktree: vi.fn()
}))
vi.mock('../../../git/worktrees/creation/worktree-base-ref-probe', () => ({
resolveLocalWorktreeBaseRef: mocks.resolveBaseRef
}))
vi.mock('../../../git/worktrees/creation/worktree-base-divergence', () => ({
measureRetargetDivergence: vi.fn()
}))
vi.mock('../../execution-host/project-runtime-git-options', () => ({
getLocalProjectWorktreeGitOptions: vi.fn(),
getWorktreeMirrorDistro: vi.fn()
}))
vi.mock('../../../ipc/worktree-logic', () => ({
computeWorkspaceRootAsync: vi.fn(),
getWorktreePathSettings: vi.fn()
}))
import {
_resetWorktreeCreatePreparationsForTests,
consumePreparedWorktreeCreate
} from './worktree-create-preparation'
import { startPreparation } from './worktree-create-preparation-pool'
import { createWorktreeCreateTimingRecorder } from '../worktree-create-timing'
import {
_resetWorktreeCreateConcurrencyForTests,
beginWorktreeCreate
} from '../worktree-create-concurrency'
const request = {
repoPath: '/repo',
workspaceRoot: '/workspace',
worktreePath: '/workspace/feature',
branch: 'feature',
baseBranch: 'origin/main'
}
function prepare() {
return startPreparation({
repoPath: request.repoPath,
workspaceRoot: request.workspaceRoot,
baseBranch: request.baseBranch,
canonicalBase: 'refs/remotes/origin/main',
options: {}
})
}
/** Lets the claim's own phase settle before the test clock moves on to the wait. */
function settleClaim(): Promise<void> {
return new Promise((resolve) => setImmediate(resolve))
}
beforeEach(() => {
mocks.mkdir.mockReset().mockResolvedValue(undefined)
mocks.prepare.mockReset().mockResolvedValue(undefined)
mocks.finalize.mockReset().mockResolvedValue({})
mocks.discard.mockReset().mockResolvedValue(undefined)
mocks.listWorktreeGraph.mockReset().mockResolvedValue([])
mocks.resolveBaseRef.mockReset()
})
afterEach(async () => {
await _resetWorktreeCreatePreparationsForTests()
_resetWorktreeCreateConcurrencyForTests()
vi.restoreAllMocks()
})
describe('prepared checkout create timing', () => {
it('separates the remaining preparation wait from finalization', async () => {
const checkoutStarted = Promise.withResolvers<void>()
const checkout = Promise.withResolvers<void>()
const finalizeStarted = Promise.withResolvers<void>()
const finalize = Promise.withResolvers<AddWorktreeResult>()
mocks.prepare.mockImplementation(() => {
checkoutStarted.resolve()
return checkout.promise
})
mocks.finalize.mockImplementation(() => {
finalizeStarted.resolve()
return finalize.promise
})
const preparation = prepare()
await checkoutStarted.promise
let now = 40
const timing = createWorktreeCreateTimingRecorder(() => now)
const create = timing.time('git_worktree_add', () =>
consumePreparedWorktreeCreate({ ...request, timing })
)
await settleClaim()
now = 150
checkout.resolve()
await finalizeStarted.promise
now = 180
finalize.resolve({})
expect(await create).toMatchObject({ status: 'hit', retargeted: false, reset: 'none' })
await preparation
expect(timing.finish()).toEqual({
totalDurationMs: 140,
preparedCheckout: {
status: 'hit',
reset: 'none',
origin: 'prefetch',
buildMs: expect.any(Number),
idleMs: 0
},
phases: [
{ phase: 'prepared_checkout_claim', startedAtMs: 0, durationMs: 0 },
{ phase: 'prepared_checkout_wait', startedAtMs: 0, durationMs: 110 },
{ phase: 'prepared_checkout_finalize', startedAtMs: 110, durationMs: 30 },
{ phase: 'git_worktree_add', startedAtMs: 0, durationMs: 140 }
]
})
})
it('records the wait when preparation fails and the create must fall back', async () => {
const checkoutStarted = Promise.withResolvers<void>()
const checkout = Promise.withResolvers<void>()
mocks.prepare.mockImplementation(() => {
checkoutStarted.resolve()
return checkout.promise
})
const preparation = Promise.allSettled([prepare()])
await checkoutStarted.promise
let now = 0
const timing = createWorktreeCreateTimingRecorder(() => now)
const create = consumePreparedWorktreeCreate({ ...request, timing })
await settleClaim()
now = 250
checkout.reject(new Error('checkout failed'))
expect(await create).toEqual({
status: 'miss',
reason: 'prepare_failed',
rearm: expect.any(Function)
})
await preparation
expect(timing.finish().phases).toEqual([
{ phase: 'prepared_checkout_claim', startedAtMs: 0, durationMs: 0 },
{ phase: 'prepared_checkout_wait', startedAtMs: 0, durationMs: 250 }
])
expect(mocks.finalize).not.toHaveBeenCalled()
})
it('times the cleanup of a failed finalization apart from the finalization', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
await prepare()
let now = 0
const timing = createWorktreeCreateTimingRecorder(() => now)
mocks.finalize.mockImplementation(async () => {
now = 80
throw new Error('move failed')
})
mocks.discard.mockImplementation(async () => {
now = 100
})
expect(await consumePreparedWorktreeCreate({ ...request, timing })).toEqual({
status: 'miss',
reason: 'finalize_failed',
rearm: expect.any(Function)
})
expect(timing.finish().phases).toEqual([
{ phase: 'prepared_checkout_claim', startedAtMs: 0, durationMs: 0 },
{ phase: 'prepared_checkout_wait', startedAtMs: 0, durationMs: 0 },
{ phase: 'prepared_checkout_finalize', startedAtMs: 0, durationMs: 80 },
{ phase: 'prepared_checkout_discard', startedAtMs: 80, durationMs: 20 }
])
})
it('reports only the claim, and the miss, when no checkout was armed', async () => {
const timing = createWorktreeCreateTimingRecorder(() => 0)
expect(await consumePreparedWorktreeCreate({ ...request, timing })).toEqual({
status: 'miss',
reason: 'none_armed'
})
expect(timing.finish()).toMatchObject({
phases: [{ phase: 'prepared_checkout_claim', startedAtMs: 0, durationMs: 0 }],
preparedCheckout: { status: 'miss', reason: 'none_armed' }
})
})
it('times a miss probe as the claim, not as the plain add after it', async () => {
await startPreparation({
repoPath: request.repoPath,
workspaceRoot: request.workspaceRoot,
baseBranch: 'origin/release',
canonicalBase: 'refs/remotes/origin/release',
options: {}
})
let now = 0
const timing = createWorktreeCreateTimingRecorder(() => now)
mocks.resolveBaseRef.mockImplementation(async () => {
now = 400
return 'refs/remotes/origin/main'
})
const attempt = await timing.time('git_worktree_add', async () => {
const result = await consumePreparedWorktreeCreate({ ...request, timing })
now = 500
return result
})
expect(attempt).toMatchObject({ status: 'miss', reason: 'base_mismatch' })
expect(timing.finish().phases).toEqual([
{ phase: 'prepared_checkout_claim', startedAtMs: 0, durationMs: 400 },
{ phase: 'git_worktree_add', startedAtMs: 0, durationMs: 500 }
])
})
it('reports the reset a hit needed and who armed it', async () => {
await startPreparation(
{
repoPath: request.repoPath,
workspaceRoot: request.workspaceRoot,
baseBranch: request.baseBranch,
canonicalBase: 'refs/remotes/origin/main',
options: {}
},
'automatic'
)
mocks.finalize.mockResolvedValue({ preparedHeadReset: true })
const timing = createWorktreeCreateTimingRecorder(() => 0)
const attempt = await consumePreparedWorktreeCreate({ ...request, timing })
expect(attempt).toMatchObject({ status: 'hit', reset: 'base_moved', origin: 'rearm' })
expect(attempt.status === 'hit' && attempt.result).toEqual({})
expect(timing.finish().preparedCheckout).toMatchObject({
status: 'hit',
reset: 'base_moved',
origin: 'rearm'
})
})
it('reports a re-arm the new-worktree UI then asked for as well', async () => {
const args = {
repoPath: request.repoPath,
workspaceRoot: request.workspaceRoot,
baseBranch: request.baseBranch,
canonicalBase: 'refs/remotes/origin/main',
options: {}
}
await startPreparation(args, 'automatic')
await startPreparation(args, 'explicit')
expect(mocks.prepare).toHaveBeenCalledOnce()
const attempt = await consumePreparedWorktreeCreate({ ...request })
expect(attempt).toMatchObject({ status: 'hit', origin: 'rearm_then_prefetch' })
})
it('reports how long the build took and how long it sat ready before the claim', async () => {
let now = 1_000
vi.spyOn(performance, 'now').mockImplementation(() => now)
const checkout = Promise.withResolvers<void>()
mocks.prepare.mockReturnValue(checkout.promise)
const preparation = prepare()
now = 4_000
checkout.resolve()
await preparation
now = 9_000
const attempt = await consumePreparedWorktreeCreate({ ...request })
expect(attempt).toMatchObject({ status: 'hit', buildMs: 3_000, idleMs: 5_000 })
})
it('reports no idle time when the create waited for the build', async () => {
let now = 1_000
vi.spyOn(performance, 'now').mockImplementation(() => now)
const checkoutStarted = Promise.withResolvers<void>()
const checkout = Promise.withResolvers<void>()
mocks.prepare.mockImplementation(() => {
checkoutStarted.resolve()
return checkout.promise
})
const preparation = prepare()
await checkoutStarted.promise
now = 2_000
const create = consumePreparedWorktreeCreate({ ...request })
await settleClaim()
now = 6_000
checkout.resolve()
await preparation
expect(await create).toMatchObject({ status: 'hit', buildMs: 5_000, idleMs: 0 })
})
it('counts a building prepared checkout against other creates but not the one that used it', async () => {
const checkoutStarted = Promise.withResolvers<void>()
const checkout = Promise.withResolvers<void>()
mocks.prepare.mockImplementation(() => {
checkoutStarted.resolve()
return checkout.promise
})
const bystander = beginWorktreeCreate()
const preparation = prepare()
await checkoutStarted.promise
const inFlight = beginWorktreeCreate()
const timing = createWorktreeCreateTimingRecorder(undefined, inFlight)
const create = consumePreparedWorktreeCreate({ ...request, timing })
await settleClaim()
checkout.resolve()
await preparation
expect(await create).toMatchObject({ status: 'hit', origin: 'prefetch' })
expect(inFlight.end().preparations).toBe(0)
expect(bystander.end().preparations).toBe(1)
})
})
@@ -45,10 +45,6 @@ function resolveOnly(present: string[]): void {
})
}
function revParse(ref: string): string[] {
return ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]
}
beforeEach(() => {
for (const mock of Object.values(mocks)) {
mock.mockReset()
@@ -61,117 +57,6 @@ beforeEach(() => {
mocks.fetchRemoteWithCache.mockResolvedValue(undefined)
})
describe('prefetchWorktreeCreateBase local git routing', () => {
it('resolves the default base and its ref probes inside the selected WSL distro', async () => {
resolveOnly(['refs/remotes/origin/main'])
await expect(
prefetchWorktreeCreateBase({ repo, runtime: runtime(), gitOptions: WSL })
).resolves.toBe('origin/main')
expect(mocks.getBaseRefDefault).toHaveBeenCalledWith(repo.path, WSL)
expect(mocks.resolveRemoteTrackingBase).toHaveBeenCalledWith(repo.path, 'origin/main', WSL)
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse('refs/remotes/origin/main'), {
cwd: repo.path,
...WSL
})
expect(mocks.fetchRemoteWithCache).not.toHaveBeenCalled()
})
it('probes a full commit object inside the selected WSL distro', async () => {
const sha = 'a'.repeat(40)
resolveOnly([sha])
await expect(
prefetchWorktreeCreateBase({ repo, baseBranch: sha, runtime: runtime(), gitOptions: WSL })
).resolves.toBe(sha)
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse(sha), {
cwd: repo.path,
...WSL
})
expect(mocks.fetchRemoteWithCache).not.toHaveBeenCalled()
})
it('routes the exact remote-base refresh through the selected WSL distro', async () => {
const remoteTrackingBase = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
mocks.resolveRemoteTrackingBase.mockResolvedValue(remoteTrackingBase)
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBe('origin/main')
expect(mocks.hasRemoteTrackingRef).toHaveBeenCalledWith(repo.path, remoteTrackingBase, WSL)
expect(mocks.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith(
repo.path,
remoteTrackingBase,
WSL
)
})
it('routes the broad remote-fetch fallback through the selected WSL distro', async () => {
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'feature/topic',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBe('feature/topic')
expect(mocks.fetchRemoteWithCache).toHaveBeenCalledWith(repo.path, 'origin', WSL)
})
it('leaves host-routed probes on the git host they used before routing existed', async () => {
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'feature/topic',
runtime: runtime(),
gitOptions: {}
})
).resolves.toBe('feature/topic')
expect(mocks.gitExecFileAsync).toHaveBeenCalledWith(revParse('refs/remotes/feature/topic'), {
cwd: repo.path
})
for (const call of mocks.gitExecFileAsync.mock.calls) {
expect(call[1]).toEqual({ cwd: repo.path })
}
expect(mocks.resolveRemoteTrackingBase).toHaveBeenCalledWith(repo.path, 'feature/topic')
expect(mocks.fetchRemoteWithCache).toHaveBeenCalledWith(repo.path, 'origin')
})
it('does not resolve a local base for SSH repos', async () => {
const prepareCheckout = vi.fn()
await expect(
prefetchWorktreeCreateBase({
repo: { ...repo, connectionId: 'conn-1' },
prepareCheckout,
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: WSL
})
).resolves.toBeUndefined()
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
expect(mocks.resolveRemoteTrackingBase).not.toHaveBeenCalled()
expect(prepareCheckout).not.toHaveBeenCalled()
})
})
describe('checkout and refresh overlap', () => {
it.each([{}, WSL])(
'starts one checkout with a shared refresh barrier before fetch settles on %j',
@@ -221,58 +106,6 @@ describe('checkout and refresh overlap', () => {
}
)
it('waits for refresh when the selected base is not local', async () => {
mocks.resolveRemoteTrackingBase.mockResolvedValue({
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
})
let release!: () => void
mocks.getOrStartRemoteTrackingBaseRefresh.mockImplementation(
() =>
new Promise<{ ok: boolean }>((resolve) => {
release = () => resolve({ ok: true })
})
)
const prepareCheckout = vi.fn().mockResolvedValue(undefined)
const result = prefetchWorktreeCreateBase({
repo,
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: {},
prepareCheckout
})
await vi.waitFor(() =>
expect(mocks.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledTimes(1)
)
expect(prepareCheckout).not.toHaveBeenCalled()
release()
await result
expect(prepareCheckout).toHaveBeenCalledTimes(1)
})
it('does not fail a successful refresh because preparation fails', async () => {
mocks.resolveRemoteTrackingBase.mockResolvedValue({
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
})
mocks.hasRemoteTrackingRef.mockResolvedValue(true)
const prepareCheckout = vi.fn().mockRejectedValue(new Error('checkout failed'))
await expect(
prefetchWorktreeCreateBase({
repo,
baseBranch: 'origin/main',
runtime: runtime(),
gitOptions: {},
prepareCheckout
})
).resolves.toBe('origin/main')
expect(prepareCheckout).toHaveBeenCalledTimes(1)
})
it('settles preparation before propagating refresh failure', async () => {
mocks.resolveRemoteTrackingBase.mockResolvedValue({
remote: 'origin',
@@ -336,17 +169,3 @@ describe('checkout and refresh overlap', () => {
expect(mocks.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledOnce()
})
})
it('does not prepare folder repositories', async () => {
const prepareCheckout = vi.fn()
await expect(
prefetchWorktreeCreateBase({
repo: { ...repo, kind: 'folder' },
runtime: runtime(),
gitOptions: {},
prepareCheckout
})
).resolves.toBeUndefined()
expect(prepareCheckout).not.toHaveBeenCalled()
expect(mocks.gitExecFileAsync).not.toHaveBeenCalled()
})
@@ -1,305 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { RetiredNameRegistry } from '../../../../shared/worktree/retired-name-registry'
import {
MAX_RETIREMENT_NAMESPACES,
migrateRetirementNamespaceHostIdentity,
recordRetirementNamespaceRegistry,
retirementHostIdentity,
retirementNamespaceKey,
UNKNOWN_SSH_HOST_IDENTITY
} from './worktree-retirement-namespace'
function registry(...names: string[]): RetiredNameRegistry {
return { exhaustedTiers: 0, names }
}
const TARGET = {
configHost: 'builder',
host: 'builder.example.com',
port: 22,
username: 'dev'
}
describe('recordRetirementNamespaceRegistry', () => {
it('evicts the least recently recorded namespace once the map is full', () => {
const namespaces: Record<string, RetiredNameRegistry> = {}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES; index += 1) {
recordRetirementNamespaceRegistry(namespaces, `local:posix:/w/${index}`, registry('nautilus'))
}
recordRetirementNamespaceRegistry(namespaces, 'local:posix:/w/new', registry('seahorse'))
expect(Object.keys(namespaces)).toHaveLength(MAX_RETIREMENT_NAMESPACES)
expect(namespaces['local:posix:/w/0']).toBeUndefined()
expect(namespaces['local:posix:/w/1']).toEqual(registry('nautilus'))
expect(namespaces['local:posix:/w/new']).toEqual(registry('seahorse'))
})
it('refreshes a namespace it rewrites so an actively used repo is never the eviction victim', () => {
const namespaces: Record<string, RetiredNameRegistry> = {}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES; index += 1) {
recordRetirementNamespaceRegistry(namespaces, `local:posix:/w/${index}`, registry('nautilus'))
}
recordRetirementNamespaceRegistry(namespaces, 'local:posix:/w/0', registry('nautilus', 'orca'))
recordRetirementNamespaceRegistry(namespaces, 'local:posix:/w/new', registry('seahorse'))
expect(namespaces['local:posix:/w/0']).toEqual(registry('nautilus', 'orca'))
expect(namespaces['local:posix:/w/1']).toBeUndefined()
})
it('stays at one entry per namespace no matter how often it is rewritten', () => {
const namespaces: Record<string, RetiredNameRegistry> = {}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES * 2; index += 1) {
recordRetirementNamespaceRegistry(namespaces, 'local:posix:/w/a', registry(`name-${index}`))
}
expect(Object.keys(namespaces)).toEqual(['local:posix:/w/a'])
})
})
describe('migrateRetirementNamespaceHostIdentity', () => {
it('folds a migrated namespace into one the new identity already owns', () => {
const namespaces = {
'ssh:old-id:posix:/srv/a': registry('nautilus'),
'ssh:new|22|dev:posix:/srv/a': registry('seahorse')
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
moveFrom: ['ssh:old-id'],
to: 'ssh:new|22|dev'
})
).toBe(true)
expect(Object.keys(namespaces)).toEqual(['ssh:new|22|dev:posix:/srv/a'])
expect(namespaces['ssh:new|22|dev:posix:/srv/a'].names.toSorted()).toEqual([
'nautilus',
'seahorse'
])
})
it('keeps the source bucket when an endpoint identity moves, since a live target may share it', () => {
const namespaces = {
'ssh:old|22|dev:posix:/srv/a': registry('nautilus')
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
).toBe(true)
expect(Object.keys(namespaces).toSorted()).toEqual([
'ssh:new|22|dev:posix:/srv/a',
'ssh:old|22|dev:posix:/srv/a'
])
expect(namespaces['ssh:old|22|dev:posix:/srv/a'].names).toEqual(['nautilus'])
expect(namespaces['ssh:new|22|dev:posix:/srv/a'].names).toEqual(['nautilus'])
})
it('reports no change when a repeated copy adds nothing the destination lacks', () => {
const namespaces = {
'ssh:old|22|dev:posix:/srv/a': registry('nautilus'),
'ssh:new|22|dev:posix:/srv/a': registry('nautilus')
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
).toBe(false)
expect(Object.keys(namespaces).toSorted()).toEqual([
'ssh:new|22|dev:posix:/srv/a',
'ssh:old|22|dev:posix:/srv/a'
])
expect(namespaces['ssh:old|22|dev:posix:/srv/a'].names).toEqual(['nautilus'])
expect(namespaces['ssh:new|22|dev:posix:/srv/a'].names).toEqual(['nautilus'])
})
it('still copies a name the destination folded into its watermark', () => {
// Equal sizes can hide a new name when an old one folds into the watermark.
const namespaces = {
'ssh:old|22|dev:posix:/srv/a': registry('seahorse-2'),
'ssh:new|22|dev:posix:/srv/a': { exhaustedTiers: 1, names: ['nautilus'] }
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
).toBe(true)
expect(namespaces['ssh:new|22|dev:posix:/srv/a'].names).toContain('seahorse-2')
})
it('keeps the namespace map within its cap by evicting bystanders, not the copy itself', () => {
// Old source buckets must survive trimming too, or copying would effectively move their tombstones.
const namespaces: Record<string, RetiredNameRegistry> = {
'ssh:old|22|dev:posix:/srv/a': registry('nautilus'),
'ssh:old|22|dev:posix:/srv/b': registry('manta')
}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES - 2; index += 1) {
namespaces[`local:posix:/w/${index}`] = registry('dolphin')
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
).toBe(true)
expect(Object.keys(namespaces).length).toBe(MAX_RETIREMENT_NAMESPACES)
expect(namespaces['ssh:old|22|dev:posix:/srv/a']).toBeDefined()
expect(namespaces['ssh:old|22|dev:posix:/srv/b']).toBeDefined()
expect(namespaces['ssh:new|22|dev:posix:/srv/a']).toBeDefined()
expect(namespaces['ssh:new|22|dev:posix:/srv/b']).toBeDefined()
expect(namespaces['local:posix:/w/0']).toBeUndefined()
expect(namespaces['local:posix:/w/1']).toBeUndefined()
expect(namespaces['local:posix:/w/2']).toBeDefined()
})
it('refreshes a retained source even when its own merge added nothing', () => {
// A no-op source remains live while another copied namespace grows the map.
const namespaces: Record<string, RetiredNameRegistry> = {
'ssh:old|22|dev:posix:/srv/a': registry('seahorse'),
'ssh:new|22|dev:posix:/srv/a': registry('seahorse'),
'ssh:old|22|dev:posix:/srv/b': registry('nautilus')
}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES - 3; index += 1) {
namespaces[`local:posix:/w/${index}`] = registry('dolphin')
}
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
expect(Object.keys(namespaces).length).toBe(MAX_RETIREMENT_NAMESPACES)
expect(namespaces['ssh:old|22|dev:posix:/srv/a']).toBeDefined()
expect(namespaces['local:posix:/w/0']).toBeUndefined()
})
it('refreshes a move destination that the move just made the only copy', () => {
const namespaces: Record<string, RetiredNameRegistry> = {
'ssh:new|22|dev:posix:/srv/a': registry('seahorse'),
'local:posix:/w/0': registry('dolphin'),
'ssh:old-id:posix:/srv/a': registry('seahorse')
}
migrateRetirementNamespaceHostIdentity(namespaces, {
moveFrom: ['ssh:old-id'],
to: 'ssh:new|22|dev'
})
expect(Object.keys(namespaces).at(-1)).toBe('ssh:new|22|dev:posix:/srv/a')
})
it('leaves a merged destination newest, so the very next ordinary write cannot evict it', () => {
const namespaces: Record<string, RetiredNameRegistry> = {
'ssh:new|22|dev:posix:/srv/a': registry('seahorse')
}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES - 2; index += 1) {
namespaces[`local:posix:/w/${index}`] = registry('dolphin')
}
namespaces['ssh:old|22|dev:posix:/srv/a'] = registry('nautilus')
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
recordRetirementNamespaceRegistry(namespaces, 'local:posix:/fresh', registry('manta'))
const destination = namespaces['ssh:new|22|dev:posix:/srv/a']
expect(destination).toBeDefined()
expect(destination.names.toSorted()).toEqual(['nautilus', 'seahorse'])
})
it('does not let the cap evict a destination it just merged into', () => {
const namespaces: Record<string, RetiredNameRegistry> = {
'ssh:new|22|dev:posix:/srv/a': registry('seahorse')
}
for (let index = 0; index < MAX_RETIREMENT_NAMESPACES - 2; index += 1) {
namespaces[`local:posix:/w/${index}`] = registry('dolphin')
}
namespaces['ssh:old|22|dev:posix:/srv/a'] = registry('nautilus')
namespaces['ssh:old|22|dev:posix:/srv/b'] = registry('manta')
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
copyFrom: ['ssh:old|22|dev'],
to: 'ssh:new|22|dev'
})
).toBe(true)
const destination = namespaces['ssh:new|22|dev:posix:/srv/a']
expect(destination).toBeDefined()
expect(destination.names.toSorted()).toEqual(['nautilus', 'seahorse'])
})
it('leaves an identity that merely shares a prefix with the old one alone', () => {
const namespaces = {
'ssh:h|22|dev2:posix:/srv/a': registry('nautilus')
}
expect(
migrateRetirementNamespaceHostIdentity(namespaces, {
moveFrom: ['ssh:h|22|dev'],
to: 'ssh:h|22|other'
})
).toBe(false)
expect(Object.keys(namespaces)).toEqual(['ssh:h|22|dev2:posix:/srv/a'])
})
it('reports no change when there is nothing to move or the identity is unchanged', () => {
expect(
migrateRetirementNamespaceHostIdentity(undefined, { moveFrom: ['ssh:a'], to: 'ssh:b' })
).toBe(false)
expect(
migrateRetirementNamespaceHostIdentity(
{ 'ssh:a:posix:/srv': registry('nautilus') },
{ to: 'ssh:a' }
)
).toBe(false)
expect(
migrateRetirementNamespaceHostIdentity(
{ 'ssh:a:posix:/srv': registry('nautilus') },
{ moveFrom: ['ssh:a'], to: 'ssh:a' }
)
).toBe(false)
})
})
describe('retirementHostIdentity', () => {
it('resolves an SSH repo to the endpoint its target reaches, not the target row id', () => {
const identity = retirementHostIdentity({ connectionId: 'ssh-1' }, () => TARGET)
expect(identity).toBe('ssh:builder.example.com|22|dev')
expect(retirementHostIdentity({ connectionId: 'ssh-2' }, () => TARGET)).toBe(identity)
})
it('falls back to one shared bucket when the target row is gone', () => {
expect(retirementHostIdentity({ connectionId: 'ssh-1' }, () => undefined)).toBe(
UNKNOWN_SSH_HOST_IDENTITY
)
expect(retirementHostIdentity({ connectionId: 'ssh-1' })).toBe(UNKNOWN_SSH_HOST_IDENTITY)
})
it('leaves a non-SSH repo on its execution host id', () => {
expect(retirementHostIdentity({})).toBe('local')
})
})
describe('retirementNamespaceKey', () => {
it('keeps Windows paths case- and separator-insensitive without colliding with a POSIX path', () => {
expect(retirementNamespaceKey('local', 'C:\\Workspaces\\Probe')).toBe(
retirementNamespaceKey('local', 'C:/workspaces/probe')
)
expect(retirementNamespaceKey('local', 'C:\\workspaces\\probe')).not.toBe(
retirementNamespaceKey('local', '/workspaces/probe')
)
})
it('keeps POSIX paths case-sensitive, matching the filesystems they name', () => {
expect(retirementNamespaceKey('local', '/srv/Probe')).not.toBe(
retirementNamespaceKey('local', '/srv/probe')
)
})
})
@@ -1,6 +1,6 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import 'node:fs'
import 'node:os'
import 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ProcessResult } from '@orca/process-host/process-spec'
@@ -17,23 +17,11 @@ const FAILED = exited('', 1)
import {
_resetWslCachesForTests,
_setWslCachesForTests,
listRunningWslDistrosAsync,
listRunningWslHomeDirsAsync
listRunningWslDistrosAsync
} from './distro-catalog'
import { filterPathsToRunningWslDistrosAsync } from '../filesystem/wsl-running-path-filter'
import { resetWslInteropSpawnDirectoryCache } from '../execution/wsl-interop-spawn-directory'
import {
observeWslTranscriptRunningState,
resetWslTranscriptRunningObserverForTests
} from '../../native-chat/wsl-transcript-running-observer'
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
import '../execution/wsl-interop-spawn-directory'
import { resetWslTranscriptRunningObserverForTests } from '../../native-chat/wsl-transcript-running-observer'
async function withPlatform<T>(value: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
const original = process.platform
@@ -56,62 +44,6 @@ describe('running WSL distro discovery', () => {
vi.useRealTimers()
})
it('lists only running user distros without starting them', async () => {
runProcessMock.mockResolvedValue(exited('Ubuntu\u0000\nDocker-Desktop\u0000\n'))
await withPlatform('win32', async () => {
await expect(listRunningWslDistrosAsync()).resolves.toEqual(['Ubuntu'])
expect(runProcessMock).toHaveBeenCalledWith(
expect.objectContaining({
program: 'wsl.exe',
args: ['--list', '--running', '--quiet'],
env: expect.objectContaining({ WSL_UTF8: '1' }),
timeoutMs: 5000
})
)
})
})
it('runs the startup probe from the system root, never the profile (#29845)', async () => {
runProcessMock.mockResolvedValue(exited('Ubuntu\n'))
const saved = { SystemRoot: process.env.SystemRoot, profile: process.env.ORCA_USER_DATA_PATH }
const systemRoot = mkdtempSync(join(tmpdir(), 'orca-wsl-probe-system-root-'))
const profile = mkdtempSync(join(tmpdir(), 'orca-wsl-probe-profile-'))
process.env.SystemRoot = systemRoot
process.env.ORCA_USER_DATA_PATH = profile
resetWslInteropSpawnDirectoryCache()
try {
await withPlatform('win32', () => listRunningWslDistrosAsync({ requireConfirmed: true }))
expect(runProcessMock).toHaveBeenCalledWith(
expect.objectContaining({ args: ['--list', '--running', '--quiet'], cwd: systemRoot })
)
} finally {
restoreEnv('SystemRoot', saved.SystemRoot)
restoreEnv('ORCA_USER_DATA_PATH', saved.profile)
resetWslInteropSpawnDirectoryCache()
rmSync(systemRoot, { recursive: true, force: true })
rmSync(profile, { recursive: true, force: true })
}
})
it('confirms an empty list when no distro runs', async () => {
// WSL 2.7 --quiet returns empty stdout with exit 0; non-quiet output is localized and exits -1.
runProcessMock.mockResolvedValueOnce(exited('Ubuntu\r\n')).mockResolvedValueOnce(exited(''))
await withPlatform('win32', async () => {
await expect(listRunningWslDistrosAsync()).resolves.toEqual(['Ubuntu'])
await expect(listRunningWslDistrosAsync({ requireConfirmed: true })).resolves.toEqual([])
})
})
it('fails closed when running-distro discovery fails', async () => {
runProcessMock.mockResolvedValue(FAILED)
await withPlatform('win32', async () => {
await expect(listRunningWslDistrosAsync()).resolves.toEqual([])
})
})
it('rejects stale running state for launch admission through failure and backoff', async () => {
vi.useFakeTimers()
runProcessMock.mockResolvedValueOnce(exited('Ubuntu\n'))
@@ -165,161 +97,4 @@ describe('running WSL distro discovery', () => {
})
}
)
it('resolves homes only for the running distro set', async () => {
runProcessMock.mockImplementation(
({ args }: { args: string[] }) =>
new Promise<ProcessResult>((resolve) => {
resolve(exited(args.includes('--running') ? 'Ubuntu\n' : '/home/ada\n'))
})
)
await withPlatform('win32', async () => {
await expect(listRunningWslHomeDirsAsync()).resolves.toEqual([
'\\\\wsl.localhost\\Ubuntu\\home\\ada'
])
expect(runProcessMock.mock.calls.map(([spec]) => spec.args)).toEqual([
['--list', '--running', '--quiet'],
['-d', 'Ubuntu', '--exec', 'bash', '-c', 'echo $HOME']
])
})
})
it('single-flights concurrent probes without caching the result', async () => {
let finishProbe: ((output: string) => void) | undefined
runProcessMock.mockImplementationOnce(
() =>
new Promise<ProcessResult>((resolve) => {
finishProbe = (output) => resolve(exited(output))
})
)
await withPlatform('win32', async () => {
const concurrent = [
listRunningWslDistrosAsync(),
listRunningWslDistrosAsync(),
listRunningWslDistrosAsync()
]
expect(runProcessMock).toHaveBeenCalledTimes(1)
finishProbe?.('Ubuntu\n')
await expect(Promise.all(concurrent)).resolves.toEqual([['Ubuntu'], ['Ubuntu'], ['Ubuntu']])
runProcessMock.mockResolvedValueOnce(exited(''))
await expect(listRunningWslDistrosAsync()).resolves.toEqual([])
expect(runProcessMock).toHaveBeenCalledTimes(2)
})
})
it('single-flights cold HOME probes across concurrent callers', async () => {
let finishList: ((output: string) => void) | undefined
let finishHome: ((output: string) => void) | undefined
runProcessMock.mockImplementation(
({ args }: { args: string[] }) =>
new Promise<ProcessResult>((resolve) => {
if (args.includes('--running')) {
finishList = (output) => resolve(exited(output))
} else {
finishHome = (output) => resolve(exited(output))
}
})
)
await withPlatform('win32', async () => {
const concurrent = [
listRunningWslHomeDirsAsync(),
listRunningWslHomeDirsAsync(),
listRunningWslHomeDirsAsync()
]
finishList?.('Ubuntu\n')
await vi.waitFor(() => expect(runProcessMock).toHaveBeenCalledTimes(2))
finishHome?.('/home/ada\n')
await expect(Promise.all(concurrent)).resolves.toEqual([
['\\\\wsl.localhost\\Ubuntu\\home\\ada'],
['\\\\wsl.localhost\\Ubuntu\\home\\ada'],
['\\\\wsl.localhost\\Ubuntu\\home\\ada']
])
expect(
runProcessMock.mock.calls.filter(([spec]) => spec.args.includes('echo $HOME'))
).toHaveLength(1)
})
})
it('shares a confirmed fresh probe between discovery and launch admission', async () => {
runProcessMock.mockResolvedValueOnce(exited('Ubuntu\n'))
await withPlatform('win32', async () => {
expect(
await Promise.all([
listRunningWslDistrosAsync(),
listRunningWslDistrosAsync({ requireConfirmed: true })
])
).toEqual([['Ubuntu'], ['Ubuntu']])
expect(runProcessMock).toHaveBeenCalledOnce()
})
})
it('filters stopped-distro UNC paths while preserving host paths', async () => {
runProcessMock.mockResolvedValue(exited('Ubuntu\n'))
await withPlatform('win32', async () => {
await expect(
filterPathsToRunningWslDistrosAsync([
'C:\\Users\\ada\\codex-home',
'\\\\wsl.localhost\\Ubuntu\\home\\ada',
'\\\\wsl.localhost\\Debian\\home\\other'
])
).resolves.toEqual(['C:\\Users\\ada\\codex-home', '\\\\wsl.localhost\\Ubuntu\\home\\ada'])
})
})
it('does not probe WSL on non-Windows hosts', async () => {
await withPlatform('linux', async () => {
await expect(listRunningWslDistrosAsync()).resolves.toEqual([])
expect(runProcessMock).not.toHaveBeenCalled()
})
})
it('keeps reporting a session running through a sustained wsl.exe outage, without unbounded spawns', async () => {
let spawnCount = 0
runProcessMock.mockImplementation(
() =>
new Promise<ProcessResult>((resolve) => {
spawnCount += 1
resolve(exited('Ubuntu\n'))
})
)
await withPlatform('win32', async () => {
await expect(listRunningWslDistrosAsync()).resolves.toEqual(['Ubuntu'])
expect(spawnCount).toBe(1)
runProcessMock.mockImplementation(
() =>
new Promise<ProcessResult>((resolve) => {
spawnCount += 1
resolve(FAILED)
})
)
vi.useFakeTimers()
const observedStates: boolean[] = []
const stop = observeWslTranscriptRunningState(
'\\\\wsl.localhost\\Ubuntu\\home\\ada\\a.jsonl',
() => {
observedStates.push(true)
},
() => {
observedStates.push(false)
}
)
await vi.advanceTimersByTimeAsync(30 * 60_000)
stop()
expect(observedStates.length).toBeGreaterThan(0)
expect(observedStates.every((state) => state === true)).toBe(true)
expect(spawnCount).toBeLessThan(15)
})
})
})
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { getDefaultPersistedState } from '../../../shared/constants'
import type { PersistedState } from '../../../shared/persisted-state-types'
import { foldersAndAutomationsWslMoverProfile } from '../../persistence/__fixtures__/wsl-mover/wsl-mover-folders-profile'
import '../../persistence/__fixtures__/wsl-mover/wsl-mover-folders-profile'
import { heavyWslMoverProfile } from '../../persistence/__fixtures__/wsl-mover/wsl-mover-heavy-profile'
import {
repo,
@@ -42,45 +42,7 @@ function convert(state: PersistedState, repoIds: string[], folderWorkspaceIds: s
return rewriteWslDriveUncState(state, { distro: WSL_MOVER_DISTRO, repoIds, folderWorkspaceIds })
}
const WINDOWS_GIT = { kind: 'wsl', distro: WSL_MOVER_DISTRO, git: 'windows-host' } as const
describe('rewriteWslDriveUncState', () => {
it('re-keys every session, UI and catalog row of the converted projects', () => {
const before = persisted(heavyWslMoverProfile(unc))
const { state, worktreeIds } = convert(before, ['repo-a', 'repo-b'])
const expected = persisted(heavyWslMoverProfile(drive))
for (const [oldId, newId] of worktreeIds) {
expected.worktreeMeta[newId] = {
...expected.worktreeMeta[newId],
priorWorktreeIds: [oldId]
}
}
expected.projects = expected.projects.map((project) => ({
...project,
localWindowsRuntimePreference: WINDOWS_GIT
}))
expect(state).toEqual(expected)
expect(Object.fromEntries(worktreeIds)).toEqual({
[`repo-a::${unc.at('/home/dev/app')}`]: `repo-a::${drive.at('/home/dev/app')}`,
[`repo-a::${unc.at('/home/dev/app-feature')}`]: `repo-a::${drive.at('/home/dev/app-feature')}`,
[`repo-b::${unc.at('/home/dev/lib', 'wsl$')}`]: `repo-b::${drive.at('/home/dev/lib')}`
})
})
it('converts folder workspaces and their group, and keeps automation history as it ran', () => {
const before = persisted(foldersAndAutomationsWslMoverProfile(unc))
const { state } = convert(before, ['repo-auto'], ['folder-1'])
const expected = persisted(foldersAndAutomationsWslMoverProfile(drive))
const wtAuto = `repo-auto::${drive.at('/srv/jobs')}`
expected.worktreeMeta[wtAuto] = {
...expected.worktreeMeta[wtAuto],
priorWorktreeIds: [`repo-auto::${unc.at('/srv/jobs')}`]
}
expected.projects = [{ ...expected.projects[0], localWindowsRuntimePreference: WINDOWS_GIT }]
expected.automationRuns = before.automationRuns
expect(state).toEqual(expected)
})
it('rewrites minted PTY session ids, keeping the incarnation suffix', () => {
const before = persisted(heavyWslMoverProfile(unc))
const oldId = `repo-a::${unc.at('/home/dev/app')}`
@@ -121,19 +83,6 @@ describe('rewriteWslDriveUncState', () => {
)
})
it("keeps a project's own runtime choice and only adds Windows Git to a WSL one", () => {
const before = persisted(heavyWslMoverProfile(unc))
before.projects = [
{ ...before.projects[0], localWindowsRuntimePreference: { kind: 'windows-host' } },
{ ...before.projects[1], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Other' } }
]
const { state } = convert(before, ['repo-a', 'repo-b'])
expect(state.projects.map((row) => row.localWindowsRuntimePreference)).toEqual([
{ kind: 'windows-host' },
{ kind: 'wsl', distro: 'Other', git: 'windows-host' }
])
})
it('is idempotent, so a resumed conversion changes nothing', () => {
const once = convert(persisted(heavyWslMoverProfile(unc)), ['repo-a', 'repo-b']).state
const twice = convert(once, ['repo-a', 'repo-b'])
@@ -11,21 +11,18 @@ import {
assertWslUncPathRunning,
guardWslUncPathAccess,
runAsWslUserAct,
WSL_UNC_RUNNING_LEASE_MS,
WslDistroStoppedError,
WslDistroUnverifiableError
} from './wsl-unc-access-gate'
import {
_resetWslBackgroundDistroGateForTests,
deferUntilWslDistroRunning,
noteWslDistroLaunched,
noteWslDistroMayHaveStopped,
noteWslDistrosRunning,
WSL_STOP_SETTLE_MS
noteWslDistrosRunning
} from '../discovery/wsl-background-distro-gate'
import { EXIT_SETTLE_MS } from '../orcad/wsl-orcad-liveness-evidence'
import '../orcad/wsl-orcad-liveness-evidence'
import { isWslUserActFor } from '../discovery/wsl-user-act-scope'
import { isWslDistroNotRunningRefusal } from '../../../shared/wsl-distro-not-running-refusal'
import '../../../shared/wsl-distro-not-running-refusal'
import {
_resetRunningWslDistroCacheForTests,
resolveRunningWslDistros
@@ -64,43 +61,6 @@ describe('guardWslUncPathAccess', () => {
expect(access).not.toHaveBeenCalled()
})
it('lets a touch of a running distro through', async () => {
await expect(guardWslUncPathAccess(UNC, async () => 'read')).resolves.toBe('read')
})
it('never probes WSL for a non-UNC path or off Windows', async () => {
await expect(guardWslUncPathAccess('C:\\repo', async () => 'read')).resolves.toBe('read')
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
await expect(guardWslUncPathAccess(UNC, async () => 'read')).resolves.toBe('read')
expect(probe).not.toHaveBeenCalled()
})
it('shares one running read across a burst of touches within the lease', async () => {
await Promise.all([1, 2, 3].map(() => guardWslUncPathAccess(UNC, async () => 'read')))
await guardWslUncPathAccess(UNC, async () => 'read')
expect(probe).toHaveBeenCalledTimes(1)
})
it('without a stop signal, trusts a running read for 15 s (at most four wsl.exe a minute)', async () => {
await guardWslUncPathAccess(UNC, async () => 'read')
const now = performance.now()
const clock = vi.spyOn(performance, 'now')
for (const later of [5_000, 10_000, WSL_UNC_RUNNING_LEASE_MS - 1_000]) {
clock.mockReturnValue(now + later)
await guardWslUncPathAccess(UNC, async () => 'read')
}
expect(WSL_UNC_RUNNING_LEASE_MS).toBe(15_000)
expect(probe).toHaveBeenCalledTimes(1)
})
it('re-reads once the lease expires', async () => {
await guardWslUncPathAccess(UNC, async () => 'read')
const now = performance.now()
vi.spyOn(performance, 'now').mockReturnValue(now + WSL_UNC_RUNNING_LEASE_MS + 1)
await guardWslUncPathAccess(UNC, async () => 'read')
expect(probe).toHaveBeenCalledTimes(2)
})
it('does not trust a running read that started before the distro may have stopped', async () => {
await guardWslUncPathAccess(UNC, async () => 'read')
// The terminate: the read above still says running, but a link into the distro just died.
@@ -112,13 +72,6 @@ describe('guardWslUncPathAccess', () => {
expect(probe).toHaveBeenCalledTimes(2)
})
it('retries once past the failure backoff when WSL does not answer, and then reads', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
probe.mockRejectedValueOnce(new Error('wsl.exe timed out'))
await expect(guardWslUncPathAccess(UNC, async () => 'read')).resolves.toBe('read')
expect(probe).toHaveBeenCalledTimes(2)
})
it("says WSL didn't answer, not that the distro stopped, and never touches the path", async () => {
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
probe.mockRejectedValue(new Error('wsl.exe timed out'))
@@ -134,29 +87,6 @@ describe('guardWslUncPathAccess', () => {
expect(access).not.toHaveBeenCalled()
})
it('never trusts a read that started while a just-signalled stop was settling', async () => {
noteWslDistroMayHaveStopped('Ubuntu')
// Another caller's read lands inside the settle window, while WSL still lists the stopping distro.
await resolveRunningWslDistros(async () => ['Ubuntu'])
running = []
const startedAt = performance.now()
await expect(guardWslUncPathAccess(UNC, async () => 'read')).rejects.toBeInstanceOf(
WslDistroStoppedError
)
expect(probe).toHaveBeenCalledOnce()
expect(performance.now() - startedAt).toBeGreaterThanOrEqual(WSL_STOP_SETTLE_MS - 50)
})
it('treats a stop another read observed like a signalled one', async () => {
await guardWslUncPathAccess(UNC, async () => 'read')
running = []
// Any confirmed read that no longer lists the distro resets the lease (wsl --shutdown, idle stop).
await resolveRunningWslDistros(probe)
running = ['Ubuntu']
await guardWslUncPathAccess(UNC, async () => 'read')
expect(probe).toHaveBeenCalledTimes(3)
})
it('a user act touches without asking and, on success, resumes work waiting for the distro', async () => {
running = []
const resumed = vi.fn()
@@ -184,17 +114,6 @@ describe('guardWslUncPathAccess', () => {
})
})
describe('assertWslUncPathRunning for Git', () => {
it('covers a host path whose Git runs through wsl.exe in a distro', async () => {
running = []
await expect(assertWslUncPathRunning('C:\\repo', 'Ubuntu')).rejects.toBeInstanceOf(
WslDistroStoppedError
)
await expect(assertWslUncPathRunning('C:\\repo')).resolves.toBeUndefined()
expect(probe).toHaveBeenCalledTimes(1)
})
})
describe('runAsWslUserAct', () => {
it('lets gated work inside a user act start its own distro, and no other', async () => {
running = []
@@ -220,31 +139,6 @@ describe('runAsWslUserAct', () => {
})
})
describe('explicit acts that start a distro', () => {
it('a wsl.exe launch for a distro last read as stopped makes background reads look again', async () => {
running = []
await expect(guardWslUncPathAccess(UNC, async () => 'read')).rejects.toBeInstanceOf(
WslDistroStoppedError
)
// A commit or a WSL terminal ran through wsl.exe and started it.
running = ['Ubuntu']
noteWslDistroLaunched('Ubuntu')
await expect(guardWslUncPathAccess(UNC, async () => 'read')).resolves.toBe('read')
expect(probe).toHaveBeenCalledTimes(2)
})
it('a launch for a distro already read as running costs no extra read', async () => {
await guardWslUncPathAccess(UNC, async () => 'read')
noteWslDistroLaunched('Ubuntu')
await guardWslUncPathAccess(UNC, async () => 'read')
expect(probe).toHaveBeenCalledOnce()
})
it('settles for as long as the orcad liveness exit window', () => {
expect(WSL_STOP_SETTLE_MS).toBe(EXIT_SETTLE_MS)
})
})
describe('user-act scope does not leak', () => {
it('deferred work drained inside a user act runs outside it', async () => {
const inherited: boolean[] = []
@@ -265,22 +159,3 @@ describe('user-act scope does not leak', () => {
expect(inherited).toEqual([false])
})
})
describe('a poller recognizes the refusal however it arrives', () => {
it.each([
['in process', (error: Error) => error],
['over IPC', (error: Error) => new Error(`Error invoking remote method 'gh:issue': ${error}`)],
['over RPC', (error: Error) => new Error(error.message)]
])('%s', (_transport, deliver) => {
expect(isWslDistroNotRunningRefusal(deliver(new WslDistroStoppedError('Ubuntu')))).toBe(true)
expect(
isWslDistroNotRunningRefusal(
deliver(new WslDistroStoppedError('Ubuntu', { retryStartsIt: false }))
)
).toBe(true)
expect(isWslDistroNotRunningRefusal(deliver(new WslDistroUnverifiableError('Ubuntu')))).toBe(
true
)
expect(isWslDistroNotRunningRefusal(deliver(new Error('gh: not logged in')))).toBe(false)
})
})
@@ -1,331 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
assertWslHostReachable,
configureWslHostsGate,
isGatedOffWslHost,
noteWslHostsLivePolicyBlock,
onWslHostsNewWorkChanged,
resetWslHostsGateForTests,
wslHostsAcceptNewWork,
wslHostsOptedIn,
wslHostsGateOpen,
wslHostsAppliedMovedProjects,
wslHostsAppliedPolicyState,
wslHostsBringBackMovedProjects,
wslHostsGateState,
wslHostsPolicyBlocked,
wslHostsServing,
wslHostsWindowArguments,
WSL_HOSTS_GATED_OFF_MESSAGE
} from './wsl-host-gate'
import {
ignoreInheritedWslHostsGateState,
isWslHostsServing,
WSL_HOSTS_GATE_STATE_ENV
} from '../../../shared/wsl-host-internal-gate'
const wslHost = {
orcadDeployment: { kind: 'wsl' as const, distro: 'Ubuntu', localPort: 41000, remotePort: 6768 }
}
const sshHost = { orcadDeployment: null }
const facts = (hasSavedWslHost: boolean, hasWslMoveCutover: boolean) => ({
hasSavedWslHost: () => hasSavedWslHost,
hasWslMoveCutover: () => hasWslMoveCutover
})
afterEach(() => {
vi.unstubAllEnvs()
resetWslHostsGateForTests()
})
describe('WSL hosts gate', () => {
it('flag-off with nothing saved: closed, exactly as before', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
configureWslHostsGate(facts(false, false))
expect(wslHostsGateState()).toBe('closed')
expect(wslHostsServing()).toBe(false)
expect(isGatedOffWslHost(wslHost)).toBe(true)
expect(() => assertWslHostReachable(wslHost)).toThrow(WSL_HOSTS_GATED_OFF_MESSAGE)
expect(isGatedOffWslHost(sshHost)).toBe(false)
})
it.each([
['a saved host', facts(true, false)],
['a move journal entry', facts(false, true)]
])('serves %s behind a closed gate without opening it', (_label, saved) => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
configureWslHostsGate(saved)
expect(wslHostsGateState()).toBe('serve-only')
expect(wslHostsServing()).toBe(true)
expect(wslHostsGateOpen()).toBe(false)
expect(isGatedOffWslHost(wslHost)).toBe(false)
expect(() => assertWslHostReachable(wslHost)).not.toThrow()
})
it('treats unreadable saved state as absent', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
configureWslHostsGate({
hasSavedWslHost: () => {
throw new Error('unreadable')
},
hasWslMoveCutover: () => {
throw new Error('unreadable')
}
})
expect(wslHostsGateState()).toBe('closed')
})
it('stays open with the internal gate on, and reads nothing to decide', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
const hasSavedWslHost = vi.fn(() => false)
const hasWslMoveCutover = vi.fn(() => false)
configureWslHostsGate({ hasSavedWslHost, hasWslMoveCutover })
expect(wslHostsGateState()).toBe('open')
expect(hasSavedWslHost).not.toHaveBeenCalled()
expect(hasWslMoveCutover).not.toHaveBeenCalled()
})
it('is closed before startup configures it', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
expect(wslHostsGateState()).toBe('closed')
})
it('stamps window argv only in serve-only', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
configureWslHostsGate(facts(false, false))
expect(wslHostsWindowArguments()).toEqual([])
configureWslHostsGate(facts(true, false))
expect(wslHostsWindowArguments()).toEqual(['--orca-wsl-hosts-serve-only'])
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
expect(wslHostsWindowArguments()).toEqual([])
})
describe('remote policy block', () => {
const blocked = (hasSavedWslHost: boolean, hasWslMoveCutover: boolean) => ({
...facts(hasSavedWslHost, hasWslMoveCutover),
policy: () => 'block' as const
})
it('closes an open internal gate for the run, in main and in shared readers', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(blocked(false, false))
expect(wslHostsGateOpen()).toBe(false)
expect(wslHostsGateState()).toBe('closed')
expect(wslHostsPolicyBlocked()).toBe(true)
expect(isGatedOffWslHost(wslHost)).toBe(true)
expect(isWslHostsServing(process.env)).toBe(false)
expect(wslHostsWindowArguments()).toEqual(['--orca-wsl-hosts-policy-blocked'])
})
it('keeps serving saved WSL work, exactly as a closed internal gate does', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(blocked(true, false))
expect(wslHostsGateState()).toBe('serve-only')
expect(wslHostsGateOpen()).toBe(false)
expect(wslHostsServing()).toBe(true)
expect(isWslHostsServing(process.env)).toBe(true)
expect(wslHostsWindowArguments()).toEqual([
'--orca-wsl-hosts-policy-blocked',
'--orca-wsl-hosts-serve-only'
])
})
it('is not read, and changes nothing, behind a closed internal gate', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
const policy = vi.fn(() => 'block' as const)
configureWslHostsGate({ ...facts(true, false), policy })
expect(policy).not.toHaveBeenCalled()
expect(wslHostsPolicyBlocked()).toBe(false)
expect(wslHostsAppliedPolicyState()).toBeNull()
expect(wslHostsWindowArguments()).toEqual(['--orca-wsl-hosts-serve-only'])
})
it('fails open to the internal gate when the policy cannot be read', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate({
...facts(false, false),
policy: () => {
throw new Error('unreadable')
}
})
expect(wslHostsGateState()).toBe('open')
expect(wslHostsAppliedPolicyState()).toBe('missing')
expect(wslHostsWindowArguments()).toEqual([])
})
it('leaves SSH hosts untouched', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(blocked(false, false))
expect(isGatedOffWslHost(sshHost)).toBe(false)
expect(() => assertWslHostReachable(sshHost)).not.toThrow()
})
})
describe('publishing the resolved state to child processes', () => {
it.each([
['open', '1', facts(false, false), undefined],
['serve-only', undefined, facts(true, false), undefined],
['closed', undefined, facts(false, false), undefined],
['closed', '1', facts(false, false), 'block' as const],
['serve-only', '1', facts(false, true), 'block' as const]
])('publishes %s', (state, gateEnv, saved, policy) => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', gateEnv)
configureWslHostsGate({ ...saved, ...(policy ? { policy: () => policy } : {}) })
expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBe(state)
})
// A relaunch or the updater's installer inherits the parent's value; main must recompute.
it.each([
['closed', '1', 'open', true],
['open', undefined, 'closed', false]
])(
'ignores a stale inherited %s and overwrites it',
(inherited, gateEnv, recomputed, serving) => {
vi.stubEnv(WSL_HOSTS_GATE_STATE_ENV, inherited)
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', gateEnv)
ignoreInheritedWslHostsGateState()
expect(isWslHostsServing(process.env)).toBe(gateEnv === '1')
configureWslHostsGate(facts(false, false))
expect(wslHostsGateState()).toBe(recomputed)
expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBe(recomputed)
expect(wslHostsServing()).toBe(serving)
expect(isWslHostsServing(process.env)).toBe(serving)
}
)
// A blocked run's child inherits ORCA_INTERNAL_WSL_HOSTS=1; only the published state says closed.
it('lets a child that never configured the gate trust what main published', () => {
const gate = { ORCA_INTERNAL_WSL_HOSTS: '1' }
expect(isWslHostsServing({ ...gate, [WSL_HOSTS_GATE_STATE_ENV]: 'closed' })).toBe(false)
// Children of a serve-only run see no gate env at all, yet must still serve saved WSL work.
expect(isWslHostsServing({ [WSL_HOSTS_GATE_STATE_ENV]: 'serve-only' })).toBe(true)
})
// A child of an older Orca, or a garbled value, must keep today's env-only reading.
it('falls back to the internal gate env in a child when nothing valid was published', () => {
expect(isWslHostsServing({ ORCA_INTERNAL_WSL_HOSTS: '1' })).toBe(true)
// With the gate env on, a garbled value must not read as 'closed'.
expect(
isWslHostsServing({ ORCA_INTERNAL_WSL_HOSTS: '1', [WSL_HOSTS_GATE_STATE_ENV]: 'bogus' })
).toBe(true)
})
it('withdraws it on reset', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(facts(false, false))
resetWslHostsGateForTests()
expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBeUndefined()
})
})
describe('moved projects under a block', () => {
it('asks for moved projects back only when the block says so', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate({
...facts(true, true),
policy: () => 'block',
movedProjects: () => 'bring-back'
})
expect(wslHostsBringBackMovedProjects()).toBe(true)
expect(wslHostsAppliedMovedProjects()).toBe('bring-back')
expect(wslHostsGateState()).toBe('serve-only')
})
it('never reads it without a block', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
const movedProjects = vi.fn(() => 'bring-back' as const)
configureWslHostsGate({ ...facts(true, true), policy: () => 'allow', movedProjects })
expect(movedProjects).not.toHaveBeenCalled()
expect(wslHostsBringBackMovedProjects()).toBe(false)
expect(wslHostsAppliedMovedProjects()).toBeNull()
})
it('keeps moved projects served when the choice cannot be read', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate({
...facts(true, true),
policy: () => 'block',
movedProjects: () => {
throw new Error('unreadable')
}
})
expect(wslHostsBringBackMovedProjects()).toBe(false)
expect(wslHostsAppliedMovedProjects()).toBe('keep')
})
it('clears on reset', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate({
...facts(true, true),
policy: () => 'block',
movedProjects: () => 'bring-back'
})
resetWslHostsGateForTests()
expect(wslHostsBringBackMovedProjects()).toBe(false)
})
})
describe('a block learned mid-run', () => {
// New work stops at once, but what is served must not change until the next launch.
it('stops new work without closing the gate or stranding saved work', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(facts(true, true))
const changed = vi.fn()
onWslHostsNewWorkChanged(changed)
noteWslHostsLivePolicyBlock()
noteWslHostsLivePolicyBlock()
expect(wslHostsAcceptNewWork()).toBe(false)
expect(changed).toHaveBeenCalledOnce()
expect(wslHostsGateOpen()).toBe(true)
expect(wslHostsGateState()).toBe('open')
expect(wslHostsServing()).toBe(true)
expect(isGatedOffWslHost(wslHost)).toBe(false)
expect(wslHostsWindowArguments()).toEqual([])
})
// Stage 1's opt-in (D1c Git routing, conversions, new-project Git default) ends with it.
it('ends the Stage 1 opt-in even with the setting on', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
configureWslHostsGate(facts(false, false))
expect(wslHostsOptedIn({ experimentalWslHosts: true })).toBe(true)
noteWslHostsLivePolicyBlock()
expect(wslHostsOptedIn({ experimentalWslHosts: true })).toBe(false)
})
// Behind a gate that is already closed there is nothing new to stop, so nobody is told.
it('is a no-op behind a closed gate', () => {
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', undefined)
configureWslHostsGate(facts(true, false))
const changed = vi.fn()
onWslHostsNewWorkChanged(changed)
noteWslHostsLivePolicyBlock()
expect(changed).not.toHaveBeenCalled()
expect(wslHostsGateState()).toBe('serve-only')
})
})
})
@@ -27,73 +27,6 @@ async function stripLayout(page: Page) {
test.use({ viewport: { width: 1900, height: 720 } })
test('content tabs use intrinsic widths and shrink to their minimum before overflowing', async ({
page
}) => {
await openFixture(page, 'nonAgentTabs')
const strip = page.locator('.terminal-tab-strip')
const assertWidths = async () => {
for (const id of ['browser-0', 'file-0']) {
expect(await width(slot(page, id))).toBeCloseTo(144, 0)
}
for (const id of ['tab-1', 'browser-1', 'file-1']) {
expect(await width(slot(page, id))).toBeCloseTo(300, 0)
const label = slot(page, id).locator('.tab-label')
expect(await label.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(true)
expect(await label.evaluate((node) => getComputedStyle(node).textOverflow)).toBe('ellipsis')
}
}
await assertWidths()
expect(await strip.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(false)
await page.locator('#component-fixture-frame').evaluate((node) => {
node.style.width = '1280px'
})
for (const id of ['tab-0', 'tab-1', 'browser-1', 'file-1']) {
await expect.poll(() => width(slot(page, id))).toBeGreaterThan(180)
expect(await width(slot(page, id))).toBeLessThan(250)
}
expect(await strip.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(false)
await page.locator('#component-fixture-frame').evaluate((node) => {
node.style.width = '420px'
})
await expect(page.getByRole('button', { name: 'Scroll tabs right' })).toBeVisible()
for (const id of ['tab-0', 'browser-0', 'file-0', 'tab-1', 'browser-1', 'file-1']) {
expect(await width(slot(page, id))).toBeCloseTo(144, 0)
}
expect(await strip.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(true)
await test.info().attach('overflowing-content-tabs', {
body: await page.locator('#component-fixture-frame').screenshot(),
contentType: 'image/png'
})
})
test('agent tabs keep their preferred width and share space down to their minimum', async ({
page
}) => {
await openFixture(page, 'twoTabs')
expect(await width(slot(page, 'tab-0'))).toBeCloseTo(300, 0)
expect(await width(slot(page, 'tab-1'))).toBeCloseTo(300, 0)
await page.locator('#component-fixture-frame').evaluate((node) => {
node.style.width = '420px'
})
await expect.poll(() => width(slot(page, 'tab-0'))).toBeLessThan(210)
expect(await width(slot(page, 'tab-0'))).toBeGreaterThanOrEqual(144)
expect(await width(slot(page, 'tab-0'))).toBeCloseTo(await width(slot(page, 'tab-1')), 0)
await page.locator('#component-fixture-frame').evaluate((node) => {
node.style.width = '280px'
})
await expect.poll(() => width(slot(page, 'tab-0'))).toBeCloseTo(144, 0)
expect(await width(slot(page, 'tab-1'))).toBeCloseTo(144, 0)
expect(
await page
.locator('.terminal-tab-strip')
.evaluate((node) => node.scrollWidth > node.clientWidth)
).toBe(true)
})
for (const fixture of ['nonAgentTabs', 'nonAgentTabsOverflowing']) {
test(`renaming reserves usable input space and keeps typing stable in ${fixture}`, async ({
page
@@ -128,25 +61,3 @@ for (const fixture of ['nonAgentTabs', 'nonAgentTabsOverflowing']) {
}
})
}
test('long file tabs share available space before the strip overflows', async ({ page }) => {
await openFixture(page, 'constrainedFiles')
for (const id of ['file-0', 'file-1']) {
expect(await width(slot(page, id))).toBeGreaterThan(240)
expect(await width(slot(page, id))).toBeLessThan(270)
}
expect(
await page
.locator('.terminal-tab-strip')
.evaluate((node) => node.scrollWidth > node.clientWidth)
).toBe(false)
})
test('terminal sizing stays stable as agent identity appears and disappears', async ({ page }) => {
await openFixture(page, 'agentIdentityChanges')
for (let index = 0; index < 3; index++) {
expect(await width(slot(page, 'tab-0'))).toBeCloseTo(300, 0)
expect(await width(slot(page, 'tab-1'))).toBeCloseTo(300, 0)
await page.getByRole('button', { name: 'Toggle agent identity' }).click()
}
})
@@ -1,12 +1,12 @@
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { beforeAll, describe, expect, it } from 'vitest'
import { DeviceRegistry } from '../../../src/main/runtime/rpc/server/pairing/device-registry'
import { usesOlderPairingMethod } from '../../../src/main/runtime/rpc/server/pairing/pairing-ticket-exchange'
import { PAIRING_TICKET_EXCHANGE_RUNTIME_CAPABILITY } from '../../../src/shared/remote-runtime/pairing/ticket-exchange-capability'
import { encodePairingOffer } from '../../../src/shared/remote-runtime/pairing/pairing'
import { exchangeRemotePairingTicket } from '../../../src/shared/remote-runtime/pairing/remote-pairing-ticket-exchange'
import '../../../src/shared/remote-runtime/pairing/ticket-exchange-capability'
import '../../../src/shared/remote-runtime/pairing/pairing'
import '../../../src/shared/remote-runtime/pairing/remote-pairing-ticket-exchange'
import {
importReleaseCheckoutModule,
materializeReleaseCheckout,
@@ -24,18 +24,15 @@ type ReleasedRegistryLike = {
}
let ReleasedRegistry: new (userDataPath: string) => ReleasedRegistryLike
let releasedCapabilities: readonly string[]
beforeAll(async () => {
const checkout = await materializeReleaseCheckout(resolveBaselineReleaseRef())
const [registryModule, protocolModule] = await Promise.all([
importReleaseCheckoutModule(checkout, 'src/main/runtime/rpc/server/pairing/device-registry.ts'),
importReleaseCheckoutModule(checkout, 'src/shared/protocol-version.ts')
])
const registryModule = await importReleaseCheckoutModule(
checkout,
'src/main/runtime/rpc/server/pairing/device-registry.ts'
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release's DeviceRegistry class; a changed shape fails the assertions below.
ReleasedRegistry = registryModule.DeviceRegistry as typeof ReleasedRegistry
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release's capability list; a changed shape fails the assertions below.
releasedCapabilities = protocolModule.RUNTIME_CAPABILITIES as readonly string[]
}, SUITE_TIMEOUT_MS)
function pairedTicket(registry: DeviceRegistry, name: string) {
@@ -45,30 +42,6 @@ function pairedTicket(registry: DeviceRegistry, name: string) {
}
describe('cross-version: single-use pairing tickets', () => {
it('a released host does not advertise the exchange, so updated clients keep the code', () => {
expect(releasedCapabilities).not.toContain(PAIRING_TICKET_EXCHANGE_RUNTIME_CAPABILITY)
})
it('Add server and the CLI save the link unchanged for a released host, without calling it', async () => {
const offer = {
v: 2 as const,
endpoint: 'ws://192.168.1.10:6768',
deviceToken: 'link-token',
publicKeyB64: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
}
const pairingCode = encodePairingOffer(offer)
const send = vi.fn()
await expect(
exchangeRemotePairingTicket({
pairingCode,
offer,
hostCapabilities: releasedCapabilities,
send
})
).resolves.toBe(pairingCode)
expect(send).not.toHaveBeenCalled()
})
it('after a downgrade, exchanged phones keep their own token and older phones their code', () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-xv-ticket-'))
const current = new DeviceRegistry(userDataPath)
@@ -208,20 +208,6 @@ test.describe('terminal link click ownership', () => {
await sendToTerminal(orcaPage, ptyId, 'q')
})
test('an Orca-owned plain link click emits no child PTY mouse frames', async ({
orcaPage
}, testInfo) => {
const { mouseLogPath, ptyId, target } = await startMouseAwareLinkFixture(orcaPage, testInfo)
await orcaPage.mouse.click(target.x, target.y)
await expect(orcaPage.locator('[data-terminal-link-action-popover]')).toBeVisible()
await expect(orcaPage.locator('[data-terminal-link-destination]')).toHaveText(LINK)
await expectOrcaOwnedMouseOutcome(mouseLogPath)
await sendToTerminal(orcaPage, ptyId, 'q')
})
test('an Orca-owned OSC link click emits no child PTY mouse frames', async ({
orcaPage
}, testInfo) => {
@@ -241,21 +227,6 @@ test.describe('terminal link click ownership', () => {
await sendToTerminal(orcaPage, ptyId, 'q')
})
test('a plain click stays child-owned when link actions are disabled', async ({
orcaPage
}, testInfo) => {
const { mouseLogPath, ptyId, target } = await startMouseAwareLinkFixture(orcaPage, testInfo)
await orcaPage.evaluate(async () => {
await window.__store?.getState().updateSettings({ terminalLinkActionPopoverEnabled: false })
})
await orcaPage.mouse.click(target.x, target.y)
await expect(orcaPage.locator('[data-terminal-link-action-popover]')).toHaveCount(0)
await expectChildMouseReports(mouseLogPath)
await sendToTerminal(orcaPage, ptyId, 'q')
})
test('a drag across a link stays child-owned', async ({ orcaPage }, testInfo) => {
const { mouseLogPath, ptyId, target } = await startMouseAwareLinkFixture(orcaPage, testInfo)
+1 -40
View File
@@ -7,10 +7,7 @@ import {
setPaneTitleFromTerminalMenu,
openTerminalContextMenu
} from './helpers/terminal-pane-title-actions'
import {
readVisibleXtermContainerBox,
expectTerminalToReserveTitleSpace
} from './helpers/terminal-pane-geometry'
import './helpers/terminal-pane-geometry'
import { registerTerminalPaneMountReadiness } from './helpers/terminal-pane-mount-readiness'
async function openPaneTitleContextMenu(page: Page, title: string): Promise<void> {
@@ -103,42 +100,6 @@ test.describe('Terminal Panes', () => {
await expect(orcaPage.locator('.pane-title-text', { hasText: title })).toHaveCount(1)
})
test('Set Title editor renders in Orca overlay while terminal reserves title space', async ({
orcaPage
}) => {
const title = `Reserved overlay title ${Date.now()}`
const terminalBoxBefore = await readVisibleXtermContainerBox(orcaPage)
await openTerminalContextMenu(orcaPage)
await orcaPage.getByText('Set Title…', { exact: true }).click()
const titleInput = orcaPage.locator('.pane-title-overlay-layer .pane-title-input').first()
await expect(titleInput).toBeVisible()
await expect(titleInput).toBeFocused()
await expect(orcaPage.getByText('Set Title…', { exact: true })).toBeHidden()
await expect(orcaPage.locator('.pane .pane-title-input')).toHaveCount(0)
await expect(orcaPage.locator('.pane[data-has-title]')).toHaveCount(1)
await expect
.poll(() =>
orcaPage
.locator('.pane-title-bar')
.first()
.evaluate((titleBar) => getComputedStyle(titleBar).backgroundColor)
)
.not.toBe('rgba(0, 0, 0, 0)')
const terminalBoxEditing = await readVisibleXtermContainerBox(orcaPage)
expectTerminalToReserveTitleSpace(terminalBoxEditing, terminalBoxBefore)
await titleInput.fill(title)
await titleInput.press('Enter')
await expect(orcaPage.locator('.pane-title-text', { hasText: title })).toBeVisible()
await expect(orcaPage.locator('.pane[data-has-title]')).toHaveCount(1)
expectTerminalToReserveTitleSpace(
await readVisibleXtermContainerBox(orcaPage),
terminalBoxBefore
)
})
test('Set Title context menu opens from the title overlay strip', async ({ orcaPage }) => {
const title = `Overlay menu title ${Date.now()}`
const updatedTitle = `Overlay menu updated ${Date.now()}`
@@ -1,7 +1,6 @@
import type { Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import {
moveTerminalPaneByLeafId,
readPaneIdentitySnapshot,
readTerminalPaneDomLeafOrder,
splitActiveTerminalPane,
@@ -133,92 +132,6 @@ test.describe('Terminal Panes', () => {
.toEqual({ activeLeafId: otherPane.leafId, targetHasPath: true, otherHasPath: false })
})
test('Set Title overlay follows its pane after same-count pane move', async ({ orcaPage }) => {
const title = `Moved overlay title ${Date.now()}`
await setPaneTitleFromTerminalMenu(orcaPage, title)
const initialSnapshot = await waitForPaneIdentitySnapshot(orcaPage, 1)
const titledLeafId = initialSnapshot.activeLeafId ?? initialSnapshot.panes[0]?.leafId
if (!titledLeafId) {
throw new Error('No titled pane leaf id found before move')
}
await splitActiveTerminalPane(orcaPage, 'vertical')
await waitForPaneCount(orcaPage, 2)
const beforeMove = await waitForPaneIdentitySnapshot(orcaPage, 2)
const target = beforeMove.panes.find((pane) => pane.leafId !== titledLeafId)
if (!target) {
throw new Error('No target pane found for titled pane move')
}
const beforeOrder = await readTerminalPaneDomLeafOrder(orcaPage)
await expectPaneTitleAttachedToLeaf(orcaPage, title, titledLeafId)
await moveTerminalPaneByLeafId(orcaPage, titledLeafId, target.leafId, 'right')
await expect
.poll(async () => readTerminalPaneDomLeafOrder(orcaPage), {
timeout: 10_000,
message: 'Pane move did not update DOM order'
})
.not.toEqual(beforeOrder)
await expectPaneTitleAttachedToLeaf(orcaPage, title, titledLeafId)
})
test('Set Title keeps the pane drag handle available over the title strip', async ({
orcaPage
}) => {
const title = `Draggable title ${Date.now()}`
await setPaneTitleFromTerminalMenu(orcaPage, title)
const initialSnapshot = await waitForPaneIdentitySnapshot(orcaPage, 1)
const titledLeafId = initialSnapshot.activeLeafId ?? initialSnapshot.panes[0]?.leafId
if (!titledLeafId) {
throw new Error('No titled pane leaf id found before split')
}
await splitActiveTerminalPane(orcaPage, 'vertical')
await waitForPaneCount(orcaPage, 2)
await expectPaneTitleAttachedToLeaf(orcaPage, title, titledLeafId)
const titleTopHit = await orcaPage.evaluate(
({ title, titledLeafId }) => {
const titleBar = Array.from(document.querySelectorAll<HTMLElement>('.pane-title-bar')).find(
(element) => element.textContent?.includes(title)
)
const titleDragHandle =
titleBar?.querySelector<HTMLElement>('.pane-title-drag-handle') ?? null
const pane = document.querySelector<HTMLElement>(`.pane[data-leaf-id="${titledLeafId}"]`)
if (!titleBar || !pane || !titleDragHandle) {
return null
}
const titleRect = titleBar.getBoundingClientRect()
const hitElement = document.elementFromPoint(
titleRect.left + titleRect.width / 2,
titleRect.top + 4
)
return {
hitDragHandle:
hitElement instanceof HTMLElement &&
hitElement.closest('.pane-title-drag-handle') !== null,
pointerEvents: getComputedStyle(titleDragHandle).pointerEvents,
titleTop: titleRect.top,
handleTop: titleDragHandle.getBoundingClientRect().top
}
},
{ title, titledLeafId }
)
expect(titleTopHit).not.toBeNull()
expect(titleTopHit?.hitDragHandle).toBe(true)
expect(titleTopHit?.pointerEvents).toBe('auto')
expect(Math.abs((titleTopHit?.handleTop ?? 0) - (titleTopHit?.titleTop ?? 0))).toBeLessThan(1)
await orcaPage.locator('.pane-title-bar', { hasText: title }).click({
position: { x: 20, y: 18 }
})
await expect(orcaPage.locator('.pane-title-input')).toBeVisible()
})
test('@headful Set Title pane can be dragged from the title strip', async ({ orcaPage }) => {
const title = `Dragged title ${Date.now()}`
+6 -155
View File
@@ -1,40 +1,17 @@
import type { Locator, Page } from '@stablyai/playwright-test'
import { mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import 'node:fs'
import 'node:path'
import { test, expect } from './helpers/orca-app'
import { addRepoWithWorkspaces, linkWorkspaces } from './helpers/repo-with-workspaces'
import { SIDEBAR_INPUTS_BUILD } from './helpers/sidebar-inputs-build'
import './helpers/repo-with-workspaces'
import './helpers/sidebar-inputs-build'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
markWorkspaceTerminalSlept,
seedLineageScenario,
seedWorkspaceAgentStatus,
seedWorkspaceLiveTerminal
} from './worktree-lineage-state'
import { seedLineageScenario } from './worktree-lineage-state'
import { worktreeRow } from './worktree-row-locators'
function worktreeOption(page: Page, worktreeId: string) {
return worktreeRow(page, worktreeId)
}
async function captureEvidence(page: Page, name: string, locator?: Locator): Promise<void> {
if (process.env.ORCA_CAPTURE_EVIDENCE !== '1') {
return
}
const outputDir = resolve(process.cwd(), 'pr-evidence')
mkdirSync(outputDir, { recursive: true })
const path = resolve(outputDir, name)
if (locator) {
await locator.screenshot({ path })
return
}
await page.screenshot({ path })
}
async function captureSidebarEvidence(page: Page, name: string): Promise<void> {
await captureEvidence(page, name, page.locator('[data-worktree-sidebar]').first())
}
test.describe('Worktree Lineage', () => {
test.describe.configure({ mode: 'serial' })
@@ -107,35 +84,6 @@ test.describe('Worktree Lineage', () => {
await expect(childRow).toBeVisible()
})
test('renders legacy-only inline lineage when side-map hydration is absent', async ({
orcaPage
}) => {
test.skip(
SIDEBAR_INPUTS_BUILD,
"the window's legacy inline link is unused when rows come from main"
)
const { parentId, childId } = await seedLineageScenario(orcaPage, { inlineOnly: true })
const parentRow = worktreeOption(orcaPage, parentId)
const childRow = worktreeOption(orcaPage, childId)
await expect(parentRow.getByRole('button', { name: 'Hide 1 child workspace' })).toBeVisible()
await expect(childRow).toBeVisible()
await expect
.poll(async () => {
const [parentBox, childBox] = await Promise.all([
parentRow.boundingBox(),
childRow.boundingBox()
])
return parentBox && childBox ? childBox.y > parentBox.y : false
})
.toBe(true)
// The link must come from the records alone, not a side map hydrated after the seed.
expect(
await orcaPage.evaluate((id) => window.__store!.getState().worktreeLineageById[id], childId)
).toBeUndefined()
await captureSidebarEvidence(orcaPage, 'legacy-inline-lineage-nested.png')
})
test('injects filtered parents structurally without showing a parent badge', async ({
orcaPage
}) => {
@@ -199,101 +147,4 @@ test.describe('Worktree Lineage', () => {
expect(positions).not.toBeNull()
expect(positions!.childTop).toBeGreaterThan(positions!.parentTop)
})
test('updates nested child preview status when the child terminal sleeps', async ({
orcaPage
}) => {
const { parentId, childId } = await seedLineageScenario(orcaPage)
const parentRow = worktreeOption(orcaPage, parentId)
const childRow = worktreeOption(orcaPage, childId)
await expect(parentRow).toBeVisible()
await expect(childRow).toBeVisible()
const childTabId = await seedWorkspaceLiveTerminal(orcaPage, childId)
await expect(childRow).toContainText('Active')
await markWorkspaceTerminalSlept(orcaPage, { worktreeId: childId, tabId: childTabId })
await expect(childRow).toContainText('Inactive')
})
test('sleeps a workspace and every descendant from the parent context menu', async ({
orcaPage
}, testInfo) => {
// A repo's own checkout offers no delete, so the parent is one of its worktrees.
const repo = await addRepoWithWorkspaces(orcaPage, testInfo, 2)
const [parentId, childId] = [repo.ids[0]!, repo.ids[1]!]
await linkWorkspaces(orcaPage, [{ childId, parentId }])
await orcaPage.evaluate(() => window.__store!.getState().setShowSleepingWorkspaces(true))
const parentTabId = await seedWorkspaceLiveTerminal(orcaPage, parentId)
const childTabId = await seedWorkspaceLiveTerminal(orcaPage, childId)
await orcaPage.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
store.setState({
shutdownWorktreeBrowsers: async (worktreeId: string) => {
store.setState((current) => ({
browserTabsByWorktree: { ...current.browserTabsByWorktree, [worktreeId]: [] }
}))
},
shutdownWorktreeTerminals: async (worktreeId: string) => {
const tabIds = (store.getState().tabsByWorktree[worktreeId] ?? []).map((tab) => tab.id)
store.setState((current) => ({
ptyIdsByTabId: {
...current.ptyIdsByTabId,
...Object.fromEntries(tabIds.map((tabId) => [tabId, []]))
}
}))
}
})
})
await worktreeOption(orcaPage, parentId).click({ button: 'right' })
const sleepSubtree = orcaPage.getByRole('menuitem', {
name: 'Sleep with Descendants (1)'
})
await expect(sleepSubtree).toBeVisible()
await expect(sleepSubtree).toBeEnabled()
await expect(orcaPage.getByRole('menuitem', { name: 'Delete with Descendants…' })).toBeVisible()
await captureEvidence(orcaPage, 'workspace-descendant-actions.png')
await sleepSubtree.click()
await expect
.poll(() =>
orcaPage.evaluate(
({ parentTabId, childTabId }) => {
const state = window.__store?.getState()
return {
parentPtys: state?.ptyIdsByTabId[parentTabId],
childPtys: state?.ptyIdsByTabId[childTabId]
}
},
{ parentTabId, childTabId }
)
)
.toEqual({ parentPtys: [], childPtys: [] })
})
test('shows parent and child agent rows while the parent workspace is active', async ({
orcaPage
}) => {
const { parentId, childId } = await seedLineageScenario(orcaPage)
const parentRow = worktreeOption(orcaPage, parentId)
const childRow = worktreeOption(orcaPage, childId)
await parentRow.click()
await expect(parentRow).toHaveAttribute('aria-current', 'page')
await expect(childRow).toBeVisible()
const parentAgentPrompt = await seedWorkspaceAgentStatus(orcaPage, parentId, 'PARENT')
const childAgentPrompt = await seedWorkspaceAgentStatus(orcaPage, childId, 'CHILD')
await expect(
parentRow.getByRole('treeitem').filter({ hasText: parentAgentPrompt })
).toBeVisible()
await expect(childRow.getByRole('treeitem').filter({ hasText: childAgentPrompt })).toBeVisible()
})
})