mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): apply Claude discovery membership consistently
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mergeDiscoveredAuthoritativeModels } from '../../../../shared/agent-session-option-catalog'
|
||||
import {
|
||||
mergeDiscoveredAuthoritativeModels,
|
||||
type CatalogModel
|
||||
} from '../../../../shared/agent-session-option-catalog'
|
||||
import { CLAUDE_SESSION_OPTION_CATALOG } from '../../../../shared/agent-session-option-catalog-claude-codex'
|
||||
import { GROK_SESSION_OPTION_CATALOG } from '../../../../shared/agent-session-option-catalog-grok'
|
||||
import { updateNativeChatSessionOptionDefaults } from '../../../../shared/native-chat-session-option-defaults'
|
||||
import type { PersistedNativeChatSessionOptions } from '../../../../shared/native-chat-session-options'
|
||||
@@ -94,3 +98,60 @@ describe('retired grok model untracking', () => {
|
||||
expect(persisted().grok?.model).toBe('grok-5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('retired Claude model untracking', () => {
|
||||
beforeEach(() => clearNativeChatSessionOptionCacheForTests())
|
||||
|
||||
const discoveredClaude = (): CatalogModel[] => [
|
||||
{
|
||||
...CLAUDE_SESSION_OPTION_CATALOG.models.find((model) => model.id === 'fable')!,
|
||||
isDefault: true,
|
||||
options: CLAUDE_SESSION_OPTION_CATALOG.models.find((model) => model.id === 'fable')!.options
|
||||
},
|
||||
{ id: 'sonnet', label: 'Sonnet', options: [] }
|
||||
]
|
||||
|
||||
function createClaudeSurface(args?: Partial<CreateNativeChatPtySessionOptionsArgs>): {
|
||||
surface: NativeChatPtySessionOptionsSurface
|
||||
persisted: () => PersistedNativeChatSessionOptions
|
||||
} {
|
||||
let persisted: PersistedNativeChatSessionOptions = {}
|
||||
const surface = createNativeChatPtySessionOptions({
|
||||
agent: 'claude',
|
||||
scopeKey: 'claude-pty-1',
|
||||
mode: 'live',
|
||||
dispatchCommand: vi.fn(),
|
||||
persistSelection: ({ modelId, optionId, value, adoptModelAsLaunchDefault }) => {
|
||||
persisted = updateNativeChatSessionOptionDefaults({
|
||||
persisted,
|
||||
agent: 'claude',
|
||||
modelId,
|
||||
optionId,
|
||||
value,
|
||||
adoptModelAsLaunchDefault
|
||||
})
|
||||
},
|
||||
...args
|
||||
})!
|
||||
return { surface, persisted: () => persisted }
|
||||
}
|
||||
|
||||
it('untracks an omitted Claude alias', () => {
|
||||
seedNativeChatAppliedSessionOptions('claude-pty-1', 'claude', { model: 'opus' })
|
||||
const { surface, persisted } = createClaudeSurface()
|
||||
|
||||
surface.replaceModels(discoveredClaude())
|
||||
|
||||
expect(readNativeChatSessionOptionCache('claude-pty-1')?.model).toBeUndefined()
|
||||
expect(persisted().claude).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not adopt an omitted typed Claude alias as launch default', () => {
|
||||
const { surface, persisted } = createClaudeSurface({ initialModels: discoveredClaude() })
|
||||
|
||||
surface.recordOutgoingCommand('/model opus')
|
||||
expect(persisted().claude?.model).toBeUndefined()
|
||||
surface.recordOutgoingCommand('/model fable')
|
||||
expect(persisted().claude?.model).toBe('fable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -631,12 +631,11 @@ describe('native chat PTY session options', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a tracked alias selectable when the host catalog omits it', async () => {
|
||||
it('drops a tracked Claude alias when the host catalog omits it', () => {
|
||||
seedNativeChatAppliedSessionOptions('pty-1', 'claude', {
|
||||
model: 'opus',
|
||||
effort: 'xhigh'
|
||||
})
|
||||
const dispatch = vi.fn()
|
||||
const surface = createNativeChatPtySessionOptions({
|
||||
agent: 'claude',
|
||||
scopeKey: 'pty-1',
|
||||
@@ -646,23 +645,13 @@ describe('native chat PTY session options', () => {
|
||||
{ id: 'sonnet', label: 'Sonnet', options: [] }
|
||||
],
|
||||
mode: 'live',
|
||||
dispatchCommand: dispatch
|
||||
dispatchCommand: vi.fn()
|
||||
})!
|
||||
|
||||
expect(surface.getSnapshot()[0].kind).toMatchObject({
|
||||
currentValue: 'opus',
|
||||
choices: expect.arrayContaining([
|
||||
{ value: 'opus', label: 'Opus', description: expect.any(String) }
|
||||
])
|
||||
choices: expect.not.arrayContaining([{ value: 'opus' }])
|
||||
})
|
||||
expect(surface.getSnapshot().find(({ id }) => id === 'effort')).toMatchObject({
|
||||
settable: true,
|
||||
kind: { currentValue: 'xhigh' }
|
||||
})
|
||||
|
||||
await surface.setOption('effort', 'high')
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('/effort high')
|
||||
expect(readNativeChatSessionOptionCache('pty-1')?.model).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops the reconciled row once the tracked model moves onto the host catalog', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
discoveredModelsDefineCatalogMembership,
|
||||
getAgentSessionOptionCatalog,
|
||||
type CatalogModel
|
||||
} from '../../../../shared/agent-session-option-catalog'
|
||||
@@ -80,7 +81,7 @@ export function createNativeChatPtySessionOptions(
|
||||
* the picker via re-injection and re-persist the fatal `-m` on any later option
|
||||
* write, undoing the settings retirement. */
|
||||
const untrackRetiredModel = (): boolean => {
|
||||
if (!catalog.discoveredModelsAreAuthoritative || !modelsAreDiscovered) {
|
||||
if (!discoveredModelsDefineCatalogMembership(catalog) || !modelsAreDiscovered) {
|
||||
return false
|
||||
}
|
||||
const trackedId = typeof record.model?.value === 'string' ? record.model.value : null
|
||||
@@ -143,7 +144,8 @@ export function createNativeChatPtySessionOptions(
|
||||
* adopting either would emit an `-m` that is fatal on an account without it. */
|
||||
const modelIsAdoptableAsLaunchDefault = (modelId: string): boolean =>
|
||||
modelsAreDiscovered
|
||||
? !catalog.discoveredModelsAreAuthoritative || models.some((model) => model.id === modelId)
|
||||
? !discoveredModelsDefineCatalogMembership(catalog) ||
|
||||
models.some((model) => model.id === modelId)
|
||||
: record.model !== undefined
|
||||
|
||||
/** Every persist path — picker applies and typed commands — funnels through here. */
|
||||
|
||||
@@ -60,6 +60,16 @@ describe('retirePersistedModelMissingFromDiscovery', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('clears a persisted Claude alias omitted by a replacing probe', async () => {
|
||||
persist({ claude: { model: 'opus', valuesByModel: { opus: { effort: 'high' } } } })
|
||||
await retirePersistedModelMissingFromDiscovery('claude', models('fable', 'sonnet'))
|
||||
expect(mocks.storeState.updateSettings).toHaveBeenCalledWith({
|
||||
nativeChatSessionOptions: {
|
||||
claude: { valuesByModel: { opus: { effort: 'high' } } }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a persisted id the probe still lists', async () => {
|
||||
persist({ grok: { model: 'grok-4.5' } })
|
||||
await retirePersistedModelMissingFromDiscovery('grok', models('grok-4.5', 'grok-build'))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentType } from '../../../../shared/agent-status-types'
|
||||
import {
|
||||
createClaudeCatalogOptions,
|
||||
discoveredModelsDefineCatalogMembership,
|
||||
getAgentSessionOptionCatalog,
|
||||
type CatalogModel
|
||||
} from '../../../../shared/agent-session-option-catalog'
|
||||
@@ -79,7 +80,8 @@ export async function discoverNativeChatCatalogModels(
|
||||
!result.success ||
|
||||
result.models.length === 0 ||
|
||||
// Why: static fallbacks must never impersonate a probe whose rows replace the seed.
|
||||
((catalog?.discoveredModelsReplaceSeed || catalog?.discoveredModelsAreAuthoritative) &&
|
||||
(catalog &&
|
||||
discoveredModelsDefineCatalogMembership(catalog) &&
|
||||
result.catalogOrigin !== 'probe')
|
||||
) {
|
||||
return null
|
||||
|
||||
@@ -357,6 +357,22 @@ describe('native chat session option enrichment', () => {
|
||||
).toEqual({ model: 'retired' })
|
||||
})
|
||||
|
||||
it('drops a persisted Claude alias missing from a replacing probe but keeps Fable', async () => {
|
||||
const persisted = {
|
||||
claude: { model: 'opus', valuesByModel: { opus: { effort: 'high' } } }
|
||||
}
|
||||
const discover = vi.fn().mockResolvedValue([{ id: 'sonnet', label: 'Sonnet', options: [] }])
|
||||
ensureNativeChatModelEnrichment({ agent: 'claude', hostKey: 'local', discover })
|
||||
await vi.waitFor(() => expect(readNativeChatEnrichedModels('claude', 'local')).not.toBeNull())
|
||||
|
||||
expect(resolveNativeChatLaunchSessionOptions(persisted, 'claude')).toBeUndefined()
|
||||
expect(resolveNativeChatLaunchSessionOptions({ claude: { model: 'fable' } }, 'claude')).toEqual(
|
||||
{
|
||||
model: 'fable'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a spec fallback for Claude, whose successful probe replaces the seed', async () => {
|
||||
mocks.discoverRuntimeCommitMessageModels.mockResolvedValue({
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentType } from '../../../../shared/agent-status-types'
|
||||
import {
|
||||
discoveredModelsDefineCatalogMembership,
|
||||
getAgentSessionOptionCatalog,
|
||||
mergeCatalogModels,
|
||||
mergeDiscoveredModelsWithConsentGatedSeeds,
|
||||
@@ -55,7 +56,8 @@ export function resolveNativeChatLaunchSessionOptions(
|
||||
agent: AgentType
|
||||
): Record<string, SessionOptionValue> | undefined {
|
||||
const values = resolveNativeChatSessionOptionDefaults(persisted, agent)
|
||||
if (!values || !getAgentSessionOptionCatalog(agent)?.discoveredModelsAreAuthoritative) {
|
||||
const catalog = getAgentSessionOptionCatalog(agent)
|
||||
if (!values || !catalog || !discoveredModelsDefineCatalogMembership(catalog)) {
|
||||
return values
|
||||
}
|
||||
let probed = false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
||||
import type { AgentType } from '../../../../shared/agent-status-types'
|
||||
import {
|
||||
discoveredModelsDefineCatalogMembership,
|
||||
getAgentSessionOptionCatalog,
|
||||
type CatalogModel
|
||||
} from '../../../../shared/agent-session-option-catalog'
|
||||
@@ -68,7 +69,8 @@ export async function retirePersistedModelMissingFromDiscovery(
|
||||
agent: AgentType,
|
||||
models: readonly CatalogModel[]
|
||||
): Promise<void> {
|
||||
if (!getAgentSessionOptionCatalog(agent)?.discoveredModelsAreAuthoritative) {
|
||||
const catalog = getAgentSessionOptionCatalog(agent)
|
||||
if (!catalog || !discoveredModelsDefineCatalogMembership(catalog)) {
|
||||
return
|
||||
}
|
||||
// An empty list means the probe failed, not that the account has no models.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
discoveredModelsDefineCatalogMembership,
|
||||
getAgentSessionOptionCatalog,
|
||||
mergeCatalogModels,
|
||||
mergeDiscoveredModelsWithConsentGatedSeeds
|
||||
@@ -11,6 +12,18 @@ import {
|
||||
} from './native-chat-session-option-defaults'
|
||||
|
||||
describe('agent session option catalog', () => {
|
||||
it('shares membership policy across replacing and authoritative catalogs', () => {
|
||||
expect(discoveredModelsDefineCatalogMembership(getAgentSessionOptionCatalog('claude')!)).toBe(
|
||||
true
|
||||
)
|
||||
expect(discoveredModelsDefineCatalogMembership(getAgentSessionOptionCatalog('grok')!)).toBe(
|
||||
true
|
||||
)
|
||||
expect(discoveredModelsDefineCatalogMembership(getAgentSessionOptionCatalog('cursor')!)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns no catalog for unknown agents', () => {
|
||||
expect(getAgentSessionOptionCatalog('future-agent')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -40,6 +40,16 @@ export function getAgentSessionOptionCatalog(agent: AgentType): AgentSessionOpti
|
||||
return CATALOGS[agent] ?? null
|
||||
}
|
||||
|
||||
/** Whether a successful discovery result defines the catalog's model membership. */
|
||||
export function discoveredModelsDefineCatalogMembership(
|
||||
catalog: AgentSessionOptionCatalog
|
||||
): boolean {
|
||||
return (
|
||||
catalog.discoveredModelsReplaceSeed === true ||
|
||||
catalog.discoveredModelsAreAuthoritative === true
|
||||
)
|
||||
}
|
||||
|
||||
export function findCatalogModel(
|
||||
catalog: AgentSessionOptionCatalog,
|
||||
modelId: string
|
||||
|
||||
Reference in New Issue
Block a user