mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(claude): ignore a launcher's own version when choosing hook events (#36119)
* fix(claude): ignore a launcher's own version when choosing hook events * Bind Claude capability versions to identified output * Complete Claude version probe test process results
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
|
||||
@@ -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<string, string>, 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()
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user