diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 6620efc300c..a6c41389b9a 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -1407,9 +1407,8 @@ 1 test-timeout-needs-reason:src/shared/check-job-log-text-performance.test.ts 1 test-timeout-needs-reason:src/shared/dsh-real-cli.test.ts 1 test-timeout-needs-reason:src/shared/setup-agent-sequencing.windows.test.ts -1 test-timeout-needs-reason:src/shared/terminal-links.test.ts 2 test-timeout-needs-reason:src/shared/workspace-space-entry-traversal.test.ts -2 test-timeout-needs-reason:src/shared/wsl-login-shell-command.test.ts +1 test-timeout-needs-reason:src/shared/wsl-login-shell-command.test.ts 1 test-timeout-needs-reason:src/wsl-guest/agent-hook-server.test.ts 1 test-timeout-needs-reason:tests/e2e/active-view-restart-restore.spec.ts 1 test-timeout-needs-reason:tests/e2e/agent-send-picker-reveal.spec.ts diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 08aec15e153..18c76c7476a 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -13059,7 +13059,6 @@ "src/shared/windows-cmd-runner-delayed-launch.test.ts": 34, "src/shared/windows-command-line-budget.test.ts": 258, "src/shared/windows-console-input.test.ts": 54, - "src/shared/windows-environment-expansion.test.ts": 79, "src/shared/windows-interactive-login-spawn.test.ts": 38, "src/shared/windows-long-path-git-args.test.ts": 29, "src/shared/windows-path-access.test.ts": 35, diff --git a/src/shared/setup-agent-sequencing.test.ts b/src/shared/setup-agent-sequencing.test.ts index 04f974498d6..42b864fcc68 100644 --- a/src/shared/setup-agent-sequencing.test.ts +++ b/src/shared/setup-agent-sequencing.test.ts @@ -3,18 +3,13 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { join } from 'node:path' import { tmpdir } from 'node:os' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' -import { getDefaultRepoHookSettings } from './constants' import { createSequencedSetupAgentCommands, - createSetupAgentSequenceNonce, - SETUP_COMPLETE_MESSAGE, - resolveSetupAgentSequenceLaunchCommand, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from './setup-agent-sequencing' -import { shouldWaitForSetupBeforeAgentStartup } from './setup-agent-startup-policy' const TEMP_DIRS: string[] = [] const WINDOWS_PROCESS_TEST_TIMEOUT_MS = 30_000 @@ -26,127 +21,6 @@ afterEach(() => { }) describe('createSequencedSetupAgentCommands', () => { - it('defaults agent startup to immediate unless the wait policy is explicit', () => { - expect(getDefaultRepoHookSettings().setupAgentStartupPolicy).toBe('start-immediately') - expect(shouldWaitForSetupBeforeAgentStartup(undefined)).toBe(false) - expect(shouldWaitForSetupBeforeAgentStartup('start-immediately')).toBe(false) - expect(shouldWaitForSetupBeforeAgentStartup('wait-for-setup')).toBe(true) - }) - - it('uses the original sequenced startup command as the launch hint when present', () => { - expect( - resolveSetupAgentSequenceLaunchCommand( - { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: 'omp --resume' }, - 'powershell wait-wrapper' - ) - ).toBe('omp --resume') - expect( - resolveSetupAgentSequenceLaunchCommand( - { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: ' ' }, - 'powershell wait-wrapper' - ) - ).toBe('powershell wait-wrapper') - }) - - it('wraps POSIX setup and startup commands with a matching nonce marker', () => { - const result = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: "codex 'fix bug'", - platform: 'posix', - nonce: 'nonce-123', - waitTimeoutSeconds: 9 - }) - - expect(result.setupCommand).toMatch(/^bash -lc /) - expect(result.setupCommand).toContain('bash /repo/.git/orca/setup-runner.sh') - expect(result.setupCommand).toContain('printf') - expect(result.setupCommand).toContain('nonce-123 "$status"') - expect(result.setupCommand).toContain( - 'mv -f /repo/.git/orca/setup-runner.sh.nonce-123.done.tmp' - ) - // Pair-inserting line editors (zsh-autopair) corrupt typed brackets (#18059). - expect(result.setupCommand).not.toMatch(/[()[\]{}]/) - const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] - expect(result.startupCommand).toBe( - `bash -lc 'eval "$${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}"'` - ) - expect(startupScript).toContain('deadline=$((SECONDS + 9))') - expect(startupScript).not.toContain('date +%s') - expect(startupScript).toContain('Waiting for setup to finish before starting agent...') - expect(startupScript).toContain('[ "$seen" = nonce-123 ]') - expect(startupScript).toContain( - 'rm -f /repo/.git/orca/setup-runner.sh.nonce-123.done /repo/.git/orca/setup-runner.sh.nonce-123.done.tmp' - ) - expect(startupScript).toContain('exec codex') - expect(startupScript).toContain('fix bug') - expect(result.startupEnv).toEqual( - expect.objectContaining({ - [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: "codex 'fix bug'", - [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: startupScript - }) - ) - }) - - it('announces success so the pane stops showing the waiting line', () => { - const commands = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: 'codex', - platform: 'posix', - nonce: 'nonce-1' - }) - const script = commands.startupEnv?.ORCA_SEQUENCED_STARTUP_SCRIPT ?? '' - // Why ordering: `eval`/`exec` never returns, so a later message never renders. - expect(script.indexOf(SETUP_COMPLETE_MESSAGE)).toBeGreaterThan(-1) - expect(script.indexOf(SETUP_COMPLETE_MESSAGE)).toBeLessThan( - script.indexOf('eval "$ORCA_SEQUENCED_STARTUP_COMMAND"') - ) - }) - - it('announces success on the native Windows gate too', () => { - const commands = createSequencedSetupAgentCommands({ - runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.cmd', - platform: 'windows', - startupCommand: 'codex', - nonce: 'nonce-2' - }) - const decoded = Buffer.from( - commands.startupCommand.split('-EncodedCommand ')[1] ?? '', - 'base64' - ).toString('utf16le') - expect(decoded).toContain(SETUP_COMPLETE_MESSAGE) - expect(decoded.indexOf(SETUP_COMPLETE_MESSAGE)).toBeLessThan( - decoded.indexOf('Invoke-Expression $startup') - ) - }) - - it('doubles typographic single quotes in native Windows gate literals', () => { - const commands = createSequencedSetupAgentCommands({ - runnerScriptPath: 'C:\\O\u2019Brien\\.git\\orca\\setup-runner.cmd', - platform: 'windows', - startupCommand: 'codex', - nonce: 'nonce-3' - }) - const decoded = Buffer.from( - commands.setupCommand.split('-EncodedCommand ')[1] ?? '', - 'base64' - ).toString('utf16le') - expect(decoded).toContain("$runner = 'C:\\O\u2019\u2019Brien\\.git\\orca\\setup-runner.cmd'") - }) - - it('leaves the failure and timeout messages as the only other outcomes', () => { - const script = - createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: 'codex', - platform: 'posix', - nonce: 'nonce-3' - }).startupEnv?.ORCA_SEQUENCED_STARTUP_SCRIPT ?? '' - // Silence on success is what made a healthy worktree look stuck. - expect(script).toContain(SETUP_COMPLETE_MESSAGE) - expect(script).toContain('Setup failed; skipping agent startup.') - expect(script).toContain('Timed out waiting for setup before starting agent.') - }) - it('keeps the POSIX terminal submission below the canonical input floor', () => { const result = createSequencedSetupAgentCommands({ runnerScriptPath: `/repo/${'nested-worktree/'.repeat(100)}setup-runner.sh`, @@ -162,79 +36,6 @@ describe('createSequencedSetupAgentCommands', () => { ) }) - it('uses launch-specific marker paths for overlapping setup gates', () => { - const first = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: 'claude', - platform: 'posix', - nonce: 'first-launch' - }) - const second = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: 'codex', - platform: 'posix', - nonce: 'second-launch' - }) - - expect(first.setupCommand).toContain('/repo/.git/orca/setup-runner.sh.first-launch.done') - expect(first.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( - '/repo/.git/orca/setup-runner.sh.first-launch.done' - ) - expect(second.setupCommand).toContain('/repo/.git/orca/setup-runner.sh.second-launch.done') - expect(second.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( - '/repo/.git/orca/setup-runner.sh.second-launch.done' - ) - expect(first.setupCommand).not.toContain('/repo/.git/orca/setup-runner.sh.second-launch.done') - expect(second.setupCommand).not.toContain('/repo/.git/orca/setup-runner.sh.first-launch.done') - }) - - it('keeps simple POSIX startup commands eligible for exec when quoted text has separators', () => { - const result = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: "codex 'fix this; then test'", - platform: 'posix', - nonce: 'nonce-quoted', - waitTimeoutSeconds: 9 - }) - - const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] - expect(startupScript).toContain("exec codex 'fix this; then test'") - expect(startupScript).not.toContain('eval codex') - }) - - it('preserves POSIX inline environment assignment startup commands', () => { - const result = createSequencedSetupAgentCommands({ - runnerScriptPath: '/repo/.git/orca/setup-runner.sh', - startupCommand: 'FOO=bar claude', - platform: 'posix', - nonce: 'nonce-env', - waitTimeoutSeconds: 9 - }) - - const startupScript = result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV] - expect(startupScript).toContain('FOO=bar claude') - expect(startupScript).toContain('exit "$?"') - expect(startupScript).not.toContain('exec FOO=bar claude') - }) - - it('uses the converted Linux marker path for WSL UNC runners on Windows', () => { - const result = createSequencedSetupAgentCommands({ - runnerScriptPath: - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo\\.git\\worktrees\\feature\\orca\\setup-runner.sh', - startupCommand: 'claude', - platform: 'windows', - nonce: 'nonce-wsl' - }) - - expect(result.setupCommand).toContain( - 'bash /home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh' - ) - expect(result.setupCommand).toContain( - '/home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh.nonce-wsl.done' - ) - expect(result.setupCommand).not.toContain('wsl.localhost') - }) - it('keeps remote POSIX runners in bash even from a Windows client', () => { const result = createSequencedSetupAgentCommands({ runnerScriptPath: '/remote/repo/.git/worktrees/feature/orca/setup-runner.sh', @@ -269,60 +70,6 @@ describe('createSequencedSetupAgentCommands', () => { ) }) - it('wraps native Windows runners in a cmd-pinned setup and startup gate', () => { - const result = createSequencedSetupAgentCommands({ - runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.cmd', - startupCommand: "codex --model gpt-5 'fix !PATH! & test'", - platform: 'windows', - nonce: 'nonce-win', - waitTimeoutSeconds: 3 - }) - const setupPowerShell = decodePowerShellScript(result.setupCommand) - const startupPowerShell = decodePowerShellScript(result.startupCommand) - - expect(result.setupCommand).toContain( - 'powershell.exe -NoProfile -NonInteractive -EncodedCommand' - ) - expect(setupPowerShell).toContain("$runner = 'C:\\repo\\.git\\orca\\setup-runner.cmd'") - expect(setupPowerShell).toContain('$nonce + ":" + $setupStatus') - expect(result.startupCommand.match(/powershell\.exe/g)).toHaveLength(1) - expect(result.startupCommand).toContain( - 'powershell.exe -NoProfile -NonInteractive -EncodedCommand' - ) - expect(startupPowerShell).toContain('AddSeconds(3)') - expect(startupPowerShell).toContain('Missing setup marker path.') - expect(startupPowerShell).toContain('Timed out waiting for setup before starting agent.') - expect(startupPowerShell).toContain('Setup failed; skipping agent startup.') - expect(startupPowerShell).toContain( - 'Remove-Item -LiteralPath $marker, $tmp -Force -ErrorAction SilentlyContinue' - ) - expect(result.startupCommand).not.toContain('%ERRORLEVEL%') - expect(startupPowerShell).toContain('Invoke-Expression') - expect(result.startupCommand).not.toContain('fix !PATH! & test') - expect(result.startupEnv).toEqual({ - [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: "codex --model gpt-5 'fix !PATH! & test'" - }) - // EncodedCommand is not policy-gated; command-line ExecutionPolicy Bypass was a no-op with an EDR-heavy shape. - // Keep base64 because terminal panes reparse these strings. - expect(result.setupCommand).not.toMatch(/-ExecutionPolicy/i) - expect(result.startupCommand).not.toMatch(/-ExecutionPolicy/i) - // User startup may invoke policy-gated .ps1; move relief inside the payload instead of deleting it. - expect(startupPowerShell).toContain( - 'Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction Stop' - ) - // Stop routes autoload failures (e.g. duplicate PS5.1 type data) to a reporting catch. - // Report without rethrowing so broken policy cmdlets cannot abort startup. - expect(startupPowerShell).not.toContain('catch {}') - expect(startupPowerShell).toMatch(/catch \{ \[Console\]::Error\.WriteLine\(/) - expect(startupPowerShell).toContain('$_.FullyQualifiedErrorId') - expect(startupPowerShell).not.toMatch(/catch \{[^}]*throw/) - // Why: the autoloaded module's progress record would otherwise corrupt this gate's stderr. - expect(startupPowerShell).toContain("$ProgressPreference = 'SilentlyContinue'") - expect(startupPowerShell).toContain('$ProgressPreference = $orcaProgress') - // The setup gate only ever launches a .cmd/.bat runner, so it needs no relief. - expect(setupPowerShell).not.toMatch(/Set-ExecutionPolicy/i) - }) - it('launches a batch runner through the cmd launcher inside a Git Bash gate', () => { // Batch setup stays a cmd runner inside Git Bash; the gate remains bash to parse the already-POSIX-quoted startup command (#6896). const result = createSequencedSetupAgentCommands({ @@ -584,17 +331,6 @@ describe('createSequencedSetupAgentCommands', () => { ) }) -describe('createSetupAgentSequenceNonce', () => { - it('prefers crypto.randomUUID when available', () => { - const originalCrypto = globalThis.crypto - vi.stubGlobal('crypto', { randomUUID: () => 'uuid-1' }) - - expect(createSetupAgentSequenceNonce()).toBe('uuid-1') - - vi.stubGlobal('crypto', originalCrypto) - }) -}) - function makeTempDir(): string { const dir = mkdtempSync(join(tmpdir(), 'orca-setup-sequencing-')) TEMP_DIRS.push(dir) diff --git a/src/shared/setup-runner-command.test.ts b/src/shared/setup-runner-command.test.ts index 6619e8bcc7d..1bf5410c811 100644 --- a/src/shared/setup-runner-command.test.ts +++ b/src/shared/setup-runner-command.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - buildSetupRunnerCommand, - getSetupRunnerCommandPlatformForPath, - nativeWindowsPathToPosixShellPath, - resolveSetupRunnerCommand -} from './setup-runner-command' +import { buildSetupRunnerCommand } from './setup-runner-command' describe('buildSetupRunnerCommand', () => { it('uses bash for WSL UNC runner scripts regardless of host casing', () => { @@ -16,29 +11,12 @@ describe('buildSetupRunnerCommand', () => { ).toBe('bash /home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh') }) - it('uses bash with Linux paths for forward-slash WSL UNC runner scripts', () => { - expect( - buildSetupRunnerCommand( - '//wsl.localhost/Ubuntu/home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh', - 'windows' - ) - ).toBe('bash /home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh') - }) - it('keeps generic forward-slash UNC runner scripts on cmd.exe', () => { expect( buildSetupRunnerCommand('//server/share/repo/.git/orca/setup-runner.cmd', 'windows') ).toBe('cmd.exe /c "//server/share/repo/.git/orca/setup-runner.cmd"') }) - it('uses POSIX launch semantics for native Windows runners when the setup shell is POSIX', () => { - expect( - buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.sh', 'windows', { - family: 'posix' - }) - ).toBe('bash /c/repo/.git/orca/setup-runner.sh') - }) - it('uses the active WSL shell with WSL paths for native Windows POSIX runners', () => { expect( buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.sh', 'windows', { @@ -48,30 +26,12 @@ describe('buildSetupRunnerCommand', () => { ).toBe('bash /mnt/c/repo/.git/orca/setup-runner.sh') }) - it('keeps cmd.exe launch semantics for cmd setup runners', () => { - expect( - buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.cmd', 'windows', { - family: 'cmd' - }) - ).toBe('cmd.exe /c "C:\\repo\\.git\\orca\\setup-runner.cmd"') - }) - it('infers generated POSIX runner shell semantics from extension when metadata is absent', () => { expect(buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.sh', 'windows')).toBe( 'bash /c/repo/.git/orca/setup-runner.sh' ) }) - it('never hands a batch runner to bash, even from a Git Bash pane', () => { - // Runner syntax follows its file, not the terminal: POSIX panes still execute batch scripts through cmd. - const command = buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.cmd', 'windows', { - family: 'posix' - }) - - expect(command).not.toContain('bash ') - expect(command).not.toContain('/c/repo') - }) - it('avoids the bare /c switch when a POSIX pane launches a batch runner', () => { // MSYS rewrites bare cmd.exe /c into a drive path, leaving an interactive cmd instead of executing (#6896). const command = buildSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.cmd', 'windows', { @@ -83,18 +43,6 @@ describe('buildSetupRunnerCommand', () => { /^powershell\.exe -NoProfile -NonInteractive -EncodedCommand [A-Za-z0-9+/=]+$/ ) }) - - it('keeps the batch runner path in native form for a POSIX pane launch', () => { - // cmd cannot read /c/... MSYS paths; retain native runner spelling, also used for markers. - expect( - resolveSetupRunnerCommand('C:\\repo\\.git\\orca\\setup-runner.cmd', 'windows', { - family: 'posix' - }) - ).toMatchObject({ - runnerScriptPathForShell: 'C:\\repo\\.git\\orca\\setup-runner.cmd', - shell: 'windows' - }) - }) }) describe('buildSetupRunnerCommand cmd metacharacter guard', () => { @@ -110,30 +58,6 @@ describe('buildSetupRunnerCommand cmd metacharacter guard', () => { return decoded } - it.each(['%', '&', '|', '<', '>', '^', '(', ')', '!', ',', ';', '=', '$', '`'])( - 'hardens the launch when the runner path contains %s', - (character) => { - const command = buildSetupRunnerCommand(cmdRunner(`\\a${character}b`), 'windows', { - family: 'cmd' - }) - - expect(command).toMatch( - /^powershell\.exe -NoProfile -NonInteractive -EncodedCommand [A-Za-z0-9+/=]+$/ - ) - } - ) - - it.each([ - ['plain', 'C:\\repo\\.git\\orca\\setup-runner.cmd'], - ['spaces', 'C:\\Program Files\\repo\\.git\\orca\\setup-runner.cmd'], - ['single quote', "C:\\o'brien\\.git\\orca\\setup-runner.cmd"], - ['brackets and dash', 'C:\\repo-[2]\\.git\\orca\\setup-runner.cmd'] - ])('keeps the plain cmd launch for a %s path', (_label, runnerScriptPath) => { - expect(buildSetupRunnerCommand(runnerScriptPath, 'windows', { family: 'cmd' })).toBe( - `cmd.exe /c "${runnerScriptPath}"` - ) - }) - it('passes the runner path through the environment rather than the cmd argument string', () => { const runnerScriptPath = cmdRunner('\\100%%\\a&b') const script = decodePowerShellCommand( @@ -158,24 +82,6 @@ describe('buildSetupRunnerCommand cmd metacharacter guard', () => { expect(script).toContain("$runner = 'C:\\o''brien&co\\.git\\orca\\setup-runner.cmd'") }) - it('leaves runnerScriptPathForShell untouched so marker paths keep the native form', () => { - const runnerScriptPath = cmdRunner('\\a&b') - - expect(resolveSetupRunnerCommand(runnerScriptPath, 'windows', { family: 'cmd' })).toMatchObject( - { - runnerScriptPathForShell: runnerScriptPath, - shell: 'windows' - } - ) - }) - - it.each([ - ['native POSIX runner', 'C:\\repo\\a&b\\.git\\orca\\setup-runner.sh', undefined], - ['WSL UNC runner', '\\\\wsl.localhost\\Ubuntu\\home\\a&b\\orca\\setup-runner.sh', undefined] - ])('does not disturb the %s launch', (_label, runnerScriptPath) => { - expect(buildSetupRunnerCommand(runnerScriptPath, 'windows')).toMatch(/^bash /) - }) - it('does not disturb the wsl.exe POSIX launch', () => { expect( buildSetupRunnerCommand('C:\\repo\\a&b\\.git\\orca\\setup-runner.sh', 'windows', { @@ -185,52 +91,3 @@ describe('buildSetupRunnerCommand cmd metacharacter guard', () => { ).toBe("bash '/mnt/c/repo/a&b/.git/orca/setup-runner.sh'") }) }) - -describe('nativeWindowsPathToPosixShellPath', () => { - it('converts a drive path to the MSYS form Git Bash uses', () => { - expect(nativeWindowsPathToPosixShellPath('C:\\Users\\jin\\repo')).toBe('/c/Users/jin/repo') - }) - - it('is idempotent, so a double-applied conversion cannot corrupt a value', () => { - const once = nativeWindowsPathToPosixShellPath('D:\\repo\\worktrees\\feature') - expect(nativeWindowsPathToPosixShellPath(once)).toBe(once) - }) -}) - -describe('getSetupRunnerCommandPlatformForPath', () => { - it('prefers POSIX for absolute POSIX runner paths even from Windows clients', () => { - expect( - getSetupRunnerCommandPlatformForPath('/remote/repo/.git/orca/setup-runner.sh', 'windows') - ).toBe('posix') - }) - - it('prefers Windows for native Windows runner paths even from POSIX clients', () => { - expect( - getSetupRunnerCommandPlatformForPath('C:\\repo\\.git\\orca\\setup-runner.cmd', 'posix') - ).toBe('windows') - }) - - it('keeps WSL UNC paths on the Windows resolver so they can be converted', () => { - expect( - getSetupRunnerCommandPlatformForPath( - '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo\\.git\\orca\\setup-runner.sh', - 'posix' - ) - ).toBe('windows') - }) - - it('keeps forward-slash UNC paths on the Windows resolver', () => { - expect( - getSetupRunnerCommandPlatformForPath( - '//wsl.localhost/Ubuntu/home/jin/repo/.git/orca/setup-runner.sh', - 'posix' - ) - ).toBe('windows') - expect( - getSetupRunnerCommandPlatformForPath( - '//server/share/repo/.git/orca/setup-runner.cmd', - 'posix' - ) - ).toBe('windows') - }) -}) diff --git a/src/shared/setup-script-imports.test.ts b/src/shared/setup-script-imports.test.ts index 3215ab815df..0385e3d503e 100644 --- a/src/shared/setup-script-imports.test.ts +++ b/src/shared/setup-script-imports.test.ts @@ -121,29 +121,6 @@ describe('inspectSetupScriptImportCandidates', () => { ).resolves.toEqual([]) }) - it('imports setup and teardown commands from Superset config', async () => { - const candidates = await inspectSetupScriptImportCandidates( - makeReader({ - '.superset/config.json': JSON.stringify({ - setup: ['./.superset/setup.sh', 'bun install'], - teardown: ['./.superset/teardown.sh'], - run: ['bun dev'] - }) - }) - ) - - expect(candidates).toEqual([ - { - provider: 'superset', - label: 'Superset', - files: ['.superset/config.json'], - setup: './.superset/setup.sh\nbun install', - archive: './.superset/teardown.sh', - unsupportedFields: ['run'] - } - ]) - }) - it('applies Superset local before and after setup overlays', async () => { const candidates = await inspectSetupScriptImportCandidates( makeReader({ @@ -175,34 +152,6 @@ describe('inspectSetupScriptImportCandidates', () => { ]) }) - it('reports unsupported Superset local script object fields', async () => { - const candidates = await inspectSetupScriptImportCandidates( - makeReader({ - '.superset/config.json': JSON.stringify({ - setup: ['bun install'] - }), - '.superset/config.local.json': JSON.stringify({ - setup: { - before: ['corepack enable'], - after: ['bun run db:migrate'], - cwd: 'packages/web' - } - }) - }) - ) - - expect(candidates).toEqual([ - { - provider: 'superset', - label: 'Superset', - files: ['.superset/config.json', '.superset/config.local.json'], - setup: 'corepack enable\nbun install\nbun run db:migrate', - archive: undefined, - unsupportedFields: ['config.local.setup.cwd'] - } - ]) - }) - it('imports setup commands from cmux project config', async () => { const candidates = await inspectSetupScriptImportCandidates( makeReader({ @@ -237,58 +186,6 @@ describe('inspectSetupScriptImportCandidates', () => { ]) }) - it('imports setup commands from root cmux config when project config is absent', async () => { - const candidates = await inspectSetupScriptImportCandidates( - makeReader({ - 'cmux.json': JSON.stringify({ - commands: [ - { - title: 'Workspace Setup', - keywords: ['setup'], - command: 'pnpm install' - } - ] - }) - }) - ) - - expect(candidates).toEqual([ - { - provider: 'cmux', - label: 'cmux', - files: ['cmux.json'], - setup: 'pnpm install', - unsupportedFields: [] - } - ]) - }) - - it('imports setup and archive commands from Conductor config', async () => { - const candidates = await inspectSetupScriptImportCandidates( - makeReader({ - 'conductor.json': JSON.stringify({ - scripts: { - setup: 'pnpm install', - archive: 'pnpm clean', - run: 'pnpm dev' - }, - runScriptMode: 'manual' - }) - }) - ) - - expect(candidates).toEqual([ - { - provider: 'conductor', - label: 'Conductor', - files: ['conductor.json'], - setup: 'pnpm install', - archive: 'pnpm clean', - unsupportedFields: ['runScriptMode', 'scripts.run'] - } - ]) - }) - it('imports setup and cleanup scripts from Codex environment config', async () => { const candidates = await inspectSetupScriptImportCandidates( makeReader({ diff --git a/src/shared/shell-process-readiness.test.ts b/src/shared/shell-process-readiness.test.ts index 22ed0416c9e..7de11d6fb57 100644 --- a/src/shared/shell-process-readiness.test.ts +++ b/src/shared/shell-process-readiness.test.ts @@ -5,19 +5,12 @@ import { basename, dirname, join } from 'node:path' import { spawnProcess } from '@orca/process-host' import { describe, expect, it } from 'vitest' import { - parseDarwinExecutablePath, readShellProcessReadiness, resolveInstalledShellExecutablePaths, resolveShellExecutablePath } from './shell-process-readiness' describe('shell process readiness', () => { - it('extracts the primary text image from macOS lsof output', () => { - expect(parseDarwinExecutablePath('p42\nftxt\nn/bin/zsh\nftxt\nn/usr/lib/zsh/zle.so\n')).toBe( - '/bin/zsh' - ) - }) - it.runIf(process.platform === 'darwin')( 'reads a child process executable path from real lsof on macOS', async () => { @@ -125,24 +118,4 @@ describe('shell process readiness', () => { } } ) - - it.skipIf(process.platform === 'win32')( - 'omits a same-name executable that no PATH entry reaches', - async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-offpath-shell-')) - const onPath = join(root, 'bin') - const offPath = join(root, 'dropped') - await mkdir(onPath) - await mkdir(offPath) - await symlink(process.execPath, join(onPath, 'shell-name')) - await symlink(process.execPath, join(offPath, 'shell-name')) - try { - await expect( - resolveInstalledShellExecutablePaths('shell-name', root, onPath) - ).resolves.not.toContain(join(offPath, 'shell-name')) - } finally { - await rm(root, { recursive: true, force: true }) - } - } - ) }) diff --git a/src/shared/startup-command-staging.test.ts b/src/shared/startup-command-staging.test.ts index 8b8cce30903..7e51b91fd71 100644 --- a/src/shared/startup-command-staging.test.ts +++ b/src/shared/startup-command-staging.test.ts @@ -14,7 +14,6 @@ import { STAGED_STARTUP_COMMAND_STALE_MS, discardStagedStartupCommand, stageStartupCommand, - startupStagingFailureNotice, sweepStaleStagedStartupCommands } from './startup-command-staging' @@ -33,11 +32,6 @@ function stage(command: string, shellPath = '/bin/zsh', platform: NodeJS.Platfor } describe('stageStartupCommand', () => { - it('types a short single-line command as is', () => { - expect(stage(`claude 'fix it'`)).toEqual({ command: `claude 'fix it'`, delivery: 'typed' }) - expect(readdirSync(directory)).toEqual([]) - }) - it('types exactly 512 bytes, stages 513', () => { expect(stage('a'.repeat(512)).delivery).toBe('typed') expect(stage('a'.repeat(513)).delivery).toBe('staged') @@ -70,15 +64,6 @@ describe('stageStartupCommand', () => { expect(readFileSync(scriptPath, 'utf8')).toBe(`command rm -f -- '${scriptPath}'\n${command}\n`) }) - it('evals the script in fish, which runs a sourced file without job control', () => { - const staged = stage('a'.repeat(600), '/opt/homebrew/bin/fish') - expect(staged.command).toBe(`eval (string collect < '${staged.scriptPath}')`) - }) - - it('recognizes a login shell name', () => { - expect(stage('a'.repeat(600), '-zsh').delivery).toBe('staged') - }) - // Why: ksh runs a sourced file's commands in the shell's own process group, so Ctrl-Z is lost. it.each([['/bin/ksh'], ['/usr/bin/mksh']])( 'runs a long Orca-built line through /bin/sh in %s, and types a short one as is', @@ -113,22 +98,6 @@ describe('stageStartupCommand', () => { } ) - // Why: typed raw, tcsh runs a prompt's second line as a command and MAX_CANON cuts a long one. - it.each([['/usr/bin/nu'], ['/bin/tcsh'], ['/usr/local/bin/pwsh'], [undefined]])( - 'runs an Orca-built launch line through /bin/sh for a shell that cannot source it (%s)', - (shellPath) => { - const staged = stageStartupCommand({ - command: `claude 'one\ntwo'`, - shellPath, - orcaBuiltLine: true, - platform: 'darwin', - directory - }) - expect(staged.delivery).toBe('staged') - expect(staged.command).toBe(`/bin/sh '${staged.scriptPath}'`) - } - ) - // Measured: tcsh typed `'fix C:'"\\"'dir'` as `fix C:\\dir` and expanded `!!` in `'wow!! great'`. it.each([['/bin/tcsh'], ['/usr/bin/nu'], [undefined]])( 'stages even a short Orca-built line for a shell that cannot source it (%s)', @@ -178,13 +147,6 @@ describe('stageStartupCommand', () => { } ) - it('types a short Orca-built line in a shell that reads its quoting literally', () => { - const command = `claude 'wow!! great'` - expect( - stageStartupCommand({ command, shellPath: '/bin/zsh', orcaBuiltLine: true, directory }) - ).toEqual({ command, delivery: 'typed' }) - }) - // Why: a quick command or --command written for tcsh or nu is not sh syntax. it.each([['/bin/tcsh'], ['/usr/bin/nu'], [undefined]])( 'types a command the user wrote as it always was in a shell that cannot source it (%s)', @@ -196,13 +158,6 @@ describe('stageStartupCommand', () => { } ) - it('leaves Windows hosts alone', () => { - expect(stage('a'.repeat(600), 'C:\\Program Files\\Git\\bin\\bash.exe', 'win32').delivery).toBe( - 'typed' - ) - expect(stage('a'.repeat(600), '/bin/bash', 'win32').delivery).toBe('typed') - }) - it('types the full command and says so when the script cannot be written', () => { const command = 'a'.repeat(600) const staged = stageStartupCommand({ @@ -216,12 +171,6 @@ describe('stageStartupCommand', () => { expect(staged.failure).toMatch(/ENOENT/) expect(staged.scriptPath).toBeUndefined() }) - - it('gives every launch its own script', () => { - const first = stage('a'.repeat(600)) - const second = stage('a'.repeat(600)) - expect(first.scriptPath).not.toBe(second.scriptPath) - }) }) describe('discardStagedStartupCommand', () => { @@ -230,12 +179,6 @@ describe('discardStagedStartupCommand', () => { discardStagedStartupCommand(staged) expect(readdirSync(directory)).toEqual([]) }) - - it('is a no-op once the script deleted itself', () => { - const staged = stage('a'.repeat(600)) - rmSync(staged.scriptPath!) - expect(() => discardStagedStartupCommand(staged)).not.toThrow() - }) }) describe('sweepStaleStagedStartupCommands', () => { @@ -254,21 +197,3 @@ describe('sweepStaleStagedStartupCommands', () => { expect(readdirSync(directory).sort()).toEqual(['orca-launch-bbbb.sh', 'something-else.sh']) }) }) - -describe('startupStagingFailureNotice', () => { - it('says nothing when staging did not fail', () => { - expect(startupStagingFailureNotice(stage(`claude 'fix it'`))).toBeNull() - expect(startupStagingFailureNotice(stage(`claude '${'x'.repeat(600)}'`))).toBeNull() - }) - - it('prints one line naming the failure, with no control bytes from the reason', () => { - const notice = startupStagingFailureNotice({ - command: 'x', - delivery: 'typed-after-stage-failed', - failure: 'EACCES: permission denied\nsecond line' - }) - expect(notice).toBe( - '\r\n[orca] Could not stage the launch command (EACCES: permission denied second line); typed it in full. If the shell shows quote> or waits for more input, press Ctrl-C and launch again.\r\n' - ) - }) -}) diff --git a/src/shared/startup-line-prompt-carry.test.ts b/src/shared/startup-line-prompt-carry.test.ts index ac3b3827c7e..7426267a9b3 100644 --- a/src/shared/startup-line-prompt-carry.test.ts +++ b/src/shared/startup-line-prompt-carry.test.ts @@ -6,8 +6,6 @@ import { ZSH_MULTI_LINE_STARTUP_LINE_BUDGET_BYTES } from './startup-line-prompt-carry' import type { TuiAgent } from './tui-agent' -import { RUNTIME_CAPABILITIES } from './protocol-version' -import { AGENT_LAUNCH_PROMPT_CARRY_RUNTIME_CAPABILITY } from './agent-launch-runtime-capability' function offer( agent: TuiAgent, @@ -46,19 +44,6 @@ function promptForClaudeLineOf(bytes: number): string { } describe('whether a launch prompt rides the typed startup line', () => { - it('carries a short single-line prompt on the launch command', () => { - const { plan, promptCarried } = offer('claude', 'explain this repo') - expect(promptCarried).toBe(true) - expect(plan?.launchCommand).toContain('explain this repo') - }) - - it('carries a short Windows multi-line prompt as one encoded physical launch line', () => { - const { plan, promptCarried } = offer('claude', linesOf(5, 40), { platform: 'win32' }) - expect(promptCarried).toBe(true) - expect(plan?.launchCommand).not.toContain('\n') - expect(plan?.launchCommand).toContain('`n') - }) - it('carries a line of exactly the budget and refuses one byte past it', () => { const atBudget = offer('claude', promptForClaudeLineOf(TYPED_STARTUP_LINE_PROMPT_BUDGET_BYTES)) expect(new TextEncoder().encode(atBudget.plan?.launchCommand ?? '').byteLength).toBe( @@ -130,16 +115,6 @@ describe('whether a launch prompt rides the typed startup line', () => { expect(plan).not.toBeNull() expect(Object.values(plan?.env ?? {}).join('')).not.toContain('xxxx') }) - - it('never carries a stdin-after-start agent’s prompt, whose CLI takes none', () => { - const { plan, promptCarried } = offer('aider', 'fix it') - expect(promptCarried).toBe(false) - expect(plan?.followupPrompt).toBeNull() - }) - - it('reports nothing carried for an empty prompt', () => { - expect(offer('claude', ' ').promptCarried).toBe(false) - }) }) // Pinned live by startup-line-typed-length.live-shell.test.ts; the refusals below were measured on @@ -163,11 +138,6 @@ describe('a multi-line prompt typed into a shell the host names', () => { expect(offer('claude', prompt, { shellName: 'zsh' }).promptCarried).toBe(false) }) - it('keeps a long single line off a zsh launch command, which a late write truncates', () => { - const prompt = promptForClaudeLineOf(TYPED_STARTUP_LINE_PROMPT_BUDGET_BYTES + 1) - expect(offer('claude', prompt, { shellName: 'zsh' }).promptCarried).toBe(false) - }) - it.each([['bash'], ['fish'], ['pwsh'], [undefined]])( 'never types a multi-line line into %s', (shellName) => { @@ -212,10 +182,3 @@ describe('a short multi-line prompt on a Windows PowerShell host', () => { expect(plan?.launchCommand).toContain(prompt.replaceAll('\n', '`n')) }) }) - -describe('the capability clients gate a prompted launch on', () => { - it('is advertised by every host that applies the typed-line rule', () => { - // An older host folds any prompt into the typed line, so its absence is the gate. - expect(RUNTIME_CAPABILITIES).toContain(AGENT_LAUNCH_PROMPT_CARRY_RUNTIME_CAPABILITY) - }) -}) diff --git a/src/shared/terminal-github-pr-link-detector.test.ts b/src/shared/terminal-github-pr-link-detector.test.ts index de07fdf0647..5fadb7d4c4c 100644 --- a/src/shared/terminal-github-pr-link-detector.test.ts +++ b/src/shared/terminal-github-pr-link-detector.test.ts @@ -1,25 +1,9 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { createTerminalGitHubPRLinkDetector } from './terminal-github-pr-link-detector' const issue8126Url = 'https://github.com/owner/repo/pull/10' -afterEach(() => { - vi.restoreAllMocks() -}) - describe('createTerminalGitHubPRLinkDetector', () => { - it('extracts GitHub pull request URLs from terminal output', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('Created https://github.com/acme/orca/pull/42\r\n')).toEqual([ - { - url: 'https://github.com/acme/orca/pull/42', - slug: { owner: 'acme', repo: 'orca', host: 'github.com' }, - number: 42 - } - ]) - }) - it('skips nested schemes and continues through mixed HTTP and HTTPS candidates', () => { const observe = createTerminalGitHubPRLinkDetector() const urls = [ @@ -36,18 +20,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { expect(observe(output).map((link) => link.url)).toEqual(urls) }) - it('detects issue 8126 Claude Code PR links with attached ANSI reset', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe(`${issue8126Url}\x1b[22m\n`)).toEqual([ - { - url: issue8126Url, - slug: { owner: 'owner', repo: 'repo', host: 'github.com' }, - number: 10 - } - ]) - }) - it('strips an ANSI reset split across PTY chunks', () => { const observe = createTerminalGitHubPRLinkDetector() @@ -61,20 +33,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { ]) }) - it('rejects PR URLs corrupted by cursor movement', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('https://github.com/owne\x1b[1Cr/repo/pull/10\n')).toEqual([]) - }) - - it('rejects PR URLs fused across terminal rows', () => { - for (const cursorMove of ['\x1b[1A', '\x1b[1B']) { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe(`https://github.com/owner/repo/pull/${cursorMove}10\n`)).toEqual([]) - } - }) - it('does not fuse screen-editing controls into PR URLs', () => { for (const screenEdit of ['\x08', '\x0b', '\x0c', '\x1bD', '\x1b[2J', '\x1b[2K', '\x1b[1S']) { const observe = createTerminalGitHubPRLinkDetector() @@ -121,51 +79,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { ]) }) - it('trims terminal punctuation around printed URLs', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('Opened (https://github.com/acme/orca/pull/42).\n')[0]?.url).toBe( - 'https://github.com/acme/orca/pull/42' - ) - }) - - it('does not repeat the same PR URL from overlapping carry text', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('https://github.com/acme/orca/pull/42\n')).toHaveLength(1) - expect(observe('more output\n')).toEqual([]) - }) - - it('ignores non-PR GitHub-shaped links', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('https://github.com/acme/orca/issues/42\n')).toEqual([]) - }) - - it('extracts GitHub Enterprise pull request URLs from terminal output', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('Created https://github.my-company.net/MyOrg/my_repo/pull/395\r\n')).toEqual([ - { - url: 'https://github.my-company.net/MyOrg/my_repo/pull/395', - slug: { owner: 'MyOrg', repo: 'my_repo', host: 'github.my-company.net' }, - number: 395 - } - ]) - }) - - it('extracts HTTP GitHub Enterprise pull request URLs from terminal output', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe('Created http://github.internal/MyOrg/my_repo/pull/395\r\n')).toEqual([ - { - url: 'http://github.internal/MyOrg/my_repo/pull/395', - slug: { owner: 'MyOrg', repo: 'my_repo', host: 'github.internal' }, - number: 395 - } - ]) - }) - it('extracts GitHub Enterprise pull request URLs with a custom port', () => { const observe = createTerminalGitHubPRLinkDetector() @@ -179,28 +92,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { ]) }) - it('scans huge terminal chunks containing pull markers without global regex iteration', () => { - const matchAll = vi.spyOn(String.prototype, 'matchAll') - const observe = createTerminalGitHubPRLinkDetector() - const noise = `${'/pull/not-a-url '.repeat(20_000)}\n` - - expect(observe(`${noise}Created https://github.com/acme/orca/pull/42\r\n`)).toEqual([ - { - url: 'https://github.com/acme/orca/pull/42', - slug: { owner: 'acme', repo: 'orca', host: 'github.com' }, - number: 42 - } - ]) - expect(matchAll).not.toHaveBeenCalled() - }) - - it('drops overlong incomplete URL carry instead of retaining pasted megabytes', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe(`https://github.com/acme/orca/pull/${'4'.repeat(10_000)}`)).toEqual([]) - expect(observe('2\r\n')).toEqual([]) - }) - // Pin both sides of the 512-character carry cap and the overlong-URL drop path. it('still joins a PR URL split across chunks after a large scheme-free chunk', () => { const observe = createTerminalGitHubPRLinkDetector() @@ -217,15 +108,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { ]) }) - it('drops carry when the scheme sits further back than the carry cap', () => { - const observe = createTerminalGitHubPRLinkDetector() - - // The URL opened >512 bytes before the chunk end, so it already overran the - // cap and must not resurrect on the next chunk. - expect(observe(`https://github.com/acme/orca/pull/${'4'.repeat(600)}`)).toEqual([]) - expect(observe('2\r\n')).toEqual([]) - }) - it('keeps carry when the scheme-to-end tail is exactly at the cap', () => { const observe = createTerminalGitHubPRLinkDetector() @@ -261,15 +143,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { expect(observe('://github.com/acme/orca/pull/12\n')).toEqual([]) }) - it('does not fabricate a link by splicing a stale fragment onto later output', () => { - const observe = createTerminalGitHubPRLinkDetector() - - // Keeping the 'h' would splice it onto the next chunk and emit a PR link for - // a repo that never appeared in the stream. - expect(observe(`https://github.com/acme/orca/pull/1${'x'.repeat(600)}h`)).toEqual([]) - expect(observe('ttps://github.com/zz/yy/pull/9\n')).toEqual([]) - }) - it('joins a plain http:// PR URL split across chunks', () => { const observe = createTerminalGitHubPRLinkDetector() @@ -283,13 +156,6 @@ describe('createTerminalGitHubPRLinkDetector', () => { ]) }) - it('drops a trailing fragment when a plain http:// scheme sits behind the window', () => { - const observe = createTerminalGitHubPRLinkDetector() - - expect(observe(`http://github.internal/o/r/pull/1${'x'.repeat(600)}https`)).toEqual([]) - expect(observe('://github.com/a/b/pull/12\n')).toEqual([]) - }) - it('keeps a trailing scheme fragment when no scheme sits behind the window', () => { const observe = createTerminalGitHubPRLinkDetector() diff --git a/src/shared/terminal-links.test.ts b/src/shared/terminal-links.test.ts index 65550532bdc..d5b07a6404f 100644 --- a/src/shared/terminal-links.test.ts +++ b/src/shared/terminal-links.test.ts @@ -1,11 +1,9 @@ -import { performance } from 'node:perf_hooks' import { describe, expect, it } from 'vitest' import { TERMINAL_FILE_LINK_TAP_CONFORMANCE_CASES, columnForTerminalFileLinkTap } from './terminal-file-link-conformance' import { - extractTerminalFileLinkCandidates, extractTerminalFileLinks, findExactTerminalFileLink, isPathInsideWorktree, @@ -34,11 +32,6 @@ describe('terminal path helpers', () => { }) }) - it('keeps worktree-relative paths on Windows external files', () => { - expect(isPathInsideWorktree('C:\\repo\\src\\file.ts', 'C:\\repo')).toBe(true) - expect(toWorktreeRelativePath('C:\\repo\\src\\file.ts', 'C:\\repo')).toBe('src/file.ts') - }) - it('keeps worktree-relative paths for forward-slash Windows UNC paths', () => { expect(isPathInsideWorktree('//server/share/repo/src/file.ts', '\\\\server\\share\\repo')).toBe( true @@ -64,54 +57,10 @@ describe('terminal path helpers', () => { ).toEqual([]) }) - it('emits tentative link candidates for each token in ls-style output', () => { - const line = 'CLAUDE.md package.json pnpm-lock.yaml README.md' - const links = extractTerminalFileLinks(line) - const texts = links.map((link) => link.displayText) - expect(texts).toEqual(['CLAUDE.md', 'package.json', 'pnpm-lock.yaml', 'README.md']) - const claudeMd = links[0] - expect(line.slice(claudeMd.startIndex, claudeMd.endIndex)).toBe('CLAUDE.md') - }) - - it('recognises common extensionless project files (Makefile, LICENSE, …)', () => { - const links = extractTerminalFileLinks('Makefile LICENSE README Dockerfile src tests') - expect(links.map((link) => link.displayText).sort()).toEqual([ - 'Dockerfile', - 'LICENSE', - 'Makefile', - 'README' - ]) - }) - - it('ignores pure numbers, flag-looking tokens, and dotfile-only strings', () => { - expect(extractTerminalFileLinks('42 100 .. . -v --verbose src dist')).toEqual([]) - }) - - it('still strips trailing punctuation from bare filenames', () => { - const links = extractTerminalFileLinks('See package.json, pnpm-lock.yaml.') - expect(links.map((link) => link.displayText)).toEqual(['package.json', 'pnpm-lock.yaml']) - }) - it('does not double-link bare tokens that are part of an already-matched path', () => { const links = extractTerminalFileLinks('./src/file.ts is the entry point') expect(links.map((link) => link.displayText)).toEqual(['./src/file.ts']) }) - - it('carries line:column suffix on bare filenames (e.g. stack-trace output)', () => { - const links = extractTerminalFileLinks('foo.ts:12:3 failed') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ pathText: 'foo.ts', line: 12, column: 3 }) - }) - - it('keeps huge no-separator terminal tokens off the hover hot path', () => { - const line = `${'b'.repeat(80 * 500)} package.json` - const startedAt = performance.now() - - const links = extractTerminalFileLinks(line) - - expect(performance.now() - startedAt).toBeLessThan(100) - expect(links.map((link) => link.displayText)).toEqual(['package.json']) - }) }) describe('extractTerminalFileLinks local path tokens', () => { @@ -124,35 +73,6 @@ describe('terminal path helpers', () => { ).toEqual(['docs/café/report.pdf']) }) - it('detects tilde-prefixed POSIX paths', () => { - const links = extractTerminalFileLinks('~/Documents/Path/file_name') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: '~/Documents/Path/file_name', - displayText: '~/Documents/Path/file_name' - }) - }) - - it('detects absolute paths with spaces before the filename', () => { - const links = extractTerminalFileLinks('/Users/Path/FolderName with Space/content.js') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: '/Users/Path/FolderName with Space/content.js', - displayText: '/Users/Path/FolderName with Space/content.js' - }) - }) - - it('stops a spaced absolute path before trailing prose', () => { - const links = extractTerminalFileLinks( - 'Open /Users/Path/FolderName with Space/content.js for details' - ) - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: '/Users/Path/FolderName with Space/content.js', - displayText: '/Users/Path/FolderName with Space/content.js' - }) - }) - it('detects an extensionless absolute path ending in a spaced segment', () => { const links = extractTerminalFileLinks('/Users/alice/My Folder') expect(links).toHaveLength(1) @@ -162,29 +82,11 @@ describe('terminal path helpers', () => { }) }) - it('trims terminal padding after line-ending spaced paths', () => { - const links = extractTerminalFileLinks('/Users/alice/My Folder ') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: '/Users/alice/My Folder', - displayText: '/Users/alice/My Folder' - }) - }) - it('does not treat mid-line command arguments as line-ending spaced paths', () => { const links = extractTerminalFileLinks('run /usr/bin/env node, then continue') expect(links.map((link) => link.pathText)).not.toContain('/usr/bin/env node') }) - it('keeps trailing separators on directory-like absolute paths', () => { - const links = extractTerminalFileLinks('/Users/alice/worktree/') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: '/Users/alice/worktree/', - displayText: '/Users/alice/worktree/' - }) - }) - it('does not linkify root-only or relative trailing separator tokens', () => { expect(extractTerminalFileLinks('progress 1 / 3')).toEqual([]) expect(extractTerminalFileLinks('/')).toEqual([]) @@ -194,15 +96,6 @@ describe('terminal path helpers', () => { expect(extractTerminalFileLinks('C:\\')).toEqual([]) }) - it('detects an extensionless relative path ending in a spaced segment', () => { - const links = extractTerminalFileLinks('./My Folder') - expect(links).toHaveLength(1) - expect(links[0]).toMatchObject({ - pathText: './My Folder', - displayText: './My Folder' - }) - }) - it('detects framework route paths with bracket and paren segments', () => { const links = extractTerminalFileLinks( 'Error in app/(shop)/products/[productId]/page.tsx:42:7' @@ -215,84 +108,6 @@ describe('terminal path helpers', () => { displayText: 'app/(shop)/products/[productId]/page.tsx:42:7' }) }) - - it('handles large spaced path lists without quadratic overlap scans', () => { - const line = Array.from({ length: 20_000 }, () => '/tmp/Foo Bar/file').join(', ') - - const links = extractTerminalFileLinks(line) - - expect(links).toHaveLength(20_000) - expect(links[0].pathText).toBe('/tmp/Foo Bar/file') - }, 5_000) - - it('keeps extension-heavy assistant prose on a bounded scan path', () => { - const filenames = Array.from({ length: 20_000 }, (_, index) => `report-${index}.txt`) - const links = extractTerminalFileLinks(`/tmp/root/${filenames.join(' ')}`) - - expect(links).toHaveLength(2) - expect(links[0].pathText).toBe(`/tmp/root/${filenames.slice(0, -1).join(' ')}`) - expect(links.at(-1)?.pathText).toBe('report-19999.txt') - }) - }) - - it('supports Windows cwd resolution for terminal file links', () => { - expect( - resolveTerminalFileLink( - { - pathText: '.\\src\\file.ts', - line: 12, - column: 3, - startIndex: 0, - endIndex: 13, - displayText: '.\\src\\file.ts:12:3' - }, - 'C:\\repo' - ) - ).toEqual({ - absolutePath: 'C:/repo/src/file.ts', - line: 12, - column: 3 - }) - }) - - it('resolves tilde-prefixed POSIX paths against the cwd user home', () => { - expect( - resolveTerminalFileLink( - { - pathText: '~/Documents/Path/file_name', - line: null, - column: null, - startIndex: 0, - endIndex: 26, - displayText: '~/Documents/Path/file_name' - }, - '/Users/alice/project' - ) - ).toEqual({ - absolutePath: '/Users/alice/Documents/Path/file_name', - line: null, - column: null - }) - }) - - it('resolves tilde-prefixed Windows paths against the cwd user home', () => { - expect( - resolveTerminalFileLink( - { - pathText: '~/Documents/Path/file_name', - line: null, - column: null, - startIndex: 0, - endIndex: 26, - displayText: '~/Documents/Path/file_name' - }, - 'C:\\Users\\Alice\\project' - ) - ).toEqual({ - absolutePath: 'C:/Users/Alice/Documents/Path/file_name', - line: null, - column: null - }) }) it('resolves tilde-prefixed paths against explicit terminal home when cwd is outside home', () => { @@ -316,14 +131,6 @@ describe('terminal path helpers', () => { }) }) - it('finds exact repo-relative OSC hyperlink text', () => { - expect(findExactTerminalFileLink('docs/README.md')).toMatchObject({ - pathText: 'docs/README.md', - line: null, - column: null - }) - }) - it('keeps line and column suffixes from exact OSC hyperlink text', () => { expect(findExactTerminalFileLink('docs/README.md:12:3')).toMatchObject({ pathText: 'docs/README.md', @@ -354,12 +161,5 @@ describe('terminal path helpers', () => { const links = extractTerminalFileLinks('file:///Users/dev/orca/report.html') expect(links.map((link) => link.displayText)).toEqual(['file:///Users/dev/orca/report.html']) }) - - it('exposes file:// URIs to the hover candidate pass as well', () => { - const candidates = extractTerminalFileLinkCandidates('file:///tmp/out.txt:9') - expect(candidates.some((link) => link.pathText === '/tmp/out.txt' && link.line === 9)).toBe( - true - ) - }) }) }) diff --git a/src/shared/terminal-mode-2031-final-state.test.ts b/src/shared/terminal-mode-2031-final-state.test.ts index 3f3052193b9..3355bc129d0 100644 --- a/src/shared/terminal-mode-2031-final-state.test.ts +++ b/src/shared/terminal-mode-2031-final-state.test.ts @@ -1,11 +1,6 @@ // fish toggles 2031 around each prompt, often in one chunk. Acting on any subscribe instead of final state sends replies into the shell/child (#9993). import { describe, expect, it } from 'vitest' -import { - INITIAL_MODE_2031_REPLY_SCAN_STATE, - scanMode2031ReplyDecision, - scanMode2031Sequences, - type Mode2031ReplyDecision -} from './terminal-color-scheme-protocol' + import { createTerminalTitleTracker, type TerminalTitleTrackerCallbacks @@ -14,9 +9,6 @@ import { const ESC = '\x1b' const BEL = '\x07' -// A fish prompt cycle: subscribe, paint the prompt, hand the tty to the child. -const FISH_PROMPT_HANDOFF = `${ESC}[?2031h${ESC}[0m~/orca ${ESC}[32m❯${ESC}[0m ${ESC}[?2031l` - function trackerRecording(overrides: TerminalTitleTrackerCallbacks = {}): { subscribes: number tracker: ReturnType @@ -37,52 +29,6 @@ function trackerRecording(overrides: TerminalTitleTrackerCallbacks = {}): { } describe('DECSET 2031 replies follow the chunk-final state (#9993)', () => { - it('reports a subscribe-then-unsubscribe chunk as unsubscribed', () => { - const scan = scanMode2031Sequences('', FISH_PROMPT_HANDOFF) - - // Both toggles occurred, but only finalState says whether the shell is still listening. - expect(scan.subscribe).toBe(true) - expect(scan.unsubscribe).toBe(true) - expect(scan.finalState).toBe('unsubscribed') - }) - - it('does not emit a 2031-subscribe fact when the shell unsubscribed in the same chunk', () => { - const recorded = trackerRecording() - - recorded.tracker.handleChunk(FISH_PROMPT_HANDOFF) - - expect(recorded.subscribes).toBe(0) - }) - - it('still emits a fact when the chunk ends subscribed', () => { - const recorded = trackerRecording() - - recorded.tracker.handleChunk(`${ESC}[?2031l${ESC}[?2031h`) - - expect(recorded.subscribes).toBe(1) - }) - - it('emits once per chunk that ends subscribed, across a fish prompt loop', () => { - const recorded = trackerRecording() - - // Three prompt cycles, then a TUI that subscribes and keeps listening. - recorded.tracker.handleChunk(FISH_PROMPT_HANDOFF) - recorded.tracker.handleChunk(FISH_PROMPT_HANDOFF) - recorded.tracker.handleChunk(FISH_PROMPT_HANDOFF) - recorded.tracker.handleChunk(`${ESC}[?2031h`) - - expect(recorded.subscribes).toBe(1) - }) - - it('keeps answering a subscribe split across chunk boundaries', () => { - const recorded = trackerRecording() - - recorded.tracker.handleChunk(`${ESC}[?20`) - recorded.tracker.handleChunk('31h') - - expect(recorded.subscribes).toBe(1) - }) - it('does not answer a subscribe whose withdrawal straddles the chunk boundary', () => { const recorded = trackerRecording() @@ -162,19 +108,6 @@ function trackerRecordingBothFacts(): { } describe('a fish prompt-accept burst is tracked, never answered (#9993)', () => { - it('yields registry transitions only — subscribe then unsubscribe, two chunks apart', () => { - let state = INITIAL_MODE_2031_REPLY_SCAN_STATE - const decisions: Mode2031ReplyDecision[] = FISH_PROMPT_ACCEPT_CHUNKS.map((chunk) => { - const result = scanMode2031ReplyDecision(state, chunk) - state = result.state - return result.decision - }) - - expect(decisions).toEqual(['subscribed', null, null, 'unsubscribed', null]) - // The shell withdraws before exec; a reply queued at subscribe can reach only its child. - expect(decisions.indexOf('unsubscribed')).toBeLessThan(FISH_PROMPT_ACCEPT_CHUNKS.length - 1) - }) - it('still emits both side-effect facts so the subscription registry stays truthful', () => { const recorded = trackerRecordingBothFacts() @@ -185,13 +118,4 @@ describe('a fish prompt-accept burst is tracked, never answered (#9993)', () => // Facts survive the fix: maybePushMode2031Flip needs them to know who is listening. expect(recorded.facts).toEqual(['2031-subscribe', '2031-unsubscribe']) }) - - it('contains no color-scheme query, so silence is the whole correct answer', () => { - const burst = FISH_PROMPT_ACCEPT_CHUNKS.join('') - - // A captured full fish session subscribes but never queries CSI ?996n, so ?997;Nn would be unsolicited. - expect(burst).toContain(`${ESC}[?2031h`) - expect(burst).not.toContain('996') - expect(burst).not.toContain('997') - }) }) diff --git a/src/shared/terminal-output-side-effects.test.ts b/src/shared/terminal-output-side-effects.test.ts index 468ca93dbc8..94a4aa5cf75 100644 --- a/src/shared/terminal-output-side-effects.test.ts +++ b/src/shared/terminal-output-side-effects.test.ts @@ -35,23 +35,6 @@ function createRecordingTracker(overrides: TerminalTitleTrackerCallbacks = {}): } describe('createTerminalTitleTracker command-finished facts', () => { - it('emits command-finished with best-effort exit codes', () => { - const { events, tracker } = createRecordingTracker() - - tracker.handleChunk(`before${ESC}]133;A${BEL}prompt${ESC}]133;B${BEL}`) - tracker.handleChunk(`${ESC}]133;C${BEL}running${ESC}]133;D;0${BEL}`) - tracker.handleChunk(`${ESC}]133;D;130${BEL}`) - tracker.handleChunk(`${ESC}]133;D;not-a-number${BEL}`) - tracker.handleChunk(`${ESC}]133;D${BEL}`) - - expect(events).toEqual([ - ['finished', 0], - ['finished', 130], - ['finished', null], - ['finished', null] - ]) - }) - it('detects OSC 133;D split across chunk boundaries (BEL and ST terminated)', () => { const { events, tracker } = createRecordingTracker() @@ -90,19 +73,6 @@ describe('createTerminalTitleTracker command-finished facts', () => { }) describe('createTerminalTitleTracker pr-link facts', () => { - it('emits one fact per PR URL including multiple links in one chunk', () => { - const { events, tracker } = createRecordingTracker() - - tracker.handleChunk( - 'see https://github.com/acme/orca/pull/42 and https://github.com/acme/orca/pull/43 \r\n' - ) - - expect(events).toEqual([ - ['pr', 'https://github.com/acme/orca/pull/42', 42], - ['pr', 'https://github.com/acme/orca/pull/43', 43] - ]) - }) - it('waits for a boundary when a URL splits across chunks and dedupes repeats', () => { const { events, tracker } = createRecordingTracker() @@ -113,18 +83,6 @@ describe('createTerminalTitleTracker pr-link facts', () => { expect(events).toEqual([['pr', 'https://github.com/acme/orca/pull/42', 42]]) }) - - it('skips the 133/URL scans entirely when no consumer is registered', () => { - // Headless serve has no side-effect consumer; it must not allocate scanners or pay their per-chunk cost. - const titles: string[] = [] - const tracker = createTerminalTitleTracker({ - onTitle: (normalized) => titles.push(normalized) - }) - - tracker.handleChunk(`${ESC}]133;D;0${BEL}https://github.com/acme/orca/pull/42\r\n`) - - expect(titles).toEqual([]) - }) }) describe('createTerminalTitleTracker 2031-subscribe facts', () => { @@ -136,33 +94,6 @@ describe('createTerminalTitleTracker 2031-subscribe facts', () => { expect(events).toEqual([['2031-subscribe'], ['bell']]) }) - it('detects a subscribe split across chunk boundaries', () => { - const { events, tracker } = createRecordingTracker() - - tracker.handleChunk(`${ESC}[?20`) - tracker.handleChunk('31h') - - expect(events).toEqual([['2031-subscribe']]) - }) - - it('emits an unsubscribe fact so gated views can retire the subscription', () => { - // Gated views miss withdrawal bytes; this fact prevents unsolicited theme replies after unsubscribe (#9993). - const { events, tracker } = createRecordingTracker() - - tracker.handleChunk(`${ESC}[?2031l`) - - expect(events).toEqual([['2031-unsubscribe']]) - }) - - it('emits nothing for chunks that carry no 2031 bytes at all', () => { - // finalState is null here, not 'unsubscribed' — ordinary output must stay silent. - const { events, tracker } = createRecordingTracker() - - tracker.handleChunk('plain build output\r\n') - - expect(events).toEqual([]) - }) - it('reports only the chunk-final state when a chunk toggles 2031 both ways', () => { // fish enables and disables 2031 around every prompt; one chunk, one decision. const { events, tracker } = createRecordingTracker() @@ -172,17 +103,6 @@ describe('createTerminalTitleTracker 2031-subscribe facts', () => { expect(events).toEqual([['2031-unsubscribe'], ['2031-subscribe']]) }) - - it('skips the 2031 scan entirely when no consumer is registered', () => { - const { events, tracker } = createRecordingTracker({ - onMode2031Subscribe: undefined, - onMode2031Unsubscribe: undefined - }) - - tracker.handleChunk(`${ESC}[?2031h`) - - expect(events).toEqual([]) - }) }) describe('createTerminalTitleTracker synthetic-frame isolation', () => { diff --git a/src/shared/terminal-partial-escape-tail.test.ts b/src/shared/terminal-partial-escape-tail.test.ts index 3153f331d28..5ce347d583c 100644 --- a/src/shared/terminal-partial-escape-tail.test.ts +++ b/src/shared/terminal-partial-escape-tail.test.ts @@ -24,55 +24,9 @@ describe('advanceTerminalEscapeBoundary', () => { } } ) - - it('matches the full-tail scanner at every split, without retaining payloads', () => { - const streams = [ - '\x1b[38;2;115;118;123;48;2;65;69;76m⠁', - '\x1b\x00\t[31;1mred\x1b[0m', - '\x1b]0;title\x1b\\plain', - '\x1b\x00]0;title\x07', - '\x1bP$qm\x1b\\', - '\x1b_Gpayload\x1b\\', - '\x1b q\x1b[31;\x18plain', - '\x1b]0;title\x1b\x1b[31mred', - '\x1bPpayload\x1b[32mgreen', - '\x1b[31;\x1a', - `\x1b]0;${'x'.repeat(MAX_PARTIAL_ESCAPE_TAIL_LENGTH + 1)}\x1b\\` - ] - for (const stream of streams) { - let state = '' - for (let i = 0; i < stream.length; i += 1) { - state = advanceTerminalEscapeBoundary(state, stream[i]) - expect(state.length).toBeLessThanOrEqual(3) - expect(state === '', JSON.stringify({ stream: stream.slice(0, 50), i })).toBe( - extractPartialEscapeTail(stream.slice(0, i + 1)) === '' - ) - } - } - }) }) describe('extractPartialEscapeTail', () => { - it('returns empty for parser-clean streams', () => { - expect(extractPartialEscapeTail('')).toBe('') - expect(extractPartialEscapeTail('plain text no escapes')).toBe('') - expect(extractPartialEscapeTail('\x1b[38;5;196mred\x1b[0m done')).toBe('') - expect(extractPartialEscapeTail('\x1b[2J\x1b[H')).toBe('') - }) - - it('returns the dangling CSI when a chunk ends mid-sequence', () => { - expect(extractPartialEscapeTail('hello\x1b[3')).toBe('\x1b[3') - expect(extractPartialEscapeTail('a\x1b[38;5;')).toBe('\x1b[38;5;') - expect(extractPartialEscapeTail('\x1b')).toBe('\x1b') - expect(extractPartialEscapeTail('\x1b[')).toBe('\x1b[') - }) - - it('returns the dangling OSC (unterminated) sequence', () => { - expect(extractPartialEscapeTail('\x1b]0;my-title')).toBe('\x1b]0;my-title') - expect(extractPartialEscapeTail('\x1b]0;title\x07after')).toBe('') - expect(extractPartialEscapeTail('\x1b]0;title\x1b\\after')).toBe('') - }) - it('treats a fresh ESC as aborting a pending CSI', () => { expect(extractPartialEscapeTail('\x1b[3\x1b[0m')).toBe('') expect(extractPartialEscapeTail('\x1b[3\x1b[')).toBe('\x1b[') @@ -106,40 +60,9 @@ describe('extractPartialEscapeTail', () => { expect(extractPartialEscapeTail('\x1bPq\x1b\x1b[3')).toBe('\x1b[3') expect(extractPartialEscapeTail('\x1b]0;t\x1b\x1b')).toBe('\x1b') }) - - it('is fold-safe across chunk boundaries', () => { - // extract(a + b) === extract(extract(a) + b) — the invariant ingest relies on. - const cases: [string, string][] = [ - ['first\x1b[3', '8;5;196mred'], - ['\x1b', '[0m'], - ['\x1b]0;ti', 'tle\x07'], - ['clean', '\x1b[1'], - // Fold-safety must hold across the CAN abort too. - ['\x1b', '\x18after'], - ['\x1b ', '\x18after'], - // Boundary landing inside stringEsc/oscEsc — the two states that used to break the fold. - ['\x1bPx\x1b\x18', 'X0abc'], - ['\x1b]0;t\x1b\x1a', 'X0abc'], - ['\x1b] \x1b\x1b', '^'], - ['\x1bPq\x1b\x1b', '[3'] - ] - for (const [a, b] of cases) { - expect(extractPartialEscapeTail(extractPartialEscapeTail(a) + b)).toBe( - extractPartialEscapeTail(a + b) - ) - } - }) }) describe('advancePartialEscapeTail', () => { - it('accumulates a split sequence across chunks', () => { - let tail = '' - tail = advancePartialEscapeTail(tail, 'ls\r\n\x1b[3') - expect(tail).toBe('\x1b[3') - tail = advancePartialEscapeTail(tail, '8;5;196m') - expect(tail).toBe('') // sequence completed - }) - it('abandons tracking (returns empty) past the cap', () => { // An unterminated OSC longer than the cap degrades to pre-fix behavior. const huge = `\x1b]0;${'x'.repeat(MAX_PARTIAL_ESCAPE_TAIL_LENGTH + 10)}` @@ -148,35 +71,6 @@ describe('advancePartialEscapeTail', () => { }) describe('advancePartialEscapeTail ESC-free fast path', () => { - // Compare every pending-state/chunk pairing against the unconditional fold. - const pieces = [ - '', - 'plain output\n', - '\x1b[32mgreen\x1b[0m', - '\x1b[3', - '\x1b]0;title\x07', - '\x1b]0;partial', - '\x1bP dcs payload', - '\x1b', - '\x18', - '\x1a', - '\x1b]8;;https://example.com\x1b\\', - '\x1b(', - '\x1b[1;2;3' - ] - - it('matches an unconditional fold for every pending-tail and chunk pairing', () => { - for (const pending of pieces.map((piece) => extractPartialEscapeTail(piece))) { - for (const chunk of pieces) { - // Apply the cap to the oracle too or large pairings compare different contracts. - const unguarded = extractPartialEscapeTail(pending + chunk) - expect(advancePartialEscapeTail(pending, chunk), JSON.stringify({ pending, chunk })).toBe( - unguarded.length > MAX_PARTIAL_ESCAPE_TAIL_LENGTH ? '' : unguarded - ) - } - } - }) - it('still carries a pending tail through an ESC-free chunk', () => { expect(advancePartialEscapeTail('\x1b]0;my-title', ' still in the OSC')).toBe( '\x1b]0;my-title still in the OSC' diff --git a/src/shared/terminal-query-reply.test.ts b/src/shared/terminal-query-reply.test.ts index 7fcf4a5b136..f3eb0ac02aa 100644 --- a/src/shared/terminal-query-reply.test.ts +++ b/src/shared/terminal-query-reply.test.ts @@ -29,42 +29,6 @@ describe('isTerminalQueryReply', () => { } }) - it('matches synthetic query replies that must be sent immediately', () => { - // CPR cursor position report (answer to CSI 6n) — the #7329 culprit. - expect(isTerminalQueryReply('\x1b[3;1R')).toBe(true) - expect(isTerminalQueryReply('\x1b[22;1R')).toBe(true) - // DSR device status. - expect(isTerminalQueryReply('\x1b[0n')).toBe(true) - // Contour color-scheme report (answer to CSI ?996n / mode-2031 push). - expect(isTerminalQueryReply('\x1b[?997;1n')).toBe(true) - expect(isTerminalQueryReply('\x1b[?997;2n')).toBe(true) - // DA1/DA2/DA3 device attributes. - expect(isTerminalQueryReply('\x1b[?1;2c')).toBe(true) - expect(isTerminalQueryReply('\x1b[?61;4c')).toBe(true) - expect(isTerminalQueryReply('\x1b[>0;276;0c')).toBe(true) - // Window/cell pixel-size reports. - expect(isTerminalQueryReply('\x1b[6;16;8t')).toBe(true) - expect(isTerminalQueryReply('\x1b[4;384;640t')).toBe(true) - // DECRPM mode report — private (with ?) and ANSI (without ?). - expect(isTerminalQueryReply('\x1b[?2026;2$y')).toBe(true) - expect(isTerminalQueryReply('\x1b[4;1$y')).toBe(true) - // OSC 10/11 color responses (the #7329 culprit) — BEL and ST terminated. - expect(isTerminalQueryReply('\x1b]11;rgb:2828/2c2c/3434\x1b\\')).toBe(true) - expect(isTerminalQueryReply('\x1b]10;rgb:c0c0/c0c0/c0c0\x07')).toBe(true) - // DECXCPR extended cursor position report (answer to CSI ? 6n). - expect(isTerminalQueryReply('\x1b[?12;5R')).toBe(true) - // Text-area size in characters (answer to CSI 18t). - expect(isTerminalQueryReply('\x1b[8;24;80t')).toBe(true) - // Crossterm probes Kitty flags at startup; delaying replies corrupts input like CPR did. - expect(isTerminalQueryReply('\x1b[?0u')).toBe(true) - expect(isTerminalQueryReply('\x1b[?31u')).toBe(true) - // DCS DECRQSS reports (vim queries cursor style via DCS $ q) + XTVERSION. - expect(isTerminalQueryReply('\x1bP1$r2 q\x1b\\')).toBe(true) - expect(isTerminalQueryReply('\x1bP1$r0m\x1b\\')).toBe(true) - expect(isTerminalQueryReply('\x1bP0$r\x1b\\')).toBe(true) - expect(isTerminalQueryReply('\x1bP>|xterm.js(5.6.0)\x1b\\')).toBe(true) - }) - it('classifies the fully framed XTVERSION reply emitted by real xterm', async () => { const terminal = new Terminal() const replies: string[] = [] @@ -82,11 +46,6 @@ describe('isTerminalQueryReply', () => { } }) - it('documents the accepted modified-F3/CPR collision', () => { - // Modified F3 is byte-identical to CPR; accept the ambiguity documented in terminal-query-reply.ts. - expect(isTerminalQueryReply('\x1b[1;2R')).toBe(true) - }) - it('routes only cooked-echo-risk replies through the ECHO-safe write path', () => { // Color-scheme private DSR + OSC color — cooked prompt paint risk (#13137). expect(needsCookedEchoSafeQueryReply('\x1b[?997;1n')).toBe(true) @@ -206,54 +165,4 @@ describe('query reply ordering (termenv OSC-then-CPR)', () => { ingress.drainAndClose() vi.useRealTimers() }) - - it('keeps a CPR immediate when no color reply is deferred', () => { - vi.useFakeTimers() - const pty: string[] = [] - const ingress = new PtyStartupIngress({ - ownerBackend: 'posix-pty', - write: (data) => pty.push(data), - onEmission: () => {} - }) - - hostWrites(ingress, pty)(CPR_REPLY) - expect(pty).toEqual([CPR_REPLY]) - ingress.drainAndClose() - vi.useRealTimers() - }) - - it('never takes ordinary typed input', () => { - const pty: string[] = [] - const ingress = new PtyStartupIngress({ - ownerBackend: 'posix-pty', - write: (data) => pty.push(data), - onEmission: () => {} - }) - - for (const keystroke of ['y', 'gh auth login\r', '\x1b[A', '\x1b', '\x03']) { - expect(ingress.answerLiveQueryReply(keystroke)).toBe(false) - } - ingress.drainAndClose() - }) -}) - -// Known limit: FIFO covers recognized replies, not all PTY bytes. Whole-string matching excludes mixed payloads and ordinary input; routing them through FIFO later is an improvement. -describe('writes that still bypass the ordered queue', () => { - const BYPASSING = [ - { what: 'reply coalesced with a keystroke', data: '\x1b[6;1Ry' }, - { what: 'keystroke coalesced with a reply', data: 'y\x1b[6;1R' }, - { what: 'ordinary typed input', data: 'ls\r' } - ] - - it.each(BYPASSING)('does not take $what', ({ data }) => { - const ingress = new PtyStartupIngress({ - ownerBackend: 'posix-pty', - write: () => {}, - onEmission: () => {} - }) - // Deferral is open, so an ordered write WOULD be queued here. - expect(ingress.answerLiveQueryReply('\x1b]11;rgb:00/00/00\x07')).toBe(true) - expect(ingress.answerLiveQueryReply(data)).toBe(false) - ingress.drainAndClose() - }) }) diff --git a/src/shared/terminal-quick-commands.test.ts b/src/shared/terminal-quick-commands.test.ts index 23e3ed850dd..366f922fad7 100644 --- a/src/shared/terminal-quick-commands.test.ts +++ b/src/shared/terminal-quick-commands.test.ts @@ -1,28 +1,13 @@ import { describe, expect, it } from 'vitest' import { applyTerminalQuickCommandMutation, - buildTerminalQuickCommandInput, flattenTerminalQuickCommand, - getTerminalQuickCommandAction, - getTerminalQuickCommandBody, - getDefaultTerminalQuickCommands, - isTerminalQuickCommandComplete, normalizeTerminalQuickCommands, parseNormalizedTerminalQuickCommands, - supportsTerminalAgentQuickCommand, terminalQuickCommandMatchesRepo } from './terminal-quick-commands' describe('terminal quick commands', () => { - it('returns safe defaults when persisted settings are missing', () => { - expect(normalizeTerminalQuickCommands(undefined)).toEqual([]) - expect(getDefaultTerminalQuickCommands()).toEqual([]) - }) - - it('keeps an intentionally empty command list', () => { - expect(normalizeTerminalQuickCommands([])).toEqual([]) - }) - it('preserves lists larger than forty commands across loading and protocol parsing', () => { const commands = Array.from({ length: 100 }, (_, index) => ({ id: `command-${index}`, @@ -37,106 +22,6 @@ describe('terminal quick commands', () => { expect(parseNormalizedTerminalQuickCommands(commands)).toEqual(commands) }) - it('removes quick commands from the abandoned preset rollout', () => { - expect( - normalizeTerminalQuickCommands([ - { - id: 'default-pwd', - label: 'Print Working Directory', - command: 'pwd', - appendEnter: true - }, - { - id: 'default-git-status', - label: 'Git Status', - command: 'git status', - appendEnter: true - } - ]) - ).toEqual([]) - }) - - it('drops malformed entries and normalizes valid commands and drafts', () => { - expect( - normalizeTerminalQuickCommands([ - null, - { id: 'status', label: ' Status ', command: 'git status\n', appendEnter: false }, - { id: 'empty-command', label: 'Empty', command: ' ' }, - { id: 'status', label: 'Duplicate', command: 'pwd' }, - { label: 'No ID', command: 'date' } - ]) - ).toEqual([ - { - id: 'status', - label: 'Status', - action: 'terminal-command', - command: 'git status', - appendEnter: false, - scope: { type: 'global' } - }, - { - id: 'empty-command', - label: 'Empty', - action: 'terminal-command', - command: '', - appendEnter: true, - scope: { type: 'global' } - }, - { - id: 'status-2', - label: 'Duplicate', - action: 'terminal-command', - command: 'pwd', - appendEnter: true, - scope: { type: 'global' } - }, - { - id: 'quick-command-4', - label: 'No ID', - action: 'terminal-command', - command: 'date', - appendEnter: true, - scope: { type: 'global' } - } - ]) - }) - - it('normalizes repository scoped commands and falls back to global for invalid scopes', () => { - expect( - normalizeTerminalQuickCommands([ - { - id: 'repo-dev', - label: 'Dev', - command: 'pnpm dev', - scope: { type: 'repo', repoId: ' repo-1 ' } - }, - { - id: 'bad-repo', - label: 'Bad', - command: 'echo bad', - scope: { type: 'repo', repoId: ' ' } - } - ]) - ).toEqual([ - { - id: 'repo-dev', - label: 'Dev', - action: 'terminal-command', - command: 'pnpm dev', - appendEnter: true, - scope: { type: 'repo', repoId: 'repo-1' } - }, - { - id: 'bad-repo', - label: 'Bad', - action: 'terminal-command', - command: 'echo bad', - appendEnter: true, - scope: { type: 'global' } - } - ]) - }) - it('normalizes agent prompt commands without storing generated shell text', () => { expect( normalizeTerminalQuickCommands([ @@ -298,88 +183,9 @@ describe('terminal quick commands', () => { ) ).toBe(false) }) - - it('formats terminal input without assuming shell semantics', () => { - expect( - buildTerminalQuickCommandInput({ - id: 'status', - label: 'Status', - command: 'git status', - appendEnter: true - }) - ).toBe('git status\r') - expect( - buildTerminalQuickCommandInput({ - id: 'status', - label: 'Status', - command: 'git status', - appendEnter: false - }) - ).toBe('git status') - }) - - it('classifies quick command actions and body text', () => { - const terminal = { - id: 'status', - label: 'Status', - command: 'git status', - appendEnter: true - } - const agent = { - id: 'agent', - label: 'Agent', - action: 'agent-prompt' as const, - agent: 'claude' as const, - prompt: 'Fix the tests' - } - - expect(getTerminalQuickCommandAction(terminal)).toBe('terminal-command') - expect(getTerminalQuickCommandBody(terminal)).toBe('git status') - expect(isTerminalQuickCommandComplete(terminal)).toBe(true) - expect(getTerminalQuickCommandAction(agent)).toBe('agent-prompt') - expect(getTerminalQuickCommandBody(agent)).toBe('Fix the tests') - expect(isTerminalQuickCommandComplete(agent)).toBe(true) - }) - - it('only allows agent prompt quick commands for launch-time prompt agents', () => { - expect(supportsTerminalAgentQuickCommand('claude')).toBe(true) - expect(supportsTerminalAgentQuickCommand('gemini')).toBe(true) - expect(supportsTerminalAgentQuickCommand('aider')).toBe(false) - expect(supportsTerminalAgentQuickCommand('not-real')).toBe(false) - }) }) describe('flattenTerminalQuickCommand', () => { - it('returns the same object when there are no line breaks', () => { - const command = { - id: 'test', - label: 'Test', - command: 'git status', - appendEnter: true - } as const - expect(flattenTerminalQuickCommand(command)).toBe(command) - }) - - it('replaces newlines with semicolons and spaces', () => { - const result = flattenTerminalQuickCommand({ - id: 'test', - label: 'Test', - command: 'cd packages\nbun run build\ncd ..', - appendEnter: true - }) - expect(result.command).toBe('cd packages; bun run build; cd ..') - }) - - it('collapses consecutive newlines into a single separator', () => { - const result = flattenTerminalQuickCommand({ - id: 'test', - label: 'Test', - command: 'echo one\n\n\necho two', - appendEnter: true - }) - expect(result.command).toBe('echo one; echo two') - }) - it('handles Windows-style CRLF endings', () => { const result = flattenTerminalQuickCommand({ id: 'test', diff --git a/src/shared/terminal-startup-cwd.test.ts b/src/shared/terminal-startup-cwd.test.ts index 941940e2554..b6a752c1782 100644 --- a/src/shared/terminal-startup-cwd.test.ts +++ b/src/shared/terminal-startup-cwd.test.ts @@ -9,68 +9,6 @@ import { import { folderWorkspaceKey } from './workspace-scope' describe('resolveTerminalStartupCwd', () => { - it('accepts absolute child paths inside the worktree', () => { - expect(resolveTerminalStartupCwd('/repo/app', '/repo/app/packages/web')).toBe( - '/repo/app/packages/web' - ) - }) - - it('resolves relative paths against the worktree', () => { - expect(resolveTerminalStartupCwd('/repo/app', 'packages/web')).toBe('/repo/app/packages/web') - }) - - it('allows absolute cwds outside the worktree (#7685)', () => { - expect(resolveTerminalStartupCwd('/repo/app', '/repo/app-other')).toBe('/repo/app-other') - }) - - it('resolves parent traversal to a path outside the worktree (#7685)', () => { - expect(resolveTerminalStartupCwd('/repo/app', '../other')).toBe('/repo/other') - }) - - it('trims whitespace-padded requested cwds before resolving', () => { - expect(resolveTerminalStartupCwd('/repo/app', ' packages/web ')).toBe('/repo/app/packages/web') - }) - - it('returns undefined for an empty requested cwd', () => { - expect(resolveTerminalStartupCwd('/repo/app', '')).toBeUndefined() - expect(resolveTerminalStartupCwd('/repo/app', ' ')).toBeUndefined() - expect(resolveTerminalStartupCwd('/repo/app', null)).toBeUndefined() - }) - - it('normalizes Windows separators and allows out-of-worktree drives', () => { - expect(resolveTerminalStartupCwd('C:\\Repo\\App', 'packages\\web')).toBe( - 'C:/Repo/App/packages/web' - ) - expect(resolveTerminalStartupCwd('C:\\Repo\\App', 'C:\\Repo\\AppOther')).toBe( - 'C:/Repo/AppOther' - ) - }) - - it('resolves renderer PTY cwd values against raw worktree IDs', () => { - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: 'repo-1::/repo/app', - requestedCwd: '/repo/app/packages/web' - }) - ).toBe('/repo/app/packages/web') - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: 'repo-1::/repo/app', - requestedCwd: '/repo/app-other' - }) - ).toBe('/repo/app-other') - }) - - it('passes floating terminal cwds through untouched', () => { - // Main validates floating cwds against trusted-directory grants; there is no workspace root to anchor them. - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: FLOATING_TERMINAL_WORKTREE_ID, - requestedCwd: '/Volumes/work/notes' - }) - ).toBe('/Volumes/work/notes') - }) - it('falls back to the provider default when no workspace root is resolvable', () => { expect( resolveTerminalStartupCwdForWorkspace({ @@ -152,26 +90,6 @@ describe('resolveTerminalStartupCwd', () => { expect(directoryExists).not.toHaveBeenCalled() }) - it('falls back from a missing parent-traversal cwd to the workspace root', () => { - expect( - resolveTerminalStartupCwd('/repo/app', '../deleted', { - directoryExists: (path) => path === '/repo/app' - }) - ).toBe('/repo/app') - }) - - it('recovers missing renderer cwd values against raw worktree IDs', () => { - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: 'repo-1::/repo/app', - requestedCwd: '/repo/app/deleted-folder', - missingDirFallback: { - directoryExists: (path) => path === '/repo/app' - } - }) - ).toBe('/repo/app') - }) - it('recovers missing cwd values against a resolved folder workspace root', () => { expect( resolveTerminalStartupCwdForWorkspace({ @@ -196,23 +114,6 @@ describe('resolveTerminalStartupCwd', () => { ).toBe('/Volumes/work/notes') expect(directoryExists).not.toHaveBeenCalled() }) - - it('resolves renderer PTY cwd values against folder workspace keys', () => { - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: folderWorkspaceKey('folder-1'), - requestedCwd: 'packages/web', - resolveFolderWorkspacePath: (id) => (id === 'folder-1' ? '/repo/app' : null) - }) - ).toBe('/repo/app/packages/web') - expect( - resolveTerminalStartupCwdForWorkspace({ - workspaceId: folderWorkspaceKey('folder-1'), - requestedCwd: '../other', - resolveFolderWorkspacePath: (id) => (id === 'folder-1' ? '/repo/app' : null) - }) - ).toBe('/repo/other') - }) }) describe('requestsCwdOutsideWorkspaceRoot', () => { @@ -256,11 +157,6 @@ describe('requestsCwdOutsideWorkspaceRoot', () => { expect(requestsCwdOutsideWorkspaceRoot(root, cwd)).toBe(true) }) - it('reads no cwd, or only whitespace, as no custom cwd', () => { - expect(requestsCwdOutsideWorkspaceRoot('/repo/app', undefined)).toBe(false) - expect(requestsCwdOutsideWorkspaceRoot('/repo/app', ' ')).toBe(false) - }) - it('reads a cwd against an unknown root as custom, because it cannot prove otherwise', () => { expect(requestsCwdOutsideWorkspaceRoot(undefined, '/repo/app')).toBe(true) }) diff --git a/src/shared/windows-environment-expansion.test.ts b/src/shared/windows-environment-expansion.test.ts deleted file mode 100644 index 47fe01d04f2..00000000000 --- a/src/shared/windows-environment-expansion.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - expandWindowsEnvironmentVariables, - expandWindowsPathEnvironmentVariables -} from './windows-environment-expansion' - -/** The per-miss `Object.keys().find()` form the lazy index replaced; the differential oracle. */ -function referenceExpand(value: string, env: Readonly>): string { - return value.replace(/%([^%]+)%/g, (match, name: string) => { - const exactValue = env[name] - if (typeof exactValue === 'string') { - return exactValue - } - const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()) - const fallback = key ? env[key] : undefined - return typeof fallback === 'string' ? fallback : match - }) -} - -function countingEnv(source: Record): { - env: Record - enumerations: () => number -} { - let enumerations = 0 - const env = new Proxy(source, { - ownKeys(target) { - enumerations += 1 - return Reflect.ownKeys(target) - } - }) - return { env, enumerations: () => enumerations } -} - -describe('expandWindowsEnvironmentVariables', () => { - it('expands names case-insensitively and preserves unknown variables', () => { - expect( - expandWindowsEnvironmentVariables('%localappdata%\\agy\\bin;%MISSING%\\bin', { - LOCALAPPDATA: 'C:\\Users\\orca\\AppData\\Local' - }) - ).toBe('C:\\Users\\orca\\AppData\\Local\\agy\\bin;%MISSING%\\bin') - }) - - it('expands variables with empty values', () => { - expect(expandWindowsEnvironmentVariables('before%EMPTY%after', { EMPTY: '' })).toBe( - 'beforeafter' - ) - }) - - it('enumerates fallback keys once for a PATH containing repeated mixed-case variables', () => { - const { env, enumerations } = countingEnv({ ROOT: 'C:\\root', OTHER: 'unused' }) - const value = Array.from({ length: 1000 }, () => '%root%').join(';') - expect(expandWindowsEnvironmentVariables(value, env)).toBe( - Array(1000).fill('C:\\root').join(';') - ) - expect(enumerations()).toBe(1) - }) - - it('never enumerates when every name resolves by exact case', () => { - const { env, enumerations } = countingEnv({ ROOT: 'C:\\root', EMPTY: '' }) - expect(expandWindowsEnvironmentVariables('%ROOT%;%EMPTY%;plain', env)).toBe('C:\\root;;plain') - expect(enumerations()).toBe(0) - }) - - it('preserves exact casing authority and first fallback keys including undefined values', () => { - const env = { Root: undefined, ROOT: 'upper', root: 'lower' } - expect(expandWindowsEnvironmentVariables('%ROOT%:%root%:%rOoT%', env)).toBe( - 'upper:lower:%rOoT%' - ) - }) - - // Case-insensitive fallback keeps the first insertion-order variant even if another casing shadows it. - it('resolves an inexact name to the first case variant, not the last', () => { - const env = { Path: 'first', PATH: 'second', pAtH: 'third' } - expect(expandWindowsEnvironmentVariables('%PaTh%', env)).toBe('first') - }) - - it('does not resolve names from the prototype chain', () => { - for (const name of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) { - expect(expandWindowsEnvironmentVariables(`%${name}%`, { ROOT: 'x' })).toBe(`%${name}%`) - } - }) - - it('matches the per-miss lookup oracle across randomized mixed-case environments', () => { - const names = ['PATH', 'Path', 'path', 'pAtH', 'ROOT', 'root', 'Temp', 'TEMP', 'MISSING'] - const values = ['a', '', 'C:\\x', undefined] - let seed = 0x9e3779b9 - const next = (): number => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff - for (let index = 0; index < 4000; index += 1) { - const env: Record = {} - for (let entry = 0; entry < Math.floor(next() * 5); entry += 1) { - env[names[Math.floor(next() * names.length)]!] = values[Math.floor(next() * values.length)] - } - const value = Array.from( - { length: Math.floor(next() * 6) }, - () => `%${names[Math.floor(next() * names.length)]}%` - ).join(';') - expect(expandWindowsEnvironmentVariables(value, env)).toBe(referenceExpand(value, env)) - } - }) -}) - -describe('expandWindowsPathEnvironmentVariables', () => { - it('expands every Windows PATH casing without changing other variables', () => { - const env = { - ORCA_PATH_ROOT: 'C:\\Users\\orca', - Path: '%ORCA_PATH_ROOT%\\bin', - PATH: '%orca_path_root%\\tools', - TEMPLATE: '%ORCA_PATH_ROOT%\\template' - } - - expandWindowsPathEnvironmentVariables(env, 'win32') - - expect(env.Path).toBe('C:\\Users\\orca\\bin') - expect(env.PATH).toBe('C:\\Users\\orca\\tools') - expect(env.TEMPLATE).toBe('%ORCA_PATH_ROOT%\\template') - }) - - it('leaves non-Windows PATH values unchanged', () => { - const env = { ROOT: '/opt/orca', PATH: '%ROOT%/bin:/usr/bin' } - - expandWindowsPathEnvironmentVariables(env, 'linux') - - expect(env.PATH).toBe('%ROOT%/bin:/usr/bin') - }) -}) diff --git a/src/shared/workspace-attachment-mutation.test.ts b/src/shared/workspace-attachment-mutation.test.ts index 2e7c42935c2..de117cd58a8 100644 --- a/src/shared/workspace-attachment-mutation.test.ts +++ b/src/shared/workspace-attachment-mutation.test.ts @@ -30,25 +30,6 @@ describe('source-aware reference deltas', () => { expect(getWorkspaceAttachments(removed)).toEqual([]) }) - it('keeps an unknown-source link when several same-number URLs could own it', () => { - const other = { ...legacy, url: 'https://github.com/other/repo/pull/42' } - expect(getWorkspaceAttachments({ linkedItems: [legacy, foreign, other] })).toEqual([ - legacy, - foreign, - other - ]) - }) - - it('merges observations from an unknown-source link into the only same-number URL', () => { - const original = { linkedItems: [{ ...legacy, origins: [first] }] } - const updated = normalizeWorkspaceAttachmentUpdate(original, { - linkedItemsBase: original.linkedItems, - linkedItems: [...original.linkedItems, foreign] - }) - expect(updated.linkedItems).toEqual([{ ...foreign, origins: [first] }]) - expect(getWorkspaceAttachments(updated)).toEqual(updated.linkedItems) - }) - it('does not resurrect a stored unknown-source duplicate removed by an older client', () => { const stored = { linkedItems: [legacy, foreign] } const updated = normalizeWorkspaceAttachmentUpdate(stored, { @@ -92,28 +73,6 @@ describe('source-aware reference deltas', () => { expect(updated.linkedItems).toEqual([{ ...current, origins: [first, second] }]) }) - it('does not resurrect canonical links removed during metadata enrichment', () => { - const updated = normalizeWorkspaceAttachmentUpdate( - { linkedItems: [] }, - { - linkedItemsBase: [{ ...foreign, repoId: 'repo-a' }], - linkedItems: [{ ...foreign, repoId: 'repo-b' }] - } - ) - expect(updated.linkedItems).toEqual([]) - }) - - it('deduplicates stored canonical references and merges their observations', () => { - expect( - getWorkspaceAttachments({ - linkedItems: [ - { ...foreign, repoId: 'repo-a', origins: [first] }, - { ...foreign, repoId: 'repo-b', origins: [second] } - ] - }) - ).toEqual([{ ...foreign, repoId: 'repo-a', origins: [first, second] }]) - }) - it('does not duplicate rich compatibility links whose local context differs', () => { expect( getWorkspaceAttachments({ @@ -130,18 +89,6 @@ describe('source-aware reference deltas', () => { ).toEqual([foreign]) }) - it('collapses an unknown-source link through a full collection write', () => { - const updated = normalizeWorkspaceAttachmentUpdate( - { linkedPR: 42 }, - { linkedItems: [legacy, foreign] } - ) - expect(getWorkspaceAttachments(updated)).toEqual([foreign]) - }) - - it('returns one reference for a legacy scalar and its matching URL', () => { - expect(getWorkspaceAttachments({ linkedPR: 42, linkedItems: [foreign] })).toEqual([foreign]) - }) - it('collapses stored unknown-source duplicates on unrelated scalar writes', () => { const updated = normalizeWorkspaceAttachmentUpdate( { linkedItems: [legacy, foreign] }, diff --git a/src/shared/workspace-attachments.test.ts b/src/shared/workspace-attachments.test.ts index c923de764f9..723871509e4 100644 --- a/src/shared/workspace-attachments.test.ts +++ b/src/shared/workspace-attachments.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest' import type { WorkspaceAttachment } from './worktree/types' import { getWorkspaceAttachments, - normalizeWorkspaceAttachmentUpdate, - workspaceAttachmentReferenceLabel + normalizeWorkspaceAttachmentUpdate } from './workspace-attachments' import { getWorkspaceAttachmentKey, @@ -36,20 +35,6 @@ describe('workspace attachments', () => { ]) }) - it('reads old metadata and enriches matching collection entries without duplicate compatibility links', () => { - const item = { - ...pr(7), - title: 'Seven', - url: 'https://github.com/a/repo/pull/7', - repoId: 'repo-a' - } - expect(getWorkspaceAttachments({ linkedPR: 7, linkedItems: [item] })).toEqual([item]) - expect(getWorkspaceAttachments({ linkedPR: 7, linkedIssue: 8 })).toEqual([ - { provider: 'github', type: 'issue', number: 8 }, - pr(7) - ]) - }) - it('retains previous attachments when replacing an active scalar and removes only the explicitly unlinked review', () => { const first = normalizeWorkspaceAttachmentUpdate({ linkedPR: 7 }, { linkedPR: 8 }) expect(first.linkedItems).toEqual([pr(7), pr(8)]) @@ -73,16 +58,6 @@ describe('workspace attachments', () => { expect(getWorkspaceAttachments(cleared)).toEqual([]) }) - it('honors a newly selected review across providers', () => { - const mr: WorkspaceAttachment = { provider: 'gitlab', type: 'mr', number: 20 } - const updated = normalizeWorkspaceAttachmentUpdate( - { linkedPR: 7, linkedItems: [pr(7), mr] }, - { linkedItems: [pr(7), mr], linkedGitLabMR: 20 } - ) - expect(updated.linkedPR).toBeNull() - expect(updated.linkedGitLabMR).toBe(20) - }) - it('repairs malformed entries and duplicates while retaining reviews from separate URL/repository scopes', () => { const one = { ...pr(7), url: 'https://github.com/A/one/pull/7' } const two = { ...pr(7), url: 'https://github.com/A/two/pull/7' } @@ -102,23 +77,6 @@ describe('workspace attachments', () => { expect(normalizeWorkspaceAttachments([pr(7), one])).toEqual([one]) }) - it('keeps Linear issue identities distinct across organizations', () => { - const one: WorkspaceAttachment = { - provider: 'linear', - type: 'issue', - number: 0, - identifier: 'APP-1', - linearOrganizationUrlKey: 'one' - } - const two = { ...one, linearOrganizationUrlKey: 'two' } - expect(normalizeWorkspaceAttachments([one, two])).toEqual([one, two]) - }) - - it('leaves unrelated metadata writes untouched', () => { - const updates = { comment: 'Ready for review' } - expect(normalizeWorkspaceAttachmentUpdate({ linkedPR: 7 }, updates)).toBe(updates) - }) - it('clears the other compatibility representation of an unlinked rich task', () => { const item = { provider: 'github' as const, @@ -285,18 +243,6 @@ describe('workspace attachments', () => { ).toEqual([enriched, other]) }) - it('keeps title and terminal observations when a richer identity replaces a duplicate', () => { - const origin = { kind: 'observed' as const, tabId: 'terminal' } - expect( - normalizeWorkspaceAttachments([ - { ...pr(7), title: 'Seven', origins: [origin] }, - { ...pr(7), url: 'https://github.com/a/repo/pull/7' } - ]) - ).toEqual([ - { ...pr(7), title: 'Seven', origins: [origin], url: 'https://github.com/a/repo/pull/7' } - ]) - }) - it('preserves concurrent origins when both editors enrich an unscoped reference', () => { const first = { kind: 'observed' as const, tabId: 'first' } const second = { kind: 'observed' as const, tabId: 'second' } @@ -344,27 +290,6 @@ describe('workspace attachments', () => { ).linkedItems ).toEqual([other, first]) }) - - it('collapses enrichment chains in one normalization without losing origins', () => { - const origin = { kind: 'observed' as const, tabId: 'first' } - const withUrl = { ...pr(7), url: 'https://github.com/a/repo/pull/7' } - const scoped = { - ...withUrl, - taskSourceContext: { - provider: 'github' as const, - hostId: 'local' as const, - projectId: 'project' - } - } - const normalized = normalizeWorkspaceAttachments([ - { ...pr(7), origins: [origin] }, - withUrl, - scoped - ]) - expect(normalized).toHaveLength(1) - expect(normalized[0]).toMatchObject({ ...scoped, origins: [origin] }) - expect(normalizeWorkspaceAttachments(normalized)).toEqual(normalized) - }) it('keeps an explicitly empty selection when incompatible references remain', () => { const mr: WorkspaceAttachment = { provider: 'gitlab', type: 'mr', number: 2 } const result = normalizeWorkspaceAttachmentUpdate( @@ -385,34 +310,3 @@ describe('workspace attachments', () => { expect(result.linkedGitLabMR).toBeNull() }) }) - -describe('workspaceAttachmentReferenceLabel', () => { - it('reads a reference as the desktop card and the phone details both show it', () => { - expect(workspaceAttachmentReferenceLabel({ provider: 'github', type: 'pr', number: 12 })).toBe( - 'PR #12' - ) - expect(workspaceAttachmentReferenceLabel({ provider: 'gitlab', type: 'mr', number: 3 })).toBe( - 'MR !3' - ) - expect( - workspaceAttachmentReferenceLabel({ provider: 'github', type: 'issue', number: 4 }) - ).toBe('Issue #4') - expect( - workspaceAttachmentReferenceLabel({ - provider: 'jira', - type: 'issue', - number: 0, - jiraIdentifier: 'ORCA-4' - }) - ).toBe('ORCA-4') - expect( - workspaceAttachmentReferenceLabel({ provider: 'linear', type: 'issue', number: 9 }) - ).toBe('#9') - expect( - workspaceAttachmentReferenceLabel( - { provider: 'github', type: 'pr', number: 12 }, - { pr: (n) => `Demande #${n}`, mr: String, issue: String } - ) - ).toBe('Demande #12') - }) -}) diff --git a/src/shared/workspace-name.test.ts b/src/shared/workspace-name.test.ts index 53e764200ae..f8d709abe82 100644 --- a/src/shared/workspace-name.test.ts +++ b/src/shared/workspace-name.test.ts @@ -1,18 +1,12 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { - applyJiraIssueKeyCasing, getLinearIssueWorkspaceName, - getLinkedWorkItemSuggestedName, getLinkedWorkItemWorkspaceName, getWorkspaceIntentName, resolveWorkspaceCreateName, slugifyForWorkspaceName } from './workspace-name' -afterEach(() => { - vi.restoreAllMocks() -}) - describe('slugifyForWorkspaceName', () => { it('keeps workspace seed slugs short, ascii-safe, and git-ref-safe', () => { expect(slugifyForWorkspaceName('../../Fix mobile Tasks 🚀')).toBe('fix-mobile-tasks') @@ -28,60 +22,9 @@ describe('slugifyForWorkspaceName', () => { 'cant-enable-browser-notifications' ) }) - - it('folds pasted workspace-name whitespace without regex replacement', () => { - const replace = vi.spyOn(String.prototype, 'replace') - const name = ['Fix', String.fromCharCode(160), '\nPasted\tWorkspace'].join('') - - expect(slugifyForWorkspaceName(name)).toBe('fix-pasted-workspace') - expect( - replace.mock.calls.filter( - ([pattern]) => pattern instanceof RegExp && pattern.source === '\\s+' - ) - ).toHaveLength(0) - }) -}) - -describe('getLinkedWorkItemSuggestedName', () => { - it('removes duplicated issue and PR numbers from linked titles', () => { - expect(getLinkedWorkItemSuggestedName({ title: 'Issue #123: Fix mobile Tasks' })).toBe( - 'fix-mobile-tasks' - ) - expect(getLinkedWorkItemSuggestedName({ title: 'Add mobile drawer (#812)' })).toBe( - 'add-mobile-drawer' - ) - }) }) describe('getLinkedWorkItemWorkspaceName', () => { - it('uses the resolved GitHub title instead of the source URL or provider number', () => { - expect( - getLinkedWorkItemWorkspaceName({ - type: 'pr', - number: 2049, - title: 'Fix pasted URL workspace names' - }) - ).toEqual({ - displayName: 'Fix pasted URL workspace names', - seedName: 'fix-pasted-url-workspace-names' - }) - }) - - it('keeps external provider identifiers without duplicating title prefixes', () => { - expect( - getLinkedWorkItemWorkspaceName({ - type: 'issue', - provider: 'jira', - number: 0, - title: 'PROJ-7 Fix flaky import', - jiraIdentifier: 'PROJ-7' - }) - ).toEqual({ - displayName: 'PROJ-7 Fix flaky import', - seedName: 'proj-7-fix-flaky-import' - }) - }) - it('keeps the canonical uppercase Jira key when the casing policy preserves it', () => { const item = { type: 'issue' as const, @@ -98,21 +41,6 @@ describe('getLinkedWorkItemWorkspaceName', () => { getLinkedWorkItemWorkspaceName(item, { jiraIssueKeyCasing: 'lowercase' })?.seedName ).toBe('proj-7-fix-flaky-import') }) - - it('ignores the Jira casing policy for non-Jira work items', () => { - expect( - getLinkedWorkItemWorkspaceName( - { - type: 'issue', - provider: 'linear', - number: 0, - title: 'Fix it', - linearIdentifier: 'ENG-1' - }, - { jiraIssueKeyCasing: 'preserve' } - )?.seedName - ).toBe('eng-1-fix-it') - }) }) describe('Jira generated-name source identity', () => { @@ -145,28 +73,6 @@ describe('Jira generated-name source identity', () => { }) }) -describe('applyJiraIssueKeyCasing', () => { - it('uppercases any Jira project key shape without an allowlist', () => { - expect(applyJiraIssueKeyCasing('alt-42-title', 'ALT-42', 'preserve')).toBe('ALT-42-title') - expect(applyJiraIssueKeyCasing('team_alpha9-7-title', 'TEAM_ALPHA9-7', 'preserve')).toBe( - 'TEAM_ALPHA9-7-title' - ) - expect(applyJiraIssueKeyCasing('proj-123', 'proj-123', 'preserve')).toBe('PROJ-123') - }) - - it('leaves the seed alone for the default policy or a non-leading key', () => { - expect(applyJiraIssueKeyCasing('proj-123-title', 'PROJ-123', undefined)).toBe('proj-123-title') - expect(applyJiraIssueKeyCasing('proj-123-title', 'PROJ-123', 'lowercase')).toBe( - 'proj-123-title' - ) - expect(applyJiraIssueKeyCasing('proj-1234-title', 'PROJ-123', 'preserve')).toBe( - 'proj-1234-title' - ) - expect(applyJiraIssueKeyCasing('fix-proj-123', 'PROJ-123', 'preserve')).toBe('fix-proj-123') - expect(applyJiraIssueKeyCasing('title', 'not a key', 'preserve')).toBe('title') - }) -}) - describe('getWorkspaceIntentName', () => { it('uses explicit user intent for linked issues without copying long titles', () => { expect( @@ -215,68 +121,6 @@ describe('getWorkspaceIntentName', () => { }) }) - it('uses a compressed subject when a linked issue has no action', () => { - expect( - getWorkspaceIntentName({ - sourceText: 'https://github.com/acme/app/issues/9876', - workItem: { - type: 'issue', - number: 9876, - title: 'Make importer handle archived rows' - } - }) - ).toEqual({ - displayName: 'Issue 9876 Make Importer Handle', - seedName: 'issue-9876-make-importer-handle' - }) - }) - - it('keeps contractions readable in linked issue display names', () => { - expect( - getWorkspaceIntentName({ - sourceText: 'https://github.com/acme/app/issues/4802', - workItem: { - type: 'issue', - number: 4802, - title: "Can't enable browser notifications from within a browser tab" - } - }) - ).toEqual({ - displayName: "Issue 4802 Can't Enable Browser", - seedName: 'issue-4802-cant-enable-browser' - }) - }) - - it('keeps single-letter contractions lowercase after the apostrophe', () => { - expect( - getWorkspaceIntentName({ - sourceText: 'https://github.com/acme/app/issues/17', - workItem: { - type: 'issue', - number: 17, - title: "i'm blocked on notifications" - } - }) - ).toEqual({ - displayName: "Issue 17 I'm Blocked Notifications", - seedName: 'issue-17-im-blocked-notifications' - }) - - expect( - getWorkspaceIntentName({ - sourceText: 'https://github.com/acme/app/issues/18', - workItem: { - type: 'issue', - number: 18, - title: "i'll update login" - } - }) - ).toEqual({ - displayName: "Issue 18 I'll Update Login", - seedName: 'issue-18-ill-update-login' - }) - }) - it('does not treat an auto-generated slug as explicit user intent', () => { expect( getWorkspaceIntentName({ @@ -292,69 +136,9 @@ describe('getWorkspaceIntentName', () => { seedName: 'issue-456-make-importer-handle' }) }) - - it('uses external provider identifiers without duplicating them in the subject', () => { - expect( - getWorkspaceIntentName({ - workItem: { - type: 'issue', - provider: 'jira', - number: 0, - title: 'PROJ-7 Fix flaky import', - jiraIdentifier: 'PROJ-7' - } - }) - ).toEqual({ - displayName: 'PROJ-7 Fix Flaky Import', - seedName: 'proj-7-fix-flaky-import' - }) - }) - - it('summarizes unlinked task text into a shared display and seed', () => { - expect(getWorkspaceIntentName({ sourceText: 'add keyboard shortcut settings' })).toEqual({ - displayName: 'Add Keyboard Shortcut Settings', - seedName: 'add-keyboard-shortcut-settings' - }) - }) - - it('compacts pasted task text without whitespace regex splitting', () => { - const split = vi.spyOn(String.prototype, 'split') - const sourceText = [ - 'https://github.com/acme/app/issues/123', - '\nadd', - String.fromCharCode(160), - 'keyboard\tshortcut settings' - ].join('') - - expect(getWorkspaceIntentName({ sourceText })).toEqual({ - displayName: 'Add Keyboard Shortcut Settings', - seedName: 'add-keyboard-shortcut-settings' - }) - expect( - split.mock.calls.filter(([pattern]) => pattern instanceof RegExp && pattern.source === '\\s+') - ).toHaveLength(0) - }) }) describe('getLinearIssueWorkspaceName', () => { - it('keeps the Linear identifier in the workspace seed', () => { - expect( - getLinearIssueWorkspaceName({ - identifier: 'ENG-42', - title: 'Ship Linear parity' - }) - ).toBe('eng-42-ship-linear-parity') - }) - - it('does not duplicate an identifier already present in the Linear title', () => { - expect( - getLinearIssueWorkspaceName({ - identifier: 'ENG-42', - title: 'ENG-42 Ship Linear parity' - }) - ).toBe('eng-42-ship-linear-parity') - }) - it('keeps the combined Linear seed within the workspace-name limit', () => { const seed = getLinearIssueWorkspaceName({ identifier: 'ENG-42', @@ -380,9 +164,4 @@ describe('resolveWorkspaceCreateName', () => { }) ).toBe('日本語 テスト') }) - - it('uses the stable fallback when the draft is blank', () => { - expect(resolveWorkspaceCreateName({ draft: ' ', fallback: 'pr-9' })).toBe('pr-9') - expect(resolveWorkspaceCreateName({ draft: undefined, fallback: 'issue-4' })).toBe('issue-4') - }) }) diff --git a/src/shared/workspace-reference-identity.test.ts b/src/shared/workspace-reference-identity.test.ts index 01f1e255465..4a415a6c50c 100644 --- a/src/shared/workspace-reference-identity.test.ts +++ b/src/shared/workspace-reference-identity.test.ts @@ -111,12 +111,6 @@ describe('reference URLs', () => { ])('rejects ambiguous or invalid input %s', (input) => { expect(() => parseWorkspaceReferenceUrl(input)).toThrow() }) - - it('accepts a known provider hint for self-hosted issue metadata', () => { - expect( - parseWorkspaceReferenceUrl('https://git.example/acme/api/issues/42', 'gitea').provider - ).toBe('gitea') - }) }) describe('reference identity', () => { @@ -126,17 +120,6 @@ describe('reference identity', () => { ) }) - it.each([ - ['https://www.github.com/acme/api/pull/42', 'https://github.com/acme/api/pull/42'], - [ - 'https://gitlab.com/Group/Api/-/merge_requests/42', - 'https://gitlab.com/group/api/-/merge_requests/42' - ], - ['https://codeberg.org/Acme/Api/pulls/42', 'https://codeberg.org/acme/api/pulls/42'] - ])('treats case-insensitive host routes as one source: %s vs %s', (a, b) => { - expect(identity(a)).toBe(identity(b)) - }) - it.each([ 'https://github.com/ACME/Api/pull/42', 'https://git.example/Root/Group/Api/-/merge_requests/42', @@ -174,13 +157,6 @@ describe('reference identity', () => { expect(identity(a)).not.toBe(identity(b)) }) - it('does not let local repository IDs split a proven external identity', () => { - const reference = parseWorkspaceReferenceUrl('https://github.com/acme/api/pull/42') - expect(getWorkspaceReferenceIdentity({ ...reference, repoId: 'repo-a' })).toBe( - getWorkspaceReferenceIdentity({ ...reference, repoId: 'repo-b' }) - ) - }) - it('matches URL-less entries only when stored provider metadata proves the source', () => { const item: WorkspaceAttachment = { provider: 'github', diff --git a/src/shared/worktree-execution-host-resolution.test.ts b/src/shared/worktree-execution-host-resolution.test.ts index 3d326c5168c..026629997da 100644 --- a/src/shared/worktree-execution-host-resolution.test.ts +++ b/src/shared/worktree-execution-host-resolution.test.ts @@ -20,16 +20,6 @@ describe('resolveWorktreeExecutionHost', () => { ).toEqual({ kind: 'resolved', hostId: 'ssh:m4air', connectionId: 'm4air', owner: null }) }) - it('answers before the repo row hydrates, because the host is not a guess', () => { - // An explicit host needs no repo hydration to resolve; #6648 blocks unknown hosts only. - expect(resolve([], { repoId: 'r', hostId: 'ssh:m4air' })).toEqual({ - kind: 'resolved', - hostId: 'ssh:m4air', - connectionId: 'm4air', - owner: null - }) - }) - it('picks the row on that host when both SSH hosts carry the id', () => { const openclaw = { id: 'r', connectionId: 'openclaw' } const m4air = { id: 'r', connectionId: 'm4air' } @@ -47,16 +37,6 @@ describe('resolveWorktreeExecutionHost', () => { }) }) - it('matches a row that names the host in either spelling', () => { - const stamped = { id: 'r', executionHostId: 'ssh:m4air' } - expect(resolve([stamped], { repoId: 'r', hostId: 'ssh:m4air' })).toEqual({ - kind: 'resolved', - hostId: 'ssh:m4air', - connectionId: 'm4air', - owner: stamped - }) - }) - it('takes no connection from a row on a different host, whatever this host is', () => { for (const hostId of ['local', 'runtime:env-a']) { expect(resolve([{ id: 'r', connectionId: 'openclaw' }], { repoId: 'r', hostId })).toEqual({ @@ -91,30 +71,6 @@ describe('resolveWorktreeExecutionHost', () => { }) describe('the worktree names no host', () => { - it('resolves from the sole row, in either spelling', () => { - const legacy = { id: 'r', connectionId: 'openclaw' } - expect(resolve([legacy], { repoId: 'r' })).toEqual({ - kind: 'resolved', - hostId: 'ssh:openclaw', - connectionId: 'openclaw', - owner: legacy - }) - const stamped = { id: 'r', executionHostId: 'ssh:m4air' } - expect(resolve([stamped], { repoId: 'r' })).toEqual({ - kind: 'resolved', - hostId: 'ssh:m4air', - connectionId: 'm4air', - owner: stamped - }) - const local = { id: 'r' } - expect(resolve([local], { repoId: 'r' })).toEqual({ - kind: 'resolved', - hostId: 'local', - connectionId: null, - owner: local - }) - }) - it('refuses when rival rows disagree about the host, including two SSH hosts', () => { expect( resolve( @@ -144,10 +100,6 @@ describe('resolveWorktreeExecutionHost', () => { ).toMatchObject({ kind: 'resolved', hostId: 'ssh:m4air', connectionId: 'm4air' }) }) - it('reports an unknown owner distinctly from a conflicting one', () => { - expect(resolve([], { repoId: 'r' })).toEqual({ kind: 'unresolved', reason: 'unknown' }) - }) - // Unknown may fall back to a local folder; a declared but unparseable host must fail closed. it('reports a row naming an unparseable host distinctly from an unknown one', () => { for (const executionHostId of ['ssh:', 'ssh:a|b', 'ssh:%zz', 'runtime:', 'quantum:box']) { @@ -165,21 +117,6 @@ describe('resolveWorktreeExecutionHost', () => { }) ).toEqual({ kind: 'unresolved', reason: 'malformed' }) }) - - it('still resolves every row that names a parseable host', () => { - expect(resolve([{ id: 'r', executionHostId: 'ssh:box' }], { repoId: 'r' })).toMatchObject({ - kind: 'resolved', - hostId: 'ssh:box' - }) - expect(resolve([{ id: 'r', connectionId: 'box' }], { repoId: 'r' })).toMatchObject({ - kind: 'resolved', - hostId: 'ssh:box' - }) - expect(resolve([{ id: 'r' }], { repoId: 'r' })).toMatchObject({ - kind: 'resolved', - hostId: 'local' - }) - }) }) it('ignores an unparseable host id rather than treating it as a host', () => { @@ -224,31 +161,4 @@ describe('createRepoRowExecutionHostLookup', () => { } expect(idReads()).toBe(afterBuild) }) - - it('answers missing, ambiguous and resolved exactly as a per-call scan would', () => { - expect(createRepoRowExecutionHostLookup([]).byId('r')).toEqual({ kind: 'missing' }) - - const openclaw = { id: 'r', connectionId: 'openclaw' } - const m4air = { id: 'r', connectionId: 'm4air' } - expect(createRepoRowExecutionHostLookup([openclaw, m4air]).byId('r')).toEqual({ - kind: 'ambiguous' - }) - - const first: ExecutionHostOwnerRow = { id: 'r', connectionId: 'm4air' } - const second: ExecutionHostOwnerRow = { id: 'r', executionHostId: 'ssh:m4air' } - expect(createRepoRowExecutionHostLookup([first, second]).byId('r')).toEqual({ - kind: 'resolved', - owner: first - }) - }) - - it('keeps byHost hits, misses and repo-list order', () => { - const openclaw = { id: 'r', connectionId: 'openclaw' } - const m4air = { id: 'r', connectionId: 'm4air' } - const lookup = createRepoRowExecutionHostLookup([openclaw, m4air]) - expect(lookup.byHost('r', 'ssh:m4air')).toBe(m4air) - expect(lookup.byHost('r', 'ssh:openclaw')).toBe(openclaw) - expect(lookup.byHost('r', 'local')).toBeNull() - expect(lookup.byHost('other', 'local')).toBeNull() - }) }) diff --git a/src/shared/worktree/id.test.ts b/src/shared/worktree/id.test.ts index 3129aa52a62..0af0de6a6f6 100644 --- a/src/shared/worktree/id.test.ts +++ b/src/shared/worktree/id.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' import { - WORKTREE_ID_SEPARATOR, - composeWorktreeId, - getRepoIdFromWorktreeId, - getRepoMainWorktreeId, getWorktreePathBasenameFromId, splitWorktreeId, splitWorktreeIdForFilesystem, @@ -11,94 +7,7 @@ import { worktreeIdsEqual } from './id' -describe('getRepoMainWorktreeId', () => { - it('round-trips through the id parsers on posix and Windows paths', () => { - for (const repo of [ - { id: 'repo-123', path: '/abs/path' }, - { id: 'repo-123', path: 'C:\\Users\\me\\repo' } - ]) { - const worktreeId = getRepoMainWorktreeId(repo) - expect(worktreeId).toBe(`${repo.id}${WORKTREE_ID_SEPARATOR}${repo.path}`) - expect(splitWorktreeId(worktreeId)).toEqual({ repoId: repo.id, worktreePath: repo.path }) - } - }) -}) - -describe('composeWorktreeId', () => { - it('keeps the path spelling exactly as given', () => { - for (const path of [ - '/srv/Repo/', - 'C:\\Users\\me\\repo', - '//wsl$/Ubuntu/home/a', - '/private/tmp/x' - ]) { - expect(composeWorktreeId('repo-1', path)).toBe(`repo-1${WORKTREE_ID_SEPARATOR}${path}`) - } - }) -}) - -describe('getRepoIdFromWorktreeId', () => { - it('returns the repo id for a canonical worktree id', () => { - expect(getRepoIdFromWorktreeId('repo-123::/abs/path')).toBe('repo-123') - }) - - it('returns the whole input when there is no separator', () => { - expect(getRepoIdFromWorktreeId('just-a-repo-id')).toBe('just-a-repo-id') - }) - - it('returns the empty string for an empty input', () => { - expect(getRepoIdFromWorktreeId('')).toBe('') - }) - - it('returns an empty repo id for a bare separator', () => { - expect(getRepoIdFromWorktreeId('::')).toBe('') - }) - - it('returns an empty repo id for a leading separator', () => { - expect(getRepoIdFromWorktreeId('::path')).toBe('') - }) - - it('returns the repo id when only a trailing separator is present', () => { - expect(getRepoIdFromWorktreeId('repo::')).toBe('repo') - }) - - it('splits on the first separator when the path itself contains "::"', () => { - expect(getRepoIdFromWorktreeId('repo::a::b')).toBe('repo') - }) -}) - describe('splitWorktreeId', () => { - it('splits a canonical worktree id into repo id and path', () => { - expect(splitWorktreeId('repo-123::/abs/path')).toEqual({ - repoId: 'repo-123', - worktreePath: '/abs/path' - }) - }) - - it('returns null when there is no separator', () => { - expect(splitWorktreeId('just-a-repo-id')).toBeNull() - }) - - it('returns null for an empty input', () => { - expect(splitWorktreeId('')).toBeNull() - }) - - it('returns empty repo id and empty path for a bare separator', () => { - expect(splitWorktreeId('::')).toEqual({ repoId: '', worktreePath: '' }) - }) - - it('returns an empty repo id when the separator is leading', () => { - expect(splitWorktreeId('::path')).toEqual({ repoId: '', worktreePath: 'path' }) - }) - - it('returns an empty path when the separator is trailing', () => { - expect(splitWorktreeId('repo::')).toEqual({ repoId: 'repo', worktreePath: '' }) - }) - - it('splits on the first separator when the path itself contains "::"', () => { - expect(splitWorktreeId('repo::a::b')).toEqual({ repoId: 'repo', worktreePath: 'a::b' }) - }) - it('preserves folder workspace instance suffixes in the literal parsed path', () => { expect( splitWorktreeId('repo::/folder::workspace:123e4567-e89b-12d3-a456-426614174000') @@ -118,18 +27,6 @@ describe('splitWorktreeIdForFilesystem', () => { }) describe('getWorktreePathBasenameFromId', () => { - it('returns the path basename for POSIX worktree ids', () => { - expect(getWorktreePathBasenameFromId('repo-123::/abs/path/nightly-checks')).toBe( - 'nightly-checks' - ) - }) - - it('returns the path basename for Windows worktree ids', () => { - expect(getWorktreePathBasenameFromId('repo-123::C:\\workspaces\\nightly-checks')).toBe( - 'nightly-checks' - ) - }) - it('returns the real folder basename for folder workspace instance ids', () => { expect( getWorktreePathBasenameFromId( @@ -137,11 +34,6 @@ describe('getWorktreePathBasenameFromId', () => { ) ).toBe('project') }) - - it('returns null when no worktree path is available', () => { - expect(getWorktreePathBasenameFromId('repo-123')).toBeNull() - expect(getWorktreePathBasenameFromId('repo-123::')).toBeNull() - }) }) /** ID selectors must fold exactly the path spellings path: selectors fold (#16243). */ diff --git a/src/shared/worktree/ownership-configured-base-visibility.test.ts b/src/shared/worktree/ownership-configured-base-visibility.test.ts index a5827d239ac..ca39c2f1580 100644 --- a/src/shared/worktree/ownership-configured-base-visibility.test.ts +++ b/src/shared/worktree/ownership-configured-base-visibility.test.ts @@ -6,7 +6,6 @@ import { resolveConfiguredWorktreeBasePaths } from './configured-worktree-base-p import { createWorktreeVisibilitySourceMatcher } from './visibility-sources' import { buildKnownOrcaWorkspaceLayouts, - classifyWorktreeOwnership, toDetectedWorktree, EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT } from './ownership' @@ -71,39 +70,6 @@ describe('a configured worktree base that collides with a built-in visibility so const configuredBaseWorktree = '/repos/OrbisCXM/.claude/worktrees/OrbisCXM/squash-migrations' const tempScratchpadWorktree = '/tmp/claude/abc/scratchpad/sonda-voter' - it('classifies a plain git worktree in the configured base as external, not agent scratch', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - const settings = makeSettings() - - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree(configuredBaseWorktree), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - - it('shows identically-provenanced worktrees the same way wherever they live', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - - expect(detect(repo, tempScratchpadWorktree)).toMatchObject({ - ownership: 'external', - visible: true - }) - expect(detect(repo, configuredBaseWorktree)).toMatchObject({ - ownership: 'external', - visible: true - }) - }) - - it('does not attribute the configured base to the built-in source row', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - - expect(detect(repo, configuredBaseWorktree).visibilitySource).toBeUndefined() - }) - it('hands the configured base to the project external-worktree setting', () => { const repo = makeRepo({ worktreeBasePath: '.claude/worktrees', @@ -184,15 +150,6 @@ describe('a configured worktree base that collides with a built-in visibility so expect(detected.visibilitySource).toBeUndefined() }) - it('resolves an absolute configured base that matches a built-in source', () => { - const repo = makeRepo({ worktreeBasePath: '/repos/OrbisCXM/.claude/worktrees' }) - - expect(detect(repo, configuredBaseWorktree)).toMatchObject({ - ownership: 'external', - visible: true - }) - }) - it('keeps the configured base external when workspace nesting is flat', () => { const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) @@ -208,45 +165,6 @@ describe('a configured worktree base that collides with a built-in visibility so describe('agent scratch stays hidden for repos that did not configure that base (#9388)', () => { const scratchWorktree = '/repos/OrbisCXM/.claude/worktrees/OrbisCXM/agent-a04ccaaa' - it('hides an unconfigured .claude/worktrees checkout', () => { - const detected = detect(makeRepo(), scratchWorktree) - - expect(detected).toMatchObject({ - ownership: 'agent-scratch', - visible: false, - visibilitySource: { kind: 'built-in', id: 'claude' } - }) - }) - - it('hides it when the configured base points somewhere else', () => { - const repo = makeRepo({ worktreeBasePath: '../worktrees' }) - - expect(detect(repo, scratchWorktree)).toMatchObject({ - ownership: 'agent-scratch', - visible: false - }) - }) - - it('hides scratch nested under a linked checkout even with a configured base', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - const linkedCheckout = '/orca/workspaces/OrbisCXM/feature-x' - const settings = makeSettings() - - expect( - toDetectedWorktree({ - repo, - settings, - worktree: makeWorktree(`${linkedCheckout}/.claude/worktrees/agent-a04ccaaa`), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo), - worktreeVisibilitySourceMatcher: createWorktreeVisibilitySourceMatcher( - [repo.path, linkedCheckout], - [], - resolveConfiguredWorktreeBasePaths(repo) - ) - }) - ).toMatchObject({ ownership: 'agent-scratch', visible: false }) - }) - it('hides scratch when the configured base only contains the scratch root', () => { for (const worktreeBasePath of ['.', '..', '../worktrees/..']) { expect(detect(makeRepo({ worktreeBasePath }), scratchWorktree)).toMatchObject({ @@ -276,13 +194,4 @@ describe('agent scratch stays hidden for repos that did not configure that base }) ).toMatchObject({ ownership: 'agent-scratch', visible: false }) }) - - it('hides the other built-in source inside a repo that configured only the claude one', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - - expect(detect(repo, '/repos/OrbisCXM/.gsd-workspaces/phase-1')).toMatchObject({ - ownership: 'agent-scratch', - visible: false - }) - }) }) diff --git a/src/shared/worktree/ownership-worktree-base-path.test.ts b/src/shared/worktree/ownership-worktree-base-path.test.ts index 2033cc4d416..c039beb587d 100644 --- a/src/shared/worktree/ownership-worktree-base-path.test.ts +++ b/src/shared/worktree/ownership-worktree-base-path.test.ts @@ -30,34 +30,6 @@ function makeSettings(overrides: Partial = {}): GlobalSettings { } describe('repo-specific worktree ownership layouts', () => { - it('lets an explicitly configured Claude base outrank the built-in scratch path', () => { - const repo = makeRepo({ worktreeBasePath: '.claude/worktrees' }) - const settings = makeSettings() - const worktree = makeWorktree('/projects/a/repo/.claude/worktrees/repo/feature') - - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree, - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - - it('keeps the same path classified as scratch without the explicit base', () => { - const repo = makeRepo() - const settings = makeSettings() - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree('/projects/a/repo/.claude/worktrees/repo/feature'), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('agent-scratch') - }) - it('resolves the same relative base path from each repo root', () => { const settings = makeSettings() const repoA = makeRepo({ path: '/projects/a/repo', worktreeBasePath: '../worktrees' }) @@ -89,47 +61,6 @@ describe('repo-specific worktree ownership layouts', () => { ).toBe('external') }) - it('uses repo-specific nested layouts for Windows-style paths', () => { - const repo = makeRepo({ - path: 'C:\\projects\\App\\repo', - worktreeBasePath: '..\\worktrees' - }) - const settings = makeSettings({ workspaceDir: 'D:\\global' }) - - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree('C:\\projects\\App\\worktrees\\repo\\Feature'), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - - it('resolves an absolute Linux base path of a WSL repo into its distro layout (STA-4772)', () => { - const repo = makeRepo({ - path: '\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\src\\repo', - worktreeBasePath: '/home/jin/src/.orca-worktrees' - }) - const settings = makeSettings({ workspaceDir: 'C:\\global' }) - const layouts = buildKnownOrcaWorkspaceLayouts(settings, repo) - - expect(layouts[0]).toEqual({ - path: '//wsl.localhost/Ubuntu-24.04/home/jin/src/.orca-worktrees', - nestWorkspaces: true - }) - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree( - '\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\src\\.orca-worktrees\\repo\\feature' - ), - knownOrcaLayouts: layouts - }) - ).toBe('external') - }) - it('classifies worktrees under a dotted Linux base exactly where creation collapses it', () => { const repo = makeRepo({ path: '\\\\wsl.localhost\\Ubuntu-24.04\\home\\jin\\src\\repo', @@ -154,25 +85,6 @@ describe('repo-specific worktree ownership layouts', () => { ).toBe('external') }) - it('preserves mixed-case WSL paths while resolving the configured base', () => { - const repo = makeRepo({ - path: '\\\\wsl.localhost\\Ubuntu-24.04\\home\\Dev\\Repo', - worktreeBasePath: '/home/Dev/Repo/.claude/worktrees' - }) - const settings = makeSettings({ workspaceDir: 'C:\\global' }) - - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree( - '\\\\wsl.localhost\\Ubuntu-24.04\\home\\Dev\\Repo\\.claude\\worktrees\\feature' - ), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - it('includes relative global layouts for SSH repos without applying absolute desktop paths', () => { const repo = makeRepo({ path: '/remote/repo', connectionId: 'ssh-1' }) const relativeSettings = makeSettings({ workspaceDir: '../worktrees' }) diff --git a/src/shared/worktree/ownership.test.ts b/src/shared/worktree/ownership.test.ts index 1ba49f0cc6a..f19486b6a11 100644 --- a/src/shared/worktree/ownership.test.ts +++ b/src/shared/worktree/ownership.test.ts @@ -3,14 +3,12 @@ import type { GlobalSettings } from '../global-settings-types' import type { Repo } from '../repo-types' import type { WorktreeMeta } from './meta-types' import type { Worktree } from './types' -import { createAgentScratchWorktreePathMatcher } from '../agent-scratch-worktrees' + import { migrateExternalWorktreeVisibilityDefaults } from '../external-worktree-visibility' import { applyMetadataFallbackVisibility, buildKnownOrcaWorkspaceLayouts, classifyWorktreeOwnership, - effectiveExternalWorktreeVisibility, - isLegacyRepoForExternalWorktreeVisibility, shouldShowWorktree, toDetectedWorktree, EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT @@ -99,75 +97,6 @@ function makeSettings(overrides: Partial = {}): GlobalSettings { } describe('worktree ownership classification', () => { - it('treats explicit Orca metadata as managed even outside the workspace root', () => { - const repo = makeRepo() - const settings = makeSettings() - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/tmp/outside' }), - meta: makeMeta({ orcaCreatedAt: 1 }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('orca-managed') - }) - - it('treats nested Orca workspace paths without metadata as external', () => { - const repo = makeRepo() - const settings = makeSettings() - const layouts = buildKnownOrcaWorkspaceLayouts(settings, repo) - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/orca/workspaces/app/feature' }), - knownOrcaLayouts: layouts - }) - ).toBe('external') - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/orca/workspaces/other/feature' }), - knownOrcaLayouts: layouts - }) - ).toBe('external') - }) - - it('treats explicit Orca creation layout metadata as managed', () => { - const repo = makeRepo() - const settings = makeSettings() - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/orca/workspaces/app/feature' }), - meta: makeMeta({ - orcaCreationWorkspaceLayout: { path: '/orca/workspaces', nestWorkspaces: true } - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('orca-managed') - }) - - it('does not treat metadata-free nested workspace paths as Orca-managed for new repos', () => { - const repo = makeRepo({ externalWorktreeVisibility: 'hide' }) - const settings = makeSettings() - const detected = toDetectedWorktree({ - repo, - settings, - worktree: makeWorktree({ - path: '/orca/workspaces/app/manual-git-worktree', - isMainWorktree: false - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - - expect(detected.ownership).toBe('external') - expect(detected.visible).toBe(false) - }) - it('does not treat generic discovery metadata on nested workspace paths as Orca-managed', () => { const repo = makeRepo({ externalWorktreeVisibility: 'hide' }) const settings = makeSettings() @@ -186,56 +115,6 @@ describe('worktree ownership classification', () => { expect(detected.visible).toBe(false) }) - it('keeps nested workspace paths visible for legacy repos without explicit visibility', () => { - const repo = makeRepo() - const settings = makeSettings() - const detected = toDetectedWorktree({ - repo, - settings, - worktree: makeWorktree({ - path: '/orca/workspaces/app/manual-git-worktree', - isMainWorktree: false - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - - expect(detected.ownership).toBe('external') - expect(detected.visible).toBe(true) - }) - - it('hides metadata-free nested workspace paths for legacy repos that hide external worktrees', () => { - const repo = makeRepo({ - externalWorktreeVisibility: 'hide', - externalWorktreeVisibilityLegacy: true - }) - const settings = makeSettings() - const detected = toDetectedWorktree({ - repo, - settings, - worktree: makeWorktree({ - path: '/orca/workspaces/app/manual-git-worktree', - isMainWorktree: false - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - - expect(detected.ownership).toBe('external') - expect(detected.visible).toBe(false) - }) - - it('treats flat workspace-root descendants as unknown legacy without strong metadata', () => { - const repo = makeRepo() - const settings = makeSettings({ nestWorkspaces: false }) - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/orca/workspaces/feature' }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('unknown-legacy') - }) - it('keeps flat-layout history weak after switching the same root to nested mode', () => { const repo = makeRepo() const settings = makeSettings({ @@ -252,22 +131,6 @@ describe('worktree ownership classification', () => { ).toBe('unknown-legacy') }) - it('uses each historical layout nest mode when matching old roots', () => { - const repo = makeRepo() - const settings = makeSettings({ - workspaceDir: '/new/workspaces', - workspaceDirHistory: [{ path: '/old/workspaces', nestWorkspaces: true }] - }) - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: '/old/workspaces/app/feature' }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - it('builds known layouts from large workspace history lists', () => { const repo = makeRepo() const workspaceDirHistory = Array.from( @@ -293,23 +156,6 @@ describe('worktree ownership classification', () => { }) }) - it('handles Windows drive casing and separators', () => { - const repo = makeRepo({ path: 'C:\\repos\\App' }) - const settings = makeSettings({ workspaceDir: 'C:\\Orca\\Workspaces' }) - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ - id: 'repo-1::C:\\ORCA\\WORKSPACES\\App\\Feature', - path: 'C:\\ORCA\\WORKSPACES\\App\\Feature', - isMainWorktree: false - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('external') - }) - it('keeps selected linked checkouts visible without trusting Git main-worktree', () => { const repo = makeRepo({ path: '/repos/app-linked', externalWorktreeVisibility: 'hide' }) const settings = makeSettings() @@ -339,21 +185,6 @@ describe('worktree ownership classification', () => { }) describe('external worktree visibility policy', () => { - it('defaults undefined visibility to hide for new repos and show for legacy repos', () => { - expect(effectiveExternalWorktreeVisibility(makeRepo(), false)).toBe('hide') - expect(effectiveExternalWorktreeVisibility(makeRepo(), true)).toBe('show') - }) - - it('resolves repo override before host default before the compatibility fallback', () => { - expect( - effectiveExternalWorktreeVisibility(makeRepo({ externalWorktreeVisibility: 'show' }), false, { - external: 'hide' - }) - ).toBe('show') - expect(effectiveExternalWorktreeVisibility(makeRepo(), true, { external: 'hide' })).toBe('hide') - expect(effectiveExternalWorktreeVisibility(makeRepo(), true)).toBe('show') - }) - it('materializes only implicit legacy visibility when adding the safe global default', () => { const legacy = makeRepo({ externalWorktreeVisibilityLegacy: undefined }) const explicit = makeRepo({ id: 'explicit', externalWorktreeVisibility: 'hide' }) @@ -393,47 +224,6 @@ describe('external worktree visibility policy', () => { expect(migrated.repos[0]?.externalWorktreeVisibility).toBe('show') }) - it('treats persisted repos without explicit visibility as legacy for upgrade safety', () => { - expect(isLegacyRepoForExternalWorktreeVisibility(makeRepo())).toBe(true) - }) - - it('computes legacy status from rollout timing, not from the stored visibility value', () => { - expect( - isLegacyRepoForExternalWorktreeVisibility( - makeRepo({ - addedAt: EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT - 1, - externalWorktreeVisibility: 'hide' - }) - ) - ).toBe(true) - }) - - it('honors an explicit legacy marker after visibility changes', () => { - expect( - isLegacyRepoForExternalWorktreeVisibility( - makeRepo({ - externalWorktreeVisibility: 'hide', - externalWorktreeVisibilityLegacy: true - }) - ) - ).toBe(true) - expect( - isLegacyRepoForExternalWorktreeVisibility( - makeRepo({ - externalWorktreeVisibility: 'hide', - externalWorktreeVisibilityLegacy: false - }) - ) - ).toBe(false) - }) - - it('treats repos without a valid addedAt as legacy for upgrade safety', () => { - expect( - isLegacyRepoForExternalWorktreeVisibility(makeRepo({ addedAt: undefined as never })) - ).toBe(true) - expect(isLegacyRepoForExternalWorktreeVisibility(makeRepo({ addedAt: Number.NaN }))).toBe(true) - }) - it('shows explicitly imported external worktrees while repo visibility stays hide', () => { const repo = makeRepo({ externalWorktreeVisibility: 'hide', @@ -481,40 +271,6 @@ describe('external worktree visibility policy', () => { describe('agent scratch worktrees', () => { const scratchPath = '/repos/app/.claude/worktrees/agent-a04ccaaa55ddadb91' - it('classifies sub-agent scratch paths as agent-scratch without metadata', () => { - const repo = makeRepo() - const settings = makeSettings() - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ path: scratchPath, isMainWorktree: false }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - ).toBe('agent-scratch') - }) - - it('classifies scratch worktrees created inside another linked checkout', () => { - const repo = makeRepo() - const settings = makeSettings() - const linkedCheckoutPath = '/orca/workspaces/app/feature-x' - expect( - classifyWorktreeOwnership({ - repo, - settings, - worktree: makeWorktree({ - path: `${linkedCheckoutPath}/.claude/worktrees/agent-a04ccaaa`, - isMainWorktree: false - }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo), - agentScratchWorktreePathMatcher: createAgentScratchWorktreePathMatcher( - [repo.path, linkedCheckoutPath], - [] - ) - }) - ).toBe('agent-scratch') - }) - it('keeps strong Orca metadata authoritative over the scratch path match', () => { const repo = makeRepo() const settings = makeSettings() @@ -546,25 +302,6 @@ describe('agent scratch worktrees', () => { } }) - it('shows agent scratch when its repo policy is enabled', () => { - const repo = makeRepo({ - externalWorktreeVisibility: 'hide', - agentWorktreeVisibility: 'show' - }) - const settings = makeSettings() - - for (const path of [scratchPath, '/repos/app/.gsd-workspaces/phase-1']) { - const detected = toDetectedWorktree({ - repo, - settings, - worktree: makeWorktree({ path, isMainWorktree: false }), - knownOrcaLayouts: buildKnownOrcaWorkspaceLayouts(settings, repo) - }) - - expect(detected).toMatchObject({ ownership: 'agent-scratch', visible: true }) - } - }) - it('applies built-in source preferences independently after legacy migration', () => { const repo = makeRepo({ agentWorktreeVisibility: 'show', @@ -615,32 +352,6 @@ describe('agent scratch worktrees', () => { expect(applyMetadataFallbackVisibility(detected).visible).toBe(false) }) - it('still shows agent scratch for the selected checkout or an explicit import', () => { - const repo = makeRepo({ - externalWorktreeVisibility: 'hide', - importedExternalWorktreePaths: [scratchPath] - }) - expect( - shouldShowWorktree({ - repo, - worktree: makeWorktree({ path: scratchPath, isMainWorktree: false }), - ownership: 'agent-scratch', - isLegacyRepoForVisibility: false, - isSelectedCheckout: false, - importedExternalWorktreePaths: repo.importedExternalWorktreePaths - }) - ).toBe(true) - expect( - shouldShowWorktree({ - repo: makeRepo(), - worktree: makeWorktree({ path: scratchPath, isMainWorktree: false }), - ownership: 'agent-scratch', - isLegacyRepoForVisibility: false, - isSelectedCheckout: true - }) - ).toBe(true) - }) - it('keeps agent scratch hidden in the metadata fallback while revealing the rest', () => { const repo = makeRepo() const settings = makeSettings() diff --git a/src/shared/worktree/visibility-sources.test.ts b/src/shared/worktree/visibility-sources.test.ts index eb2473d32e6..044434ba0d6 100644 --- a/src/shared/worktree/visibility-sources.test.ts +++ b/src/shared/worktree/visibility-sources.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import type { Repo } from '../repo-types' import { buildDefaultWorktreeSourcePreferenceUpdate, @@ -25,19 +25,6 @@ function repo(overrides: Partial = {}): Repo { } describe('worktree visibility sources', () => { - it('classifies each built-in independently across linked checkouts', () => { - const classify = createWorktreeVisibilitySourceMatcher(['/repo', '/worktrees/feature'], [], []) - expect(classify('/repo/.claude/worktrees/review')).toEqual({ - kind: 'built-in', - id: 'claude' - }) - expect(classify('/worktrees/feature/.gsd-workspaces/phase-1')).toEqual({ - kind: 'built-in', - id: 'gsd' - }) - expect(classify('/other/.claude/worktrees/review')).toBeNull() - }) - it('matches custom descendants with Windows and WSL comparison semantics', () => { const windows = createWorktreeVisibilitySourceMatcher( [], @@ -61,26 +48,6 @@ describe('worktree visibility sources', () => { expect(wsl('//wsl.localhost/Ubuntu/home/Dev/team/feature')).toBeNull() }) - it('yields a built-in root to a worktree base the project configured there', () => { - const classify = createWorktreeVisibilitySourceMatcher( - ['/repo'], - [], - ['/repo/.claude/worktrees'] - ) - expect(classify('/repo/.claude/worktrees/review')).toBeNull() - expect(classify('/repo/.gsd-workspaces/phase-1')).toEqual({ kind: 'built-in', id: 'gsd' }) - }) - - it('preserves WSL path casing when a configured base supersedes a built-in root', () => { - const classify = createWorktreeVisibilitySourceMatcher( - ['//wsl$/Ubuntu/home/Dev/repo'], - [], - ['//wsl.localhost/Ubuntu/home/Dev/repo/.claude/worktrees'] - ) - - expect(classify('//wsl.localhost/Ubuntu/home/Dev/repo/.claude/worktrees/review')).toBeNull() - }) - it('keeps a built-in root a base merely contains', () => { for (const configuredBase of ['/repo', '/']) { const classify = createWorktreeVisibilitySourceMatcher(['/repo'], [], [configuredBase]) @@ -125,37 +92,6 @@ describe('worktree visibility sources', () => { }) }) - it('normalizes each candidate once regardless of checkout count', () => { - const classify = createWorktreeVisibilitySourceMatcher( - Array.from({ length: 200 }, (_, index) => `/repo/worktree-${index}`), - Array.from({ length: 32 }, (_, index) => ({ - id: `custom-${index}`, - rootPath: `/custom/${index}` - })), - [] - ) - const normalize = vi.spyOn(String.prototype, 'normalize') - - classify('/unmatched/worktree') - - expect(normalize).toHaveBeenCalledTimes(1) - normalize.mockRestore() - }) - - it('normalizes each candidate once when a configured base supersedes a built-in root', () => { - const classify = createWorktreeVisibilitySourceMatcher( - ['/repo'], - [], - ['/repo/.claude/worktrees'] - ) - const normalize = vi.spyOn(String.prototype, 'normalize') - - classify('/repo/.claude/worktrees/review') - - expect(normalize).toHaveBeenCalledTimes(1) - normalize.mockRestore() - }) - it('migrates the optional legacy agent policy lazily for both built-ins', () => { const legacy = repo({ agentWorktreeVisibility: 'show' }) expect(effectiveBuiltInWorktreeSourceVisibility(legacy, 'claude')).toBe('show') diff --git a/src/shared/wsl-login-shell-command.test.ts b/src/shared/wsl-login-shell-command.test.ts index 8b280841bc8..4b8b730088a 100644 --- a/src/shared/wsl-login-shell-command.test.ts +++ b/src/shared/wsl-login-shell-command.test.ts @@ -9,7 +9,6 @@ import { buildWslInteractiveLoginShellCommand, buildWslLoginShellCommand } from './wsl-login-shell-command' -import { quotePosixShell } from './shell-argument-quoting' const WSL_TEST_COMMAND_TIMEOUT_MS = 10_000 let wslShAvailable: boolean | null = null @@ -32,42 +31,7 @@ function canRunWslSh(): boolean { return wslShAvailable } -function expectValidShSyntax(command: string): void { - try { - execFileSync('sh', ['-n'], { input: command, timeout: WSL_TEST_COMMAND_TIMEOUT_MS }) - return - } catch (error) { - if ( - process.platform !== 'win32' || - !(error instanceof Error && 'code' in error && error.code === 'ENOENT') - ) { - throw error - } - } - if (!canRunWslSh()) { - return - } - execFileSync('wsl.exe', ['--exec', 'sh', '-n'], { - input: command, - timeout: WSL_TEST_COMMAND_TIMEOUT_MS - }) -} - describe('wsl login shell command helpers', () => { - it('quotes single quotes for POSIX shell arguments', () => { - expect(quotePosixShell("a'b")).toBe("'a'\\''b'") - }) - - it('runs commands through the distro user login shell', () => { - const command = buildWslLoginShellCommand("printf 'hello'") - - expect(command).toContain('getent passwd') - expect(command).toContain('bash|zsh|ksh|mksh|ash)') - expect(command).toContain('exec "$_orca_wsl_shell" -ilc') - expect(command).toContain('exec /bin/sh -lc') - expect(command).toContain("printf '\\''hello'\\''") - }) - it.skipIf(process.platform === 'win32')( 'resolves env-node launchers from the current login-shell PATH on every run', () => { @@ -123,31 +87,6 @@ describe('wsl login shell command helpers', () => { } ) - it('keeps command-scoped environment variables in the quoted payload', () => { - const command = buildWslLoginShellCommand('HISTFILE=/tmp/orca-history printf "$HISTFILE"') - - expect(command).toContain('\'HISTFILE=/tmp/orca-history printf "$HISTFILE"\'') - expectValidShSyntax(command) - }, 30_000) - - it('routes through --exec so wsl.exe cannot preprocess argv', () => { - expect(buildWslExecArgs('Ubuntu', ['sh', '-lc', 'printf "$HOME"'])).toEqual([ - '-d', - 'Ubuntu', - '--exec', - 'sh', - '-lc', - 'printf "$HOME"' - ]) - // A distro-less target still has to bypass the `--` preprocessor. - expect(buildWslExecArgs(undefined, ['sh', '-c', 'true'])).toEqual([ - '--exec', - 'sh', - '-c', - 'true' - ]) - }) - it('preserves user command variables across the Windows-to-WSL argv boundary', () => { if (!canRunWslSh()) { return @@ -192,17 +131,6 @@ describe('wsl login shell command helpers', () => { ) describe('captured login shell', () => { - it('returns only the fenced payload, dropping rc banner noise', () => { - const captured = buildWslCapturedLoginShellCommand('printf directory', 'nonce1') - - expect( - captured.readStdout( - 'To run a command as administrator (user "root"), use "sudo ".\n\n' + - '__ORCA_WSL_CAPTURE_BEGIN_nonce1__directory__ORCA_WSL_CAPTURE_END_nonce1__' - ) - ).toBe('directory') - }) - it.skipIf(!canRunWslSh())( 'propagates the payload exit status to the caller', () => { @@ -225,13 +153,6 @@ describe('wsl login shell command helpers', () => { 30_000 ) - it('emits the status plumbing the payload needs', () => { - const captured = buildWslCapturedLoginShellCommand('exit 2', 'nonce1') - - expect(captured.command).toContain('_orca_capture_status=$?') - expect(captured.command).toContain('exit $_orca_capture_status') - }) - it('keeps payload bytes that themselves contain a fence', () => { // A per-call nonce prevents payloads quoting an older marker from truncating capture. const captured = buildWslCapturedLoginShellCommand('cat -- /f', 'nonce2') @@ -244,12 +165,6 @@ describe('wsl login shell command helpers', () => { ).toBe(payload) }) - it('returns null when the fence never appeared', () => { - const captured = buildWslCapturedLoginShellCommand('printf hi', 'nonce1') - - expect(captured.readStdout('bash: line 1: command not found\n')).toBeNull() - }) - it('returns the tail when a payload exits before the closing fence', () => { const captured = buildWslCapturedLoginShellCommand('printf partial; exit 2', 'nonce1') @@ -257,57 +172,10 @@ describe('wsl login shell command helpers', () => { 'partial' ) }) - - it('gives each invocation a distinct fence', () => { - const first = buildWslCapturedLoginShellCommand('printf hi') - const second = buildWslCapturedLoginShellCommand('printf hi') - - expect(first.command).not.toBe(second.command) - }) - - it.skipIf(!canRunWslSh())( - 'reads a clean payload back from a real distro login shell', - () => { - const captured = buildWslCapturedLoginShellCommand('printf directory') - const stdout = execFileSync( - 'wsl.exe', - buildWslExecArgs(undefined, ['sh', '-lc', captured.command]), - { encoding: 'utf8', timeout: WSL_TEST_COMMAND_TIMEOUT_MS } - ) - - expect(captured.readStdout(stdout)).toBe('directory') - }, - 30_000 - ) - }) - - it('starts an interactive login shell without assuming bash', () => { - const command = buildWslInteractiveLoginShellCommand() - - expect(command).toContain('getent passwd') - expect(command).toContain('if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then') - expect(command).toContain('_orca_shell_ready_root=""') - expect(command).toContain('if [ -n "${ORCA_USER_DATA_PATH:-}" ]; then') - expect(command).toContain('_orca_wsl_shell_name=$(basename "$_orca_wsl_shell"') - expect(command).toContain('bash)') - expect(command).toContain('--rcfile "${_orca_shell_ready_root}/bash/rcfile"') - expect(command).toContain('zsh)') - expect(command).toContain('export ZDOTDIR="${_orca_shell_ready_root}/zsh"') - expect(command).toContain('exec "$_orca_wsl_shell" -l') - expectValidShSyntax(command) }) }) describe('in-guest wrapper root resolution', () => { - // Content-addressed wrappers include a host-chosen hash. Guessing from userData lost shell-ready/OSC 133/preflight for every WSL pane. - it('prefers the host-published root over the legacy user-data guess', () => { - const script = buildWslInteractiveLoginShellCommand() - expect(script).toContain('if [ -n "${ORCA_SHELL_READY_ROOT:-}" ]; then') - expect(script).toContain('_orca_shell_ready_root="${ORCA_SHELL_READY_ROOT%/}"') - // Retain the userData fallback for older hosts. - expect(script).toContain('elif [ -n "${ORCA_USER_DATA_PATH:-}" ]; then') - }) - it('resolves the published root ahead of the legacy path under a real shell', () => { const script = buildWslInteractiveLoginShellCommand() const prologue = script.split('_orca_wsl_shell_name=')[0] as string diff --git a/src/shared/wsl-paths.test.ts b/src/shared/wsl-paths.test.ts index ff218f0a019..82d77185885 100644 --- a/src/shared/wsl-paths.test.ts +++ b/src/shared/wsl-paths.test.ts @@ -3,31 +3,12 @@ import { foldWslUncPathCaseInsensitiveParts, getWslFilesystemBoundaryDistro, isDrvfsLinuxPath, - isWslUncPath, - parseWslUncPath, resolveWslRepoWorktreeBasePath, toWindowsWslDrivePath, - toWindowsWslPath, - toWindowsWslUncPath + toWindowsWslPath } from './wsl-paths' describe('wsl path helpers', () => { - it('parses modern and legacy WSL UNC paths without platform checks', () => { - expect(parseWslUncPath('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')).toEqual({ - distro: 'Ubuntu', - linuxPath: '/home/jin/repo' - }) - expect(parseWslUncPath('\\\\wsl$\\Debian\\home\\jin')).toEqual({ - distro: 'Debian', - linuxPath: '/home/jin' - }) - }) - - it('rejects ordinary Windows and POSIX paths', () => { - expect(isWslUncPath('C:\\Users\\jin\\repo')).toBe(false) - expect(isWslUncPath('/home/jin/repo')).toBe(false) - }) - it.each([ ['/home/jin/repo', '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo'], ['/', '\\\\wsl.localhost\\Ubuntu\\'], @@ -53,21 +34,6 @@ describe('wsl path helpers', () => { ] as const)('converts the DrvFs path %j without a distro lookup', (linuxPath, expected) => { expect(toWindowsWslDrivePath(linuxPath)).toBe(expected) }) - - it.each(['\r', '\n', '\u2028', '\u2029'])( - 'leaves a DrvFs line ending in %j on the distro UNC view', - (terminator) => { - expect(toWindowsWslPath(`/mnt/c/Users/jin${terminator}`, 'Ubuntu')).toBe( - `\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\jin${terminator}` - ) - } - ) - - it('keeps mounted-drive paths on the distro UNC view when requested', () => { - expect(toWindowsWslUncPath('/mnt/c/Users/jin', 'Ubuntu')).toBe( - '\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\jin' - ) - }) }) describe('resolveWslRepoWorktreeBasePath', () => { @@ -110,14 +76,6 @@ describe('resolveWslRepoWorktreeBasePath', () => { expect(resolveWslRepoWorktreeBasePath(WSL_REPO, 'D:\\trees')).toBe('D:\\trees') expect(resolveWslRepoWorktreeBasePath(WSL_REPO, '../worktrees')).toBe('../worktrees') }) - - it('never rewrites base paths of non-WSL repos', () => { - expect(resolveWslRepoWorktreeBasePath('C:\\src\\repo', '/home/jin/trees')).toBe( - '/home/jin/trees' - ) - expect(resolveWslRepoWorktreeBasePath('/srv/repo', '/srv/trees')).toBe('/srv/trees') - expect(resolveWslRepoWorktreeBasePath('//server/share/repo', '/srv/trees')).toBe('/srv/trees') - }) }) describe('foldWslUncPathCaseInsensitiveParts', () => { @@ -144,41 +102,9 @@ describe('foldWslUncPathCaseInsensitiveParts', () => { '//wsl.localhost/ubuntu/MNT/c/Repo' ) }) - - it('returns null for non-WSL paths', () => { - expect(foldWslUncPathCaseInsensitiveParts('C:\\Users\\jin')).toBeNull() - expect(foldWslUncPathCaseInsensitiveParts('//server/share/x')).toBeNull() - expect(foldWslUncPathCaseInsensitiveParts('/home/jin')).toBeNull() - }) }) describe('getWslFilesystemBoundaryDistro', () => { - it('names the runtime distro for a Windows drive project running WSL git', () => { - expect( - getWslFilesystemBoundaryDistro({ - projectPath: 'C:\\Users\\alice\\orca', - wslRuntimeDistro: 'Ubuntu-24.04' - }) - ).toBe('Ubuntu-24.04') - }) - - it('stays silent for a Windows drive project running host git', () => { - for (const wslRuntimeDistro of [undefined, null, '']) { - expect( - getWslFilesystemBoundaryDistro({ projectPath: 'C:/Users/alice/orca', wslRuntimeDistro }) - ).toBeNull() - } - }) - - it('stays silent for a project already inside the distro', () => { - expect( - getWslFilesystemBoundaryDistro({ - projectPath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\orca', - wslRuntimeDistro: 'Ubuntu' - }) - ).toBeNull() - }) - // UNC drvfs paths cross onto the Windows drive regardless of runtime preference. it('names the path distro for the UNC spelling of a drvfs mount', () => { expect( @@ -187,12 +113,6 @@ describe('getWslFilesystemBoundaryDistro', () => { }) ).toBe('Ubuntu') }) - - it('stays silent for POSIX, relative, and plain UNC share paths', () => { - for (const projectPath of ['/home/alice/orca', 'orca', '\\\\fileserver\\share\\orca']) { - expect(getWslFilesystemBoundaryDistro({ projectPath, wslRuntimeDistro: 'Ubuntu' })).toBeNull() - } - }) }) describe('isDrvfsLinuxPath', () => {