mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* fix(native-chat): keep saved options on structured route
* fix(native-chat): seed structured session options
* fix(native-chat): preserve create wire compatibility
* fix(native-chat): keep create replayable across an option change
Seeding host-resolved options into the attach fingerprint put a mutable
value into the durable operation identity. A create whose outcome was
unknown, retried under the same operation id after the user reselected a
model, re-resolved different options and hashed to a different
fingerprint — so the ledger refused it as a conflict instead of
replaying. That refusal is not definitive, so no legacy fallback fires
and the launch has no recovery.
Options are the session's initial state, not its identity, and the
reservation still carries them to the record. Excluding them also makes
the digest byte-identical to the pre-change one in every case, not just
when no options resolve.
* refactor(native-chat): name the structured launch option seed
The create-intent resolver narrowed saved options to model/effort with an
inline key literal, inside a file carrying @ts-nocheck — so neither the
key list nor the string narrowing had a typechecked or testable home, and
the repo already expresses this concept as a named shared shape.
Move it to resolveStructuredLaunchSeedOptions beside the persisted
settings it reads, where it is typechecked and unit-tested, and document
why the seed is exactly model and effort: they are the only ids the
picker persists that both providers also accept as strings.
No behavior change. Adds coverage for a non-string persisted effort,
which settings.json can hold and the durable record must not carry.
* test(native-chat): name the structured routing pin for what it asserts
The case drives a saved Codex model and effort through the launch path,
but the structured create intent it asserts on carries no options, so it
pins the route and not the preservation its name claimed. Preservation is
pinned host-side, where the seeding actually happens.
* test(native-chat): pin the empty seed the record cannot carry
valuesByModel is merged over the resolved model, so a stored `model` key
can blank it — the seed then empties out and must resolve to undefined.
Nothing covered that branch, so returning the empty map unguarded stayed
green.
It matters because emitting `{ model: '' }` fails the record's
bounded-string guard, and agent_session_options_invalid is not a wire
refusal code: classifyStoreFailure rethrows it, the client reads the raw
error as an unknown outcome, and the launch strands with no fallback.
Corrects eb33ee6bb3, whose message claimed no behavior change. That
extraction also stopped emitting empty and whitespace-only values, which
is a fix on six input shapes, not a pure move.
---------
Co-authored-by: Merge Sim <sim@local>
141 lines
5.6 KiB
TypeScript
141 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
clearNativeChatSessionOptionModel,
|
|
resolveNativeChatSessionOptionDefaults,
|
|
resolveStructuredLaunchSeedOptions,
|
|
updateNativeChatSessionOptionDefaults
|
|
} from './native-chat-session-option-defaults'
|
|
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
|
|
|
|
const persistedGrok = (
|
|
model: string | undefined,
|
|
valuesByModel: Record<string, Record<string, string>> = {}
|
|
): PersistedNativeChatSessionOptions => ({
|
|
grok: { ...(model ? { model } : {}), valuesByModel }
|
|
})
|
|
|
|
describe('clearNativeChatSessionOptionModel', () => {
|
|
it('drops the model a retired id would otherwise launch as -m', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
persistedGrok('grok-build', { 'grok-build': { effort: 'low' } }),
|
|
'grok'
|
|
)
|
|
expect(cleared.grok?.model).toBeUndefined()
|
|
// Resolution keys off `model`, so clearing it is what stops the flag going out.
|
|
expect(resolveNativeChatSessionOptionDefaults(cleared, 'grok')).toBeUndefined()
|
|
})
|
|
|
|
it('keeps the per-model values so a reselect restores the old effort', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
persistedGrok('grok-build', { 'grok-build': { effort: 'low' } }),
|
|
'grok'
|
|
)
|
|
expect(cleared.grok?.valuesByModel).toEqual({ 'grok-build': { effort: 'low' } })
|
|
const reselected = updateNativeChatSessionOptionDefaults({
|
|
persisted: cleared,
|
|
agent: 'grok',
|
|
modelId: 'grok-build',
|
|
optionId: 'model',
|
|
value: 'grok-build'
|
|
})
|
|
expect(resolveNativeChatSessionOptionDefaults(reselected, 'grok')).toEqual({
|
|
model: 'grok-build',
|
|
effort: 'low'
|
|
})
|
|
})
|
|
|
|
it('leaves every other agent untouched', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
{ ...persistedGrok('grok-build'), claude: { model: 'opus', valuesByModel: {} } },
|
|
'grok'
|
|
)
|
|
expect(cleared.claude).toEqual({ model: 'opus', valuesByModel: {} })
|
|
})
|
|
|
|
it('is a no-op when nothing is persisted for the agent', () => {
|
|
expect(clearNativeChatSessionOptionModel(undefined, 'grok')).toEqual({})
|
|
expect(clearNativeChatSessionOptionModel({}, 'grok')).toEqual({})
|
|
const untouched = persistedGrok(undefined, { 'grok-4.5': { effort: 'high' } })
|
|
expect(clearNativeChatSessionOptionModel(untouched, 'grok')).toEqual(untouched)
|
|
})
|
|
})
|
|
|
|
describe('resolveNativeChatSessionOptionDefaults', () => {
|
|
it('emits nothing until a model is explicitly picked, preserving the CLI default', () => {
|
|
expect(resolveNativeChatSessionOptionDefaults(undefined, 'grok')).toBeUndefined()
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok(undefined), 'grok')).toBeUndefined()
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok(' '), 'grok')).toBeUndefined()
|
|
})
|
|
|
|
it('returns a stale id verbatim, which is why retirement happens upstream', () => {
|
|
// Nothing here validates the id against the host; a retired one still resolves
|
|
// and becomes `-m <id>`. Only clearing the persisted value prevents that.
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok('grok-build'), 'grok')).toEqual({
|
|
model: 'grok-build'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveStructuredLaunchSeedOptions', () => {
|
|
const persistedCodex = (
|
|
valuesByModel: Record<string, Record<string, string | boolean>>
|
|
): PersistedNativeChatSessionOptions =>
|
|
({ codex: { model: 'gpt-5.6-sol', valuesByModel } }) as PersistedNativeChatSessionOptions
|
|
|
|
it('seeds the saved model and effort a structured create must apply', () => {
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: 'medium' } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
|
|
})
|
|
|
|
it('drops ids the providers only accept mid-session', () => {
|
|
// `fastMode` is a boolean and `personality` is settable only mid-session;
|
|
// neither belongs in the reservation's Record<string, string>.
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({
|
|
'gpt-5.6-sol': { effort: 'high', fastMode: true, personality: 'concise' }
|
|
}),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol', effort: 'high' })
|
|
})
|
|
|
|
it('drops a seeded id whose persisted value is not a usable string', () => {
|
|
// settings.json is user-writable, so a non-string `effort` must not reach a
|
|
// record typed Record<string, string> and be emitted as a turn option.
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: true } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol' })
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: ' ' } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol' })
|
|
})
|
|
|
|
it('seeds nothing when the stored values empty the model out', () => {
|
|
// `valuesByModel` is merged over the resolved model, so a stored `model` key
|
|
// can blank it. Emitting `{ model: '' }` fails the record's bounded-string
|
|
// guard, and that throw is not a wire refusal code — it escapes as a raw
|
|
// error the client reads as unknown, stranding the launch with no fallback.
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(persistedCodex({ 'gpt-5.6-sol': { model: '' } }), 'codex')
|
|
).toBeUndefined()
|
|
})
|
|
|
|
it('seeds nothing until a model is picked, so the CLI default survives', () => {
|
|
expect(resolveStructuredLaunchSeedOptions(undefined, 'codex')).toBeUndefined()
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions({ codex: { valuesByModel: {} } }, 'codex')
|
|
).toBeUndefined()
|
|
})
|
|
})
|