diff --git a/src/main/agent-cli-version-probe.test.ts b/src/main/agent-cli-version-probe.test.ts index aa42f6828cd..11fde137963 100644 --- a/src/main/agent-cli-version-probe.test.ts +++ b/src/main/agent-cli-version-probe.test.ts @@ -122,7 +122,7 @@ it('probes the agent behind a launcher with tree teardown', async () => { }) it('probes Claude through its prefix with the same tree teardown', async () => { - prints('2.1.280') + prints('2.1.280 (Claude Code)') await expect( probeClaudeCliVersion(INPUT.program, { cwd: INPUT.cwd, diff --git a/src/main/agent-hooks/managed-hook-detection-commands.ts b/src/main/agent-hooks/managed-hook-detection-commands.ts index 12e94231984..48166c91d72 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.ts @@ -7,7 +7,7 @@ import { MANAGED_AGENT_HOOK_TARGETS } from '../../shared/managed-agent-hook-targ import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' import type { GlobalSettings } from '../../shared/global-settings-types' import type { TuiAgentDetectionCommand } from '../../shared/tui-agent-detection-commands' -import { parseClaudeCliVersion } from '../claude/claude-hook-event-versions' +import { parseClaudeCliVersionOutput } from '../claude/claude-hook-event-versions' export type ManagedHookDetectionSettings = Partial< Pick< @@ -67,7 +67,7 @@ export function readManagedHookDetectionResult(value: unknown): { : null return { agents, - claudeVersion: parseClaudeCliVersion( + claudeVersion: parseClaudeCliVersionOutput( typeof rawClaudeVersion === 'string' ? rawClaudeVersion : null ) } diff --git a/src/main/agent-hooks/wsl-hook-relay-manager.test.ts b/src/main/agent-hooks/wsl-hook-relay-manager.test.ts index 7058cf46062..0905a65dd78 100644 --- a/src/main/agent-hooks/wsl-hook-relay-manager.test.ts +++ b/src/main/agent-hooks/wsl-hook-relay-manager.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { RelayDispatcher } from '../../wsl-guest/dispatcher' import { registerWslHookFsHandlers } from '../../wsl-guest/wsl-hook-fs-bridge' import { SshChannelMultiplexer, type MultiplexerTransport } from '../ssh/ssh-channel-multiplexer' -import { createWslHookSftpAdapter } from './wsl-hook-fs-adapter' +import { createWslHookSftpAdapter, installWslGuestHooks } from './wsl-hook-fs-adapter' import { installRemoteManagedAgentHooks, REMOTE_MANAGED_HOOK_INSTALLER_AGENTS @@ -123,7 +123,8 @@ describe.skipIf(process.platform === 'win32')( it('runs the unchanged remote managed hook installers against a WSL guest home', async () => { const adapter = createWslHookSftpAdapter(harness.mux) const results = await installRemoteManagedAgentHooks(adapter, home, { - agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS + agents: REMOTE_MANAGED_HOOK_INSTALLER_AGENTS, + claudeVersion: '2.1.310' }) expect(results.length).toBeGreaterThan(0) @@ -132,7 +133,34 @@ describe.skipIf(process.platform === 'win32')( const claudeSettings = JSON.parse( readFileSync(join(home, '.claude', 'settings.json'), 'utf8') ) - expect(claudeSettings.hooks).toBeTruthy() + expect(claudeSettings.hooks.StopFailure).toBeTruthy() + let rawVersion = '11.17.0' + harness.guestDispatcher.onRequest('preflight.detectAgents', async () => ({ + agents: ['claude'], + versions: { claude: rawVersion } + })) + const reinstall = () => + installWslGuestHooks({ + mux: harness.mux, + guestHome: home, + codexHomePath: null, + distro: 'Ubuntu', + installHooks: installRemoteManagedAgentHooks, + settings: null, + warn: (message) => { + throw new Error(message) + }, + installCodex: async () => null + }) + await reinstall() + expect(JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'))).toEqual( + claudeSettings + ) + rawVersion = 'launcher 11.17.0\n1.0.64 (Claude Code)' + await reinstall() + const oldSettings = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8')) + expect(oldSettings.hooks.StopFailure).toBeUndefined() + expect(oldSettings.hooks.PreToolUse).toBeTruthy() const script = readFileSync(join(home, '.orca', 'agent-hooks', 'claude-hook.sh'), 'utf8') expect(script).toContain('/hook/claude') }, 20_000) @@ -465,21 +493,30 @@ describe('WslHookRelayManager', () => { expect(manager.getGuestEndpointFilePath('Ubuntu')).toBeNull() }) - it('forwards the WSL guest Claude version to the shared remote installer', async () => { - const waitForSentinel = vi.fn(async () => - guestTransport({ detectedAgents: ['claude'], claudeVersion: '2.1.261 (Claude Code)' }) - ) - const { manager, deps } = createManager({ waitForSentinel }) + it.each([ + ['2.1.221 (Claude Code)\r\n', '2.1.221'], + ['launcher 0.1.0\n2.1.310 (Claude Code)', '2.1.310'], + ['launcher 11.17.0\n1.0.64 (Claude Code)', '1.0.64'], + ['11.17.0', undefined], + [undefined, undefined] + ] as const)( + 'forwards identified WSL version without losing agent presence: %s', + async (raw, expected) => { + const waitForSentinel = vi.fn(async () => + guestTransport({ detectedAgents: ['claude'], claudeVersion: raw }) + ) + const { manager, deps } = createManager({ waitForSentinel }) - manager.ensureForDistro('Ubuntu') - await vi.waitFor(() => expect(deps.installHooks).toHaveBeenCalledTimes(1)) + manager.ensureForDistro('Ubuntu') + await vi.waitFor(() => expect(deps.installHooks).toHaveBeenCalledTimes(1)) - expect(deps.installHooks).toHaveBeenCalledWith(expect.anything(), home, { - agents: ['claude'], - claudeVersion: '2.1.261' - }) - manager.disposeAll() - }) + expect(deps.installHooks).toHaveBeenCalledWith(expect.anything(), home, { + agents: ['claude'], + ...(expected ? { claudeVersion: expected } : {}) + }) + manager.disposeAll() + } + ) it('reinstalls into a newly resolved runtime home without restarting the relay', async () => { const { manager, deps } = createManager({}) diff --git a/src/main/claude-accounts/claude-profile-router.test.ts b/src/main/claude-accounts/claude-profile-router.test.ts index f0ee60593ba..e97874c932a 100644 --- a/src/main/claude-accounts/claude-profile-router.test.ts +++ b/src/main/claude-accounts/claude-profile-router.test.ts @@ -1,3 +1,4 @@ +import { runProcess } from '@orca/process-host' import { spawnSync } from 'node:child_process' import { chmodSync, @@ -36,6 +37,10 @@ import { } from './claude-profile-installed-router' import { ClaudeObservedConfigDirs } from './claude-observed-config-dirs' +vi.mock('@orca/process-host', () => ({ runProcess: vi.fn() })) +const command = vi.hoisted(() => ({ path: '/unused-claude' })) +vi.mock('../codex-cli/command', () => ({ resolveClaudeCommand: () => command.path })) + const shell = vi.hoisted(() => ({ hang: false })) vi.mock('../startup/login-shell-environment', () => ({ resolveLoginShellEnvironment: () => @@ -156,6 +161,49 @@ function claudeShellProbe(root: string): (env: Record, rc?: stri } describe('ClaudeProfileRouter', () => { + it.each([ + ['launcher 0.1.0\n2.1.310 (Claude Code)', '2.1.310'], + ['11.17.0', undefined] + ] as const)( + 'passes identified version or unknown to enabled profile setup: %s', + async (stdout, version) => { + const f = fixture() + f.settings.agentStatusHooksEnabled = true + command.path = join(f.root, 'claude') + vi.mocked(runProcess).mockResolvedValue({ + stdout, + stderr: '', + code: 0, + signal: null, + timedOut: false + }) + const runSetup = vi.fn(async () => ({ + outcome: 'prepared' as const, + warnings: [], + surfaces: {} + })) + const router = new ClaudeProfileRouter({ + getSettings: () => f.settings, + dataRoot: f.dataRoot, + userHome: f.userHome, + env: {}, + runSetup + }) + await router.prepareAccount('a') + await router.prepareAccount('b') + expect(runSetup).toHaveBeenCalledTimes(2) + for (const accountId of ['a', 'b']) { + expect(runSetup).toHaveBeenCalledWith( + expect.objectContaining({ + profile: expect.objectContaining({ accountId }), + hooks: true, + claudeVersion: version + }) + ) + } + } + ) + it('runs an account with no login of its own on System default while that is signed in to it', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) const f = fixture() diff --git a/src/main/claude-accounts/claude-profile-setup.test.ts b/src/main/claude-accounts/claude-profile-setup.test.ts index 5363ea76aaf..0a8040652fa 100644 --- a/src/main/claude-accounts/claude-profile-setup.test.ts +++ b/src/main/claude-accounts/claude-profile-setup.test.ts @@ -18,6 +18,7 @@ vi.mock('node:os', async (original) => ({ homedir: () => state.home })) vi.mock('electron', () => ({ app: { getPath: () => state.home } })) +import { parseClaudeCliVersionOutput } from '../claude/claude-hook-event-versions' import { ClaudeHookService } from '../claude/hook-service' import { describeClaudeProfile } from './claude-profile-paths' import { provisionClaudeAccountProfile } from './claude-profile-setup' @@ -81,6 +82,16 @@ describe('Claude account profile setup', () => { }) expect(realpathSync(join(home, 'projects'))).toBe(realpathSync(join(f.defaultHome, 'projects'))) expect(existsSync(join(home, '.credentials.json'))).toBe(false) + const unknown = parseClaudeCliVersionOutput('11.17.0') ?? undefined + const refreshed = await provisionClaudeAccountProfile({ + dataRoot: f.dataRoot, + profile: describeClaudeProfile(f.dataRoot, 'a', local), + userHome: state.home, + installHooks: (target) => f.service.install({ ...target, claudeVersion: unknown }), + platform: 'linux' + }) + expect(refreshed.warnings).toEqual([]) + expect(JSON.parse(readFileSync(join(home, 'settings.json'), 'utf8'))).toEqual(settings) expect((await f.setup()).warnings).toEqual([]) }) itLinks('shares plugins Claude wrote into the folder before its first setup', async () => { diff --git a/src/main/claude/claude-diff-panel-auto-open.test.ts b/src/main/claude/claude-diff-panel-auto-open.test.ts index e5718c15c7f..6d12c0326d1 100644 --- a/src/main/claude/claude-diff-panel-auto-open.test.ts +++ b/src/main/claude/claude-diff-panel-auto-open.test.ts @@ -2,6 +2,7 @@ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { parseClaudeCliVersionOutput } from './claude-hook-event-versions' import { claudeDiffPanelMayAutoOpen, claudeDiffPanelStorePath } from './claude-diff-panel-auto-open' let home: string @@ -43,8 +44,16 @@ describe('claudeDiffPanelMayAutoOpen', () => { }) it('answers false for a Claude older than the panel, or an unknown version', async () => { + for (const [output, expected] of [ + ['11.17.0', false], + ['launcher 11.17.0\n1.0.64 (Claude Code)', false], + ['launcher 0.1.0\n2.1.310 (Claude Code)', true] + ] as const) { + expect(await mayAutoOpen(200, { version: parseClaudeCliVersionOutput(output) })).toBe( + expected + ) + } expect(await mayAutoOpen(200, { version: '2.1.259' })).toBe(false) - expect(await mayAutoOpen(200, { version: null })).toBe(false) expect(await mayAutoOpen(200, { version: '2.1.260' })).toBe(true) }) diff --git a/src/main/claude/claude-hook-event-install.test.ts b/src/main/claude/claude-hook-event-install.test.ts index efcf3d1cea2..0e2dbe8982b 100644 --- a/src/main/claude/claude-hook-event-install.test.ts +++ b/src/main/claude/claude-hook-event-install.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from 'vitest' import type { HooksConfig } from '../agent-hooks/installer-utils' import fixture from './__fixtures__/claude-hook-event-enums.json' -import { CLAUDE_HOOK_EVENT_FIRST_VERSIONS } from './claude-hook-event-versions' +import { readManagedHookDetectionResult } from '../agent-hooks/managed-hook-detection-commands' +import { + parseClaudeCliVersionOutput, + CLAUDE_HOOK_EVENT_FIRST_VERSIONS +} from './claude-hook-event-versions' import { getClaudeManagedHookPlan } from './claude-managed-hook-events' import { applyManagedHooks } from './hook-settings' @@ -134,9 +138,10 @@ describe('Claude managed hook events by resolved version', () => { } ) - it.each([undefined, 'unknown'])( + it.each([undefined, '11.17.0', '2.1.310\nwrapper for (Claude Code)'])( 'leaves Orca entries a version-aware install wrote untouched when the version is %s', - (version) => { + (output) => { + const version = parseClaudeCliVersionOutput(output) ?? undefined const current = install(userOwnedSettings(), '2.1.261') expect(JSON.stringify(install(current, version))).toBe(JSON.stringify(current)) @@ -177,7 +182,11 @@ describe('Claude managed hook events by resolved version', () => { const current = install({ hooks: {} }, '2.1.261') expect(managedEvents(current)).toContain('SessionEnd') - const downgraded = install(current, '2.1.32') + const { claudeVersion } = readManagedHookDetectionResult({ + agents: ['claude'], + versions: { claude: 'launcher 11.17.0\n2.1.32 (Claude Code)' } + }) + const downgraded = install(current, claudeVersion ?? undefined) for (const event of [ 'StopFailure', diff --git a/src/main/claude/claude-hook-event-versions.test.ts b/src/main/claude/claude-hook-event-versions.test.ts index b2b7668e2f7..d5f4d202a0a 100644 --- a/src/main/claude/claude-hook-event-versions.test.ts +++ b/src/main/claude/claude-hook-event-versions.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import { runProcess } from '@orca/process-host' +import { readManagedHookDetectionResult } from '../agent-hooks/managed-hook-detection-commands' +import { getClaudeManagedHookPlan } from './claude-managed-hook-events' + +vi.mock('@orca/process-host', () => ({ runProcess: vi.fn() })) import { compareAppVersions } from '../../shared/app-version' import fixture from './__fixtures__/claude-hook-event-enums.json' import { @@ -7,6 +12,8 @@ import { claudeKnowsHookEvent, claudeKnowsStatusLine, parseClaudeCliVersion, + parseClaudeCliVersionOutput, + probeClaudeCliVersion, UNRESOLVED_CLAUDE_VERSION, type ClaudeHookEventName } from './claude-hook-event-versions' @@ -105,5 +112,66 @@ describe('parseClaudeCliVersion', () => { it('extracts Claude Code version output', () => { expect(parseClaudeCliVersion('2.1.261 (Claude Code)')).toBe('2.1.261') expect(parseClaudeCliVersion('unknown')).toBeNull() + expect(parseClaudeCliVersion('2.1.261')).toBe('2.1.261') + }) +}) + +describe('identified Claude version output', () => { + // Recorded macOS 2.1.310 / Windows 2.1.221 probes; historical releases' CLI source + // registers the same suffix (1.0.23, 1.0.64, 2.0.0 and 2.1.100). + it.each([ + ['2.1.310 (Claude Code)\n', '', '2.1.310', 15, 0], + ['2.1.221 (Claude Code)\n', '', '2.1.221', 14, 1], + ['1.0.23 (Claude Code)', '', '1.0.23', 2, 13], + ['1.0.64 (Claude Code)', '', '1.0.64', 6, 9], + ['2.0.0 (Claude Code)', '', '2.0.0', 6, 9], + ['2.1.100 (Claude Code)', '', '2.1.100', 14, 1], + ['launcher 11.17.0\n1.0.64 (Claude Code)', '', '1.0.64', 6, 9], + ['launcher 0.1.0\n2.1.310 (Claude Code)', '', '2.1.310', 15, 0], + ['launcher 0.1.0', '2.1.310 (Claude Code)\n', '2.1.310', 15, 0], + ['', '2.1.310 (Claude Code)\n', '2.1.310', 15, 0], + ['2.1.94-rc.1+build.2 (Claude Code)', '', '2.1.94-rc.1+build.2', 14, 1], + ['11.17.0\n', '', null, 6, 0], + ['2.30.0', '', null, 6, 0], + ['2.1.310\nwrapper for (Claude Code)', '', null, 6, 0], + ['2.1.310\n(Claude Code)', '', null, 6, 0], + ['2.1 (Claude Code)', '', null, 6, 0] + ] as const)( + 'uses the identified version and hook plan for stdout=%s stderr=%s', + async (stdout, stderr, expected, installs, retirements) => { + vi.mocked(runProcess).mockResolvedValue({ + stdout, + stderr, + code: 0, + signal: null, + timedOut: false + }) + const local = await probeClaudeCliVersion('/bin/claude') + // Older guests send combined raw output, not canonical stored semver. + const remote = readManagedHookDetectionResult({ + agents: ['claude'], + versions: { claude: `${stdout}\n${stderr}` } + }) + expect(remote.agents).toEqual(['claude']) + expect(parseClaudeCliVersionOutput(`${stdout}\n${stderr}`)).toBe(expected) + for (const version of [local, remote.claudeVersion]) { + expect(version).toBe(expected) + const plan = getClaudeManagedHookPlan(version) + expect(plan.install).toHaveLength(installs) + expect(plan.retire).toHaveLength(retirements) + expect(plan.statusLine).toBe(expected === '1.0.23' ? 'retire' : 'install') + } + } + ) + + it('does not accept marked output from a failed probe', async () => { + vi.mocked(runProcess).mockResolvedValue({ + stdout: '2.1.310 (Claude Code)\n', + stderr: '', + code: 1, + signal: null, + timedOut: false + }) + expect(await probeClaudeCliVersion('/bin/claude')).toBeNull() }) }) diff --git a/src/main/claude/claude-hook-event-versions.ts b/src/main/claude/claude-hook-event-versions.ts index 4f1f209db6f..a8014c30af4 100644 --- a/src/main/claude/claude-hook-event-versions.ts +++ b/src/main/claude/claude-hook-event-versions.ts @@ -38,6 +38,15 @@ export function parseClaudeCliVersion(output: string | null | undefined): string return version && isValidAppVersion(version) ? version : null } +// A launcher in Command (`npx claude`, `op run -- claude`) answers `--version` with its own +// version; only Claude's own `x.y.z (Claude Code)` line may pick the hook events. +export function parseClaudeCliVersionOutput(output: string | null | undefined): string | null { + const version = output?.match( + /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)[ \t]+\(Claude Code\)/i + )?.[1] + return version && isValidAppVersion(version) ? version : null +} + export function claudeVersionReaches(version: string | null | undefined, floor: string): boolean { const parsed = parseClaudeCliVersion(version) return parsed !== null && hasReachedAppVersion(parsed, floor) @@ -95,7 +104,9 @@ export async function probeClaudeCliVersion( timeoutMs: launch?.timeoutMs ?? 5_000, maxOutputBytes: 4_096 }) - return result.code === 0 ? parseClaudeCliVersion(`${result.stdout}\n${result.stderr}`) : null + return result.code === 0 + ? parseClaudeCliVersionOutput(`${result.stdout}\n${result.stderr}`) + : null } catch { return null } diff --git a/src/main/claude/claude-structured-options.test.ts b/src/main/claude/claude-structured-options.test.ts index 26325cfa0b0..19faf593000 100644 --- a/src/main/claude/claude-structured-options.test.ts +++ b/src/main/claude/claude-structured-options.test.ts @@ -1,5 +1,14 @@ import { describe, expect, it, vi } from 'vitest' -import { setClaudeStructuredOption } from './claude-structured-options' +import { runProcess } from '@orca/process-host' +import { probeClaudeCliVersion } from './claude-hook-event-versions' +import { + CLAUDE_APPLY_FLAG_SETTINGS_CONTROL, + CLAUDE_EFFORT_FLAG, + createClaudeCliFlagSupport +} from './claude-cli-flag-support' +import { resolveClaudeLaunchFlags } from './claude-structured-launch-flags' +import { claudeStructuredSpawnOptions } from './claude-structured-spawn-options' +import { claudeTakesFlagSettings, setClaudeStructuredOption } from './claude-structured-options' import { applyClaudeMessageOptions } from './claude-structured-message-options' import type { ClaudeSession } from './structured/claude-structured-session-state' import { ClaudeControlRequestTimeoutError } from './claude-agent-sdk-control-requests' @@ -9,11 +18,14 @@ import { ClaudeSlashCommandCatalog } from './claude-slash-command-catalog' import { createClaudeInitProof } from './structured/claude-structured-init-proof' import { claudeProviderHandleLink } from './structured/claude-structured-owner-identity' import { + claudeStructuredSessionOptionsFrom, observeClaudeFastModeFacts, readClaudeStructuredSessionOptions, refreshClaudeStructuredSessionOptions } from './claude-structured-session-options' +vi.mock('@orca/process-host', () => ({ runProcess: vi.fn() })) + function sessionFor(setModel: ClaudeSession['connection']['setModel']): ClaudeSession { return { connection: { @@ -62,6 +74,71 @@ function sessionFor(setModel: ClaudeSession['connection']['setModel']): ClaudeSe } } +describe('identified versions across native Claude consumers', () => { + it.each([ + ['launcher 0.1.0\n2.1.310 (Claude Code)', 'supported'], + ['launcher 11.17.0\n1.0.64 (Claude Code)', 'unsupported'], + ['11.17.0', 'unknown'] + ] as const)( + 'derives launch flags, saved options and live controls from %s', + async (stdout, fact) => { + vi.mocked(runProcess).mockResolvedValue({ + stdout, + stderr: '', + code: 0, + signal: null, + timedOut: false + }) + const support = createClaudeCliFlagSupport({ + probe: probeClaudeCliVersion, + keyOf: async () => 'binary', + budgetMs: 1_000, + now: () => performance.now() + }) + const launch = { command: '/bin/launcher', prefixArgs: ['claude'], cwd: '/repo', env: {} } + const flags = await resolveClaudeLaunchFlags( + { + cliFlags: support, + prepareVisuals: async () => ({ + folder: '/scratch', + skill: { pluginDir: '/plugin', skillsRoot: '/skills' } + }) + }, + 'session', + launch + ) + expect(flags.thinkingDisplayArgs).toEqual( + fact === 'supported' ? { 'thinking-display': 'summarized' } : {} + ) + expect(flags.visuals?.pluginDir).toBe(fact === 'supported' ? '/plugin' : null) + const effortFlag = await support.check(CLAUDE_EFFORT_FLAG, launch) + const control = await support.check(CLAUDE_APPLY_FLAG_SETTINGS_CONTROL, launch) + expect(effortFlag).toBe(fact) + expect(control).toBe(fact) + const spawn = claudeStructuredSpawnOptions({ + launch: { options: {}, resumesTranscript: false }, + saved: { effort: 'high', fastMode: 'true' }, + effortFlag, + flagSettingsControl: control + }) + expect(spawn.sdkOptions.effort).toBe(fact === 'supported' ? 'high' : undefined) + expect(spawn.fastModeAtStart).toBe(fact !== 'unsupported') + const { session, applyFlagSettings } = fastModeSession(true) + session.cliFlag = (flag, budget, start) => support.check(flag, launch, budget, start) + expect(await claudeTakesFlagSettings(session)).toBe(fact !== 'unsupported') + expect(session.flagSettingsControl).toBe(fact) + const options = claudeStructuredSessionOptionsFrom(session, null) + expect(options.effortFixed).toBe(fact === 'unsupported' ? true : undefined) + if (fact !== 'unsupported') { + await setClaudeStructuredOption(session, { key: 'fastMode', value: 'false' }, undefined) + expect(applyFlagSettings).toHaveBeenCalled() + } else { + expect(options.fastModeSupport).toEqual({ supported: false, reason: 'cli-update-required' }) + } + } + ) +}) + describe('Claude structured option mutation fencing', () => { it('does not let a delayed earlier apply overwrite a later option', async () => { let releaseFirst!: () => void