Restore OMP thinking choices for non-running models (#36158)

* fix(acp): restore thinking choices for non-running OMP models

* test: cover OMP catalog persistence and menu scaling

* test: keep OMP scaling measurements in assertions

* test: load ACP fixtures before consumer tests

* test: preserve catalog source types in persistence cases
This commit is contained in:
Brennan Benson
2026-10-29 21:25:58 -07:00
committed by GitHub
parent 6c86dec334
commit a05cb89f1e
11 changed files with 487 additions and 20 deletions
@@ -1,5 +1,5 @@
import { act } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import type {
AgentSessionModelCatalogResult,
AgentSessionOptionResult,
@@ -7,6 +7,7 @@ import type {
} from '../../../src/shared/agent-session-wire'
import type { StructuredAgentSessionMutationResult } from './mobile-structured-agent-session-rpc'
import { rememberMobileCreatedStructuredSession } from './mobile-created-structured-sessions'
import type { AcpThinkingOptionsFixtureModule } from '../../../src/shared/acp-thinking-options.test-fixture-contract'
import {
type Controller,
settle,
@@ -24,6 +25,14 @@ import {
BASE
} from './use-mobile-structured-agent-options.test-fixture'
let recordedOmpOptions: AcpThinkingOptionsFixtureModule['recordedOmpOptions']
beforeAll(async () => {
const fixtures = await vi.importActual<AcpThinkingOptionsFixtureModule>(
'../../../src/main/acp/acp-thinking-options.test-fixture'
)
recordedOmpOptions = fixtures.recordedOmpOptions
})
describe('useMobileStructuredAgentOptions fast mode', () => {
it('round-trips a boolean fastMode pick as the wire string and remembers the decoded pick', async () => {
const client = optionsClient(queuedReads(FAST_OPTIONS, FAST_OPTIONS_ON))
@@ -121,6 +130,49 @@ describe('useMobileStructuredAgentOptions post-write refresh', () => {
})
describe('useMobileStructuredAgentOptions reads', () => {
it('offers unknown OMP suggestions for a saved model without choosing effort, then sends the literal pick', async () => {
const { models } = recordedOmpOptions('loaded').read()
const answer: AgentSessionOptionsResult = {
models,
current: { model: 'target', defaulted: [], acceptedInputSources: { model: 'saved' } }
}
const client = optionsClient(queuedReads(answer), async () => ({
origin: 'probe',
fetchedAt: 1,
models
}))
const { calls, mutate } = recordingMutate(async () =>
accepted({ key: 'effort', value: 'auto', options: { model: 'target', effort: 'auto' } }, true)
)
const harness = await mountOptions({
...BASE,
agent: 'omp',
client: client.client,
mutate,
sendSelection: true
})
const effort = descriptorFor(harness.current().optionSnapshot, 'effort')
expect(effort).toMatchObject({
settable: true,
kind: {
type: 'select',
choices: [
{ value: 'off', label: 'Off' },
expect.objectContaining({ value: 'auto', label: 'Auto' })
]
}
})
expect(currentValueOf(harness.current().optionSnapshot, 'effort')).toBeUndefined()
expect(harness.current().sendSelection()).toEqual({ selection: { model: 'target' } })
await act(async () => {
expect(await harness.current().setStructuredOption('effort', 'auto')).toBe(true)
})
expect(calls).toEqual([
{ method: 'agentSession.setOption', fields: { key: 'effort', value: 'auto' } }
])
await harness.unmount()
})
it('re-reads the options each turn, as desktop does', async () => {
const client = optionsClient(queuedReads(OPTIONS))
const { mutate } = recordingMutate(async () => ({ status: 'unknown' }))
+2
View File
@@ -107,6 +107,8 @@ export type AcpDialect = {
): { sessionId: string; promptId: string; stopReason: string } | undefined
contextWindow?(models: unknown): number | undefined
modelEfforts?(model: ModelInfo): { efforts: AgentSessionOptionChoice[]; defaultEffort?: string }
/** Native thinking choices remain valid across models; the agent clamps unsupported levels. */
clampsUnlistedEffort?: true
/** Claimed permission requests are never auto-approved. */
request?(method: string, params: unknown): AcpRequestPresentation | undefined
settleRequest?(method: string, params: unknown): AcpRequestSettlement | undefined
+2
View File
@@ -113,6 +113,8 @@ function errorDetails(error: AcpAgentError): string | undefined {
}
export const OMP_ACP_DIALECT: AcpDialect = {
// OMP resolves any thinking level against the selected model (model-controls.ts setThinkingLevel).
clampsUnlistedEffort: true,
normalizeToolUpdate,
compactionReply,
authenticationRequired: (error) => {
+145 -1
View File
@@ -7,6 +7,15 @@ import { GROK_ACP_DIALECT } from './acp-dialects/grok-dialect'
import { OMP_ACP_DIALECT } from './acp-dialects/omp-dialect'
import { OPENCODE_ACP_DIALECT } from './acp-dialects/opencode-dialect'
import type { AcpCommandOptionsFixtureModule } from '../../shared/mobile-queued-edit-host.test-fixture-contract'
import { recordedOmpOptions } from './acp-thinking-options.test-fixture'
import { SessionConfigSelectOptionSchema } from './generated/acp-protocol.generated'
import { AgentModelCatalogStore } from '../native-chat/agent-model-catalog/agent-model-catalog-store'
import { agentModelCatalogListingKey } from '../native-chat/agent-model-catalog/agent-model-catalog-entry'
import { OMP_SESSION_OPTION_CATALOG } from '../../shared/agent-session-option-catalog-omp'
import {
applyStructuredAgentSessionModelCatalog,
createStructuredAgentSessionOptionState
} from '../../shared/structured-agent-session-options'
const mobileProducer = { AcpStructuredOptions } satisfies AcpCommandOptionsFixtureModule
@@ -108,6 +117,129 @@ describe('ACP session options', () => {
}
)
it.each([10, 100])(
'bounds native OMP menu work across %i models, repeated reports and an update',
async (count) => {
const options = recordedOmpOptions('loaded', count)
const read = vi.spyOn(options, 'read')
const extract = vi.spyOn(SessionConfigSelectOptionSchema, 'safeParse')
const session = { options, connection: { initialize: async () => ({ protocolVersion: 1 }) } }
const store = new AgentModelCatalogStore()
let previousKey: string | undefined
let visits = 0
try {
for (const frame of ['initial', 'repeat', 'update']) {
if (frame === 'update') {
// A synthetic native update exercises adoption; the initial menu remains recorded output.
options.adoptConfigOptions([
{
id: 'thinking',
name: 'Thinking',
category: 'thought_level',
type: 'select',
currentValue: 'auto',
options: [
{ value: 'auto', name: 'Auto' },
{ value: 'high', name: 'High' }
]
}
])
}
const expected =
frame === 'update'
? [
{ value: 'auto', label: 'Auto' },
{ value: 'high', label: 'High' }
]
: [
{ value: 'off', label: 'Off' },
{ value: 'auto', label: 'Auto', description: 'Auto-detect per prompt' }
]
read.mockClear()
extract.mockClear()
const result = await readAcpLiveOptions(session)
expect(read).toHaveBeenCalledTimes(1)
expect(extract).toHaveBeenCalledTimes(2)
expect(result.models).toHaveLength(count)
expect(result.models[0]!.efforts).toEqual(expected)
expect(result.catalogListing.models).toBe(result.models)
for (const model of result.models) {
expect(model.efforts).toBe(result.models[0]!.efforts)
expect(model.effortsKnown).toBe(model.id === 'running' ? undefined : false)
expect(model.defaultEffort).toBeUndefined()
}
for (const choice of result.models[0]!.efforts) {
const value = choice.value
Object.defineProperty(choice, 'value', {
enumerable: true,
get: () => {
visits++
return value
}
})
}
visits = 0
const entry = store.recordSuccess(
'omp-scaling',
'omp',
{ ...result.catalogListing, origin: 'live-session' },
'live'
)!
// A repeat/update compares old and new source keys; each contains M*E values.
const publicationVisits = visits
expect(publicationVisits).toBeLessThanOrEqual(count * 2 * 2)
expect(entry.models).toHaveLength(count)
expect(entry.live?.models).toEqual(result.models)
visits = 0
const key = agentModelCatalogListingKey(entry)
expect(visits).toBe(count * 2)
expect(key.match(/"value":/g)).toHaveLength(count * 2)
const emptyKey = agentModelCatalogListingKey({
...entry,
live: entry.live
? {
...entry.live,
models: entry.live.models.map((model) => ({ ...model, efforts: [] }))
}
: null
})
expect(key.length - emptyKey.length).toBe(count * (JSON.stringify(expected).length - 2))
if (frame === 'repeat') {
expect(key).toBe(previousKey)
}
if (frame === 'update') {
expect(key).not.toBe(previousKey)
}
previousKey = key
visits = 0
const state = applyStructuredAgentSessionModelCatalog(
createStructuredAgentSessionOptionState('omp'),
OMP_SESSION_OPTION_CATALOG,
{ origin: 'probe', fetchedAt: 1, models: entry.models },
{ newLaunch: false }
)
const installationVisits = visits
expect(installationVisits).toBe(count)
const descriptors = state.catalog!.models.flatMap((model) =>
model.options.filter((option) => option.id === 'effort')
)
expect(descriptors).toHaveLength(count)
let choices = 0
for (const descriptor of descriptors) {
expect(descriptor.kind).toMatchObject({ type: 'select', choices: expected })
expect(descriptor.kind).not.toHaveProperty('defaultIsCliDefault')
if (descriptor.kind.type === 'select') {
choices += descriptor.kind.choices.length
}
}
expect(choices).toBe(count * 2)
}
} finally {
extract.mockRestore()
}
}
)
it.each([OPENCODE_ACP_DIALECT, OMP_ACP_DIALECT])(
'keeps native menus and observations separate from applied inputs',
async (dialect) => {
@@ -119,7 +251,19 @@ describe('ACP session options', () => {
const { models, current } = options.read()
// Current values supply an effort menu, never a catalog default.
expect(models).toEqual([
{ id: 'model-a', label: 'Model A', isDefault: false, efforts: [], effortsKnown: false },
{
id: 'model-a',
label: 'Model A',
isDefault: false,
efforts:
dialect === OMP_ACP_DIALECT
? [
{ value: 'low', label: 'Low' },
{ value: 'high', label: 'High' }
]
: [],
effortsKnown: false
},
{
id: 'model-b',
label: 'Model B',
+6 -3
View File
@@ -51,7 +51,7 @@ export class AcpStructuredOptions {
constructor(
private readonly dialect: Pick<
AcpDialect,
'modelEfforts' | 'withheldCommands' | 'effortChangesModel'
'modelEfforts' | 'withheldCommands' | 'effortChangesModel' | 'clampsUnlistedEffort'
> = {}
) {}
@@ -136,7 +136,7 @@ export class AcpStructuredOptions {
return null
}
/** Effort menus describe models; current session values never establish catalog defaults. */
/** Native choices never establish an unobserved model's capability or configured default. */
read(): Pick<AgentSessionOptionsResult, 'models' | 'current'> {
const modelOption = this.select('model')
const effortOption = this.select('thought_level')
@@ -167,7 +167,10 @@ export class AcpStructuredOptions {
model.id === currentModel
? { efforts: sessionEfforts }
: (modelOption || effortOption) && !advertised
? { efforts: [], effortsKnown: false as const }
? {
efforts: this.dialect.clampsUnlistedEffort ? sessionEfforts : [],
effortsKnown: false as const
}
: { efforts: [] }
return {
...model,
@@ -0,0 +1,28 @@
import type { AcpThinkingOptionsFixtureModule } from '../../shared/acp-thinking-options.test-fixture-contract'
import { AcpStructuredOptions } from './acp-structured-options'
import { OMP_ACP_DIALECT } from './acp-dialects/omp-dialect'
import { NewSessionResponseSchema } from './generated/acp-protocol.generated'
import recorded from './fixtures/omp-v17-windows-new-no-model.json'
export function recordedOmpOptions(origin: 'new' | 'loaded', count = 2): AcpStructuredOptions {
const options = new AcpStructuredOptions(OMP_ACP_DIALECT)
// The recorded native menu has no model list; these rows are synthetic catalog context.
options.adoptSession(
{
...NewSessionResponseSchema.parse(recorded),
models: {
currentModelId: 'running',
availableModels: Array.from({ length: count }, (_, index) => ({
modelId: index === 0 ? 'running' : index === 1 ? 'target' : `model-${index}`,
name: index === 0 ? 'Running' : index === 1 ? 'Target' : `Model ${index}`
}))
}
},
origin
)
return options
}
export const thinkingOptionsProducer = {
recordedOmpOptions
} satisfies AcpThinkingOptionsFixtureModule
@@ -40,11 +40,12 @@ describe('the effort menu of a model whose configured effort one listing lacks',
})
})
const unknownChat = (at: number): AgentModelCatalogListing => ({
...listing('live-session', at, []),
models: [
{ id: 'openai/gpt-6', label: 'GPT-6', isDefault: false, efforts: [], effortsKnown: false }
]
const unknownChat = (
at: number,
efforts: ReturnType<typeof menu> = []
): AgentModelCatalogListing => ({
...listing('live-session', at, efforts),
models: [{ id: 'openai/gpt-6', label: 'GPT-6', isDefault: false, efforts, effortsKnown: false }]
})
describe('a model whose newer chat listing does not know its efforts', () => {
@@ -54,11 +55,31 @@ describe('a model whose newer chat listing does not know its efforts', () => {
expect(merged).not.toHaveProperty('effortsKnown')
})
it('keeps an older listing’s explicit empty menu', () => {
expect(model(listing('probe', 1, []), unknownChat(2))).not.toHaveProperty('effortsKnown')
})
it('stays unknown when no listing knows its menu', () => {
expect(model(unknownChat(1), unknownChat(2))).toMatchObject({ effortsKnown: false })
})
it('prefers known probe evidence over an older unknown chat offering the configured effort', () => {
const merged = model(
listing('probe', 2, menu('low', 'high')),
unknownChat(1, menu('off', 'auto', 'xhigh'))
)
expect(merged?.efforts).toEqual(menu('low', 'high'))
expect(merged).not.toHaveProperty('effortsKnown')
expect(merged).not.toHaveProperty('defaultEffort')
})
it('keeps native suggestions unknown without a known model menu', () => {
const older = unknownChat(1, menu('low', 'high'))
const newer = unknownChat(2, menu('off', 'auto'))
for (const discovered of [null, { ...older, origin: 'probe' as const }]) {
const merged = agentModelCatalogEntry('omp', 'fp', discovered, newer, null)?.models[0]
expect(merged).toMatchObject({ efforts: menu('off', 'auto'), effortsKnown: false })
expect(merged).not.toHaveProperty('defaultEffort')
}
expect(model({ ...older, origin: 'probe' }, unknownChat(2))).toMatchObject({
efforts: menu('low', 'high'),
effortsKnown: false
})
})
})
@@ -149,17 +149,22 @@ function mergedModels(
// Coarser probe menus must not discard a chat-confirmed effort unless a newer chat menu does.
const keepsChatMenu =
older === live && !offersConfigured(model.efforts) && offersConfigured(olderEfforts)
const efforts = model.efforts.length > 0 && !keepsChatMenu ? model.efforts : olderEfforts
const { efforts, effortsKnown } =
olderModel && model.effortsKnown !== olderModel.effortsKnown
? model.effortsKnown === false
? olderModel
: model
: model.efforts.length > 0 && !keepsChatMenu
? model
: (olderModel ?? model)
const defaultEffort = [configuredEffort, listed?.defaultEffort, reported?.defaultEffort].find(
(effort) => effort !== undefined && efforts.some((choice) => choice.value === effort)
)
const { defaultEffort: _own, effortsKnown, ...rest } = model
const { defaultEffort: _own, effortsKnown: _known, ...rest } = model
return {
...rest,
// Efforts stay unknown only when neither listing knows this model's menu.
...(effortsKnown === false && (!olderModel || olderModel.effortsKnown === false)
? { effortsKnown }
: {}),
...(effortsKnown === false ? { effortsKnown } : {}),
// Without discovery/configuration, live flags are the only available default evidence.
isDefault: configured
? model.id === configured.modelId
@@ -0,0 +1,134 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { it, expect } from 'vitest'
import { OMP_SESSION_OPTION_CATALOG } from '../../../shared/agent-session-option-catalog-omp'
import {
applyStructuredAgentSessionOptions,
createStructuredAgentSessionOptionState,
structuredAgentSessionOptionSnapshot
} from '../../../shared/structured-agent-session-options'
import { recordedOmpOptions } from '../../acp/acp-thinking-options.test-fixture'
import {
AgentModelCatalogStore,
type AgentModelCatalogSource
} from '../agent-model-catalog/agent-model-catalog-store'
import { createAgentModelCatalogFilePersistence } from '../agent-model-catalog/agent-model-catalog-persistence'
import { createAgentModelCatalogService } from '../agent-model-catalog/agent-model-catalog-service'
import { agentModelCatalogFingerprintForRecord } from '../agent-model-catalog/agent-model-catalog-fingerprint'
import {
hostTestRestingOptionsRecord as restingRecord,
hostTestRestingOptionsJournal as restingJournal,
hostTestOptionsReadStore
} from './structured-agent-session-host-test-data'
import { NO_STRUCTURED_AGENTS } from './structured-agent-session-adapter-router-test-support'
import type { StructuredAgentSessionMutationContext } from './structured-agent-session-host-mutations'
import { readStructuredAgentSessionOptions } from './structured-agent-session-options-read'
const SESSION = 'session-1'
it.each(
(['discovery', 'live'] as const).flatMap((source) => [
{ source, knownFirst: undefined, efforts: undefined },
...[true, false].flatMap((knownFirst) =>
[
[],
[
{ value: 'low', label: 'Low' },
{ value: 'high', label: 'High' }
]
].map((efforts) => ({ source, knownFirst, efforts }))
)
])
)(
'reloads $source OMP evidence (knownFirst=$knownFirst, menu=$efforts) through the resting picker',
async ({ source, knownFirst, efforts }) => {
const directory = await mkdtemp(join(tmpdir(), 'pr36158-menu-'))
const expected = efforts ?? [
{ value: 'off', label: 'Off' },
{ value: 'auto', label: 'Auto', description: 'Auto-detect per prompt' }
]
try {
let at = 1
const store = new AgentModelCatalogStore({ now: () => at++ })
await store.attachPersistence(createAgentModelCatalogFilePersistence(directory))
const record = { ...restingRecord(), provider: 'omp', options: { model: 'target' } }
const fingerprint = agentModelCatalogFingerprintForRecord(record)
const { models } = recordedOmpOptions('loaded').read()
const known = models.map((model) =>
model.id === 'target'
? { id: model.id, label: model.label, isDefault: false, efforts: expected }
: model
)
const other = source === 'live' ? 'discovery' : 'live'
const order: AgentModelCatalogSource[] =
knownFirst === undefined ? [source] : knownFirst ? [source, other] : [other, source]
for (const currentSource of order) {
store.recordSuccess(
fingerprint,
'omp',
{
origin: currentSource === 'live' ? 'live-session' : 'probe',
models: knownFirst !== undefined && currentSource === source ? known : models
},
currentSource
)
}
await store.flushPersistence()
const restarted = new AgentModelCatalogStore()
await restarted.attachPersistence(createAgentModelCatalogFilePersistence(directory))
const modelCatalog = createAgentModelCatalogService({
store: restarted,
...hostTestOptionsReadStore(() => record),
drivesRecord: () => true,
resolveAccountHome: async () => record.accountHome
})
const resting = { child: null, params: { provider: 'omp' }, journal: restingJournal() }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resting read uses only these context members, as in the other options-read fixtures.
const context = {
deps: {
adapter: {},
agents: NO_STRUCTURED_AGENTS,
store: hostTestOptionsReadStore(() => record),
modelCatalog
},
serialize: (_id: string, task: () => Promise<unknown>) => task(),
openConversation: async () => resting,
conversation: async () => resting
} as unknown as StructuredAgentSessionMutationContext
try {
const answer = await readStructuredAgentSessionOptions(context, SESSION, {
readsWithoutModel: true
})
const target = answer.models.find((model) => model.id === 'target')
expect(target?.efforts).toEqual(expected)
if (knownFirst === undefined) {
expect(target?.effortsKnown).toBe(false)
} else {
expect(target).not.toHaveProperty('effortsKnown')
}
expect(target).not.toHaveProperty('defaultEffort')
expect(answer.current.effort).toBeUndefined()
const state = applyStructuredAgentSessionOptions(
createStructuredAgentSessionOptionState('omp'),
OMP_SESSION_OPTION_CATALOG,
answer
)
const effort = structuredAgentSessionOptionSnapshot(state).find(
(entry) => entry.id === 'effort'
)
if (expected.length === 0) {
expect(effort).toBeUndefined()
} else {
expect(effort).toMatchObject({ kind: { type: 'select', choices: expected } })
expect(effort?.kind).not.toHaveProperty('currentValue')
expect(effort?.kind).not.toHaveProperty('defaultIsCliDefault')
}
} finally {
modelCatalog.stop()
}
} finally {
await rm(directory, { recursive: true, force: true })
}
}
)
@@ -2,16 +2,29 @@
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { useState } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { OMP_SESSION_OPTION_CATALOG } from '../../../../shared/agent-session-option-catalog-omp'
import type { AcpThinkingOptionsFixtureModule } from '../../../../shared/acp-thinking-options.test-fixture-contract'
import { structuredAgentSessionOptionView } from '../../../../shared/structured-agent-session-option-view'
import {
applyStructuredAgentSessionModelCatalog,
applyStructuredAgentSessionOptions,
canSetStructuredAgentSessionOption,
createStructuredAgentSessionOptionState,
structuredAgentSessionOptionPicks,
structuredAgentSessionOptionSnapshot
} from '../../../../shared/structured-agent-session-options'
import type { SessionOptionDescriptor } from '../../../../shared/native-chat/input/native-chat-session-options'
import { RuntimeRpcCallError } from '@/runtime/runtime-rpc-result'
let recordedOmpOptions: AcpThinkingOptionsFixtureModule['recordedOmpOptions']
beforeAll(async () => {
const fixtures = await vi.importActual<AcpThinkingOptionsFixtureModule>(
'../../../../main/acp/acp-thinking-options.test-fixture'
)
recordedOmpOptions = fixtures.recordedOmpOptions
})
const toastError = vi.hoisted(() => vi.fn())
vi.mock('sonner', () => ({ toast: { error: toastError } }))
@@ -561,6 +574,59 @@ describe('NativeChatSessionOptionPickers', () => {
expect(screen.queryByText('Default')).toBeNull()
})
it.each(['new', 'loaded'] as const)(
'offers recorded OMP thinking choices for a non-running model in a %s chat without picking a default',
async (origin) => {
const projected = recordedOmpOptions(origin).read()
expect(projected.models[1]).toMatchObject({ effortsKnown: false })
expect(projected.models[1]).not.toHaveProperty('defaultEffort')
const state = applyStructuredAgentSessionModelCatalog(
createStructuredAgentSessionOptionState('omp'),
OMP_SESSION_OPTION_CATALOG,
{ origin: 'probe', fetchedAt: 1, models: projected.models },
{ newLaunch: origin === 'new' }
)
const view = structuredAgentSessionOptionView(state, undefined, { model: 'target' })
const snapshot = structuredAgentSessionOptionSnapshot(view)
expect(snapshot.find((entry) => entry.id === 'effort')).toMatchObject({
valueSource: 'unknown',
settable: true,
kind: {
type: 'select',
choices: [
{ value: 'off', label: 'Off' },
expect.objectContaining({ value: 'auto', label: 'Auto' })
]
}
})
expect(snapshot.find((entry) => entry.id === 'effort')?.kind).not.toHaveProperty(
'currentValue'
)
expect(view.record.values.effort).toBeUndefined()
expect(structuredAgentSessionOptionPicks(view, { model: 'target' })).toEqual([
{ modelId: 'target', optionId: 'model', value: 'target' }
])
const remembered = structuredAgentSessionOptionView(state, undefined, {
model: 'target',
effort: 'high'
})
expect(canSetStructuredAgentSessionOption(remembered, 'effort', 'high')).toBe(true)
const setOption = vi.fn().mockResolvedValue({ snapshot: [] })
render(
<NativeChatSessionOptionPickers
surface={{ ...surface, setOption }}
snapshot={snapshot}
isWorking={false}
/>
)
fireEvent.click(pill('Model Target'))
expect(setOption).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('radio', { name: 'Auto' }))
await waitFor(() => expect(setOption).toHaveBeenCalledExactlyOnceWith('effort', 'auto'))
}
)
it('makes the effort levels one tab stop, moves between them with the arrow keys, and sends the one picked', async () => {
const setOption = vi.fn().mockResolvedValue({ snapshot: [] })
render(
@@ -0,0 +1,10 @@
import type { AgentSessionOptionsResult } from './agent-session-wire'
export type AcpThinkingOptionsFixtureModule = {
recordedOmpOptions: (
origin: 'new' | 'loaded',
count?: number
) => {
read: () => Pick<AgentSessionOptionsResult, 'models' | 'current'>
}
}