test: remove 208 low-value test declarations (#37201)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-01 15:01:33 -08:00
committed by GitHub
co-authored by OrcaWin
parent 8538b07f9a
commit b87d18cf4d
36 changed files with 7 additions and 4774 deletions
+1 -2
View File
@@ -509,7 +509,6 @@
3 test-no-real-time-wait:src/renderer/src/lib/execute-open-editor-path-move.test.ts
1 test-no-real-time-wait:src/renderer/src/lib/native-chat-cli-fork-preparation.test.ts
1 test-no-real-time-wait:src/renderer/src/lib/orca-yaml-trust-prompt-slot-eviction.test.ts
1 test-no-real-time-wait:src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts
3 test-no-real-time-wait:src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts
1 test-no-real-time-wait:src/renderer/src/runtime/runtime-host-account-sign-in-client.test.ts
1 test-no-real-time-wait:src/renderer/src/store/slices/github-pr-checks-fetch.test.ts
@@ -1132,7 +1131,7 @@
1 test-no-wall-clock:src/renderer/src/store/slices/ui-hydration-workspace-cleanup-browse.test.ts
2 test-no-wall-clock:src/renderer/src/store/slices/workspace-cleanup-agent-stop-preflight.test.ts
4 test-no-wall-clock:src/renderer/src/store/slices/workspace-cleanup-local-evidence-invariants.test.ts
3 test-no-wall-clock:src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts
2 test-no-wall-clock:src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts
1 test-no-wall-clock:src/renderer/src/store/slices/worktrees-always-force-delete.test.ts
6 test-no-wall-clock:src/renderer/src/store/slices/worktrees-metadata-persistence.test.ts
1 test-no-wall-clock:src/renderer/src/store/slices/worktrees-terminal-reference-origins.test.ts
@@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { spawnProcess } from '@orca/process-host'
import type { SpawnedProcess, ProcessSpec } from '@orca/process-host/process-spec'
import { query, type CanUseTool, type Options } from '@anthropic-ai/claude-agent-sdk'
import type { query, CanUseTool } from '@anthropic-ai/claude-agent-sdk'
import {
openClaudeStreamJsonConnection,
type ClaudeStreamJsonConnection,
@@ -15,7 +15,6 @@ import {
} from './claude-stream-json-connection'
import { openAgentSessionJournal } from '../native-chat/agent-session-journal/journal-store-factory'
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { claudeAuthDiagnostic } from './structured/claude-structured-init-proof'
import { createClaudeJournalTranslator } from './structured/journal/claude-structured-journal-translation'
import { CLAUDE_STRUCTURED_BASE_OPTIONS } from './structured/launch/claude-structured-launch-resolution'
import {
@@ -25,7 +24,6 @@ import {
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import { providerExecutableMissing } from '../provider-process/provider-executable-missing'
import { providerStartupFailureFact } from '../native-chat/agent-session-wire/structured-agent-session-failure-text'
// Exercise the real SDK against a scripted child CLI.
const FAKE_CLI = join(__dirname, '__fixtures__', 'claude-agent-sdk-scripted-cli.mjs')
@@ -158,17 +156,6 @@ function processState(pid: number): 'running' | 'exited' {
}
describe('Claude stream-json connection', () => {
it('passes the Claude Code system-prompt preset through to SDK query', async () => {
const scenario = scriptScenario([HOLD_OPEN])
let captured: Options | undefined
await open(launchFor(scenario), {}, (params) => {
captured = params.options
return query(params)
})
expect(captured?.systemPrompt).toEqual({ type: 'preset', preset: 'claude_code' })
})
it('hands the child a derived environment, the resolved CLI path, and keeps the pid', async () => {
vi.stubEnv('ANTHROPIC_API_KEY', 'sk-ant-SHELL-LEAK')
vi.stubEnv('CLAUDE_CODE_CHILD_SESSION', '1')
@@ -258,14 +245,6 @@ describe('Claude stream-json connection', () => {
expect(env.ORCA_AGENT_SESSION_ID).toBe('f7a1c0de-1111-4222-8333-444455556666')
})
it('leaves the default CLI home unpinned so macOS Keychain OAuth keeps working', async () => {
const scenario = scriptScenario([HOLD_OPEN])
await open(launchFor(scenario))
await until(() => readReportSafely(scenario), 'the scripted CLI report')
expect(childEnv().CLAUDE_CONFIG_DIR).toBeUndefined()
})
it("drops a CLAUDE_CONFIG_DIR from Orca's own process env: only the launch may name a home", async () => {
vi.stubEnv('CLAUDE_CONFIG_DIR', '/orca/process/claude')
vi.stubEnv('ORCA_CONNECTION_MARKER', 'inherited')
@@ -330,21 +309,6 @@ describe('Claude stream-json connection', () => {
expect(readReportSafely(scenario)?.userMessages ?? []).toHaveLength(0)
})
it('delivers an unmodeled frame verbatim so the provider-fallback row survives', async () => {
const unknown = {
type: 'frame_kind_from_the_future',
session_id: SESSION_ID,
uuid: 'uuid-unknown-1',
payload: { nested: { flags: ['a', 'b'] } }
}
const scenario = scriptScenario([{ emit: unknown }, HOLD_OPEN])
const messages: Record<string, unknown>[] = []
await open(launchFor(scenario), { onMessage: (message) => messages.push(message) })
await until(() => messages.find((message) => message.uuid === 'uuid-unknown-1'), 'the frame')
expect(messages.find((message) => message.uuid === 'uuid-unknown-1')).toEqual(unknown)
})
it('commits the real partial-message cadence as one assistant item through the translator', async () => {
// Claude Code 2.1.258 partial-message order: distinct frame UUIDs correlate through message.id.
const stream = (uuid: string, event: Record<string, unknown>) => ({
@@ -501,82 +465,6 @@ describe('Claude stream-json connection', () => {
expect(written.response.response).toMatchObject({ behavior: 'deny', message: 'No' })
})
it('drives Orca control methods onto the SDK and times out with the init proof message', async () => {
const scenario = scriptScenario([HOLD_OPEN], {
initialize: { models: [{ value: 'sonnet' }], account: { tokenSource: 'oauth' } },
get_settings: { env: { ANTHROPIC_BASE_URL: 'https://settings.example.test' } }
})
const connection = await open(launchFor(scenario))
await expect(connection.initializationResult()).resolves.toMatchObject({
models: [{ value: 'sonnet' }]
})
await expect(connection.getSettings()).resolves.toEqual({
env: { ANTHROPIC_BASE_URL: 'https://settings.example.test' }
})
await expect(connection.setModel('opus')).resolves.toBeUndefined()
const requests = await until(
() =>
readReportSafely(scenario)?.controlRequests.find(
(frame) => frame.request.subtype === 'set_model'
),
'the set_model control request'
)
expect(requests.request.subtype).toBe('set_model')
})
it('reads supportedModels from the catalog the running CLI reported', async () => {
const scenario = scriptScenario([HOLD_OPEN], {
initialize: {
models: [
{ value: 'default', resolvedModel: 'claude-opus-5' },
{
value: 'opus',
displayName: 'Opus 5',
description: 'The live row, not the seed',
resolvedModel: 'claude-opus-5',
supportsEffort: true,
supportedEffortLevels: ['low', 'high']
}
]
}
})
const connection = await open(launchFor(scenario))
await expect(connection.supportedModels()).resolves.toMatchObject([
{ value: 'default', resolvedModel: 'claude-opus-5' },
{ value: 'opus', displayName: 'Opus 5', supportedEffortLevels: ['low', 'high'] }
])
})
it('feeds the auth diagnostic from the settings the running child reports', async () => {
for (const key of ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY']) {
vi.stubEnv(key, undefined)
}
const scenario = scriptScenario([HOLD_OPEN], {
get_settings: {
env: {
ANTHROPIC_BASE_URL: 'https://settings.example.test',
ANTHROPIC_AUTH_TOKEN: 'secret'
}
}
})
const connection = await open(launchFor(scenario))
const init = { providerSessionId: SESSION_ID, uuid: null, model: null, message: {} }
expect(claudeAuthDiagnostic(null, init, null)).toMatchObject({
baseUrlConfigured: false,
authTokenConfigured: false
})
const diagnostic = claudeAuthDiagnostic(null, init, await connection.getSettings())
expect(diagnostic).toMatchObject({
baseUrlConfigured: true,
authTokenConfigured: true,
apiKeyConfigured: false
})
expect(JSON.stringify(diagnostic)).not.toContain('secret')
})
it('settles an unanswered initialize when the child exits, with no timer of its own', async () => {
const scenario = scriptScenario([{ stderr: 'claude: not signed in\n' }, { exit: 1 }])
const connection = await open({
@@ -614,24 +502,6 @@ describe('Claude stream-json connection', () => {
}
})
it("names Claude's root refusal of Full access, keeping its words as the detail", async () => {
const refusal =
'--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons'
const scenario = scriptScenario([{ stderr: `${refusal}\n` }, { exit: 1 }])
let exit: Error | null = null
await open(launchFor(scenario), {
onExit: (error) => {
exit = error
}
})
await until(() => exit, 'the exit error')
expect(providerStartupFailureFact(exit)).toEqual({
kind: 'fullAccessAsRoot',
detail: { text: expect.stringContaining(refusal), audience: 'log' }
})
})
it.runIf(process.platform !== 'win32')(
'proves a natural SDK exit and cleans up its descendant before recovery',
async () => {
@@ -3,7 +3,6 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
import { join } from 'node:path'
import type * as CodexConfigMirror from '../codex/codex-config-mirror'
import type * as CodexHomePaths from '../codex/codex-home-paths'
import type * as CodexPaneAccountRegistry from '../codex/codex-pane-account-registry'
import type * as LegacyWslRuntimeAuthDrain from './legacy-wsl-runtime-auth-drain'
import type * as WslCodexAuthBatchReader from './wsl-codex-auth-batch-reader'
import { createSettings } from './runtime-home-settings-test-fixtures'
@@ -400,45 +399,6 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('preserves legacy auth while draining when pane attribution is unavailable', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome,
getWslHomeAsync: async () => wslHome
}))
vi.doMock('../codex/codex-pane-account-registry', async (importOriginal) => ({
...(await importOriginal<typeof CodexPaneAccountRegistry>()),
hasRecordedLegacyWslCodexPane: () => {
throw new Error('registry unavailable')
}
}))
const startLegacyWslRuntimeAuthDrain = vi.fn(() => Promise.resolve())
vi.doMock('./legacy-wsl-runtime-auth-drain', () => ({ startLegacyWslRuntimeAuthDrain }))
const store = createStore(createSettings())
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
await service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(startLegacyWslRuntimeAuthDrain).toHaveBeenCalledWith(
expect.objectContaining({ legacyPanePresent: true }),
{ throwOnFailure: true }
)
} finally {
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('../codex/codex-pane-account-registry')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('ignores stale retired runtime auth when launching a managed WSL account', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
@@ -52,11 +52,9 @@ import {
isCodexConfigTomlRefusedError,
moveHookTrustEntries,
readHookTrustEntries,
removeHookTrustEntries,
upsertHookTrustEntries,
type CodexTrustEntry
} from './config-toml-trust'
import { applyMirroredRuntimeUserHookTrustStates } from './codex-hook-user-mirroring'
import { installCodexHooksRemote } from './codex-hook-remote-install'
let dir: string
@@ -116,14 +114,6 @@ describe('hook approval writes never break a config.toml Codex can load', () =>
expect(readFileSync(tomlPath, 'utf-8')).toBe(original)
})
it('upsert may still repair a file Codex already cannot load', () => {
writeFileSync(tomlPath, 'model = \n')
upsertHookTrustEntries(tomlPath, [stopEntry()])
expect(readFileSync(tomlPath, 'utf-8')).toContain('trusted_hash')
})
it('a move refuses to land on a key the user approved as dotted keys', () => {
const original =
`hooks.state."${tomlKey(stopEntry(0))}".trusted_hash = "sha256:user"\n\n` +
@@ -139,28 +129,6 @@ describe('hook approval writes never break a config.toml Codex can load', () =>
expect(readFileSync(tomlPath, 'utf-8')).toBe(original)
})
it('a removal is checked before it is written', () => {
upsertHookTrustEntries(tomlPath, [stopEntry(0), { ...stopEntry(1), command: 'user.sh' }])
const original = readFileSync(tomlPath, 'utf-8')
mocks.rejectParse = (content) => !content.includes(computeTrustKey(stopEntry(0)))
expectRefusal(() => removeHookTrustEntries(tomlPath, [computeTrustKey(stopEntry(0))]))
expect(readFileSync(tomlPath, 'utf-8')).toBe(original)
})
it("mirroring a user hook's enabled state is checked before it is written", () => {
upsertHookTrustEntries(tomlPath, [{ ...stopEntry(), enabled: true }])
const original = readFileSync(tomlPath, 'utf-8')
mocks.rejectParse = (content) => content.includes('enabled = false')
expectRefusal(() =>
applyMirroredRuntimeUserHookTrustStates(tomlPath, [{ entry: stopEntry(), enabled: false }])
)
expect(readFileSync(tomlPath, 'utf-8')).toBe(original)
})
it('the SSH installer reports the refusal and leaves the remote config.toml untouched', async () => {
const remoteToml = '/home/u/.codex/config.toml'
const original = 'model = "m"\nhooks = { state = {} }\n'
@@ -1,508 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
const { gitExecFileAsyncMock, gitExecFileSyncMock, translateWslOutputPathsMock } = vi.hoisted(
() => ({
gitExecFileAsyncMock: vi.fn(),
gitExecFileSyncMock: vi.fn(),
translateWslOutputPathsMock: vi.fn((output: string) => output)
})
)
vi.mock('./runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock,
gitExecFileSync: gitExecFileSyncMock,
translateWslOutputPaths: translateWslOutputPathsMock
}))
// Keep the ordered runner queues; worktree-checkout-progress-real-git covers the checkout.workers read.
vi.mock('./checkout-workers-git-args', () => ({ checkoutWorkersGitArgs: async () => [] }))
import { addSparseWorktree, addWorktree, WORKTREE_ADD_TIMEOUT_MS } from './worktree'
import { registerWorktreeSuiteHooks } from './worktree-test-harness'
registerWorktreeSuiteHooks()
describe('addWorktree', () => {
const resolveRemoteBase = () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'abc123\n' })
}
const resolveCreationBaseConfigWrite = () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
}
// Pin non-Windows argv; Windows creation adds platform-specific flags.
let platformSpy: MockInstance<() => NodeJS.Platform>
beforeEach(() => {
gitExecFileAsyncMock.mockReset()
gitExecFileSyncMock.mockReset()
translateWslOutputPathsMock.mockClear()
platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
})
afterEach(() => {
platformSpy.mockRestore()
})
it('creates the worktree without touching the local base ref by default', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockRejectedValueOnce(Object.assign(new Error('key unset'), { code: 1 }))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'], { cwd: '/repo' }],
[
[
'worktree',
'add',
'--no-track',
'-b',
'feature/test',
'/repo-feature',
'refs/remotes/origin/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
],
[
[
'config',
'--local',
'--replace-all',
'branch.feature/test.base',
'refs/remotes/origin/main'
],
{ cwd: '/repo-feature' }
],
[['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }],
[['config', '--local', 'push.autoSetupRemote', 'true'], { cwd: '/repo-feature' }]
])
})
it('checks out a selected existing local branch without creating a new branch', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'feature/test', false, false, {
checkoutExistingBranch: true
})
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[
['worktree', 'add', '/repo-feature', 'feature/test'],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
]
])
})
it('enables long paths for native Windows worktree creation', async () => {
platformSpy.mockReturnValue('win32')
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree(
'C:\\repo',
'C:\\repo-feature',
'feature/test',
'feature/test',
false,
false,
{ checkoutExistingBranch: true }
)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['-c', 'core.longpaths=true', 'worktree', 'add', 'C:\\repo-feature', 'feature/test'],
{ cwd: 'C:\\repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
)
})
it('still enables long paths for a Windows-path repo that has a WSL distro configured', async () => {
// Host Git may serve C: paths even with a WSL distro; Linux Git ignores core.longpaths.
platformSpy.mockReturnValue('win32')
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree(
'C:\\repo',
'C:\\repo-feature',
'feature/test',
'feature/test',
false,
false,
{ checkoutExistingBranch: true, wslDistro: 'Ubuntu' }
)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['-c', 'core.longpaths=true', 'worktree', 'add', 'C:\\repo-feature', 'feature/test'],
{ cwd: 'C:\\repo', wslDistro: 'Ubuntu', timeout: WORKTREE_ADD_TIMEOUT_MS }
)
})
it('does not pass the Windows-only long-path option for a WSL UNC repo path', async () => {
platformSpy.mockReturnValue('win32')
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
const repoPath = '\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo'
await addWorktree(
repoPath,
'\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo-feature',
'feature/test',
'feature/test',
false,
false,
{ checkoutExistingBranch: true, wslDistro: 'Ubuntu' }
)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['worktree', 'add', '\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo-feature', 'feature/test'],
{ cwd: repoPath, wslDistro: 'Ubuntu', timeout: WORKTREE_ADD_TIMEOUT_MS }
)
})
it.each(['darwin', 'linux'] as const)(
'does not pass the long-path option on %s',
async (platform) => {
platformSpy.mockReturnValue(platform)
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'feature/test', false, false, {
checkoutExistingBranch: true
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['worktree', 'add', '/repo-feature', 'feature/test'],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
)
}
)
it('bounds the worktree add call with a positive timeout (STA-1292 OneDrive stall guard)', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'feature/test', false, false, {
checkoutExistingBranch: true
})
const worktreeAddCall = gitExecFileAsyncMock.mock.calls.find(
([argv]) => Array.isArray(argv) && argv.includes('worktree') && argv.includes('add')
)
expect(worktreeAddCall?.[1]).toMatchObject({ timeout: WORKTREE_ADD_TIMEOUT_MS })
expect(WORKTREE_ADD_TIMEOUT_MS).toBeGreaterThan(0)
})
it('raises the worktree add timeout from ORCA_WORKTREE_ADD_TIMEOUT_MS (#12696)', async () => {
vi.stubEnv('ORCA_WORKTREE_ADD_TIMEOUT_MS', '600000')
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'feature/test', false, false, {
checkoutExistingBranch: true
})
const worktreeAddCall = gitExecFileAsyncMock.mock.calls.find(
([argv]) => Array.isArray(argv) && argv.includes('worktree') && argv.includes('add')
)
expect(worktreeAddCall?.[1]).toMatchObject({ timeout: 600_000 })
})
it('does not write branch base config when no base branch is provided', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'true\n' })
await addWorktree('/repo', '/repo-feature', 'feature/no-base')
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[
['worktree', 'add', '--no-track', '-b', 'feature/no-base', '/repo-feature'],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
],
[['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }]
])
})
it('warns and unsets stale branch base config when persisting the base fails', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('config locked'))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'true\n' })
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect(
addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
).resolves.toEqual({})
expect(warnSpy).toHaveBeenCalledWith(
'addWorktree: failed to set branch.feature/test.base for /repo-feature',
expect.any(Error)
)
expect(gitExecFileAsyncMock.mock.calls.map((call) => call[0])).toContainEqual([
'config',
'--local',
'--unset-all',
'branch.feature/test.base'
])
warnSpy.mockRestore()
})
it('warns but does not throw when push.autoSetupRemote config fails', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockRejectedValueOnce(Object.assign(new Error('key unset'), { code: 1 }))
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('config locked'))
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect(
addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
).resolves.toEqual({})
expect(warnSpy).toHaveBeenCalledWith(
'addWorktree: failed to set push.autoSetupRemote for /repo-feature',
expect.any(Error)
)
warnSpy.mockRestore()
})
it('warns and skips --local set when --get fails with non-unset error (e.g. corrupt config)', async () => {
// Only exit 1 means unset; other read failures must preserve the user's config.
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockRejectedValueOnce(Object.assign(new Error('parse error'), { code: 3 })) // --get fails non-unset
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect(
addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
).resolves.toEqual({})
expect(warnSpy).toHaveBeenCalledWith(
'addWorktree: failed to set push.autoSetupRemote for /repo-feature',
expect.any(Error)
)
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'], { cwd: '/repo' }],
[
[
'worktree',
'add',
'--no-track',
'-b',
'feature/test',
'/repo-feature',
'refs/remotes/origin/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
],
[
[
'config',
'--local',
'--replace-all',
'branch.feature/test.base',
'refs/remotes/origin/main'
],
{ cwd: '/repo-feature' }
],
[['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }]
])
warnSpy.mockRestore()
})
it('preserves existing push.autoSetupRemote value (does not overwrite user-set false)', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'false\n' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'], { cwd: '/repo' }],
[
[
'worktree',
'add',
'--no-track',
'-b',
'feature/test',
'/repo-feature',
'refs/remotes/origin/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
],
[
[
'config',
'--local',
'--replace-all',
'branch.feature/test.base',
'refs/remotes/origin/main'
],
{ cwd: '/repo-feature' }
],
[['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }]
])
})
it('treats --get success with empty stdout as "already set" (key present but blank)', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'], { cwd: '/repo' }],
[
[
'worktree',
'add',
'--no-track',
'-b',
'feature/test',
'/repo-feature',
'refs/remotes/origin/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
],
[
[
'config',
'--local',
'--replace-all',
'branch.feature/test.base',
'refs/remotes/origin/main'
],
{ cwd: '/repo-feature' }
],
[['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }]
])
})
it('does not write config when worktree add itself fails', async () => {
resolveRemoteBase()
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('worktree add failed'))
await expect(
addWorktree('/repo', '/repo-feature', 'feature/test', 'origin/main')
).rejects.toThrow('worktree add failed')
expect(gitExecFileAsyncMock.mock.calls).toEqual([
[['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}'], { cwd: '/repo' }],
[
[
'worktree',
'add',
'--no-track',
'-b',
'feature/test',
'/repo-feature',
'refs/remotes/origin/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
]
])
})
it('qualifies bare branch name as refs/heads/ when a same-named tag exists', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'abc123\n' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockRejectedValueOnce(Object.assign(new Error('key unset'), { code: 1 }))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await addWorktree('/repo', '/repo-feature', 'feature/disambig', 'main')
expect(gitExecFileAsyncMock.mock.calls[0]).toEqual([
['rev-parse', '--verify', '--quiet', 'refs/heads/main^{commit}'],
{ cwd: '/repo' }
])
expect(gitExecFileAsyncMock.mock.calls[1]).toEqual([
[
'worktree',
'add',
'--no-track',
'-b',
'feature/disambig',
'/repo-feature',
'refs/heads/main'
],
{ cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS }
])
})
it('does not report a local base refresh for slash-containing local branch names', async () => {
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('no remote ref'))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'abc123\n' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockRejectedValueOnce(Object.assign(new Error('key unset'), { code: 1 }))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
const result = await addWorktree(
'/repo',
'/repo-feature',
'feature/release',
'release/main',
true
)
expect(result).toEqual({})
expect(gitExecFileAsyncMock.mock.calls.map((call) => call[0])).toEqual([
['rev-parse', '--verify', '--quiet', 'refs/remotes/release/main^{commit}'],
['rev-parse', '--verify', '--quiet', 'refs/heads/release/main^{commit}'],
[
'worktree',
'add',
'--no-track',
'-b',
'feature/release',
'/repo-feature',
'refs/heads/release/main'
],
[
'config',
'--local',
'--replace-all',
'branch.feature/release.base',
'refs/heads/release/main'
],
['config', '--get', 'push.autoSetupRemote'],
['config', '--local', 'push.autoSetupRemote', 'true']
])
})
it('unsets branch base config during sparse setup cleanup after creation succeeds', async () => {
const beforeRemoval =
'worktree /repo\nHEAD abc123\nbranch refs/heads/main\n\nworktree /repo-feature\nHEAD def456\nbranch refs/heads/feature/test\n'
resolveRemoteBase()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
resolveCreationBaseConfigWrite()
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'true\n' })
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('sparse setup failed'))
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: beforeRemoval })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' })
await expect(
addSparseWorktree('/repo', '/repo-feature', 'feature/test', ['src'], 'origin/main')
).rejects.toThrow('sparse setup failed')
expect(gitExecFileAsyncMock.mock.calls.map((call) => call[0])).toContainEqual([
'config',
'--local',
'--unset-all',
'branch.feature/test.base'
])
// Rollback may force-delete its new branch; user-initiated deletion must protect commits.
expect(gitExecFileAsyncMock.mock.calls.map((call) => call[0])).toContainEqual([
'branch',
'-D',
'--',
'feature/test'
])
})
})
@@ -118,20 +118,6 @@ describe('Hermes YAML document edits', () => {
expect(output).toContain('shared_env: *env')
})
it('keeps folded scalars, quoted strings and long lines in a real update', () => {
const input = `# header\nprompt: >-\n First line\n Second line\nquoted: 'off'\nlong: ${'word '
.repeat(30)
.trimEnd()}\nplugins: {enabled: [other]}\n`
const output = updateConfigContent(input, enablePlugin).content ?? ''
expect(output).toContain('prompt: >-\n First line\n Second line\n')
expect(output).toContain("quoted: 'off'")
expect(output).toContain(`long: ${'word '.repeat(30).trimEnd()}\n`)
expect(parse(output)).toEqual({
...parse(input),
plugins: { enabled: ['other', 'orca-status'] }
})
})
it.each(['\n', '\r\n'])(
'leaves unrelated source bytes intact with unusual indentation (%j)',
(eol) => {
-82
View File
@@ -177,41 +177,6 @@ describe('registerEphemeralVmHandlers', () => {
})
})
it('lists the recipe catalog across local git repos', async () => {
const repoPath = makeDir('orca-ephemeral-vm-ipc-repo-')
writeFileSync(
join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
' create: ./scripts/start.sh',
' destroy: none'
].join('\n')
)
const store = makeStore(repoPath)
registerEphemeralVmHandlers(store as never)
const result = await handlers.get('ephemeralVm:listRecipeCatalog')?.(null, undefined as never)
expect(result).toEqual([
{
repoId: 'repo-1',
repoName: 'Repo',
repoPath,
diagnostics: [],
recipes: [
{
id: 'cloud-sandbox',
name: 'Cloud Sandbox',
create: './scripts/start.sh',
destroyDisabled: true
}
]
}
])
})
it('merges approved plugin recipes while repository recipes shadow matching ids', async () => {
const repoPath = makeDir('orca-ephemeral-vm-ipc-repo-')
writeFileSync(
@@ -714,53 +679,6 @@ describe('registerEphemeralVmHandlers', () => {
)
})
it('returns a copyable cleanup command for a persisted runtime', async () => {
const userDataPath = makeDir('orca-ephemeral-vm-ipc-user-data-')
const repoPath = makeDir('orca-ephemeral-vm-ipc-repo-')
getPathMock.mockReturnValue(userDataPath)
mkdirSync(join(repoPath, 'scripts'), { recursive: true })
const startPath = join(repoPath, 'scripts', 'start.js')
const cleanupPath = join(repoPath, 'scripts', 'cleanup.js')
writeFileSync(
startPath,
[
'console.log(JSON.stringify({',
' schemaVersion: 1,',
` pairingCode: ${JSON.stringify(makePairingCode())},`,
" projectRoot: '/workspace/repo'",
'}))'
].join('\n')
)
writeFileSync(cleanupPath, 'process.stdin.resume()\n')
writeFileSync(
join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
` create: ${JSON.stringify(nodeCommand(startPath))}`,
` destroy: ${JSON.stringify(nodeCommand(cleanupPath))}`
].join('\n')
)
registerEphemeralVmHandlers(makeStore(repoPath) as never)
const provisioned = (await handlers.get('ephemeralVm:provision')?.(null, {
repoId: 'repo-1',
recipeId: 'cloud-sandbox',
workspaceName: 'Fix Login Race'
} as never)) as { ok: true; runtime: { id: string } }
const result = await handlers.get('ephemeralVm:getCleanupCommand')?.(null, {
runtimeId: provisioned.runtime.id
} as never)
expect(result).toMatchObject({
runtimeId: provisioned.runtime.id,
cleanupDisabled: false,
payloadJson: expect.stringContaining('"workspaceName": "Fix Login Race"'),
command: expect.stringContaining(nodeCommand(cleanupPath))
})
})
it('streams provision logs and cancels an active provision', async () => {
const userDataPath = makeDir('orca-ephemeral-vm-ipc-user-data-')
const repoPath = makeDir('orca-ephemeral-vm-ipc-repo-')
+1 -307
View File
@@ -99,10 +99,7 @@ vi.mock('../gitea/client', () => ({
getGiteaAuthStatus: getGiteaAuthStatusMock
}))
import {
detectInstalledAgents,
detectInstalledAgentsWithShellPathHydration
} from '../preflight/agent-detection'
import { detectInstalledAgents } from '../preflight/agent-detection'
import { registerPreflightHandlers } from './preflight'
import { createGlobalSettingsFixture } from '../../shared/global-settings-test-fixture'
import { resetPreflightMocks, type HandlerMap } from './preflight-test-harness'
@@ -207,128 +204,6 @@ describe('preflight', () => {
expect(isCommandOnLocalPathMock).not.toHaveBeenCalled()
})
it('only reports agents when which/where resolves to a real executable path', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
const target = String(args[0])
if (target === 'claude') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/claude\n',
stderr: '',
timedOut: false
}
}
if (target === 'continue') {
return {
environmentResolved: true,
code: 0,
stdout: 'continue: shell built-in command\n',
stderr: '',
timedOut: false
}
}
if (target === 'cursor-agent') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/cursor-agent\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual(['claude', 'cursor'])
})
it('does not report Claude Agent Teams when only the Orca shim is present', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'orca') {
return {
environmentResolved: true,
code: 0,
stdout: '/Applications/Orca.app/Contents/MacOS/orca\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual([])
})
it('reports Claude Agent Teams when both Orca and Claude are present', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'claude') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/claude\n',
stderr: '',
timedOut: false
}
}
if (String(args[0]) === 'orca') {
return {
environmentResolved: true,
code: 0,
stdout: '/Applications/Orca.app/Contents/MacOS/orca\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual(['claude', 'claude-agent-teams'])
})
it('does not report Claude Agent Teams on native Windows', async () => {
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'where') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'claude') {
return {
environmentResolved: true,
code: 0,
stdout: '/mock/windows/npm/claude.cmd\n',
stderr: '',
timedOut: false
}
}
if (String(args[0]) === 'orca') {
return {
environmentResolved: true,
code: 0,
stdout: '/mock/windows/programs/orca.cmd\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual(['claude'])
})
it('detects agents via the install-dir resolver when which fails (stripped GUI PATH)', async () => {
// Cold GUI detection can precede shell-PATH hydration; install directories remain a fallback.
execFileAsyncMock.mockImplementation(async (command) => {
@@ -359,33 +234,6 @@ describe('preflight', () => {
expect(resolveCliCommandsMock).toHaveBeenCalledTimes(1)
})
it('does not double-count an agent already found on PATH via the install-dir resolver', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'claude') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/claude\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
resolveCliCommandsMock.mockImplementation(
(commands: string[]) => new Map(commands.map((cmd) => [cmd, cmd]))
)
await expect(detectInstalledAgents()).resolves.toEqual(['claude'])
expect(resolveCliCommandsMock).toHaveBeenCalledTimes(1)
expect(resolveCliCommandsMock).toHaveBeenCalledWith(expect.not.arrayContaining(['claude']), {
skipPathSearch: true
})
})
it('treats an agent as not installed when the install-dir resolver throws', async () => {
execFileAsyncMock.mockImplementation(async (command) => {
if (command !== 'which') {
@@ -400,160 +248,6 @@ describe('preflight', () => {
await expect(detectInstalledAgents()).resolves.toEqual([])
})
it('registers agent detection through the shared launch config commands', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'openclaude') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/openclaude\n',
stderr: '',
timedOut: false
}
}
if (String(args[0]) === 'cursor-agent') {
return {
environmentResolved: true,
code: 0,
stdout: '/Users/test/.local/bin/cursor-agent\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
registerPreflightHandlers()
await expect(handlers['preflight:detectAgents']()).resolves.toEqual(['openclaude', 'cursor'])
})
it('hydrates shell PATH before user-facing agent detection', async () => {
const originalPath = process.env.PATH
process.env.PATH = '/usr/bin'
hydrateShellPathMock.mockResolvedValueOnce({
segments: ['/home/test/.local/bin'],
ok: true,
failureReason: 'none'
})
mergePathSegmentsMock.mockImplementationOnce((segments: string[]) => {
process.env.PATH = [...segments, '/usr/bin'].join(':')
return segments
})
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'codex' && process.env.PATH?.startsWith('/home/test/.local/bin')) {
return {
environmentResolved: true,
code: 0,
stdout: '/home/test/.local/bin/codex\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
try {
await expect(detectInstalledAgentsWithShellPathHydration()).resolves.toEqual(['codex'])
} finally {
if (originalPath === undefined) {
delete process.env.PATH
} else {
process.env.PATH = originalPath
}
}
expect(hydrateShellPathMock).toHaveBeenCalledWith()
expect(mergePathSegmentsMock).toHaveBeenCalledWith(['/home/test/.local/bin'])
})
it('does not run host shell hydration for WSL agent detection', async () => {
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
runWslProcessMock.mockImplementation(async ({ script }: { script: string }) => {
if (script.includes("'claude'")) {
return {
environmentResolved: true,
code: 0,
stdout: '__ORCA_AGENT_PATH__claude\t/home/test/.local/bin/claude\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(
detectInstalledAgentsWithShellPathHydration({ wslDistro: 'Ubuntu' })
).resolves.toEqual(['claude'])
expect(hydrateShellPathMock).not.toHaveBeenCalled()
})
it('does not report Claude Agent Teams from WSL agent detection', async () => {
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
runWslProcessMock.mockImplementation(async ({ script }: { script: string }) => {
expect(script).not.toContain("'orca'")
expect(script).not.toContain("'orca-dev'")
expect(script).not.toContain("'orca-ide'")
if (script.includes("'claude'")) {
return {
environmentResolved: true,
code: 0,
stdout: '__ORCA_AGENT_PATH__claude\t/home/test/.local/bin/claude\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents({ wslDistro: 'Ubuntu' })).resolves.toEqual(['claude'])
})
it('detects Mistral Vibe from the installed vibe executable', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'vibe') {
return {
environmentResolved: true,
code: 0,
stdout: '/home/test/.local/bin/vibe\n',
stderr: '',
timedOut: false
}
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual(['mistral-vibe'])
})
it('deduplicates Mistral Vibe when both current and legacy executables exist', async () => {
execFileAsyncMock.mockImplementation(async (command, args) => {
if (command !== 'which') {
throw new Error(`unexpected command ${String(command)}`)
}
if (String(args[0]) === 'vibe' || String(args[0]) === 'mistral-vibe') {
return { stdout: `/home/test/.local/bin/${String(args[0])}\n` }
}
throw new Error('not found')
})
await expect(detectInstalledAgents()).resolves.toEqual(['mistral-vibe'])
})
it('detects agents from the selected WSL distro for a WSL workspace', async () => {
Object.defineProperty(process, 'platform', {
configurable: true,
@@ -2,18 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
listWorktreesMock,
addWorktreeMock,
getBranchConflictKindMock,
getPRForBranchMock,
getHostedReviewForBranchMock,
computeWorktreePathMock,
gitExecFileAsyncMock
} from './worktrees-test-module-mocks'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { handlers, setupWorktreeHandlers } from './worktrees-test-harness'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
import {
hostWorktreeTarget,
type HostWorktreeTarget
} from '../../shared/worktree/host-qualified-identity'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -99,120 +93,6 @@ describe('registerWorktreeHandlers', () => {
runtimeStub = setupWorktreeHandlers()
})
it('checks out a selected existing local branch exactly', async () => {
listWorktreesMock
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
}
])
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: '/workspace/fix-bug-0',
head: 'abc123',
branch: 'refs/heads/fix/bug-0',
isBare: false,
isMainWorktree: false
}
])
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix/bug-0',
baseBranch: 'fix/bug-0',
branchNameOverride: 'fix/bug-0'
})
expect(getBranchConflictKindMock).not.toHaveBeenCalled()
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-bug-0',
'fix/bug-0',
'fix/bug-0',
false,
false,
{ checkoutExistingBranch: true }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-bug-0', hostId: 'local' }),
expect.objectContaining({ preserveBranchOnDelete: true })
)
expect(result).toMatchObject({
worktree: expect.objectContaining({
path: '/workspace/fix-bug-0',
branch: 'refs/heads/fix/bug-0'
})
})
})
it('reuses an existing local branch when the worktree folder is renamed (#5181)', async () => {
listWorktreesMock
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
}
])
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: '/workspace/my-folder',
head: 'abc123',
branch: 'refs/heads/fix/bug-0',
isBare: false,
isMainWorktree: false
}
])
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'my-folder',
baseBranch: 'fix/bug-0',
branchNameOverride: 'fix/bug-0'
})
expect(getBranchConflictKindMock).not.toHaveBeenCalled()
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/my-folder',
'fix/bug-0',
'fix/bug-0',
false,
false,
{ checkoutExistingBranch: true }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/my-folder', hostId: 'local' }),
expect.objectContaining({ preserveBranchOnDelete: true })
)
expect(result).toMatchObject({
worktree: expect.objectContaining({
path: '/workspace/my-folder',
branch: 'refs/heads/fix/bug-0'
})
})
})
it.each(['remote', 'local', 'busy'])('preserves explicit picker intent (%s)', async (kind) => {
const branchName = 'fix/bug-0'
const remoteTrackingBase = {
@@ -287,558 +167,4 @@ describe('registerWorktreeHandlers', () => {
)
expect(result).toMatchObject({ worktree: { branch: `refs/heads/${branchName}` } })
})
it.each([undefined, 'open-existing'] as const)(
'suffixes a divergent local branch collision for intent %s',
async (branchIntent) => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/something' ? 'local' : null
)
gitExecFileAsyncMock.mockImplementation(async (argv) => ({
stdout: argv.some((arg) => arg.startsWith('refs/heads/')) ? 'local-tip' : 'remote-tip',
stderr: ''
}))
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/feature-something-2',
head: 'abc123',
branch: 'refs/heads/feature/something-2',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'feature/something',
branchNameOverride: 'feature/something',
branchIntent
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['check-ref-format', '--branch', 'feature/something-2'],
{ cwd: '/workspace/repo' }
)
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/feature-something-2',
'feature/something-2',
'origin/main',
false,
false,
{}
)
}
)
it('allows a resolver-provided PR branch override to match its remote push target', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/fix' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title',
head: 'abc123',
branch: 'refs/heads/feature/fix',
isBare: false,
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
getPRForBranchMock.mockResolvedValueOnce({
number: 42,
title: 'Selected PR',
state: 'open',
url: 'https://example.com/pr/42',
checksStatus: 'success',
updatedAt: '2026-05-21T00:00:00Z',
mergeable: 'UNKNOWN'
})
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
compareBaseRef: 'refs/remotes/origin/main',
branchNameOverride: 'feature/fix',
linkedPR: 42,
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title',
'feature/fix',
'abc123',
false,
false,
{}
)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
['branch', '--set-upstream-to', 'origin/feature/fix', 'feature/fix'],
{ cwd: '/workspace/fix-title' }
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-title', hostId: 'local' }),
expect.objectContaining({
baseRef: 'refs/remotes/origin/main',
linkedPR: 42
})
)
expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix')
})
it('persists an explicit compare base ahead of the checkout remote-tracking base', async () => {
runtimeStub.resolveRemoteTrackingBase.mockResolvedValueOnce({
base: 'origin/source-branch',
remote: 'origin',
branch: 'source-branch',
ref: 'refs/remotes/origin/source-branch'
})
runtimeStub.hasRemoteTrackingRef.mockResolvedValueOnce(true)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title',
head: 'abc123',
branch: 'refs/heads/feature/fix',
isBare: false,
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'origin/source-branch',
compareBaseRef: 'refs/remotes/origin/main',
branchNameOverride: 'feature/fix',
linkedGitLabMR: 7,
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/fix-title', hostId: 'local' }),
expect.objectContaining({
baseRef: 'refs/remotes/origin/main',
linkedGitLabMR: 7
})
)
})
it('allows a selected Bitbucket PR branch override to match its remote push target', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/bitbucket' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/bitbucket-title',
head: 'abc123',
branch: 'refs/heads/feature/bitbucket',
isBare: false,
isMainWorktree: false
}
])
store.setWorktreeMeta.mockImplementation((_target: HostWorktreeTarget, meta) => meta)
getHostedReviewForBranchMock.mockResolvedValueOnce({
provider: 'bitbucket',
number: 11,
title: 'Bitbucket PR',
state: 'open',
url: 'https://bitbucket.org/team/repo/pull-requests/11',
status: 'success',
updatedAt: '2026-05-21T00:00:00Z',
mergeable: 'UNKNOWN'
})
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'bitbucket-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/bitbucket',
linkedBitbucketPR: 11,
pushTarget: { remoteName: 'origin', branchName: 'feature/bitbucket' }
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/bitbucket-title',
'feature/bitbucket',
'abc123',
false,
false,
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/bitbucket-title', hostId: 'local' }),
expect.objectContaining({ linkedBitbucketPR: 11 })
)
expect(getHostedReviewForBranchMock).toHaveBeenCalledWith(
expect.objectContaining({
repoPath: '/workspace/repo',
branch: 'feature/bitbucket',
linkedBitbucketPR: 11
})
)
expect(getPRForBranchMock).not.toHaveBeenCalled()
})
it('suffixes a selected Bitbucket PR branch when the existing PR is different', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/bitbucket' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/bitbucket-title-2',
head: 'abc123',
branch: 'refs/heads/feature/bitbucket-2',
isBare: false,
isMainWorktree: false
}
])
getHostedReviewForBranchMock.mockResolvedValueOnce({
provider: 'bitbucket',
number: 12,
title: 'Different Bitbucket PR',
state: 'open',
url: 'https://bitbucket.org/team/repo/pull-requests/12',
status: 'success',
updatedAt: '2026-05-21T00:00:00Z',
mergeable: 'UNKNOWN'
})
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'bitbucket-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/bitbucket',
linkedBitbucketPR: 11,
pushTarget: { remoteName: 'origin', branchName: 'feature/bitbucket' }
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/bitbucket-title-2',
'feature/bitbucket-2',
'abc123',
false,
false,
{}
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/bitbucket-title-2', hostId: 'local' }),
expect.objectContaining({ linkedBitbucketPR: 11 })
)
})
it('suffixes a matching push target branch without selected PR metadata', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/fix' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix-2',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix',
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title-2',
'feature/fix-2',
'abc123',
false,
false,
{}
)
})
it('suffixes a matching push target branch when selected PR metadata has no PR number', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/fix' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix-2',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix',
linkedPR: null,
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title-2',
'feature/fix-2',
'abc123',
false,
false,
{}
)
})
it('suffixes a matching push target branch when the existing PR is different', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/fix' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix-2',
isBare: false,
isMainWorktree: false
}
])
getPRForBranchMock.mockResolvedValueOnce({
number: 43,
title: 'Different PR',
state: 'open',
url: 'https://example.com/pr/43',
checksStatus: 'success',
updatedAt: '2026-05-21T00:00:00Z',
mergeable: 'UNKNOWN'
})
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix',
linkedPR: 42,
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix')
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title-2',
'feature/fix-2',
'abc123',
false,
false,
{}
)
})
it('suffixes a selected PR remote conflict when the PR lookup fails', async () => {
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'feature/fix' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix-2',
isBare: false,
isMainWorktree: false
}
])
getPRForBranchMock.mockRejectedValueOnce(new Error('gh unavailable'))
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix',
linkedPR: 42,
pushTarget: { remoteName: 'origin', branchName: 'feature/fix' }
})
expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix')
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title-2',
'feature/fix-2',
'abc123',
false,
false,
{}
)
})
it('checks out an unused existing PR branch only when it is at the resolved head SHA', async () => {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) {
return { stdout: 'abc123\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) {
return { stdout: 'abc123\n', stderr: '' }
}
return { stdout: '', stderr: '' }
})
listWorktreesMock
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
}
])
.mockResolvedValueOnce([
{
path: '/workspace/repo',
head: 'main',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: '/workspace/fix-title',
head: 'abc123',
branch: 'refs/heads/feature/fix',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix'
})
expect(getBranchConflictKindMock).not.toHaveBeenCalled()
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title',
'feature/fix',
'abc123',
false,
false,
{ checkoutExistingBranch: true }
)
})
it('suffixes an existing PR branch when its tip differs from the resolved head SHA', async () => {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) {
return { stdout: 'old123\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) {
return { stdout: 'abc123\n', stderr: '' }
}
return { stdout: '', stderr: '' }
})
getBranchConflictKindMock.mockResolvedValueOnce('local')
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix-2',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'fix-title',
baseBranch: 'abc123',
branchNameOverride: 'feature/fix'
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/fix-title-2',
'feature/fix-2',
'abc123',
false,
false,
{}
)
})
it('skips past a suffix that already belongs to a PR after an initial branch conflict', async () => {
// Network PR probes start only after the first branch collision, preserving the fast happy path.
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'improve-dashboard' ? 'remote' : null
)
getPRForBranchMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'improve-dashboard-2'
? {
number: 3127,
title: 'Existing PR',
state: 'merged',
url: 'https://example.com/pr/3127',
checksStatus: 'success',
updatedAt: '2026-04-01T00:00:00Z',
mergeable: 'UNKNOWN'
}
: null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard-3',
head: 'abc123',
branch: 'improve-dashboard-3',
isBare: false,
isMainWorktree: false
}
])
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/improve-dashboard-3',
'improve-dashboard-3',
'origin/main',
false,
false,
{}
)
expect(result).toMatchObject({
worktree: expect.objectContaining({
path: '/workspace/improve-dashboard-3',
branch: 'improve-dashboard-3'
})
})
})
it('does not call `gh pr list` on the happy path (no branch conflict)', async () => {
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard',
head: 'abc123',
branch: 'improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expect(getPRForBranchMock).not.toHaveBeenCalled()
})
})
+1 -268
View File
@@ -15,9 +15,7 @@ import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
import type { RedactableSpan } from '../observability/redactor'
import type { Store } from '../persistence/loading-store/store'
import { _resetTracerForTests, setActiveSink } from '../observability/tracer'
import { agentHookServer } from '../agent-hooks/server'
import { makePaneKey } from '../../shared/stable-pane-id'
import {
@@ -32,10 +30,7 @@ import {
advertisedUrlWatcherForgetWorktreeMock,
pruneCleanupScanSnapshotMock,
pruneSpaceAnalysisSnapshotMock,
recordRemovalSnapshotPruneMock,
killAllProcessesForWorktreeMock,
clearProviderPtyStateMock,
getLocalPtyProviderMock
killAllProcessesForWorktreeMock
} from './worktrees-test-module-mocks'
import {
handlers,
@@ -187,22 +182,6 @@ describe('registerWorktreeHandlers', () => {
)
})
it('globally invalidates when the removed repo has no registered authorization owner', async () => {
mockKnownFeatureWorktree()
store.getRepos.mockReturnValue([])
getEffectiveHooksMock.mockReturnValue(null)
removeWorktreeMock.mockResolvedValue({})
await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' })
expect(scopedRootsInvalidation.invalidateAuthorizedRootsCacheForRepo).toHaveBeenCalledOnce()
expect(scopedRootsInvalidation.invalidateAuthorizedRootsCacheForRepo).toHaveBeenCalledWith(
store,
'repo-1'
)
expect(authorizedRootsCache.invalidateAuthorizedRootsCache).toHaveBeenCalledOnce()
})
it('purges only the selected host when a normal worktree id is owned locally and over SSH', async () => {
const worktreeId = 'repo-1::/workspace/feature-wt'
const localRepo = {
@@ -254,123 +233,6 @@ describe('registerWorktreeHandlers', () => {
}
})
it('tombstones a cleanup-batch removal without scheduling singular sidecar writes', async () => {
mockKnownFeatureWorktree()
getEffectiveHooksMock.mockReturnValue(null)
removeWorktreeMock.mockResolvedValue({})
await handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
snapshotPruneBatchId: 'cleanup-batch-1'
})
expect(recordRemovalSnapshotPruneMock).toHaveBeenCalledExactlyOnceWith('/profile-a', {
batchId: 'cleanup-batch-1',
worktreeId: 'repo-1::/workspace/feature-wt',
executionHostId: 'local'
})
expect(pruneCleanupScanSnapshotMock).not.toHaveBeenCalled()
expect(pruneSpaceAnalysisSnapshotMock).not.toHaveBeenCalled()
})
it('traces the removal as worktree.remove with a stage sub-span tree', async () => {
const records: RedactableSpan[] = []
setActiveSink({
push: (record) => records.push(record as RedactableSpan),
flush: () => {},
close: () => {}
})
try {
mockKnownFeatureWorktree()
getEffectiveHooksMock.mockReturnValue(null)
removeWorktreeMock.mockResolvedValue({})
await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' })
const parent = records.find((record) => record.name === 'worktree.remove')
expect(parent).toBeDefined()
expect(parent?.attributes).toMatchObject({
kind: 'worktree',
'worktree.stage': 'remove',
'worktree.path': '/workspace/feature-wt'
})
const stages = records.filter((record) => record.name.startsWith('worktree.remove.'))
expect(stages.map((record) => record.name)).toEqual(
expect.arrayContaining([
'worktree.remove.watcher_gate',
'worktree.remove.pty_sweep',
'worktree.remove.git_remove',
'worktree.remove.metadata_purge',
'worktree.remove.cache_invalidation'
])
)
// Parent spans make removal-stage freezes attributable.
for (const stage of stages) {
expect(stage.parentSpanId).toBe(parent?.spanId)
expect(stage.attributes).toMatchObject({ kind: 'worktree', 'worktree.flow': 'local' })
}
} finally {
_resetTracerForTests()
}
})
it('traces a local archive hook as flow local, not remote', async () => {
const records: RedactableSpan[] = []
setActiveSink({
push: (record) => records.push(record as RedactableSpan),
flush: () => {},
close: () => {}
})
try {
mockKnownFeatureWorktree()
getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } })
runHookMock.mockResolvedValue({ success: true, output: '' })
removeWorktreeMock.mockResolvedValue({})
await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' })
const archiveStage = records.find((record) => record.name === 'worktree.remove.archive_hook')
expect(archiveStage?.attributes).toMatchObject({
kind: 'worktree',
'worktree.flow': 'local'
})
} finally {
_resetTracerForTests()
}
})
it('prunes git worktree tracking when removing an orphaned worktree', async () => {
mockKnownFeatureWorktree()
const orphanError = Object.assign(new Error('git worktree remove failed'), {
stderr: "fatal: '/workspace/feature-wt' is not a working tree"
})
removeWorktreeMock.mockRejectedValue(orphanError)
getEffectiveHooksMock.mockReturnValue(null)
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
await handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt'
})
expect(scopedRootsInvalidation.invalidateAuthorizedRootsCacheForRepo).toHaveBeenCalledOnce()
expect(scopedRootsInvalidation.invalidateAuthorizedRootsCacheForRepo).toHaveBeenCalledWith(
store,
'repo-1'
)
expect(authorizedRootsCache.invalidateAuthorizedRootsCache).not.toHaveBeenCalled()
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: 'repo-1::/workspace/feature-wt', hostId: 'local' })
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith('repo-1::/workspace/feature-wt')
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
})
})
it.each(['unproven', 'removal-fails'] as const)(
'keeps desktop orphan cleanup retryable when the directory is %s',
async (mode) => {
@@ -570,135 +432,6 @@ describe('registerWorktreeHandlers', () => {
expect(store.removeWorktreeMeta).not.toHaveBeenCalled()
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
})
it('kills PTYs before removing additional folder workspace metadata', async () => {
const ptyProvider = {} as never
const worktreeId = 'repo-folder::/workspace/folder::workspace:child-1'
store.getRepo.mockReturnValue({
id: 'repo-folder',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
})
getLocalPtyProviderMock.mockReturnValue(ptyProvider)
await handlers['worktrees:remove'](null, { worktreeId })
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, {
runtime: runtimeStub,
resolvedWorktreeId: worktreeId,
localProvider: ptyProvider,
onPtyStopped: clearProviderPtyStateMock,
// Structured sessions are invisible to PTY inventories, so folder removal closes them separately.
closeStructuredSessions: true
})
expect(killAllProcessesForWorktreeMock.mock.invocationCallOrder[0]).toBeLessThan(
store.removeWorktreeMeta.mock.invocationCallOrder[0]
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-folder'
})
})
it('purges a folder workspace after a rejected best-effort PTY sweep', async () => {
const worktreeId = 'repo-folder::/workspace/folder::workspace:child-1'
store.getRepo.mockReturnValue({
id: 'repo-folder',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
})
killAllProcessesForWorktreeMock.mockRejectedValue(new Error('terminal inventory unavailable'))
await handlers['worktrees:remove'](null, { worktreeId })
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'local' })
)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-folder'
})
expect(store.removeWorktreeMeta.mock.invocationCallOrder[0]).toBeLessThan(
mainWindow.webContents.send.mock.invocationCallOrder[0]
)
})
// Folder workspace IDs repeat across hosts; fence teardown to the owning connection.
it('fences an SSH folder workspace PTY sweep to the owning connection', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an identity marker; the mocked sweep never calls into it.
const localPtyProvider = { id: 'local-pty-provider' } as never
const worktreeId = 'repo-folder::/remote/folder::workspace:child-1'
store.getRepo.mockReturnValue({
id: 'repo-folder',
path: '/remote/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder',
connectionId: 'conn-1'
})
getLocalPtyProviderMock.mockReturnValue(localPtyProvider)
// Same-ID local metadata must not override the resolved SSH repo owner.
store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ hostId: 'local' }))
await handlers['worktrees:remove'](null, { worktreeId })
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, {
runtime: runtimeStub,
resolvedWorktreeId: worktreeId,
resolvedConnectionId: 'conn-1',
localProvider: localPtyProvider,
onPtyStopped: clearProviderPtyStateMock,
includeProviderInventory: false,
includeLocalRegistry: false,
closeStructuredSessions: true
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(
hostWorktreeTarget({ id: worktreeId, hostId: 'ssh:conn-1' })
)
expect(advertisedUrlWatcherForgetWorktreeMock).not.toHaveBeenCalled()
expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled()
})
it('fences a mirrored runtime folder workspace sweep to its environment', async () => {
const runtimePtyProvider = {} as never
const worktreeId = 'repo-folder::/runtime/folder::workspace:child-1'
store.getRepo.mockReturnValue({
id: 'repo-folder',
path: '/runtime/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder',
executionHostId: 'runtime:env-1'
})
getLocalPtyProviderMock.mockReturnValue(runtimePtyProvider)
await handlers['worktrees:remove'](null, {
worktreeId,
hostId: 'runtime:env-1'
})
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, {
runtime: runtimeStub,
resolvedWorktreeId: worktreeId,
resolvedRuntimeEnvironmentId: 'env-1',
localProvider: runtimePtyProvider,
onPtyStopped: clearProviderPtyStateMock,
includeProviderInventory: false,
includeLocalRegistry: false,
closeStructuredSessions: true
})
})
// Authoritative off-host removal must retire status rows too; local GC cannot reach them.
it("retires the scan-proven host rows from the agent status store, and only that host's", async () => {
const worktreeId = 'repo-1::/remote/deleted'
@@ -231,54 +231,6 @@ describe('WSL move orchestrator wiring', () => {
expect(sent).toEqual([])
})
it('moves a distro whose host is ready at start, with the window slept panes', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
host.statuses = [
{ distro: 'Ubuntu', state: 'ready', environmentId: 'wsl:Ubuntu', detail: null }
]
await register()
await answerWindow()
// The automatic mover's preview settles a move an update or crash interrupted, finishing at restart.
expect(driver.previewWslMove).toHaveBeenCalledWith(
expect.anything(),
{ distro: 'Ubuntu', sleptPanes },
{ settleInterrupted: 'finish-at-restart' }
)
expect(driver.moveProjectsToWslHost).toHaveBeenCalledWith(expect.anything(), {
distro: 'Ubuntu',
sleptPanes
})
})
it('starts on a host reaching ready, never on one that is not', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
host.statuses = [
{ distro: 'Ubuntu', state: 'connecting', environmentId: 'wsl:Ubuntu', detail: null }
]
await register()
await answerWindow()
expect(driver.previewWslMove).not.toHaveBeenCalled()
host.statuses = [
{ distro: 'Ubuntu', state: 'ready', environmentId: 'wsl:Ubuntu', detail: null }
]
for (const listener of host.listeners) {
listener()
}
await answerWindow()
expect(driver.moveProjectsToWslHost).toHaveBeenCalledTimes(1)
})
it('makes no automatic start with ORCA_WSL_MOVE_AUTO=0', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
vi.stubEnv('ORCA_WSL_MOVE_AUTO', '0')
host.statuses = [
{ distro: 'Ubuntu', state: 'ready', environmentId: 'wsl:Ubuntu', detail: null }
]
await register()
await answerWindow()
expect(driver.previewWslMove).not.toHaveBeenCalled()
})
// A block published while Orca runs stops every new move at once: automatic, opened or Retry.
it('starts nothing after a live policy block, from readiness, a project open or Retry', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
@@ -325,55 +277,6 @@ describe('WSL move orchestrator wiring', () => {
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
})
it('ignores a slept-pane answer from another window', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
host.statuses = [
{ distro: 'Ubuntu', state: 'ready', environmentId: 'wsl:Ubuntu', detail: null }
]
await register()
const request = sent.find(
(message) => message.channel === WSL_MOVE_ORCHESTRATOR_SLEPT_PANES_REQUEST_CHANNEL
)
const requestId = requestIdOf(request?.payload)
await handlers.get('wslMoveOrchestrator:answerSleptPanes')?.(
{ sender: { id: 99 } },
{ requestId, sleptPanes }
)
await new Promise((resolve) => setImmediate(resolve))
expect(driver.previewWslMove).not.toHaveBeenCalled()
})
it('T2: a bridge project opened on a running distro adds its host and moves it', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
await register()
await handlers.get('wslMoveOrchestrator:projectOpened')?.(
{ sender: { id: MAIN_WINDOW_ID } },
'Ubuntu'
)
await answerWindow()
expect(host.added).toEqual(['Ubuntu'])
expect(driver.moveProjectsToWslHost).toHaveBeenCalledTimes(1)
})
// B3: in Stage 1 opening a bridge project moves nothing; only the user's own move does.
it('T2 in Stage 1: opening a bridge project adds no host and moves nothing', async () => {
stage.autoMoves = false
try {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
await register()
await handlers.get('wslMoveOrchestrator:projectOpened')?.(
{ sender: { id: MAIN_WINDOW_ID } },
'Ubuntu'
)
await answerWindow()
expect(host.added).toEqual([])
expect(driver.previewWslMove).not.toHaveBeenCalled()
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
} finally {
stage.autoMoves = true
}
})
// Setting off is like serve-only: moved projects keep serving, but nothing moves or is added.
it('moves nothing and adds no host with the WSL hosts setting off', async () => {
setting.on = false
@@ -422,35 +325,6 @@ describe('WSL move orchestrator wiring', () => {
}
})
it('T2: never adds a host for a stopped distro or one whose state cannot be read', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
await register()
host.running = { status: 'confirmed', distros: [] }
await handlers.get('wslMoveOrchestrator:projectOpened')?.(
{ sender: { id: MAIN_WINDOW_ID } },
'Ubuntu'
)
host.running = { status: 'unavailable' }
await handlers.get('wslMoveOrchestrator:projectOpened')?.(
{ sender: { id: MAIN_WINDOW_ID } },
'Debian'
)
await answerWindow()
expect(host.added).toEqual([])
expect(driver.previewWslMove).not.toHaveBeenCalled()
})
it('T3: a ready host whose distro is not confirmed running is not resumed', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
host.running = { status: 'unavailable' }
host.statuses = [
{ distro: 'Ubuntu', state: 'ready', environmentId: 'wsl:Ubuntu', detail: null }
]
await register()
await answerWindow()
expect(driver.previewWslMove).not.toHaveBeenCalled()
})
// Review #2: "Use on this computer again" parks the distro, so opening it again moves nothing.
it('parks a distro the user brought back', async () => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
-8
View File
@@ -147,14 +147,6 @@ turn_end = "~/bin/mine" # replaces agent-hooks/jcode-hook.sh
expect(result.content).toContain('pre_tool_timeout_ms = 5000 # ms')
})
it('keeps CRLF line endings when editing a Windows-owned config', () => {
const source = '[hooks]\r\nturn_end = "~/bin/mine"\r\n'
const result = applyJcodeManagedHooks(source, EVENTS, MANAGED_COMMAND, 'jcode-hook.sh')
expect(result.content).toContain('\r\n')
expect(result.content).not.toContain('\n[hooks]')
expect(result.content).not.toContain('\r\r')
})
it.each([
['inline', '[hooks]\npre_tool = ["a.sh"] # gate\n', ['a.sh']],
['inline trailing comma', "[hooks]\npre_tool = [ 'a.sh', ]\n", ['a.sh']],
@@ -74,12 +74,6 @@ describe('kimi managed hooks TOML block', () => {
expect(readManagedKimiHookEvents(text, isManaged).size).toBe(0)
})
it('reports no change when removing from a config without the managed block', () => {
const { text, changed } = removeManagedKimiHooks('default_model = "x"\n', isManaged)
expect(changed).toBe(false)
expect(text).toBe('default_model = "x"\n')
})
it('is stable across repeated calls (no stateful global-regex lastIndex drift)', () => {
const installed = applyManagedKimiHooks('default_model = "x"\n', COMMAND, isManaged)
// Repeated detection must not inherit stateful regex lastIndex.
@@ -103,13 +97,6 @@ describe('kimi managed hooks TOML block', () => {
const reinstalled = applyManagedKimiHooks(orphaned, COMMAND, isManaged)
expect((reinstalled.match(/orca-managed-kimi-hooks \(/g) ?? []).length).toBe(1)
})
it('treats stale managed entries pointing at a moved script path as managed', () => {
const staleCommand =
"if [ -x '/old/userData/agent-hooks/kimi-hook.sh' ]; then /bin/sh '/old/userData/agent-hooks/kimi-hook.sh'; fi"
const stale = applyManagedKimiHooks('', staleCommand, isManaged)
expect(readManagedKimiHookEvents(stale, isManaged)).toEqual(new Set(KIMI_HOOK_EVENTS))
})
})
// #18861: an orphaned start marker used to make every following byte "managed".
@@ -322,21 +309,6 @@ describe('orphaned managed block ownership (#18861)', () => {
expect(readManagedKimiHookEvents(singleQuoted, isManaged)).toEqual(new Set(['Stop']))
})
it('leaves a hook table that does not invoke the managed script', () => {
const orphan = [
START_MARKER,
'[[hooks]]',
'event = "Stop"',
'command = "node my-own-hook.mjs"',
'timeout = 10',
'matcher = "Bash"',
''
].join('\n')
const { text } = removeManagedKimiHooks(orphan, isManaged)
expect(text).toContain('command = "node my-own-hook.mjs"')
expect(text).toContain('matcher = "Bash"')
})
// Reclaim recognized managed tables even when edits strand them outside the marker block.
it('reclaims managed tables stranded below user text', () => {
const managedTable = [
@@ -350,37 +322,6 @@ describe('orphaned managed block ownership (#18861)', () => {
expect(text).toBe('[user.table]\nv = 1\n')
})
it('reports a stranded managed table as live so status cannot claim uninstalled', () => {
const stranded = [
'[user.table]',
'v = 1',
'',
'[[hooks]]',
'event = "PreToolUse"',
`command = "${COMMAND.replaceAll('"', '')}"`,
'timeout = 10',
''
].join('\n')
expect(readManagedKimiHookEvents(stranded, isManaged)).toEqual(new Set(['PreToolUse']))
})
it('reinstalling over a stranded table does not double-register its event', () => {
const stranded = [
'[user.table]',
'v = 1',
'',
'[[hooks]]',
'event = "PreToolUse"',
`command = "${COMMAND.replaceAll('"', '')}"`,
'timeout = 10',
''
].join('\n')
const reinstalled = applyManagedKimiHooks(stranded, COMMAND, isManaged)
expect((reinstalled.match(/event = "PreToolUse"/g) ?? []).length).toBe(1)
expect(reinstalled).toContain('[user.table]')
expect(readManagedKimiHookEvents(reinstalled, isManaged)).toEqual(new Set(KIMI_HOOK_EVENTS))
})
it('stops an orphaned block at a second start marker', () => {
const installed = applyManagedKimiHooks('default_model = "x"\n', COMMAND, isManaged)
const duplicated = `${deleteEndMarker(installed)}\n${START_MARKER}\n[user.table]\nv = 1\n`
@@ -11,10 +11,7 @@ import { installClaudeProfileRouter } from '../../claude-accounts/claude-profile
import { resolveStructuredClaudeAccountHomePath } from '../../runtime/structured-agent-account-home'
import type { AgentSessionRecord } from '../../../shared/agent-session/lifecycle/agent-session-record'
import {
agentModelCatalogFingerprintForRecord,
agentModelCatalogSessionAccess
} from './agent-model-catalog-fingerprint'
import { agentModelCatalogFingerprintForRecord } from './agent-model-catalog-fingerprint'
import { AgentModelCatalogListingStoppedError } from './agent-model-catalog-failures'
import { createAgentModelCatalogService } from './agent-model-catalog-service'
import {
@@ -262,69 +259,6 @@ describe('model catalog prewarm', () => {
)
})
it('lists the new account after a switch and leaves the old account’s catalog alone', async () => {
const store = new AgentModelCatalogStore()
let selected = '/homes/a'
const probe = vi.fn(async (home: AgentSessionAccountHome) =>
listing('variable' in home && home.path === '/homes/a' ? 'gpt-a' : 'gpt-b')
)
const service = createAgentModelCatalogService({
store,
getConversation: () => undefined,
getRecord: () => undefined,
drivesRecord: () => true,
hasChatRecords: () => true,
resolveAccountHome: async () => HOME('CODEX_HOME', selected),
probes: { codex: probe }
})
await service.prewarm()
selected = '/homes/b'
await service.prewarm()
expect(probe.mock.calls.map(([home]) => home)).toEqual([
HOME('CODEX_HOME', '/homes/a'),
HOME('CODEX_HOME', '/homes/b')
])
const read = await service.read({ agent: 'codex' })
expect(read.origin !== 'unknown' && read.models.map((model) => model.id)).toEqual(['gpt-b'])
})
it('respects the failure back-off and skips an agent whose account cannot be resolved', async () => {
const store = new AgentModelCatalogStore()
const codex = vi.fn(
async (_home: AgentSessionAccountHome): Promise<AgentModelCatalogSuccess> => {
throw new Error('spawn codex ENOENT')
}
)
const claude = vi.fn(async (_home: AgentSessionAccountHome) => listing('opus'))
const service = createAgentModelCatalogService({
store,
getConversation: () => undefined,
getRecord: () => undefined,
drivesRecord: () => true,
hasChatRecords: () => true,
resolveAccountHome: async (agent) => {
if (agent === 'claude') {
throw new Error('account home unreadable')
}
return HOME('CODEX_HOME', '/homes/a')
},
probes: { codex, claude }
})
spans.started.length = 0
await service.prewarm()
await service.prewarm()
expect(codex).toHaveBeenCalledTimes(1)
expect(claude).not.toHaveBeenCalled()
expect(spans.started).toEqual([
{
name: 'agentModelCatalog.discovery',
attributes: { agent: 'codex', lister: 'probe' },
outcome: 'failed'
}
])
})
it('lists only agents used here: a saved catalog or a chat of it', async () => {
const store = new AgentModelCatalogStore()
const piListed = deferred<AgentModelCatalogSuccess>()
@@ -450,30 +384,6 @@ describe('one account, one catalog key', () => {
})
}
it('a session’s saves through the host and through its adapter’s handle land under its record’s key', async () => {
const store = new AgentModelCatalogStore()
const managedHome = '/orca/grok-accounts/acct-1/home'
const launched = record(managedHome)
const service = createAgentModelCatalogService({
store,
getConversation: () => agentSessionConversationView(launched),
getRecord: () => launched,
drivesRecord: () => true,
resolveAccountHome: async () => HOME('GROK_HOME', managedHome)
})
service.recordLiveListing('session-1', { models: listing('gpt-live').models })
const adapterAccess = agentModelCatalogSessionAccess(
store,
{ agent: 'grok', accountHomeVariable: 'GROK_HOME' },
managedHome
)
expect(adapterAccess?.fingerprint).toBe(agentModelCatalogFingerprintForRecord(launched))
// …is what the next new chat reads for the same home: no cold start for this account.
const read = await service.read({ agent: 'grok', waitForListing: true })
expect(read.origin !== 'unknown' && read.models.map((model) => model.id)).toEqual(['gpt-live'])
})
it('a chat that moved accounts lists under the account it runs on, never its first one', async () => {
const store = new AgentModelCatalogStore()
const founded = record('/accounts/a')
@@ -152,61 +152,6 @@ describe('agent model catalog service', () => {
expect(store.get(oldFingerprint)!.models[0]!.id).toBe('gpt-old')
})
it('a record-less read serves the selected account entry when it exists', async () => {
const store = new AgentModelCatalogStore()
store.recordSuccess(
selectedHomeFingerprint('/homes/selected'),
'grok',
listing('gpt-mine'),
'discovery'
)
store.recordSuccess(
selectedHomeFingerprint('/homes/other'),
'grok',
listing('gpt-other'),
'discovery'
)
const service = createAgentModelCatalogService({
store,
getConversation: () => undefined,
getRecord: () => undefined,
drivesRecord: () => true,
resolveAccountHome: async () => GROK_HOME('/homes/selected')
})
const result = await service.read({ agent: 'grok', waitForListing: true })
expect(result.origin === 'unknown' ? null : result.models[0]!.id).toBe('gpt-mine')
})
it('a session record outranks the current selection for its own reads', async () => {
const store = new AgentModelCatalogStore()
const sessionRecord = record('/homes/session')
store.recordSuccess(
agentModelCatalogFingerprintForRecord(sessionRecord),
'grok',
listing('gpt-session'),
'discovery'
)
store.recordSuccess(
selectedHomeFingerprint('/homes/selected'),
'grok',
listing('gpt-selected'),
'discovery'
)
const service = createAgentModelCatalogService({
store,
getConversation: () => agentSessionConversationView(sessionRecord),
getRecord: () => sessionRecord,
drivesRecord: () => true,
resolveAccountHome: async () => GROK_HOME('/homes/selected')
})
const result = await service.read({
agent: 'grok',
sessionId: 'session-1',
waitForListing: true
})
expect(result.origin === 'unknown' ? null : result.models[0]!.id).toBe('gpt-session')
})
it('a probe failure is a TTL-bounded fact, never an answer', async () => {
const store = new AgentModelCatalogStore()
const probe = vi.fn(async () => {
@@ -317,45 +262,6 @@ describe('agent model catalog service', () => {
}
})
it('answers a plain unknown when the listing fails', async () => {
const pending = deferredListing()
const { service } = coldService(() => pending.promise)
const waited = service.read({ agent: 'grok', waitForListing: true })
pending.reject(new Error('spawn failed'))
expect(await waited).toEqual({ origin: 'unknown' })
})
it('does not wait or report a listing while a failure is inside its TTL', async () => {
const probe = vi.fn(async (): Promise<AgentModelCatalogSuccess> => {
throw new Error('spawn failed')
})
const { store, service } = coldService(probe)
await store.refresh(selectedHomeFingerprint('/homes/selected'), 'grok', () =>
Promise.reject(new Error('spawn failed'))
)
expect(await service.read({ agent: 'grok', waitForListing: true })).toEqual({
origin: 'unknown'
})
expect(await service.read({ agent: 'grok', waitForListing: true })).toEqual({
origin: 'unknown'
})
expect(probe).not.toHaveBeenCalled()
})
it('reports no listing where the host has no lister for the account', async () => {
const store = new AgentModelCatalogStore()
const service = createAgentModelCatalogService({
store,
getConversation: () => undefined,
getRecord: () => undefined,
drivesRecord: () => true,
resolveAccountHome: async () => GROK_HOME('/homes/selected')
})
expect(await service.read({ agent: 'grok', waitForListing: true })).toEqual({
origin: 'unknown'
})
})
it('serves an aged entry at once and refreshes it behind the answer', async () => {
let now = 0
const store = new AgentModelCatalogStore({ now: () => now })
@@ -408,30 +314,6 @@ describe('agent model catalog service', () => {
return result.origin === 'unknown' ? null : result.models.map((model) => model.isDefault)
}
it('names no default when the workspace config could pick another model', async () => {
const { service, workspaceMayOverrideDefaultModel } = serviceWith(true)
const result = await service.read({
agent: 'grok',
waitForListing: true,
workspacePath: '/repo/wt'
})
expect(defaults(result)).toEqual([false])
expect(workspaceMayOverrideDefaultModel).toHaveBeenCalledWith({
agent: 'grok',
workspacePath: '/repo/wt',
accountHomePath: '/homes/selected'
})
})
it('keeps the listed default when nothing in the workspace can replace it', async () => {
const { service } = serviceWith(false)
expect(
defaults(
await service.read({ agent: 'grok', waitForListing: true, workspacePath: '/repo/wt' })
)
).toEqual([true])
})
it('names no default for a workspace it could not place on this machine', async () => {
const { service, workspaceMayOverrideDefaultModel } = serviceWith(false)
expect(
@@ -439,12 +321,6 @@ describe('agent model catalog service', () => {
).toEqual([false])
expect(workspaceMayOverrideDefaultModel).not.toHaveBeenCalled()
})
it('leaves a read that names no workspace as it was', async () => {
const { service, workspaceMayOverrideDefaultModel } = serviceWith(true)
expect(defaults(await service.read({ agent: 'grok', waitForListing: true }))).toEqual([true])
expect(workspaceMayOverrideDefaultModel).not.toHaveBeenCalled()
})
})
describe('an agent whose account is not one directory', () => {
@@ -1,7 +1,5 @@
import { eraseRpcMethods, isStreamingMethod } from '../../runtime/rpc/core'
import { STRUCTURED_AGENT_SESSION_OPTIONS_READ_METHODS } from '../../runtime/rpc/methods/structured-agent-session-options-read'
import * as receiving from './structured-agent-session-receiving-account'
import { observeReceivingSql } from './structured-agent-session-receiving-sql.test-support'
import {
type AgentModelCatalogSuccess,
AgentModelCatalogStore
@@ -10,7 +8,6 @@ import type { AgentModelCatalogProbe } from '../agent-model-catalog/agent-model-
import { rm } from 'node:fs/promises'
import { describe, expect, it, vi } from 'vitest'
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session/lifecycle/agent-session-mutation-envelope'
import { agentSessionAccountHomePath } from '../../../shared/agent-session/lifecycle/agent-session-account-home'
import { REST_TEST_CALLER } from './structured-agent-session-rest-test-rig'
import { hostTestOperationId } from './structured-agent-session-host-test-data'
import {
@@ -267,159 +264,6 @@ describe('catalog consumes the constructed host receiving account', () => {
}, 10_000)
})
// Real cold adoption, N serialized mutations and bounded cleanup share the existing catalog budget.
it.each([10, 100])(
'counts inventory-independent picks and explicit receiving catalog work over %i operations',
async (n) => {
const f = await createReceivingCallerFixture()
let service: ReturnType<typeof createAgentModelCatalogService> | undefined
try {
await f.found()
const { started } = await f.coldNative()
f.selectB()
const c = catalog(f, true)
service = c.service
f.rig.adapter.acquire.mockClear()
const entries = vi.spyOn(receiving, 'structuredAgentSessionReceivingAccount')
const external = vi.spyOn(f.rig.host, 'prospectiveAccountHome')
const all = vi.spyOn(f.rig.store, 'listRecords').mockImplementation(() => {
throw new Error('global history scan')
})
const pick = async () => {
const key = 'permissionMode'
const value = 'own'
expect(
await settled(
f.rig.host.setOption(REST_TEST_CALLER, {
key,
value,
envelope: {
sessionId: PARENT,
clientOperationId: hostTestOperationId(),
expectedRuntimeFence: f.rig.store.requireWritableRecord(PARENT).lease.runtimeFence,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.setOption',
sessionId: PARENT,
fields: { key, value }
})
}
}),
'owned catalog counted mutation'
)
).toMatchObject({
ok: true,
value: { key, value, options: { permissionMode: 'own' } }
})
expect(f.rig.store.requireWritableRecord(PARENT).permissionMode).toBe('own')
return undefined
}
const measure = async (count: number, work: () => Promise<void>) => {
entries.mockClear()
external.mockClear()
c.resolve.mockClear()
c.demands.mockClear()
const observed = observeReceivingSql(f.runtime.getOrchestrationDb())
try {
await work()
expect(entries).toHaveBeenCalledTimes(count)
expect(c.resolve).toHaveBeenCalledTimes(count)
expect(
c.resolve.mock.calls.every(
([, cwd, lookup]) => cwd === f.rig.root && lookup === 'snapshot'
)
).toBe(true)
expect(observed.measurements.prepares).toBeLessThanOrEqual(20 * count)
expect(observed.measurements.allRows).toBeLessThanOrEqual(8 * count)
expect(observed.measurements.plans.length).toBeGreaterThan(0)
expect(
observed.measurements.plans.filter((detail) =>
/\bSCAN (?:run|child|task|parent|messages|dispatch_contexts|agent_session_records)\b/.test(
detail
)
)
).toEqual([])
expect(c.probe).not.toHaveBeenCalled()
expect(f.rig.adapter.acquire).not.toHaveBeenCalled()
expect(f.rig.adapter.dispatch).not.toHaveBeenCalled()
expect(f.rig.adapter.readOptions).not.toHaveBeenCalled()
expect(all).not.toHaveBeenCalled()
process.stdout.write(
`owned-catalog operations=${count} receiving=${entries.mock.calls.length} snapshotResolutions=${c.resolve.mock.calls.length} sql=${observed.measurements.prepares} rows=${observed.measurements.allRows}\n`
)
} finally {
observed.stop()
}
}
entries.mockClear()
c.resolve.mockClear()
c.demands.mockClear()
const preparation = vi.spyOn(c.service, 'prepareSaved')
const reads = vi.spyOn(c.service, 'read')
const pickedSql = observeReceivingSql(f.runtime.getOrchestrationDb())
try {
for (let i = 0; i < n; i += 1) {
await pick()
}
expect(entries).not.toHaveBeenCalled()
expect(c.resolve).not.toHaveBeenCalled()
expect(c.demands).not.toHaveBeenCalled()
expect(preparation).not.toHaveBeenCalled()
expect(reads).not.toHaveBeenCalled()
expect(pickedSql.measurements.prepares).toBe(0)
expect(pickedSql.measurements.allRows).toBe(0)
} finally {
pickedSql.stop()
}
await measure(n, async () => {
for (let i = 0; i < n; i += 1) {
const options = await settled(
f.rig.host.readOptions(PARENT, { readsWithoutModel: false }),
'explicit legacy options'
)
expect(options.current.model).toBe('claude-sonnet-5')
}
})
expect(preparation).toHaveBeenCalledTimes(n)
expect(reads.mock.calls.length).toBeGreaterThan(0)
expect(external).not.toHaveBeenCalled()
expect(
c.resolve.mock.calls.every(([home]) => agentSessionAccountHomePath(home) === HOME_A.path)
).toBe(true)
entries.mockClear()
c.resolve.mockClear()
c.demands.mockClear()
expect(await c.ids(true)).toEqual(['claude-sonnet-5'])
expect(entries).toHaveBeenCalledTimes(1)
expect(c.resolve).toHaveBeenCalledTimes(1)
expect(c.demands).toHaveBeenCalledTimes(1)
const before = await f.rig.host.readOptions(PARENT)
f.runtime
.getOrchestrationDb()
.failWorkerStart(started.dispatch.id, 'agent_readiness', 'never constructed a worker')
await measure(1, async () => {
const [picked, listed] = await settled(
Promise.all([pick(), f.rig.host.readOptions(PARENT, { readsWithoutModel: false })]),
'parallel owned/external catalog consumers'
)
expect(picked).toBeUndefined()
expect(listed.current.model).toBe('claude-opus-5-5[1m]')
})
expect(external).not.toHaveBeenCalled()
expect(before.current.model).toBe('claude-sonnet-5')
expect(
c.resolve.mock.calls.every(([home]) => agentSessionAccountHomePath(home) === HOME_B.path)
).toBe(true)
} finally {
service?.stop()
await f.dispose(
() => disposeCatalogMutationHost(f.rig),
() => rm(f.rig.root, { recursive: true, force: true })
)
}
},
10_000
)
// These operations run the real host owner and catalog claim; only external provider IO is scripted.
it('keeps newer public intent while discovery waits and reports a failed listing without holding Stop', async () => {
const f = await createReceivingCallerFixture()
@@ -14,7 +14,6 @@ import { hostname, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type * as NodeFs from 'node:fs'
import { z } from 'zod'
import {
acquireOrcadInstanceLock,
ORCAD_LOCK_FILE_NAME,
@@ -198,61 +197,9 @@ describe('acquireOrcadInstanceLock', () => {
writeFileSync(join(root, ORCAD_LOCK_FILE_NAME), JSON.stringify(olderRecord))
expect(acquireOrcadInstanceLock(root, liveMismatch(1001)).record.nonce).not.toBe('stale')
})
it('says "an unknown program" when the live process name cannot be read', () => {
const root = makeRoot()
writeFileSync(join(root, ORCAD_LOCK_FILE_NAME), JSON.stringify(olderRecord))
expect(() =>
acquireOrcadInstanceLock(root, { ...liveMismatch(1000), processName: () => null })
).toThrow(
expect.objectContaining({ message: expect.stringContaining('an unknown program') })
)
})
it('keeps the plain refusal for a record that carries a boot id', () => {
const root = makeRoot()
writeFileSync(
join(root, ORCAD_LOCK_FILE_NAME),
JSON.stringify({ ...olderRecord, bootId: 'boot-a' })
)
expect(() => acquireOrcadInstanceLock(root, liveMismatch(1000))).toThrow(
expect.objectContaining({
code: 'orcad_instance_lock_held',
message: expect.stringContaining('already owns the data root')
})
)
})
}
)
it('records the incarnation in a lock an older reader still parses', () => {
const root = makeRoot()
const incarnation = { linuxStartTicks: '1234', bootId: 'boot-a' }
const lock = acquireOrcadInstanceLock(root, hooks({ incarnation: () => incarnation }))
const written: unknown = JSON.parse(readFileSync(lock.path, 'utf8'))
expect(written).toMatchObject({ startedAtMs: 1_000, ...incarnation })
// Older schemas strip additive identity fields rather than rejecting the record.
const olderReader = z.object({
pid: z.number().int().positive(),
startedAtMs: z.number().finite().nonnegative().nullable(),
identity: z.string().min(1),
version: z.string().min(1),
acquiredAt: z.iso.datetime({ offset: true }),
nonce: z.string().min(1),
role: z.enum(['orcad', 'desktop']).optional()
})
expect(olderReader.parse(written)).toEqual({
pid: process.pid,
startedAtMs: 1_000,
identity: 'uid-1000',
version: '1.0.0-test',
acquiredAt: lock.record.acquiredAt,
nonce: lock.record.nonce,
role: 'orcad'
})
lock.release()
})
it('reads an older lock without an incarnation, and salvages a malformed one', () => {
const root = makeRoot()
writeFileSync(
@@ -392,18 +339,6 @@ describe('acquireOrcadInstanceLock', () => {
}
)
it('leaves the terminal daemon alone: the lock covers only the runtime role', () => {
const root = makeRoot()
const daemonDir = join(root, 'daemon')
mkdirSync(daemonDir, { recursive: true })
writeFileSync(join(daemonDir, 'daemon-v36.pid'), JSON.stringify({ pid: 99, startedAtMs: 1 }))
const lock = acquireOrcadInstanceLock(root, hooks())
lock.release()
expect(JSON.parse(readFileSync(join(daemonDir, 'daemon-v36.pid'), 'utf8')).pid).toBe(99)
const next = acquireOrcadInstanceLock(root, hooks())
expect(next.record.pid).toBe(process.pid)
})
it('restricts a Windows data root by ACL, and refuses when the ACL cannot be applied', () => {
const root = makeRoot()
const restricted: string[] = []
@@ -121,10 +121,6 @@ describe('getPiTitlebarExtensionSource', () => {
vi.useRealTimers()
})
it('registers nothing outside an Orca pane', () => {
expect(createHarness({ paneKey: '' }).handlers).toEqual({})
})
it('stops the spinner when the agent settles', async () => {
const harness = createHarness()
@@ -176,30 +172,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('keeps spinning through threshold compaction inside an active run', async () => {
const harness = createHarness()
await harness.callHook('agent_start')
await harness.callHook('auto_compaction_start', { reason: 'threshold' })
await harness.callHook('auto_compaction_end', { reason: 'threshold' })
expect(vi.getTimerCount()).toBe(1)
vi.advanceTimersByTime(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('does not let an idle compaction adopt an already-running agent spinner', async () => {
const harness = createHarness()
await harness.callHook('agent_start')
await harness.callHook('auto_compaction_start', { reason: 'idle' })
await harness.callHook('auto_compaction_end', { reason: 'idle' })
expect(vi.getTimerCount()).toBe(1)
vi.advanceTimersByTime(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('does not let a stale idle-compaction completion stop a newer run', async () => {
const harness = createHarness()
@@ -212,21 +184,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('transfers an idle-compaction spinner to a new run without publishing idle', async () => {
const harness = createHarness()
await harness.callHook('auto_compaction_start', { reason: 'idle' })
vi.advanceTimersByTime(80)
const titleCountBeforeTransfer = harness.titles.length
await harness.callHook('agent_start')
const transferTitles = harness.titles.slice(titleCountBeforeTransfer)
expect(transferTitles).not.toContain(IDLE_TITLE)
expect(transferTitles).toHaveLength(1)
expect(transferTitles[0]).toMatch(BRAILLE_RE)
})
it.each([{ kind: 'omp' as const }, { processTitle: 'omp' }])(
'stops final OMP completion without waiting for isIdle: %j',
async (options) => {
@@ -297,17 +254,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(vi.getTimerCount()).toBe(1)
})
it('returns an idle pane to its plain title when the dialog closes', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
await harness.callHook('ui_prompt_end')
expect(harness.lastTitle()).toBe(IDLE_TITLE)
expect(vi.getTimerCount()).toBe(0)
})
it('only the outermost of nested dialogs moves the title', async () => {
const harness = createHarness()
@@ -321,15 +267,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('ignores an unmatched dialog close', async () => {
const harness = createHarness()
await harness.callHook('agent_start')
const titleCount = harness.titles.length
await harness.callHook('ui_prompt_end')
expect(harness.titles.length).toBe(titleCount)
})
it.each(['agent_settled', 'session_shutdown'])(
'keeps the marker when %s lands under an open dialog',
async (name) => {
@@ -345,49 +282,6 @@ describe('getPiTitlebarExtensionSource', () => {
}
)
it('keeps the marker across an idle compaction that finishes under a dialog', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
await harness.callHook('auto_compaction_start', { reason: 'idle' })
await harness.callHook('auto_compaction_end')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
})
it('recovers the spinner on a new turn when a dialog close was lost', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
await harness.callHook('agent_start')
await vi.advanceTimersByTimeAsync(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('leaves the marker to OMP approval events instead of painting it', () => {
expect(createHarness({ kind: 'omp' }).handlers.ui_prompt_start).toBeUndefined()
})
it('still caps idle maintenance while a dialog holds the title', async () => {
const harness = createHarness()
await harness.callHook('auto_compaction_start', { reason: 'idle' })
await harness.callHook('ui_prompt_start')
vi.advanceTimersByTime(301_000)
expect(vi.getTimerCount()).toBe(1)
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
})
it('survives a dialog event that carries no ui context', async () => {
const harness = createHarness()
await harness.callHook('agent_start')
await expect(harness.handlers.ui_prompt_start?.({}, undefined)).resolves.toBeUndefined()
await expect(harness.handlers.ui_prompt_end?.({}, undefined)).resolves.toBeUndefined()
})
it('keeps spinning when the dialog event could not paint the marker', async () => {
const harness = createHarness()
@@ -398,63 +292,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('marks a nested dialog when the outer one could not paint', async () => {
const harness = createHarness()
await harness.callHook('agent_start')
await harness.handlers.ui_prompt_start?.({}, undefined)
await harness.callHook('ui_prompt_start')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
})
it('clears the marker through the opening ctx when the close carries none', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
await harness.handlers.ui_prompt_end?.({}, undefined)
expect(harness.lastTitle()).toBe(IDLE_TITLE)
})
it('does not reject when the dialog ctx can no longer paint', async () => {
const harness = createHarness()
const throwing = {
ui: {
setTitle: () => {
throw new Error('extension runner is no longer active')
}
}
}
await expect(harness.handlers.ui_prompt_start?.({}, throwing)).resolves.toBeUndefined()
await harness.callHook('agent_start')
await vi.advanceTimersByTimeAsync(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('does not reject when the captured ctx dies before the dialog closes', async () => {
const harness = createHarness()
let live = true
const dying = {
ui: {
setTitle: (title: string) => {
if (!live) {
throw new Error('extension runner is no longer active')
}
harness.titles.push(title)
}
}
}
await harness.handlers.ui_prompt_start?.({}, dying)
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
live = false
await expect(harness.handlers.ui_prompt_end?.({}, undefined)).resolves.toBeUndefined()
await harness.callHook('agent_start')
await vi.advanceTimersByTimeAsync(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('does not strand the marker when the closing ctx throws on ui access', async () => {
const harness = createHarness()
// Pi ctx.ui throws after session replacement invalidates its runner.
@@ -475,20 +312,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('does not reject when the opening ctx throws on ui access', async () => {
const harness = createHarness()
const stale = {
get ui(): never {
throw new Error('This extension ctx is stale')
}
}
await harness.callHook('agent_start')
await expect(harness.handlers.ui_prompt_start?.({}, stale as never)).resolves.toBeUndefined()
await vi.advanceTimersByTimeAsync(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('re-asserts the marker when pi repaints the title under a dialog', async () => {
const harness = createHarness()
@@ -518,53 +341,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(vi.getTimerCount()).toBe(0)
})
it('releases the marker when a session replacement drops the dialog', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
expect(harness.lastTitle()).toBe(PROMPT_TITLE)
// Session replacement hides dialogs without emitting close.
await harness.callHook('session_start', { reason: 'switch' })
expect(vi.getTimerCount()).toBe(0)
await harness.callHook('agent_start')
await vi.advanceTimersByTimeAsync(80)
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('survives a deleted cwd instead of crashing the pi process', async () => {
const harness = createHarness({
cwdImpl: () => {
throw new Error('ENOENT: uv_cwd')
}
})
// Timer callback errors reach Pi's uncaughtException handler and exit the agent.
await expect(harness.callHook('agent_start')).resolves.toBeUndefined()
await expect(harness.callHook('ui_prompt_start')).resolves.toBeUndefined()
await vi.advanceTimersByTimeAsync(2000)
await expect(harness.callHook('ui_prompt_end')).resolves.toBeUndefined()
await expect(harness.callHook('agent_settled')).resolves.toBeUndefined()
})
it('survives a session name that throws on a stale runtime', async () => {
let live = true
const harness = createHarness({
sessionNameImpl: () => {
if (!live) {
throw new Error('This extension API is stale')
}
return SESSION
}
})
await harness.callHook('agent_start')
await harness.callHook('ui_prompt_start')
live = false
await vi.advanceTimersByTimeAsync(2000)
await expect(harness.callHook('ui_prompt_end')).resolves.toBeUndefined()
})
it('leaves the needs-input marker to the process that owns the pane', async () => {
// Child agents inherit pane identity; only its owning process may assert needs-input.
const harness = createHarness({ env: { ORCA_PI_TITLE_MARKER_OWNED: '111' } })
@@ -576,13 +352,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
})
it('leaves an OMP runtime to its own approval events', () => {
const harness = createHarness({ processTitle: 'omp' })
expect(harness.handlers.ui_prompt_start).toBeDefined()
expect(() => harness.handlers.ui_prompt_start?.({}, undefined)).not.toThrow()
})
it.each(['getter', 'title'] as const)(
'retires a stale %s during animation without throwing or rescheduling',
async (failure) => {
@@ -612,39 +381,6 @@ describe('getPiTitlebarExtensionSource', () => {
}
)
it.each(['getter', 'title'] as const)('contains stale %s during shutdown', async (failure) => {
let stale = false
const harness = createHarness({
sessionNameImpl: () => {
if (stale && failure === 'getter') {
throw new Error('expired session')
}
return SESSION
},
setTitle: () => {
if (stale && failure === 'title') {
throw new Error('expired UI')
}
},
isIdle: () => false
})
await harness.callHook('agent_start')
await harness.callHook('agent_end')
stale = true
await expect(harness.callHook('session_shutdown')).resolves.toBeUndefined()
expect(vi.getTimerCount()).toBe(0)
})
it('does not schedule a timer when the first frame fails', async () => {
const harness = createHarness({
sessionNameImpl: () => {
throw new Error('expired')
}
})
await harness.callHook('agent_start')
expect(vi.getTimerCount()).toBe(0)
})
it('retires animation when an idle recheck loses its session', async () => {
const harness = createHarness({
isIdle: () => {
@@ -657,17 +393,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(vi.getTimerCount()).toBe(0)
})
it('clears animation and pending idle checks on session replacement', async () => {
const harness = createHarness({ isIdle: () => false })
await harness.callHook('agent_start')
await harness.callHook('agent_end')
await harness.callHook('session_shutdown')
await harness.callHook('session_start')
expect(vi.getTimerCount()).toBe(0)
await harness.callHook('agent_start')
expect(vi.getTimerCount()).toBe(1)
})
it('reload replaces only its pane owner and ignores late old-generation events', async () => {
const globals = {}
const old = createHarness({ globals, isIdle: () => false })
@@ -743,13 +468,6 @@ describe('getPiTitlebarExtensionSource', () => {
expect(vi.getTimerCount()).toBe(1)
})
it('paints the rest title at session_start so a branded Pi reads as ready before its first turn', async () => {
const harness = createHarness()
await harness.callHook('session_start', { reason: 'startup' })
expect(harness.lastTitle()).toBe(IDLE_TITLE)
expect(vi.getTimerCount()).toBe(0)
})
it('drops a leftover needs-input marker when session_start replaces the session', async () => {
const harness = createHarness()
await harness.callHook('ui_prompt_start')
@@ -5,11 +5,9 @@ import {
assertWorktreeCleanForRemoval,
closeLocalWatcherForWorktreePathMock,
computeWorktreePathMock,
deleteWorktreeHistoryDirMock,
ensurePathWithinWorkspaceMock,
getEffectiveHooks,
gitRunner,
invalidateAuthorizedRootsCacheMock,
join,
listWorktrees,
listWorktreesStrict,
@@ -286,60 +284,6 @@ describe('OrcaRuntimeService', () => {
expect(removeWorktree).not.toHaveBeenCalled()
})
it('formats preflight subprocess failures and skips PTY teardown', async () => {
const runtime = createWorktreeRemovalRuntime()
const killSpy = vi.fn().mockReturnValue(true)
runtime.setPtyController({
write: () => true,
kill: (id) => killSpy(id),
getForegroundProcess: async () => null
})
syncSinglePty(runtime, 'pty-1')
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue(
Object.assign(new Error('status failed'), {
stderr: 'fatal: unable to read current working directory\n'
})
)
await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toThrow(
`Failed to delete worktree at ${TEST_WORKTREE_PATH}. fatal: unable to read current working directory`
)
expect(killSpy).not.toHaveBeenCalled()
expect(removeWorktree).not.toHaveBeenCalled()
})
it('falls through to orphan cleanup when preflight reports missing/non-repo worktree', async () => {
const runtime = createWorktreeRemovalRuntime()
vi.mocked(getEffectiveHooks).mockReturnValue(null)
vi.mocked(assertWorktreeCleanForRemoval).mockRejectedValue(
Object.assign(new Error('status failed'), {
stderr: 'fatal: not a git repository (or any of the parent directories): .git\n'
})
)
vi.mocked(removeWorktree).mockRejectedValue(
Object.assign(new Error('git worktree remove failed'), {
stderr: `fatal: '${TEST_WORKTREE_PATH}' is not a working tree`
})
)
const gitSpy = vi
.spyOn(gitRunner, 'gitExecFileAsync')
.mockResolvedValue({ stdout: '', stderr: '' })
onTestFinished(() => gitSpy.mockRestore())
await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).resolves.toEqual({})
expect(removeWorktree).toHaveBeenCalledWith(
TEST_REPO_PATH,
TEST_WORKTREE_PATH,
false,
expect.objectContaining({
knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH })
})
)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(TEST_WORKTREE_ID)
})
it('retains metadata when an unproven orphan directory survives cleanup', async () => {
const parentDir = await mkdtemp(join(tmpdir(), 'orca-runtime-orphan-retention-'))
const repoPath = join(parentDir, 'repo')
@@ -523,35 +467,6 @@ describe('OrcaRuntimeService', () => {
)
})
it('runs archive hooks for CLI worktree removal when hooks are explicitly enabled', async () => {
const runtime = createWorktreeRemovalRuntime()
vi.mocked(getEffectiveHooks).mockReturnValue({
scripts: {
archive: 'pnpm worktree:archive'
}
})
vi.mocked(runHook).mockResolvedValue({ success: true, output: '' })
vi.mocked(removeWorktree).mockResolvedValue({})
await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: false, runHooks: true })
expect(runHook).toHaveBeenCalledWith(
'archive',
TEST_WORKTREE_PATH,
expect.objectContaining({ id: TEST_REPO_ID, path: TEST_REPO_PATH }),
undefined,
undefined
)
expect(removeWorktree).toHaveBeenCalledWith(
TEST_REPO_PATH,
TEST_WORKTREE_PATH,
false,
expect.objectContaining({
knownRemovedWorktree: expect.objectContaining({ path: TEST_WORKTREE_PATH })
})
)
})
it('clears optimistic reconcile tokens when a CLI worktree removal succeeds', async () => {
const runtime = createWorktreeRemovalRuntime()
const worktreeBaseStatus = vi.fn()
@@ -613,25 +528,6 @@ describe('OrcaRuntimeService', () => {
}
})
it('emits current when the fetched base still matches created HEAD', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
const gitSpy = mockReconcileGit({ postFetchSha: 'created-base-sha' })
try {
await reconcileWithToken(runtime, token)
expect(worktreeBaseStatus).toHaveBeenCalledWith({
repoId: TEST_REPO_ID,
worktreeId: TEST_WORKTREE_ID,
base: 'origin/main',
remote: 'origin',
status: 'current'
})
} finally {
gitSpy.mockRestore()
}
})
it('emits base_changed without mutation when the fetched base rewrote history', async () => {
const { runtime, worktreeBaseStatus } = createReconcileRuntime()
const token = runtime.recordOptimisticReconcileToken(TEST_WORKTREE_ID)
@@ -697,42 +593,6 @@ describe('OrcaRuntimeService', () => {
}
})
it('invalidates the filesystem-auth cache after CLI worktree creation', async () => {
// A stale filesystem-auth cache denied branch comparison for CLI-created paths.
const runtime = new OrcaRuntimeService(store)
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/cli-worktree')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/cli-worktree')
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/cli-worktree',
head: 'abc',
branch: 'cli-worktree',
isBare: false,
isMainWorktree: false
}
])
await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'cli-worktree'
})
expect(invalidateAuthorizedRootsCacheMock).toHaveBeenCalled()
})
it('preserves create-time metadata on later runtime listings when Windows path formatting differs', async () => {
const metaById: Record<string, WorktreeMeta> = {}
const runtimeStore = {
@@ -194,21 +194,6 @@ describe('structured mailbox pointer delivery', () => {
}
)
it('claims only mailboxes whose assignee is a structured worker', () => {
const { delivery } = harness({})
expect(delivery.deliverForHandle('dispatch:d1')).toBe(true)
expect(delivery.deliverForHandle('run:run_1')).toBe(false)
})
it('sends the pointer as a turn and consumes mail on an accepted dispatch', async () => {
const { delivery, markAsDelivered, send } = harness({})
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(send).toHaveBeenCalledTimes(1)
expect(send.mock.calls[0]![0].operationId).toMatch(/^\d{13}-[0-9a-f]{32}$/)
expect(markAsDelivered).toHaveBeenCalledWith(['m1'], 'dispatch:d1')
})
it('nudges through the worker`s own handle for direct peer mail outside a dispatch', async () => {
const { delivery, send, markAsDelivered } = harness({
mailbox: IDENTITY.handle,
@@ -239,22 +224,6 @@ describe('structured mailbox pointer delivery', () => {
expect(markAsDelivered).toHaveBeenCalledWith(['m1'], 'dispatch:d1')
})
it('retains mail when the dispatch settles unknown', async () => {
const { delivery, markAsDelivered } = harness({
dispatchState: 'unknown'
})
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(markAsDelivered).not.toHaveBeenCalled()
})
it('retains mail when the session is not attached', async () => {
const { delivery, send } = harness({ attached: false })
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(send).not.toHaveBeenCalled()
})
it('redrives a detached session when the journal replays on re-attach', async () => {
// A detached session needs a parked journal-edge retry because workers are told not to poll.
const { delivery, send, setAttached, markAsDelivered } = harness({ attached: false })
@@ -549,22 +518,6 @@ describe('structured mailbox pointer delivery', () => {
expect(markAsDelivered).toHaveBeenCalledWith(['m1'], 'dispatch:d1')
expect(stored.has('dispatch:d1')).toBe(false)
})
it('reuses one operation id for the same batch and re-mints when it grows', async () => {
const { delivery, send, stored } = harness({
dispatchState: 'unknown'
})
delivery.deliverForHandle('dispatch:d1')
await flush()
const first = send.mock.calls[0]![0].operationId
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(send.mock.calls[1]![0].operationId).toBe(first)
stored.clear()
delivery.deliverForHandle('dispatch:d1')
await flush()
expect(send.mock.calls[2]![0].operationId).not.toBe(first)
})
})
describe('forgetting one settled worker', () => {
@@ -768,16 +721,6 @@ describe('a mailbox a /clear moves while its nudge is in flight', () => {
expect(pointed.has('m1')).toBe(true)
}
)
it('keeps an unchanged rejected nudge parked for the next edge', async () => {
const { delivery, send, settle } = clearingHarness()
delivery.deliverForHandle('dispatch:d1')
await flush()
settle('rejected')
await flush()
await flush()
expect(send).toHaveBeenCalledTimes(1)
})
})
it.each(['gone', 'live-other-kind', 'metadata-read-fails'])(
@@ -348,28 +348,6 @@ describe('orchestration worker-start prompt contract', () => {
expect(vi.getTimerCount()).toBe(0)
})
it('keeps a worker question answerable after turn observation times out', async () => {
vi.useFakeTimers()
const harness = await createPromptContractHarness('swallowed')
let questionId = ''
vi.spyOn(harness.runtime, 'observeTerminalAgentPrompt').mockImplementation(
async (_handle, prompt) => {
const dispatch = harness.db.findActiveDispatchForAssignee(harness.handle)!
questionId = harness.db.createQuestion({
runId: dispatch.run_id!,
dispatchId: dispatch.id,
askerHandle: harness.handle,
question: 'Which target should I use?'
}).question.message_id
return prompt
}
)
const pending = harness.dispatcher.dispatch(harness.request)
await vi.runAllTimersAsync()
expect(await pending).toMatchObject({ ok: true, result: { state: 'outcome_unknown' } })
expect(harness.db.getQuestion(questionId)?.status).toBe('pending')
})
it('reports a swallowed Enter as start_unknown while keeping the worker reportable', async () => {
vi.useFakeTimers()
const harness = await createPromptContractHarness('swallowed')
@@ -495,27 +495,6 @@ describe.skipIf(runningBaselineOracle)('real runtime session tabs census boundar
])
})
it('uses one change sequence across subscribers without duplicate fanout', async () => {
const runtime = new OrcaRuntimeService()
const created = runtimeSnapshot('wt-census-race', 1)
const first: number[] = []
const second: number[] = []
const unsubscribeFirst = runtime.onMobileSessionTabsChanged((_snapshot, sequence) =>
first.push(sequence)
)
const unsubscribeSecond = runtime.onMobileSessionTabsChanged((_snapshot, sequence) =>
second.push(sequence)
)
runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [created] })
;(runtime as unknown as RuntimeInventoryInternals).mobileSessionTabsNotifyCoalescer.flushAll()
expect(first).toEqual([expect.any(Number)])
expect(second).toEqual(first)
unsubscribeFirst()
unsubscribeSecond()
})
it('aborts the census and removes the real runtime listener on disconnect', async () => {
const runtime = new OrcaRuntimeService()
runtime.syncWindowGraph(0, { tabs: [], leaves: [], mobileSessionTabs: [] })
@@ -543,101 +522,6 @@ describe.skipIf(runningBaselineOracle)('real runtime session tabs census boundar
})
describe('session tabs inventory census boundary', () => {
it('subsumes a change that precedes the final census snapshot', async () => {
const harness = createHarness()
const prior = snapshot(1, [terminalTab('prior')], { publicationEpoch: 'epoch-prior' })
const current = snapshot(1, [terminalTab('current')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.publish(prior)
harness.census.resolve({ snapshots: [current], authoritative: true, changeSequence: 1 })
await pending
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [current], authoritative: true }
])
})
it('subsumes a creation captured at the census boundary exactly once', async () => {
const harness = createHarness()
const created = snapshot(2, [terminalTab('created')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.publish(created)
harness.census.resolve({ snapshots: [created], authoritative: true, changeSequence: 1 })
await pending
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [created], authoritative: true }
])
})
it('delivers a creation after the census boundary as the next ordered update', async () => {
const harness = createHarness()
const created = snapshot(2, [terminalTab('created')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [], authoritative: true, changeSequence: 0 })
queueMicrotask(() => harness.publish(created))
await pending
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [], authoritative: true },
{ type: 'updated', ...created }
])
})
it('delivers a removal after the census boundary as the next ordered update', async () => {
const harness = createHarness()
const existing = snapshot(1, [terminalTab('existing')])
const removed = snapshot(0, [], { publicationEpoch: 'removed:epoch', removed: true })
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [existing], authoritative: true, changeSequence: 0 })
queueMicrotask(() => harness.publish(removed))
await pending
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [existing], authoritative: true },
{ type: 'updated', ...removed }
])
})
it('delivers ordinary changes after initialization without duplication', async () => {
const harness = createHarness()
const created = snapshot(2, [terminalTab('created')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [], authoritative: true, changeSequence: 0 })
await pending
harness.publish(created)
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [], authoritative: true },
{ type: 'updated', ...created }
])
})
it('drops a delayed callback already covered by the census watermark', async () => {
const harness = createHarness()
const included = snapshot(2, [terminalTab('included')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [included], authoritative: true, changeSequence: 1 })
await pending
harness.deliver(included, 1)
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [included], authoritative: true }
])
})
it('deduplicates a post-watermark notification already projected by the census', async () => {
const harness = createHarness()
const included = snapshot(2, [terminalTab('included')])
@@ -675,30 +559,6 @@ describe('session tabs inventory census boundary', () => {
])
})
it('replays buffered follow intent before the later ordinary snapshot', async () => {
const harness = createHarness()
const followed = {
...snapshot(1, [terminalTab('followed')]),
navigationIntent: 'follow' as const
}
const latest = snapshot(2, [terminalTab('latest')])
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [], authoritative: true, changeSequence: 0 })
queueMicrotask(() => {
harness.publish(followed)
harness.publish(latest)
})
await pending
expect(harness.emit.mock.calls.map(([event]) => event)).toEqual([
{ type: 'snapshots', snapshots: [], authoritative: true },
{ type: 'updated', ...followed },
{ type: 'updated', ...latest }
])
})
it('bounds non-structural initialization churn to the latest projected state', async () => {
const harness = createHarness()
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
@@ -718,32 +578,6 @@ describe('session tabs inventory census boundary', () => {
])
})
it('bounds alternating selection churn to the latest projected state', async () => {
const harness = createHarness()
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.census.resolve({ snapshots: [], authoritative: true, changeSequence: 0 })
queueMicrotask(() => {
for (let version = 1; version <= 1_000; version += 1) {
const selected = version % 2 === 0
harness.publish({
...snapshot(version, [{ ...terminalTab('stable'), isActive: selected }]),
activeTabId: selected ? 'stable' : null,
activeTabType: selected ? 'terminal' : null
})
}
})
await pending
expect(harness.emit).toHaveBeenCalledTimes(2)
expect(harness.emit.mock.calls[1]?.[0]).toMatchObject({
type: 'updated',
snapshotVersion: 1_000,
activeTabId: 'stable'
})
})
it('bounds repeated follow intent to the latest caller request', async () => {
const harness = createHarness()
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
@@ -883,18 +717,4 @@ describe('session tabs inventory census boundary', () => {
expect(unsubscribe).toHaveBeenCalledOnce()
expect(emit).not.toHaveBeenCalled()
})
it('drops buffered work and removes the listener when disposed during census', async () => {
const harness = createHarness()
const pending = subscribeSessionTabsInventory(harness.context, harness.emit)
await Promise.resolve()
harness.cleanup()
harness.publish(snapshot(2, [terminalTab('late')]))
harness.census.resolve({ snapshots: [], authoritative: true, changeSequence: 0 })
await pending
expect(harness.emit).not.toHaveBeenCalled()
expect(harness.unsubscribe).toHaveBeenCalledTimes(1)
})
})
@@ -699,26 +699,6 @@ describe('finishing an interrupted worktree removal after a restart', () => {
}
)
it('keeps the user’s non-forced choice after Git deleted part of the checkout: the row returns as before', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
await writeFile(join(worktreePath, 'kept.txt'), 'kept\n')
await git(['add', 'kept.txt'], worktreePath)
await git(['commit', '-qm', 'kept'], worktreePath)
// Git stopped partway: some tracked files are gone, `.git` is still there.
await unlink(join(worktreePath, 'seed.txt'))
const { outcome, purged, records } = await finishAfterRestart({ force: false })
expect(outcome).toMatchObject({ status: 'failed' })
expect(await readFile(join(worktreePath, 'kept.txt'), 'utf8')).toBe('kept\n')
expect(existsSync(join(worktreePath, '.git'))).toBe(true)
expect(await isRegistered(worktreePath)).toBe(true)
expect(await git(['branch', '--list', 'feature'])).not.toBe('')
expect(purged).toEqual([])
expect(records).toMatchObject([{ failure: { message: expect.any(String) } }])
expect(await listedRows()).toEqual([{ path: worktreePath, removalError: expect.any(String) }])
})
it('deletes nothing in a folder put at the path after Git unregistered the checkout', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
await git(['worktree', 'remove', worktreePath])
@@ -853,21 +833,6 @@ describe('finishing an interrupted worktree removal after a restart', () => {
expect(purged).toEqual([`repo-1::${worktreePath}`])
})
it('returns the row live when the finish fails, instead of retrying unseen', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
// Another Git client locked it while Orca was not running.
await git(['worktree', 'lock', worktreePath])
const { outcome, purged, records } = await finishAfterRestart({ force: true })
expect(outcome).toMatchObject({ status: 'failed' })
expect(existsSync(worktreePath)).toBe(true)
expect(await isRegistered(worktreePath)).toBe(true)
expect(purged).toEqual([])
expect(records).toMatchObject([{ failure: { message: expect.any(String) } }])
expect(await listedRows()).toEqual([{ path: worktreePath, removalError: expect.any(String) }])
})
it('drops a record whose repo Orca no longer has', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -42,7 +42,6 @@ import type { RuntimeWorktreeScanCache, RuntimeWorktreeScanInFlight } from '../o
import type { RuntimeWorktreeScanResult } from '../repo-worktree-resolution-scan'
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
import type { ResolvedWorktreeScanJoin } from '../workspaces/resolved-worktree-scan-join'
import { getRuntimeFolderWorkspaceInstanceId } from '../runtime-folder-workspace'
type ScopeFixture = {
store: RuntimeStore
@@ -481,151 +480,3 @@ it('shares one failed SSH stored-row refresh at N and 10N callers without cachin
expect.soft(refresh, `${callers} callers then a new request`).toHaveBeenCalledTimes(2)
}
})
it('catalog-scale corpus measures held RPC qualification without optional work or metadata writes', async () => {
for (const repoCount of [20, 200]) {
const h = rig()
h.repos.splice(0)
for (const key of Object.keys(h.metadata)) {
delete h.metadata[key]
}
let gitRows = 0
for (let index = 0; index < repoCount; index++) {
const folder = index % 10 === 0
const repo: Repo = {
id: `r${index + 1}`,
path: `/private-fixture/w${index + 1}`,
displayName: `Repository ${index}`,
kind: folder ? 'folder' : 'git',
badgeColor: '',
addedAt: 0,
executionHostId: 'local'
}
h.repos.push(repo)
const worktrees = Array.from({ length: 4 }, (_, row) => ({
path: row === 0 ? repo.path : `${repo.path}/checkouts/feature-${row}`,
head: `commit-${index}-${row}`,
branch: `refs/heads/feature-${row}`,
isMainWorktree: row === 0,
isBare: false
}))
worktrees.forEach((row, number) => {
const id = folder
? number === 0
? `${repo.id}::${repo.path}`
: getRuntimeFolderWorkspaceInstanceId(repo, `folder-${number}`)
: `${repo.id}::${row.path}`
h.metadata[id] = {
instanceId: `instance-${index}-${number}`,
hostId: 'local',
displayName: index === 1 && number === 0 ? 'W2' : `Workspace ${index}-${number}`
}
})
if (!folder) {
gitRows += worktrees.length
h.runtime.worktreeScanCache.set(`${repo.id}\0local`, {
generation: 0,
runtimeKey: '',
result: { ok: true, worktrees },
listing: { ok: true, worktrees },
expiresAt: Date.now() + 60_000,
adminFingerprint: null,
scannedAt: Date.now()
})
}
}
const models = Array.from({ length: repoCount * 4 }, (_, index) => ({
id: `provider/model-${index}`,
label: `Provider model ${index}`,
isDefault: index === 0,
efforts: [{ value: 'high', label: 'High' }]
}))
h.catalogStore.recordSuccess(
h.fingerprint,
'opencode',
{ origin: 'probe', models },
'discovery'
)
await h.catalog.demand({
agent: 'opencode',
workspace: {
selector: 'name:W2',
identity: () => h.runtime.structuredAgentSessionCatalogWorkspaceIdentity('name:W2'),
resolve: async () => ({
identity: h.runtime.structuredAgentSessionCatalogWorkspaceIdentity('name:W2'),
path: '/private-fixture/w2'
})
}
})
let repoVisits = 0
let metadataVisits = 0
let metadataKeys = 0
const repos = h.repos.slice()
repos.forEach((repo, index) => {
Object.defineProperty(repos, index, {
get() {
repoVisits++
return repo
}
})
})
const metadata = new Proxy(h.metadata, {
get(target, key) {
if (typeof key === 'string') {
metadataVisits++
return target[key]
}
return undefined
},
ownKeys(target) {
const keys = Reflect.ownKeys(target)
metadataKeys += keys.length
return keys
}
})
vi.spyOn(h.store, 'getRepos').mockImplementation(() => repos)
vi.spyOn(h.store, 'getAllWorktreeMeta').mockImplementation(() => metadata)
const projection = vi.spyOn(h.runtime, 'projectCachedWorktrees')
const before = h.counts()
const start = performance.now()
let answer: unknown
for (let read = 0; read < 4; read++) {
answer = await h.current()
}
const elapsedMs = performance.now() - start
expect(answer).toMatchObject({
models: expect.arrayContaining([{ ...models[0], isDefault: true }])
})
expect(h.counts()).toEqual(before)
expect(projection).toHaveBeenCalledTimes(4)
expect(repoVisits).toBeGreaterThanOrEqual(repoCount)
expect(repoVisits).toBeLessThanOrEqual(4 * (repoCount ** 2 + 10 * repoCount))
expect(metadataVisits).toBeLessThanOrEqual(4 * repoCount * 4)
expect(metadataKeys).toBeGreaterThan(0)
expect(metadataKeys).toBeLessThanOrEqual(4 * (repoCount / 10) * (repoCount * 4))
expect(
projection.mock.results.every(
(result) => result.type === 'return' && result.value.worktrees.length === repoCount * 4
)
).toBe(true)
process.stdout.write(
`CATALOG_CORPUS ${JSON.stringify({
repoCount,
folderRepos: repoCount / 10,
gitRows,
workspaceRows: repoCount * 4,
modelRows: models.length,
reads: 4,
projections: projection.mock.calls.length,
repoVisits,
metadataVisits,
metadataKeys,
responseBytes: Buffer.byteLength(JSON.stringify(answer)),
elapsedMs,
optionalWork: h.counts().map((value, index) => value - before[index])
})}\n`
)
h.catalog.stop()
vi.restoreAllMocks()
}
})
@@ -30,20 +30,6 @@ describe.each(['router', 'degraded'] as const)('workspace deletion with %s inven
unregisterPty(targetId)
})
it('removes a workspace with no terminals despite an unrelated ambiguous session', async () => {
const { provider, currentSessions, legacySessions } = setup(kind)
await expect(
killAllProcessesForWorktree(worktreeId, {
localProvider: provider,
requirePhysicalStop: true
})
).resolves.toEqual({ runtimeStopped: 0, providerStopped: 0, registryStopped: 0 })
expect(currentSessions).toEqual([unrelatedId])
expect(legacySessions).toEqual([unrelatedId])
await expect(provider.listProcesses()).rejects.toThrow(unrelatedId)
})
it('stops the target terminal and preserves ambiguous sessions in other workspaces', async () => {
const { provider, currentSessions, legacySessions } = setup(kind, [targetId])
@@ -57,20 +43,6 @@ describe.each(['router', 'degraded'] as const)('workspace deletion with %s inven
expect(legacySessions).toEqual([unrelatedId])
})
it('still refuses deletion when the target terminal has ambiguous ownership', async () => {
const { provider, currentSessions, legacySessions } = setup(kind, [targetId])
legacySessions.push(targetId)
await expect(
killAllProcessesForWorktree(worktreeId, {
localProvider: provider,
requirePhysicalStop: true
})
).rejects.toThrow(`Terminal session owner could not be verified: ${targetId}`)
expect(currentSessions).toContain(targetId)
expect(legacySessions).toContain(targetId)
})
it('refuses an ambiguous target even after establishing its legacy input route', async () => {
const { provider, currentSessions, legacySessions } = setup(kind)
legacySessions.push(targetId)
@@ -355,117 +355,6 @@ describe('updater', () => {
})
})
it('suppresses a delayed background fallback error after the fallback promise handled it', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
setTimeout(() => {
autoUpdaterMock.emit('error', missingManifest)
}, 10)
return Promise.reject(missingManifest)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const mainWindow = updaterMainWindow(sendMock)
const { setupAutoUpdater } = await loadUpdaterModule()
setupAutoUpdater(mainWindow, { getLastUpdateCheckAt: () => null })
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'idle' })
})
sendMock.mockClear()
await vi.advanceTimersByTimeAsync(10)
const statusesAfterLateError = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statusesAfterLateError).not.toContainEqual(
expect.objectContaining({ state: 'error', message: missingManifest.message })
)
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('suppresses a delayed user fallback error after the fallback promise handled it', async () => {
vi.useFakeTimers()
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
setTimeout(() => {
autoUpdaterMock.emit('error', missingManifest)
}, 10)
return Promise.reject(missingManifest)
})
const sendMock = vi.fn()
const mainWindow = updaterMainWindow(sendMock)
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
messageCode: 'updateServerUnreachable',
userInitiated: true
})
})
sendMock.mockClear()
await vi.advanceTimersByTimeAsync(10)
const statusesAfterLateError = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statusesAfterLateError).not.toContainEqual(
expect.objectContaining({ state: 'error', message: missingManifest.message })
)
})
it('keeps background prerelease fallback not-available on the short retry cadence', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
@@ -670,29 +559,4 @@ describe('updater', () => {
})
})
})
// Moving latest redirects can resolve old manifests' relative asset URLs against newer releases.
it('pins the generic feed to a concrete stable tag for a stable user', async () => {
appMock.getVersion.mockReturnValue('1.3.17')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.18'])
autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined)
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
const mainWindow = updaterMainWindow(vi.fn())
setupAutoUpdater(mainWindow, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(fetchNewerReleaseTagsMock).toHaveBeenCalledWith('1.3.17', 1, {
includePrerelease: false
})
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(1)
})
expect(autoUpdaterMock.setFeedURL).toHaveBeenLastCalledWith({
provider: 'generic',
url: 'https://github.com/stablyai/orca/releases/download/v1.3.18'
})
})
})
@@ -207,15 +207,6 @@ describe('host model catalog read', () => {
}
})
it('asks a host that reports no listing nothing more', async () => {
answerCatalog([() => Promise.resolve(UNKNOWN)])
const { result, unmount } = renderOptions()
await flush()
expect(catalogReads()).toHaveLength(1)
expect(usable(result.current.optionSnapshot)).toBe(true)
unmount()
})
it('the running provider list wins over a host listing that lands later', async () => {
const live = deferred()
const waited = deferred()
@@ -164,47 +164,6 @@ afterEach(() => {
})
describe('useNativeChatExternalAttachments', () => {
it('keeps a failed paired chat from issuing an upload and says how to retry', async () => {
const attachResolvedPaths = vi.fn()
const setNotice = vi.fn()
const notice = 'Codex chat could not open. Use Retry in the chat, then attach the files again.'
const probe = await renderProbe({
structuredWorktreeId: 'folder-1',
structuredSession: {
sessionId: 'codex-1',
runtimeEnvironmentId: 'server-1',
sessionPending: true,
sessionPendingNotice: notice
},
attachResolvedPaths,
setNotice
})
await act(async () => {
await probe.latest().captureExternalDrop()(['/local/a.txt'])
})
expect(setNotice).toHaveBeenCalledWith(notice)
expect(mocks.prepareNativeChatSessionAttachmentUpload).not.toHaveBeenCalled()
expect(mocks.uploadNativeChatSessionAttachmentPaths).not.toHaveBeenCalled()
expect(attachResolvedPaths).not.toHaveBeenCalled()
})
it('attaches local worktree paths unchanged', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'local' })
const attachResolvedPaths = vi.fn()
const probe = await renderProbe({ attachResolvedPaths })
await act(async () => {
probe.latest().attachExternalPaths(['/local/a.txt'])
})
expect(mocks.stat).toHaveBeenCalledExactlyOnceWith({
filePath: '/local/a.txt',
access: { kind: 'user-file' }
})
expect(attachResolvedPaths).toHaveBeenCalledWith(['/local/a.txt'], undefined, {
destinationIsCurrent: expect.any(Function),
operation: expect.any(Object)
})
})
it('rejects a whole batch over the drop path limit, like a drop does', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'local' })
const attachResolvedPaths = vi.fn()
@@ -331,32 +290,6 @@ describe('useNativeChatExternalAttachments', () => {
)
})
it('shows the not-ready notice instead of attaching unresolved paths', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'not-ready' })
const attachResolvedPaths = vi.fn()
const setNotice = vi.fn()
const probe = await renderProbe({ attachResolvedPaths, setNotice })
await act(async () => {
probe.latest().attachExternalPaths(['/local/a.txt'])
})
expect(setNotice).toHaveBeenCalledWith('Worktree not ready — try again in a moment.')
expect(attachResolvedPaths).not.toHaveBeenCalled()
})
it('does not attach client-local paths to a remote runtime', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'remote' })
const attachResolvedPaths = vi.fn()
const setNotice = vi.fn()
const probe = await renderProbe({ attachResolvedPaths, setNotice })
await act(async () => {
probe.latest().attachExternalPaths(['/local/a.txt'])
})
expect(setNotice).toHaveBeenCalledWith(
'Local attachments are not available for remote sessions.'
)
expect(attachResolvedPaths).not.toHaveBeenCalled()
})
it('releases browser files a refused attach held, so none stay pinned in the page', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'remote' })
const file = new File(['a'], 'a.txt')
@@ -368,17 +301,6 @@ describe('useNativeChatExternalAttachments', () => {
expect(takeBrowserAttachmentFile(token!)).toBeNull()
})
it('ignores local attachment insertion while already disabled', async () => {
mocks.resolveNativeChatAttachmentOwner.mockReturnValue({ kind: 'local' })
const attachResolvedPaths = vi.fn()
const probe = await renderProbe({ disabled: true, attachResolvedPaths })
act(() => probe.latest().attachExternalPaths(['/local/a.txt']))
expect(mocks.resolveNativeChatAttachmentOwner).not.toHaveBeenCalled()
expect(attachResolvedPaths).not.toHaveBeenCalled()
})
it('releases browser files a picker returns to an already disabled composer', async () => {
const [token] = holdBrowserAttachmentFiles([new File(['a'], 'a.txt')])
const probe = await renderProbe({ disabled: true, attachResolvedPaths: vi.fn() })
@@ -439,48 +361,6 @@ describe('useNativeChatExternalAttachments', () => {
mocks.uploadNativeChatSessionAttachmentPaths.mockReset()
})
it('uploads into the chat store and attaches only server paths', async () => {
const upload = deferred<AgentSessionAttachmentPathUploadResult>()
mocks.uploadNativeChatSessionAttachmentPaths.mockReturnValueOnce(upload.promise)
const chips = trackingChips()
const attachResolvedPaths = vi.fn()
const probe = await renderProbe({
structuredWorktreeId: 'worktree-1',
structuredSession: session,
attachResolvedPaths,
pendingChips: chips
})
act(() => probe.latest().attachExternalPaths(['/Users/me/shot.png', '/Users/me/notes.md']))
expect(chips.begun).toEqual(['shot.png', 'notes.md'])
await act(async () =>
upload.resolve(
uploaded([
['/Users/me/shot.png', '/srv/agent-session-attachments/u1/shot.png'],
['/Users/me/notes.md', '/srv/agent-session-attachments/u2/notes.md']
])
)
)
expect(mocks.uploadNativeChatSessionAttachmentPaths).toHaveBeenCalledWith(
['/Users/me/shot.png', '/Users/me/notes.md'],
target
)
expect(chips.resolve).toHaveBeenCalledExactlyOnceWith(
'chip-1',
'/srv/agent-session-attachments/u1/shot.png',
null,
undefined
)
expect(chips.drop).not.toHaveBeenCalled()
expect(chips.attachReferences).toHaveBeenCalledExactlyOnceWith(
[{ id: 'chip-2', path: '/srv/agent-session-attachments/u2/notes.md' }],
undefined
)
// The client never reads its own disk for these paths: the server stores the bytes.
expect(mocks.stat).not.toHaveBeenCalled()
})
it.each([null, 'env-1'])('fences captured results for %s', async (runtimeEnvironmentId) => {
const chips = trackingChips()
const attachResolvedPaths = vi.fn()
@@ -557,38 +437,6 @@ describe('useNativeChatExternalAttachments', () => {
)
})
it('keeps each file operation ID through reference settlement, including removed chips', async () => {
const upload = deferred<AgentSessionAttachmentPathUploadResult>()
mocks.uploadNativeChatSessionAttachmentPaths.mockReturnValueOnce(upload.promise)
const chips = trackingChips()
const attachResolvedPaths = vi.fn()
const probe = await renderProbe({
structuredWorktreeId: 'worktree-1',
structuredSession: session,
attachResolvedPaths,
pendingChips: chips
})
act(() => probe.latest().attachExternalPaths(['/Users/me/report.pdf', '/Users/me/notes.md']))
chips.removed.add('chip-1')
await act(async () =>
upload.resolve(
uploaded([
['/Users/me/report.pdf', '/srv/agent-session-attachments/u1/report.pdf'],
['/Users/me/notes.md', '/srv/agent-session-attachments/u2/notes.md']
])
)
)
expect(chips.attachReferences).toHaveBeenCalledExactlyOnceWith(
[
{ id: 'chip-1', path: '/srv/agent-session-attachments/u1/report.pdf' },
{ id: 'chip-2', path: '/srv/agent-session-attachments/u2/notes.md' }
],
undefined
)
})
it('names, in one notice, each file that did not attach', async () => {
mocks.uploadNativeChatSessionAttachmentPaths.mockResolvedValueOnce(
uploaded(
@@ -622,30 +470,6 @@ describe('useNativeChatExternalAttachments', () => {
expect(mocks.toastLoading).not.toHaveBeenCalled()
})
it('refuses on a server without the attachment store, uploading nothing', async () => {
mocks.prepareNativeChatSessionAttachmentUpload.mockResolvedValueOnce({
ok: false,
notice: 'needs newer server'
})
const chips = trackingChips()
const attachResolvedPaths = vi.fn()
const notices: (string | null)[] = []
const probe = await renderProbe({
structuredWorktreeId: 'worktree-1',
structuredSession: session,
attachResolvedPaths,
pendingChips: chips,
setNotice: (notice) => notices.push(notice)
})
await act(async () => probe.latest().attachExternalPaths(['/Users/me/shot.png']))
expect(mocks.uploadNativeChatSessionAttachmentPaths).not.toHaveBeenCalled()
expect(chips.drop).toHaveBeenCalledExactlyOnceWith('chip-1')
expect(chips.attachReferences).not.toHaveBeenCalled()
expect(notices.at(-1)).toBe('needs newer server')
})
it('says which file did not attach when the upload itself fails, such as on a re-pair', async () => {
// Main refuses every chunk once the pairing changes, so the whole upload call fails.
mocks.uploadNativeChatSessionAttachmentPaths.mockRejectedValueOnce(
@@ -669,21 +493,5 @@ describe('useNativeChatExternalAttachments', () => {
expect(chips.attachReferences).not.toHaveBeenCalled()
expect(notices).toEqual(["Couldn't attach shot.png. Runtime environment changed."])
})
it('keeps a local structured chat on the worktree owner', async () => {
const attachResolvedPaths = vi.fn()
const probe = await renderProbe({
structuredWorktreeId: 'worktree-1',
structuredSession: { sessionId: 'session-1', runtimeEnvironmentId: null },
attachResolvedPaths
})
await act(async () => probe.latest().attachExternalPaths(['/local/a.txt']))
expect(mocks.resolveNativeChatAttachmentOwnerForWorktree).toHaveBeenCalled()
expect(mocks.prepareNativeChatSessionAttachmentUpload).not.toHaveBeenCalled()
expect(attachResolvedPaths).toHaveBeenCalledWith(['/local/a.txt'], undefined, {
destinationIsCurrent: expect.any(Function),
operation: expect.any(Object)
})
})
})
})
@@ -147,23 +147,6 @@ describe('attachment guard follows what the host claims', () => {
expect(structuredTransport.send).not.toHaveBeenCalled()
})
it.each([
['codex', '/permissions'],
['claude', '/fast'],
['claude', '/init'],
['claude', '/review'],
['codex', '/goal ship the fix']
] as const)('sends %s attachments along with the passed-through %s', async (agent, text) => {
const { send, structuredTransport } = harness(agent)
send(text)
await vi.waitFor(() =>
expect(structuredTransport.send).toHaveBeenCalledWith(text, [ATTACHMENT])
)
expect(structuredTransport.onError).not.toHaveBeenCalledWith(
'Remove attachments before using a chat-session command.'
)
})
it('refuses attachments on /goal where the host sets the goal, since no message is sent', () => {
const setObjective = vi.fn(async () => true)
const { send, structuredTransport } = harness('codex', { setObjective })
@@ -205,14 +188,6 @@ describe('reports the sends that bring the latest into view', () => {
expect(structuredTransport.send).toHaveBeenCalledWith('hello', [])
expect(onSubmitted).toHaveBeenCalledOnce()
})
it('reports a provider command sent without an inventory', async () => {
const onSubmitted = vi.fn()
const { send, structuredTransport } = harness('codex', undefined, onSubmitted)
await send('/permissions', [])
expect(structuredTransport.send).toHaveBeenCalledWith('/permissions', [])
expect(onSubmitted).toHaveBeenCalledOnce()
})
})
// STA-10671: an older paired host would run the turn without the approval the chat asks for.
@@ -403,25 +403,6 @@ describe('Git Bash console-capacity pane retention', () => {
expect(started.deps.onPaneProcessDied).not.toHaveBeenCalled()
})
it('tears down an ordinary failed shell after acknowledged input', async () => {
const started = await startPane()
started.spawn()
sendTerminalInputThroughPane(started.pane, 'exit\r')
await flushAsyncTicks()
started.output('bash: missing command')
started.exit()
expect(started.deps.onPaneProcessDied).not.toHaveBeenCalled()
expect(started.deps.onPtyExitRef.current).toHaveBeenCalled()
})
it('uses existing newborn handling when no overlay handler is installed', async () => {
const started = await startPane({ handler: false })
started.spawn()
started.output(MARKER)
expect(() => started.exit()).not.toThrow()
expect(started.manager.closePane).not.toHaveBeenCalled()
})
it('keys an input attempt to the bound process rather than its incoming replacement', async () => {
const exitModule = await import('./pty-connection/pty-exit-hibernate')
const install = exitModule.installPtyExitHibernate
@@ -53,41 +53,6 @@ describe('dispatchTerminalNotification', () => {
vi.unstubAllGlobals()
})
it('uses a live pane key when marking inactive worktree attention', () => {
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'codex',
paneKey
})
expect(window.api.notifications.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
source: 'agent-task-complete',
notificationId: buildAgentNotificationId({
worktreeId: 'wt-primary',
paneKey,
stateStartedAt: mockState.agentStatusByPaneKey[paneKey].stateStartedAt
}),
worktreeId: 'wt-primary',
paneKey,
repoLabel: 'orca',
worktreeLabel: 'master',
terminalTitle: 'codex',
isActiveWorktree: false,
agentType: 'codex',
agentState: 'done',
agentPrompt: 'codex-hook-notify',
agentLastAssistantMessage: 'Done.'
})
)
expect(mockState.markAgentAttentionUnread).toHaveBeenCalledWith({
workspaceId: 'wt-primary',
subjectKey: paneKey,
groupId: 'tab-1',
reason: 'agent-completion'
})
})
it('builds the notification id from a completion snapshot, not the pinned working row', () => {
const pinnedWorkingStartedAt = NOW - 60_000
// Match row agent identity to the event or rejection makes both fallback outcomes pass vacuously.
@@ -258,87 +223,6 @@ describe('dispatchTerminalNotification', () => {
})
})
it('hands the completion to the attention module whatever the pane-attention setting', () => {
mockState.settings.experimentalTerminalAttention = false
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'codex',
paneKey
})
expect(window.api.notifications.dispatch).toHaveBeenCalled()
expect(mockState.markAgentAttentionUnread).toHaveBeenCalledWith({
workspaceId: 'wt-primary',
subjectKey: paneKey,
groupId: 'tab-1',
reason: 'agent-completion'
})
})
it('offers attention-only completion to main for independent mobile delivery', () => {
mockState.settings.notifications = { ...mockState.settings.notifications, enabled: false }
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'codex',
paneKey
})
expect(mockState.markAgentAttentionUnread).toHaveBeenCalledWith({
workspaceId: 'wt-primary',
subjectKey: paneKey,
groupId: 'tab-1',
reason: 'agent-completion'
})
expect(window.api.notifications.dispatch).toHaveBeenCalled()
})
it('names the server running the workspace so a phone opens it there, and none for local work', () => {
for (const workspaceOwner of [
{ executionHostId: 'local' as const, runtimeEnvironmentId: null },
// A server's own SSH target still runs behind that server.
{ executionHostId: 'ssh:qa' as const, runtimeEnvironmentId: 'env-1' },
// A tab stamped with the server before its environment is known.
{ executionHostId: 'runtime:env-2' as const, runtimeEnvironmentId: null }
]) {
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'codex',
paneKey,
workspaceOwner
})
}
const calls = vi.mocked(window.api.notifications.dispatch).mock.calls
expect(calls.map(([request]) => request.executionHost)).toEqual([
undefined,
'runtime:env-1',
'runtime:env-2'
])
})
it('writes unread with the completion reason while the agent-complete banner toggle is off', () => {
// Main applies desktop-banner settings after unread/mobile delivery; disabling banners must preserve tray/unread attention.
mockState.settings.notifications = {
...mockState.settings.notifications,
enabled: true,
agentTaskComplete: false
}
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'codex',
paneKey
})
expect(mockState.markAgentAttentionUnread).toHaveBeenCalledWith({
workspaceId: 'wt-primary',
subjectKey: paneKey,
groupId: 'tab-1',
reason: 'agent-completion'
})
expect(window.api.notifications.dispatch).toHaveBeenCalled()
})
it('records a bell as a distinct reason and never as an agent completion', () => {
dispatchTerminalNotification('wt-primary', { source: 'terminal-bell', paneKey })
@@ -615,54 +499,6 @@ describe('dispatchTerminalNotification', () => {
expect(dispatchArgs?.agentLastAssistantMessage).toBeUndefined()
})
it('does not reuse an untyped fresh agent snapshot when the terminal title names an agent', () => {
mockState.agentStatusByPaneKey[paneKey] = makeAgentStatus(paneKey, {
agentType: undefined,
terminalTitle: 'unknown',
lastAssistantMessage: 'Previous agent done.'
})
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: 'Claude Code',
paneKey
})
const dispatchArgs = getLastNotificationDispatchArg()
expect(dispatchArgs).toEqual(
expect.objectContaining({
source: 'agent-task-complete',
worktreeId: 'wt-primary',
paneKey,
terminalTitle: 'Claude Code'
})
)
expect(dispatchArgs?.agentType).toBeUndefined()
expect(dispatchArgs?.agentLastAssistantMessage).toBeUndefined()
})
it('keeps a fresh agent snapshot when the terminal title matches the stored agent', () => {
mockState.agentStatusByPaneKey[paneKey] = makeAgentStatus(paneKey, {
agentType: 'codex',
terminalTitle: 'Codex',
lastAssistantMessage: 'Codex done.'
})
dispatchTerminalNotification('wt-primary', {
source: 'agent-task-complete',
terminalTitle: '⠋ Codex',
paneKey
})
const dispatchArgs = getLastNotificationDispatchArg()
expect(dispatchArgs).toEqual(
expect.objectContaining({
agentType: 'codex',
agentLastAssistantMessage: 'Codex done.'
})
)
})
it('drops a title-only completion when fresh hook state is still active', () => {
mockState.agentStatusByPaneKey[paneKey] = makeAgentStatus(paneKey, {
state: 'working',
@@ -280,80 +280,6 @@ describe('picker window launch contract', () => {
)
})
it.each(['claude', 'codex'] as const)(
'passes %s speed through the real picker-to-terminal compiler',
async (agent) => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
const { agentPickerDraft, agentPickerLaunchInputs } =
await import('../../../shared/agent-launch-picker')
const { editAgentPickerSpeed } = await import('../../../shared/agent-launch-picker-arguments')
const options: Record<string, string> =
agent === 'claude' ? { fastMode: 'false' } : { serviceTier: 'default' }
const entry = {
agent,
label: agent,
rows: [{ agent, kind: 'terminal' as const }],
surfaces: ['chat' as const, 'terminal' as const],
defaultSurface: 'chat' as const,
terminal: { permissionMode: 'ask' as const },
structured: {
model: agent === 'claude' ? 'opus' : 'account-model',
permissionMode: 'ask' as const,
options
},
arguments:
agent === 'claude'
? '--settings ./team.json --settings \'{"language":"french"}\''
: '--add-dir docs',
catalog: {
origin: 'probe' as const,
fetchedAt: 1,
models: [
{
id: agent === 'claude' ? 'opus' : 'account-model',
label: 'Model',
isDefault: true,
efforts: [],
supportsFastMode: true,
...(agent === 'codex'
? { serviceTiers: [{ value: 'account_fast_v9', label: 'Fast' }] }
: {})
}
]
}
}
Object.assign(store.settings, { agentPermissionDefault: 'ask' })
const draft = editAgentPickerSpeed(
entry,
agentPickerDraft(entry),
agent === 'claude' ? 'fastMode' : 'serviceTier',
agent === 'claude' ? 'true' : 'account_fast_v9',
entry.structured.model
)
const launch = {
...draft,
surface: 'terminal' as const
}
mockCreateTab.mockClear()
launchAgentInNewTab({
requestId: 'picker-fast-contract',
agent,
worktreeId: 'wt-1',
requestedSurface: 'terminal',
...agentPickerLaunchInputs(launch, entry)
})
const command = queuedStartups()[0]?.command ?? ''
expect(command).toContain(
agent === 'claude' ? '"fastMode":true' : 'service_tier=account_fast_v9'
)
if (agent === 'claude') {
expect(command).toContain('"language":"french"')
expect(command).toContain('./team.json')
}
expect(command).not.toContain('account-model')
}
)
it.each(['claude', 'codex'] as const)(
'keeps failed %s defaults unknown through acquisition, controls, projection and actual dispatch, then wakes to a permission-preserving edit',
async (agent) => {
@@ -12,7 +12,6 @@ import {
getLocalPreflightContext,
getLocalProjectExecutionRuntimeContext,
getLocalRepoProjectExecutionRuntimeContext,
getWslDistroFromPath,
localPreflightContextKey
} from './local-preflight-context'
@@ -76,12 +75,6 @@ function makeState(args: {
}
describe('local preflight context', () => {
it('extracts WSL distro names from supported UNC forms', () => {
expect(getWslDistroFromPath(String.raw`\\wsl.localhost\Ubuntu\home\alice\repo`)).toBe('Ubuntu')
expect(getWslDistroFromPath(String.raw`\\wsl$\Debian\home\alice\repo`)).toBe('Debian')
expect(getWslDistroFromPath('/Users/alice/repo')).toBeNull()
})
it('returns a stable snapshot for repeated WSL selector reads', () => {
const state = makeState({
repoPath: '/Users/alice/repo',
@@ -112,41 +105,6 @@ describe('local preflight context', () => {
expect(getLocalPreflightContext(state, 'darwin')).toBeUndefined()
})
it('reuses the same WSL snapshot across equivalent active repo and worktree paths', () => {
const fromRepo = getLocalPreflightContext(
makeState({
repoPath: String.raw`\\wsl.localhost\Ubuntu\home\alice\repo`
}),
'darwin'
)
const fromWorktree = getLocalPreflightContext(
makeState({
repoPath: '/Users/alice/repo',
worktreePath: String.raw`\\wsl.localhost\Ubuntu\home\alice\repo`
}),
'darwin'
)
const fromOtherDistro = getLocalPreflightContext(
makeState({
repoPath: '/Users/alice/repo',
worktreePath: String.raw`\\wsl.localhost\Debian\home\alice\repo`
}),
'darwin'
)
expect(fromRepo).toBe(fromWorktree)
expect(fromOtherDistro).not.toBe(fromRepo)
expect(fromOtherDistro).toEqual({ wslDistro: 'Debian' })
})
it('uses the stable host context for non-WSL paths', () => {
const state = makeState({ repoPath: '/Users/alice/repo' })
expect(getLocalPreflightContext(state)).toBeUndefined()
expect(getLocalPreflightContext(state)).toBeUndefined()
expect(localPreflightContextKey(getLocalPreflightContext(state))).toBe('host')
})
it('keys preflight by active runtime before local WSL or host context', () => {
const state: AppState = {
...makeState({
@@ -530,30 +488,6 @@ describe('local preflight context', () => {
expect(localPreflightContextKey(context)).toBe('repo-2:windows-host')
})
it('resolves a project host override for a specific worktree over a WSL default', () => {
const state: AppState = {
...makeState({ repoPath: 'C:\\Users\\alice\\repo', worktreePath: 'C:\\Users\\alice\\repo' }),
settings: {
...legacySettings(),
terminalWindowsShell: 'wsl.exe',
terminalWindowsWslDistro: 'Debian',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Debian' }
},
projects: [project({ id: 'repo-1', localWindowsRuntimePreference: { kind: 'windows-host' } })]
}
expect(getLocalProjectExecutionRuntimeContext(state, 'repo-1::worktree-1', 'win32')).toEqual({
status: 'resolved',
runtime: {
kind: 'windows-host',
hostPlatform: 'win32',
projectId: 'repo-1',
reason: 'project-override',
cacheKey: 'repo-1:windows-host'
}
})
})
it('does not assign the active project runtime to the floating workspace', () => {
const state: AppState = {
...makeState({ repoPath: 'C:\\Users\\alice\\repo', worktreePath: 'C:\\Users\\alice\\repo' }),
@@ -1,11 +1,9 @@
/** Pin native follow/pin ownership through shared headless/core BufferService. Live writes rely on xterm; app enforcement handles structural replay/remount/reflow only. Upgrades must preserve this contract. Since beta.302 (#6081), CSI 3 J releases native pins; Orca observes before erase to capture and restore the live reading offset. */
import { describe, expect, it, vi } from 'vitest'
import { Terminal } from '@xterm/headless'
import headlessPackageJson from '@xterm/headless/package.json'
import xtermPackageJson from '@xterm/xterm/package.json'
import { clearTerminalScrollbackAndFollowOutput } from './terminal-scrollback-clear'
import { installTerminalLiveScrollbackRestore } from './terminal-live-scrollback-restore'
import { markTerminalFollowOutput, markTerminalPinnedViewport } from './terminal-scroll-intent'
import { markTerminalPinnedViewport } from './terminal-scroll-intent'
type TerminalWithBufferService = Terminal & {
_core?: {
@@ -39,129 +37,6 @@ async function pinnedScrollbackTerminal(): Promise<TerminalWithBufferService> {
}
describe('xterm native user-scrolling contract', () => {
it('builds headless and renderer xterm from the same upstream commit', () => {
// Compare source commits, not versions: headless/core may publish different betas with identical BufferService/BufferLine code; publish.js stamps both.
expect(headlessPackageJson.commit).toBe(xtermPackageJson.commit)
expect(xtermPackageJson.commit).toMatch(/^[0-9a-f]{40}$/)
})
it('keeps a scrolled-up viewport stable while output is written', async () => {
const term = new Terminal({ rows: 10, cols: 40, scrollback: 1000, allowProposedApi: true })
await writeLines(term, 30, 'line')
const buffer = term.buffer.active
expect(buffer.viewportY).toBe(buffer.baseY)
term.scrollLines(-5)
const pinnedY = buffer.viewportY
expect(pinnedY).toBe(buffer.baseY - 5)
await writeLines(term, 10, 'more')
expect(buffer.viewportY).toBe(pinnedY)
expect(buffer.baseY).toBe(pinnedY + 15)
})
it('treats a viewport one row above bottom as user-scrolling through output', async () => {
const term = new Terminal({
rows: 10,
cols: 40,
scrollback: 1000,
allowProposedApi: true
}) as TerminalWithBufferService
await writeLines(term, 30, 'line')
const buffer = term.buffer.active
term.scrollLines(-1)
const pinnedY = buffer.viewportY
expect(pinnedY).toBe(buffer.baseY - 1)
expect(term._core?._bufferService?.isUserScrolling).toBe(true)
await writeLines(term, 5, 'more')
expect(buffer.viewportY).toBe(pinnedY)
})
it('follows output at the bottom and re-follows after scrolling back down', async () => {
const term = new Terminal({ rows: 10, cols: 40, scrollback: 1000, allowProposedApi: true })
await writeLines(term, 30, 'line')
const buffer = term.buffer.active
await writeLines(term, 5, 'tail')
expect(buffer.viewportY).toBe(buffer.baseY)
term.scrollLines(-5)
term.scrollToBottom()
await writeLines(term, 5, 'after')
expect(buffer.viewportY).toBe(buffer.baseY)
})
it('applies scrollOnUserInput before notifying onData listeners', async () => {
const term = new Terminal({ rows: 10, cols: 40, scrollback: 1000, allowProposedApi: true })
await writeLines(term, 30, 'line')
const buffer = term.buffer.active
term.scrollLines(-5)
let viewportSeenByOnData = -1
const subscription = term.onData(() => {
viewportSeenByOnData = buffer.viewportY
})
term.input('a', true)
// Typing-intent resync runs in onData, making this xterm event order part of the pinned contract.
expect(viewportSeenByOnData).toBe(buffer.baseY)
subscription.dispose()
})
it('distinguishes real user input from parser auto-replies', async () => {
const term = new Terminal({
rows: 10,
cols: 40,
allowProposedApi: true
}) as TerminalWithBufferService
expect(term._core?.coreService?.onUserInput).toBeTypeOf('function')
let userInputCount = 0
const subscription = term._core?.coreService?.onUserInput?.(() => {
userInputCount += 1
})
term.input('a', true)
await write(term, '\x1b[6n')
expect(userInputCount).toBe(1)
subscription?.dispose()
})
it('walks a pinned viewport down content-stably when scrollback trims', async () => {
const term = new Terminal({ rows: 5, cols: 20, scrollback: 20, allowProposedApi: true })
await writeLines(term, 30, 'x')
const buffer = term.buffer.active
term.scrollLines(-10)
const pinnedY = buffer.viewportY
const fullBaseY = buffer.baseY
await writeLines(term, 10, 'trim')
// At capacity, fixed baseY plus trimming decrements viewportY to preserve content.
expect(buffer.baseY).toBe(fullBaseY)
expect(buffer.viewportY).toBe(Math.max(0, pinnedY - 10))
})
it('exposes the isUserScrolling flag the structural restore paths depend on', async () => {
const term = new Terminal({
rows: 10,
cols: 40,
scrollback: 1000,
allowProposedApi: true
}) as TerminalWithBufferService
await writeLines(term, 30, 'line')
const bufferService = term._core?._bufferService
expect(typeof bufferService?.isUserScrolling).toBe('boolean')
// Public scroll APIs manage isUserScrolling; structural restores inherit native live-output ownership.
expect(bufferService?.isUserScrolling).toBe(false)
term.scrollLines(-5)
expect(bufferService?.isUserScrolling).toBe(true)
term.scrollToBottom()
expect(bufferService?.isUserScrolling).toBe(false)
})
it('resets native user-scrolling when a pinned scrollback is cleared', async () => {
const term = new Terminal({
rows: 10,
@@ -277,17 +152,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term._core?._bufferService?.isUserScrolling).toBe(true)
})
it('triggers on a bare erase-scrollback with no screen clear alongside it', async () => {
const { term, settle, bottomOffset, scheduleCount } = await pinnedTerminalWithRestore()
// No \x1b[2J prefix: the scrollback erase alone is the trigger.
await write(term, `\x1b[3J${'after\r\n'.repeat(150)}`)
expect(scheduleCount()).toBeGreaterThan(0)
settle()
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
})
it('ignores erase-in-display forms that keep the scrollback', async () => {
const { term, settle, scheduleCount } = await pinnedTerminalWithRestore()
const pinnedY = term.buffer.active.viewportY
@@ -311,16 +175,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term.buffer.active.viewportY).toBe(pinnedY)
})
it('detects a clear split across writes', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
await write(term, '\x1b[2J\x1b[H\x1b[')
await write(term, `3J${'after\r\n'.repeat(50)}`)
settle()
expect(term.buffer.active.viewportY).toBe(term.buffer.active.baseY - bottomOffset)
})
it('holds the pin while the redraw keeps arriving and lands it once quiet', async () => {
const { term, advance, bottomOffset } = await pinnedTerminalWithRestore()
@@ -335,29 +189,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
})
it('lands the pin after a redraw that outgrows the erased scrollback over several writes', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
// Each batch alone would settle the pin at the wrong distance from the bottom.
await write(term, `\x1b[2J\x1b[H\x1b[3J${'after\r\n'.repeat(101)}`)
await write(term, 'after\r\n'.repeat(199))
settle()
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
})
it('survives a mid-redraw write that adds no rows', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
await write(term, `\x1b[2J\x1b[H\x1b[3J${'after\r\n'.repeat(50)}`)
// A cursor/attribute-only frame is normal mid-repaint and must not end the pin.
await write(term, '\x1b[K')
await write(term, 'after\r\n'.repeat(250))
settle()
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
})
it('spends the pin once even when the rebuild stays shorter than the old scrollback', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
@@ -372,19 +203,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term.buffer.active.viewportY).toBe(restoredY)
})
it('does not drag the reader through output that arrives after the pin lands', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
await write(term, `\x1b[2J\x1b[H\x1b[3J${'after\r\n'.repeat(120)}`)
settle()
const restoredY = term.buffer.active.viewportY
expect(restoredY).toBe(term.buffer.active.baseY - bottomOffset)
await writeLines(term, 200, 'later')
settle()
expect(term.buffer.active.viewportY).toBe(restoredY)
})
it('measures the pin from the rows already written earlier in the same chunk', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
const rowsBeforeErase = 20
@@ -526,15 +344,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term.buffer.active.viewportY).toBe(term.buffer.active.baseY - bottomOffset)
})
it('restores after the subparameter erase form', async () => {
const { term, settle, bottomOffset } = await pinnedTerminalWithRestore()
await write(term, `\x1b[2J\x1b[H\x1b[3:1J${'after\r\n'.repeat(120)}`)
settle()
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
})
it('does not arm for an erase on the alternate screen', async () => {
const { term, settle, scheduleCount } = await pinnedTerminalWithRestore()
const pinnedY = term.buffer.active.viewportY
@@ -572,27 +381,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
expect(term.buffer.active.viewportY).toBe(afterUserScrollY)
})
it('leaves a follow-output pane following', async () => {
const term = new Terminal({
rows: 10,
cols: 40,
scrollback: 1000,
allowProposedApi: true
}) as TerminalWithBufferService
await writeLines(term, 100, 'before')
markTerminalFollowOutput(term)
const scheduler = manualSettleScheduler()
installTerminalLiveScrollbackRestore(term, {
now: scheduler.now,
scheduleSettle: scheduler.scheduleSettle
})
await write(term, `\x1b[2J\x1b[H\x1b[3J${'after\r\n'.repeat(50)}`)
expect(scheduler.scheduleCount()).toBe(0)
scheduler.advance(SETTLE_MS)
expect(term.buffer.active.viewportY).toBe(term.buffer.active.baseY)
})
it('cancels an armed pin on dispose', async () => {
const term = await pinnedScrollbackTerminal()
const scheduler = manualSettleScheduler()
@@ -621,18 +409,6 @@ describe('live scrollback-erase pin (CSI 3 J)', () => {
// Without Orca’s pin, xterm’s erase releases reading position and follows the redraw.
expect(term.buffer.active.viewportY).toBe(term.buffer.active.baseY)
})
it('lands the pin on the real timer when no scheduler is injected', async () => {
const term = await pinnedScrollbackTerminal()
const restore = installTerminalLiveScrollbackRestore(term)
const bottomOffset = term.buffer.active.baseY - term.buffer.active.viewportY
await write(term, `\x1b[2J\x1b[H\x1b[3J${'after\r\n'.repeat(120)}`)
await new Promise((resolve) => setTimeout(resolve, 400))
expect(term.buffer.active.baseY - term.buffer.active.viewportY).toBe(bottomOffset)
restore.dispose()
})
})
describe('live scrollback-erase pin wiring', () => {
@@ -683,16 +459,6 @@ describe('live scrollback-erase pin wiring', () => {
}
}
it('registers both the plain and private erase idents', () => {
const stub = stubTerminal()
installTerminalLiveScrollbackRestore(stub.target)
expect(stub.registrations.map((registration) => registration.id)).toEqual([
{ final: 'J' },
{ prefix: '?', final: 'J' }
])
})
it('degrades a throwing capture to "not handled" instead of wedging the parser', () => {
const stub = stubTerminal({ throwOnBuffer: true })
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -706,20 +472,4 @@ describe('live scrollback-erase pin wiring', () => {
expect(consoleError).toHaveBeenCalled()
consoleError.mockRestore()
})
it('releases both registrations and the parsed subscription on dispose', () => {
const stub = stubTerminal()
const restore = installTerminalLiveScrollbackRestore(stub.target)
restore.dispose()
expect(stub.registrations.every((registration) => registration.disposed)).toBe(true)
expect(stub.parsedDisposed()).toBe(true)
})
it('does nothing when the terminal exposes no parser or parsed event', () => {
const restore = installTerminalLiveScrollbackRestore({
buffer: { active: { type: 'normal', viewportY: 10, baseY: 50 } }
} as Parameters<typeof installTerminalLiveScrollbackRestore>[0])
expect(() => restore.dispose()).not.toThrow()
})
})
@@ -75,23 +75,6 @@ function runtimeSnapshot(
}
describe('worktree agent activation gate', () => {
windowOwnedIt('uses immediately ready development restore inventory', async () => {
const ptyId = `${WORKTREE_ID}@@live-pty`
const { deps, createTab, resume } = testDeps({
sessions: [listed(ptyId)],
surfaceOwners: new Map([[ptyId, UNOWNED]])
})
const awaitReady = vi.fn(async () => true)
await expect(
runWorktreeAgentActivationGate(WORKTREE_ID, { ...deps, awaitReady })
).resolves.toBe('adopted')
expect(awaitReady).toHaveBeenCalledOnce()
expect(createTab).toHaveBeenCalledOnce()
expect(resume).not.toHaveBeenCalled()
})
windowOwnedIt(
'waits for packaged restore hydration before reading daemon inventory',
async () => {
@@ -115,18 +98,6 @@ describe('worktree agent activation gate', () => {
}
)
it('reads structured ownership before adopting daemon PTYs', async () => {
const ptyId = `${WORKTREE_ID}@@live-pty`
const { deps } = testDeps({ sessions: [listed(ptyId)] })
const hasStructuredSession = vi.fn(async () => false)
await runWorktreeAgentActivationGate(WORKTREE_ID, { ...deps, hasStructuredSession })
expect(hasStructuredSession.mock.invocationCallOrder[0]).toBeLessThan(
deps.listSessions.mock.invocationCallOrder[0] ?? Infinity
)
})
it('blocks seeding while the queried snapshot names a live launch', async () => {
const { deps, resume } = testDeps({})
const snapshot = {
@@ -387,29 +358,6 @@ describe('worktree agent activation gate', () => {
expect(resume).not.toHaveBeenCalled()
})
it('does not resume when the runtime reports a structured session before tab sync', async () => {
const { deps, createTab, resume } = testDeps({})
const hasStructuredSession = vi.fn(async () => true)
await expect(
runWorktreeAgentActivationGate(WORKTREE_ID, { ...deps, hasStructuredSession })
).resolves.toBe('structured')
expect(hasStructuredSession).toHaveBeenCalledWith(WORKTREE_ID)
expect(createTab).not.toHaveBeenCalled()
expect(resume).not.toHaveBeenCalled()
})
it('keeps the existing resume path when the workspace has no live agent', async () => {
const { deps, createTab, resume } = testDeps({})
await expect(runWorktreeAgentActivationGate(WORKTREE_ID, deps)).resolves.toBe('resumed')
expect(createTab).not.toHaveBeenCalled()
expect(resume).toHaveBeenCalledOnce()
expect(resume).toHaveBeenCalledWith(WORKTREE_ID, { skipClaimKeys: new Set() })
})
it('resumes a dead agent when the workspace only has a non-agent PTY', async () => {
const dead = sleepingRecord('tab-dead', DEAD_LEAF_ID, 'dead-session')
const plainPtyId = `${WORKTREE_ID}@@plain-shell`
@@ -825,21 +773,4 @@ describe('worktree agent activation gate', () => {
[SIBLING_LEAF_ID]: secondPtyId
})
})
// Layout-owner build: 'makes no surface for a live PTY main has not placed yet, and resumes no agent over it'.
windowOwnedIt('still adopts a live PTY the host reports as owned by no surface', async () => {
const livePtyId = `${WORKTREE_ID}@@orphan-agent`
const { deps, createTab } = testDeps({
sessions: [listed(livePtyId)],
surfaceOwners: new Map([[livePtyId, UNOWNED]])
})
await expect(runWorktreeAgentActivationGate(WORKTREE_ID, deps)).resolves.toBe('adopted')
expect(createTab).toHaveBeenCalledWith(WORKTREE_ID, undefined, undefined, {
initialPtyId: livePtyId,
activate: false,
recordInteraction: false
})
})
})
@@ -143,44 +143,6 @@ describe('workspace cleanup removal and protection', () => {
)
})
it('forwards the snapshot batch through each successful removal', async () => {
const candidate = makeCandidate({ executionHostId: 'ssh:ssh-1' })
installWorkspaceCleanupApi(
vi.fn(async () => ({
scannedAt: NOW,
candidates: [candidate],
errors: []
}))
)
const removeWorktree = vi.fn().mockResolvedValue({ ok: true })
const store = createCleanupTestStore(removeWorktree)
await store.getState().removeWorkspaceCleanupCandidates([candidate.worktreeId], {
snapshotPruneBatchId: 'batch-1'
})
expect(removeWorktree).toHaveBeenCalledWith(
{ id: candidate.worktreeId, executionHostId: null },
false,
{
assertBeforeDispatch: expect.any(Function),
suppressPreservedBranchToast: true,
snapshotPruneBatchId: 'batch-1'
}
)
})
it('demotes an active suggested workspace when it was not viewed from cleanup', async () => {
const [candidate] = await enrichWorkspaceCleanupCandidates(
[makeCandidate()],
makeState({ activeWorktreeId: WORKTREE_ID }),
{ applyDismissals: false }
)
expect(candidate.tier).toBe('protected')
expect(candidate.blockers).toContain('active-workspace')
})
it('keeps a viewed active workspace visible but not removable', async () => {
const [candidate] = await enrichWorkspaceCleanupCandidates(
[makeCandidate()],
@@ -201,25 +163,6 @@ describe('workspace cleanup removal and protection', () => {
expect(candidate.blockers).toContain('active-workspace')
})
it('does not preserve the cleanup view exception after the row changes', async () => {
const [candidate] = await enrichWorkspaceCleanupCandidates(
[makeCandidate({ fingerprint: 'fingerprint-2' })],
makeState({
activeWorktreeId: WORKTREE_ID,
workspaceCleanupViewedCandidates: {
[WORKTREE_ID]: {
viewedAt: Date.now(),
fingerprint: 'fingerprint-1'
}
}
}),
{ applyDismissals: false }
)
expect(candidate.tier).toBe('protected')
expect(candidate.blockers).toContain('active-workspace')
})
it('protects recently visible old workspaces with open context', async () => {
const [candidate] = await enrichWorkspaceCleanupCandidates(
[makeCandidate()],
@@ -246,111 +189,6 @@ describe('workspace cleanup removal and protection', () => {
expect(candidate.blockers).toContain('recent-visible-context')
})
it('uses current renderer state after async delete preflight scan resolves', async () => {
let resolveScan: (value: WorkspaceCleanupScanResult) => void
const removeWorktree = vi.fn().mockResolvedValue({ ok: true })
const store = createCleanupTestStore(removeWorktree)
;(globalThis as { window: unknown }).window = {
api: {
workspaceCleanup: {
scan: vi.fn(
(): Promise<WorkspaceCleanupScanResult> =>
new Promise<WorkspaceCleanupScanResult>((resolve) => {
resolveScan = resolve
})
),
dismiss: vi.fn().mockResolvedValue(undefined),
clearDismissals: vi.fn().mockResolvedValue(undefined)
}
}
}
const removal = store.getState().removeWorkspaceCleanupCandidates([WORKTREE_ID])
store.setState({ activeWorktreeId: WORKTREE_ID })
resolveScan!({ scannedAt: NOW, candidates: [makeCandidate()], errors: [] })
await expect(removal).resolves.toEqual({
removedIds: [WORKTREE_ID],
removedIdentities: [getWorkspaceCleanupHostIdentity('local', WORKTREE_ID)],
failures: []
})
expect(removeWorktree).toHaveBeenCalledWith({ id: WORKTREE_ID, executionHostId: null }, false, {
assertBeforeDispatch: expect.any(Function),
suppressPreservedBranchToast: true
})
})
it('defers git checks for locally active workspaces on initial scans', async () => {
const scan = vi.fn().mockResolvedValue({
scannedAt: NOW,
candidates: [],
errors: []
} satisfies WorkspaceCleanupScanResult)
;(globalThis as { window: unknown }).window = {
api: {
workspaceCleanup: {
scan,
dismiss: vi.fn().mockResolvedValue(undefined),
clearDismissals: vi.fn().mockResolvedValue(undefined)
}
}
}
const store = createCleanupTestStore()
store.setState({
activeWorktreeId: WORKTREE_ID,
tabsByWorktree: {
'repo1::/tmp/terminal-workspace': [
{ id: 'tab-1', title: 'zsh' }
] as AppState['tabsByWorktree'][string]
},
ptyIdsByTabId: { 'tab-1': ['pty-1'] }
} as Partial<AppState>)
await store.getState().scanWorkspaceCleanup()
expect(scan).toHaveBeenCalledWith(
{
includeAllWorkspaces: true,
scanId: expect.any(String),
skipGitWorktreeIds: expect.arrayContaining([WORKTREE_ID, 'repo1::/tmp/terminal-workspace'])
},
expect.any(Function)
)
})
it('does not defer git checks for focused remove preflights', async () => {
const scan = vi.fn().mockResolvedValue({
scannedAt: NOW,
candidates: [makeCandidate()],
errors: []
} satisfies WorkspaceCleanupScanResult)
;(globalThis as { window: unknown }).window = {
api: {
workspaceCleanup: {
scan,
dismiss: vi.fn().mockResolvedValue(undefined),
clearDismissals: vi.fn().mockResolvedValue(undefined)
}
}
}
const removeWorktree = vi.fn().mockResolvedValue({ ok: true })
const store = createCleanupTestStore(removeWorktree)
store.setState({
activeWorktreeId: 'repo1::/tmp/other-workspace'
} as Partial<AppState>)
await store.getState().removeWorkspaceCleanupCandidates([WORKTREE_ID])
expect(scan).toHaveBeenCalledWith({
worktreeIds: [WORKTREE_ID],
scanId: expect.any(String),
refreshActivity: true
})
})
it('lets explicitly selected not-suggested workspaces reach the removal path', async () => {
const scan = vi.fn().mockResolvedValue({
scannedAt: NOW,
@@ -517,32 +355,6 @@ describe('workspace cleanup removal and protection', () => {
expect(removeWorktree).not.toHaveBeenCalled()
})
it('protects old workspaces when an agent process is still foregrounded', async () => {
;(globalThis as { window: unknown }).window = {
api: {
pty: {
hasChildProcesses: vi.fn().mockResolvedValue(true),
getForegroundProcess: vi.fn().mockResolvedValue('codex')
}
}
}
const [candidate] = await enrichWorkspaceCleanupCandidates(
[makeCandidate()],
makeState({
tabsByWorktree: {
[WORKTREE_ID]: [{ id: 'tab-1', title: 'zsh' }] as AppState['tabsByWorktree'][string]
},
ptyIdsByTabId: { 'tab-1': ['pty-1'] }
}),
{ applyDismissals: false }
)
expect(candidate.tier).toBe('protected')
expect(candidate.selectedByDefault).toBe(false)
expect(candidate.blockers).toContain('running-terminal')
})
it('does not let an idle title in another tab mask a running agent process', async () => {
;(globalThis as { window: unknown }).window = {
api: {