diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 64a7c520ebb..8bde61b17e2 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -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 diff --git a/src/main/claude/claude-stream-json-connection.test.ts b/src/main/claude/claude-stream-json-connection.test.ts index 2112fde2eb5..ad18841ffbd 100644 --- a/src/main/claude/claude-stream-json-connection.test.ts +++ b/src/main/claude/claude-stream-json-connection.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[] = [] - 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) => ({ @@ -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 () => { diff --git a/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts b/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts index 995070b61ae..8f3711ba21c 100644 --- a/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts +++ b/src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts @@ -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()), - 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' }) diff --git a/src/main/codex/config-toml-trust-loadability.test.ts b/src/main/codex/config-toml-trust-loadability.test.ts index 375c34c3b7e..41b08b2c33b 100644 --- a/src/main/codex/config-toml-trust-loadability.test.ts +++ b/src/main/codex/config-toml-trust-loadability.test.ts @@ -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' diff --git a/src/main/git/worktree-add-creation-config.test.ts b/src/main/git/worktree-add-creation-config.test.ts deleted file mode 100644 index 379919fae6c..00000000000 --- a/src/main/git/worktree-add-creation-config.test.ts +++ /dev/null @@ -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' - ]) - }) -}) diff --git a/src/main/hermes/hermes-config-document.test.ts b/src/main/hermes/hermes-config-document.test.ts index 84892d4a3c6..39190c78337 100644 --- a/src/main/hermes/hermes-config-document.test.ts +++ b/src/main/hermes/hermes-config-document.test.ts @@ -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) => { diff --git a/src/main/ipc/ephemeral-vm.test.ts b/src/main/ipc/ephemeral-vm.test.ts index 3c566b83e25..9890167f8a6 100644 --- a/src/main/ipc/ephemeral-vm.test.ts +++ b/src/main/ipc/ephemeral-vm.test.ts @@ -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-') diff --git a/src/main/ipc/preflight-agent-detection.test.ts b/src/main/ipc/preflight-agent-detection.test.ts index ba23059e76c..aab4876a3e9 100644 --- a/src/main/ipc/preflight-agent-detection.test.ts +++ b/src/main/ipc/preflight-agent-detection.test.ts @@ -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, diff --git a/src/main/ipc/worktrees-existing-branch-checkout.test.ts b/src/main/ipc/worktrees-existing-branch-checkout.test.ts index 690283f48c1..82ffab891b0 100644 --- a/src/main/ipc/worktrees-existing-branch-checkout.test.ts +++ b/src/main/ipc/worktrees-existing-branch-checkout.test.ts @@ -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() - }) }) diff --git a/src/main/ipc/worktrees-removal-recovery.test.ts b/src/main/ipc/worktrees-removal-recovery.test.ts index 672dc0a4724..3c3a4f7e6b1 100644 --- a/src/main/ipc/worktrees-removal-recovery.test.ts +++ b/src/main/ipc/worktrees-removal-recovery.test.ts @@ -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' diff --git a/src/main/ipc/wsl-move-orchestrator-wiring.test.ts b/src/main/ipc/wsl-move-orchestrator-wiring.test.ts index 76c86e88162..5edeedc623c 100644 --- a/src/main/ipc/wsl-move-orchestrator-wiring.test.ts +++ b/src/main/ipc/wsl-move-orchestrator-wiring.test.ts @@ -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') diff --git a/src/main/jcode/hook-config.test.ts b/src/main/jcode/hook-config.test.ts index 308ec690da3..63cc9c057bf 100644 --- a/src/main/jcode/hook-config.test.ts +++ b/src/main/jcode/hook-config.test.ts @@ -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']], diff --git a/src/main/kimi/kimi-hook-config-toml.test.ts b/src/main/kimi/kimi-hook-config-toml.test.ts index a0525e61364..bb3c06fccf0 100644 --- a/src/main/kimi/kimi-hook-config-toml.test.ts +++ b/src/main/kimi/kimi-hook-config-toml.test.ts @@ -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` diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts index 0e973682c2d..5e10a547459 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts @@ -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 => { - 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() @@ -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') diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts index f601edb6f6f..e7c4d689424 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts @@ -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 => { - 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', () => { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-receiving-catalog.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-receiving-catalog.test.ts index e9f9c925730..4d5ba876043 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-receiving-catalog.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-receiving-catalog.test.ts @@ -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 | 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) => { - 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() diff --git a/src/main/orcad/orcad-instance-lock.test.ts b/src/main/orcad/orcad-instance-lock.test.ts index df987859520..5d67b7a11c6 100644 --- a/src/main/orcad/orcad-instance-lock.test.ts +++ b/src/main/orcad/orcad-instance-lock.test.ts @@ -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[] = [] diff --git a/src/main/pi/titlebar-extension-source.test.ts b/src/main/pi/titlebar-extension-source.test.ts index b8d869ace87..742c51994a4 100644 --- a/src/main/pi/titlebar-extension-source.test.ts +++ b/src/main/pi/titlebar-extension-source.test.ts @@ -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') diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts index e74795aba93..9de55056192 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts @@ -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 = {} const runtimeStore = { diff --git a/src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts b/src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts index 511b266373c..20df45dd4b5 100644 --- a/src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts +++ b/src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts @@ -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'])( diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts index a169f26d4e9..b1cf2d2a82d 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts @@ -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') diff --git a/src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts b/src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts index 2960f1bdaa4..e604c39d1bf 100644 --- a/src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts +++ b/src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts @@ -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) - }) }) diff --git a/src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts b/src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts index 533cf7c6a95..85a4f3e9ce2 100644 --- a/src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts +++ b/src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts @@ -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(() => {}) diff --git a/src/main/runtime/session-tabs/structured-session-catalog-workspace.test.ts b/src/main/runtime/session-tabs/structured-session-catalog-workspace.test.ts index 55d0bbfe1ea..558c83992fc 100644 --- a/src/main/runtime/session-tabs/structured-session-catalog-workspace.test.ts +++ b/src/main/runtime/session-tabs/structured-session-catalog-workspace.test.ts @@ -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() - } -}) diff --git a/src/main/runtime/worktree-teardown-daemon-inventory.test.ts b/src/main/runtime/worktree-teardown-daemon-inventory.test.ts index 97495047866..f16f9669dc5 100644 --- a/src/main/runtime/worktree-teardown-daemon-inventory.test.ts +++ b/src/main/runtime/worktree-teardown-daemon-inventory.test.ts @@ -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) diff --git a/src/main/updater.prerelease-fallback.test.ts b/src/main/updater.prerelease-fallback.test.ts index 654aad5b6f0..74746506bbd 100644 --- a/src/main/updater.prerelease-fallback.test.ts +++ b/src/main/updater.prerelease-fallback.test.ts @@ -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' - }) - }) }) diff --git a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx index 9d3c445f843..4006c729832 100644 --- a/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx +++ b/src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx @@ -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() diff --git a/src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx index 23cc84a0dee..e1fe4e0d59d 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx @@ -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() - 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() - 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) - }) - }) }) }) diff --git a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx index c5fecd13f6d..48cc3c91723 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx @@ -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. diff --git a/src/renderer/src/components/terminal-pane/pty-connection-git-bash-console-capacity.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-git-bash-console-capacity.test.ts index 74463218d08..be30d137ab1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-git-bash-console-capacity.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-git-bash-console-capacity.test.ts @@ -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 diff --git a/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts b/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts index 8198f66f667..45e52692231 100644 --- a/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts +++ b/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts @@ -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', diff --git a/src/renderer/src/lib/launch-agent-picker-contract.test.ts b/src/renderer/src/lib/launch-agent-picker-contract.test.ts index 703bf9efa84..2f24b9d0536 100644 --- a/src/renderer/src/lib/launch-agent-picker-contract.test.ts +++ b/src/renderer/src/lib/launch-agent-picker-contract.test.ts @@ -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 = - 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) => { diff --git a/src/renderer/src/lib/local-preflight-context.test.ts b/src/renderer/src/lib/local-preflight-context.test.ts index 497f8ff0668..9fbaf5f34b2 100644 --- a/src/renderer/src/lib/local-preflight-context.test.ts +++ b/src/renderer/src/lib/local-preflight-context.test.ts @@ -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' }), diff --git a/src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts b/src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts index 7418a0857a5..7e1603bd7f1 100644 --- a/src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts +++ b/src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts @@ -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 { } 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[0]) - expect(() => restore.dispose()).not.toThrow() - }) }) diff --git a/src/renderer/src/lib/worktree-agent-activation-gate.test.ts b/src/renderer/src/lib/worktree-agent-activation-gate.test.ts index edc154e391a..77581f00efc 100644 --- a/src/renderer/src/lib/worktree-agent-activation-gate.test.ts +++ b/src/renderer/src/lib/worktree-agent-activation-gate.test.ts @@ -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 - }) - }) }) diff --git a/src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts b/src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts index 917641d74a7..2b13c14c643 100644 --- a/src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts +++ b/src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts @@ -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 => - new Promise((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) - - 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) - - 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: {