fix(team): refine provider diagnostics and model UI
This commit is contained in:
parent
4e3183d186
commit
96b9eab346
13 changed files with 327 additions and 31 deletions
|
|
@ -1041,6 +1041,7 @@ interface AuthStatusCommandResponse {
|
|||
interface RuntimeProviderLaunchFacts {
|
||||
defaultModel: string | null;
|
||||
modelIds: Set<string>;
|
||||
modelListParsed?: boolean;
|
||||
modelCatalog: CliProviderModelCatalog | null;
|
||||
runtimeCapabilities: CliProviderRuntimeCapabilities | null;
|
||||
providerStatus?:
|
||||
|
|
@ -1141,6 +1142,20 @@ function isCodexEffortRuntimeSupported(
|
|||
return reasoning?.configPassthrough === true && reasoning.values.includes(effort);
|
||||
}
|
||||
|
||||
function hasAuthoritativeCodexLaunchCatalog(
|
||||
facts: Pick<
|
||||
RuntimeProviderLaunchFacts,
|
||||
'modelIds' | 'modelListParsed' | 'modelCatalog' | 'runtimeCapabilities'
|
||||
>
|
||||
): boolean {
|
||||
if (facts.modelIds.size > 0 || facts.modelCatalog != null) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
facts.modelListParsed === true && facts.runtimeCapabilities?.modelCatalog?.dynamic === false
|
||||
);
|
||||
}
|
||||
|
||||
function getAnthropicFastModeDefault(): boolean {
|
||||
return (
|
||||
ConfigManager.getInstance().getConfig().providerConnections.anthropic.fastModeDefault === true
|
||||
|
|
@ -6117,11 +6132,13 @@ export class TeamProvisioningService {
|
|||
|
||||
let defaultModel: string | null = null;
|
||||
let modelIds = new Set<string>();
|
||||
let modelListParsed = false;
|
||||
if (modelListResult.status === 'fulfilled') {
|
||||
try {
|
||||
const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(
|
||||
modelListResult.value.stdout
|
||||
);
|
||||
modelListParsed = true;
|
||||
const provider = parsed.providers?.[params.providerId];
|
||||
defaultModel =
|
||||
typeof provider?.defaultModel === 'string' && provider.defaultModel.trim().length > 0
|
||||
|
|
@ -6202,6 +6219,7 @@ export class TeamProvisioningService {
|
|||
})
|
||||
: defaultModel,
|
||||
modelIds,
|
||||
modelListParsed,
|
||||
modelCatalog,
|
||||
runtimeCapabilities,
|
||||
providerStatus,
|
||||
|
|
@ -6400,6 +6418,10 @@ export class TeamProvisioningService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!hasAuthoritativeCodexLaunchCatalog(params.facts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${params.actorLabel} uses Codex model "${explicitModel}", but this Agent Teams runtime does not declare dynamic Codex model launch support yet. Upgrade the runtime or pick a listed Codex model.`
|
||||
);
|
||||
|
|
@ -17152,9 +17174,18 @@ export class TeamProvisioningService {
|
|||
|
||||
const dynamicCatalog = params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === true;
|
||||
const hasAuthoritativeCatalog =
|
||||
availableModels.size > 0 ||
|
||||
params.runtimeFacts.modelCatalog != null ||
|
||||
params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false;
|
||||
params.providerId === 'codex'
|
||||
? hasAuthoritativeCodexLaunchCatalog(params.runtimeFacts)
|
||||
: availableModels.size > 0 ||
|
||||
params.runtimeFacts.modelCatalog != null ||
|
||||
params.runtimeFacts.runtimeCapabilities?.modelCatalog?.dynamic === false;
|
||||
|
||||
if (params.providerId === 'codex' && (dynamicCatalog || !hasAuthoritativeCatalog)) {
|
||||
return {
|
||||
kind: 'available',
|
||||
resolvedModelId: trimmedModelId,
|
||||
};
|
||||
}
|
||||
|
||||
if (dynamicCatalog || !hasAuthoritativeCatalog) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ export function getProvisioningProviderBackendSummary(
|
|||
suffixes.push('runtime missing');
|
||||
break;
|
||||
case 'degraded':
|
||||
suffixes.push('degraded');
|
||||
if (inferredProviderId !== 'codex') {
|
||||
suffixes.push('degraded');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -311,6 +311,7 @@ function isSuppressibleGenericPreflightWarning(detail: string): boolean {
|
|||
}
|
||||
|
||||
return (
|
||||
lower.includes('one-shot diagnostic') ||
|
||||
lower.includes('preflight check failed') ||
|
||||
(lower.includes('preflight check for `') && lower.includes('-p` did not complete')) ||
|
||||
lower.includes('preflight ping completed but did not return the expected pong')
|
||||
|
|
@ -413,6 +414,13 @@ function resolveModelResultFromBatch(
|
|||
/selected model .* is compatible\. deep verification pending\./i.test(entry)
|
||||
);
|
||||
if (hasCompatibilityLine) {
|
||||
if (providerId !== 'opencode') {
|
||||
return {
|
||||
status: 'ready',
|
||||
line: buildModelAvailableLine(providerId, modelId),
|
||||
warningLine: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'notes',
|
||||
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
||||
|
|
@ -452,6 +460,13 @@ function resolveModelResultFromBatch(
|
|||
}
|
||||
|
||||
if (result.ready && (result.warnings?.length ?? 0) > 0 && !hasModelScopedEntries) {
|
||||
if (providerId !== 'opencode') {
|
||||
return {
|
||||
status: 'ready',
|
||||
line: buildModelAvailableLine(providerId, modelId),
|
||||
warningLine: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'notes',
|
||||
line: buildModelCompatibilityPendingLine(providerId, modelId),
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ vi.mock('@renderer/hooks/useTheme', () => ({
|
|||
|
||||
vi.mock('@renderer/utils/teamModelCatalog', () => ({
|
||||
isAnthropicHaikuTeamModel: () => false,
|
||||
isAnthropicSonnetTeamModel: (model: string | undefined) =>
|
||||
model === 'sonnet' || model === 'claude-sonnet-4-6' || model === 'sonnet[1m]',
|
||||
}));
|
||||
|
||||
vi.mock('../../ui/button', () => ({
|
||||
|
|
@ -78,9 +80,12 @@ vi.mock('../../ui/button', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
import { LeadModelRow } from './LeadModelRow';
|
||||
import { ANTHROPIC_LONG_CONTEXT_PRICING_URL, LeadModelRow } from './LeadModelRow';
|
||||
|
||||
function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType<typeof createRoot> } {
|
||||
function renderLeadModelRow(overrides: Partial<React.ComponentProps<typeof LeadModelRow>> = {}): {
|
||||
host: HTMLDivElement;
|
||||
root: ReturnType<typeof createRoot>;
|
||||
} {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
|
@ -98,6 +103,7 @@ function renderLeadModelRow(): { host: HTMLDivElement; root: ReturnType<typeof c
|
|||
onLimitContextChange: () => undefined,
|
||||
syncModelsWithTeammates: true,
|
||||
onSyncModelsWithTeammatesChange: () => undefined,
|
||||
...overrides,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -128,4 +134,38 @@ describe('LeadModelRow', () => {
|
|||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('warns that unchecked 200K limit can put Sonnet on Anthropic Extra Usage', () => {
|
||||
const { host, root } = renderLeadModelRow({
|
||||
providerId: 'anthropic',
|
||||
model: 'sonnet',
|
||||
limitContext: false,
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Sonnet with 1M context can use Anthropic Extra Usage');
|
||||
expect(host.textContent).toContain('Requests over 200K input tokens');
|
||||
const docsLink = host.querySelector(`a[href="${ANTHROPIC_LONG_CONTEXT_PRICING_URL}"]`);
|
||||
|
||||
expect(docsLink?.textContent).toContain('Anthropic pricing docs');
|
||||
expect(docsLink?.getAttribute('target')).toBe('_blank');
|
||||
expect(docsLink?.getAttribute('rel')).toBe('noreferrer');
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the Sonnet Extra Usage warning when 200K limit is enabled', () => {
|
||||
const { host, root } = renderLeadModelRow({
|
||||
providerId: 'anthropic',
|
||||
model: 'sonnet',
|
||||
limitContext: true,
|
||||
});
|
||||
|
||||
expect(host.textContent).not.toContain('Anthropic Extra Usage');
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import { getTeamColorSet } from '@renderer/constants/teamColors';
|
|||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
|
||||
import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog';
|
||||
import {
|
||||
isAnthropicHaikuTeamModel,
|
||||
isAnthropicSonnetTeamModel,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors';
|
||||
import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react';
|
||||
|
||||
|
|
@ -22,6 +25,11 @@ import { Button } from '../../ui/button';
|
|||
|
||||
import type { EffortLevel, TeamProviderId } from '@shared/types';
|
||||
|
||||
export const ANTHROPIC_SONNET_EXTRA_USAGE_WARNING =
|
||||
'Sonnet with 1M context can use Anthropic Extra Usage. Requests over 200K input tokens are billed at premium long-context rates; enable Limit context to 200K tokens to avoid that billing path.';
|
||||
export const ANTHROPIC_LONG_CONTEXT_PRICING_URL =
|
||||
'https://platform.claude.com/docs/en/about-claude/pricing';
|
||||
|
||||
interface LeadModelRowProps {
|
||||
providerId: TeamProviderId;
|
||||
model: string;
|
||||
|
|
@ -61,6 +69,12 @@ export const LeadModelRow = ({
|
|||
: 'Default';
|
||||
const modelButtonAriaLabel = `${getTeamProviderLabel(providerId)} provider, ${modelButtonLabel}`;
|
||||
const hasModelIssue = Boolean(modelIssueText);
|
||||
const showSonnetExtraUsageWarning =
|
||||
providerId === 'anthropic' && !limitContext && isAnthropicSonnetTeamModel(model);
|
||||
const warningMessages = [warningText?.trim() || null].filter((message): message is string =>
|
||||
Boolean(message)
|
||||
);
|
||||
const hasWarnings = warningMessages.length > 0 || showSonnetExtraUsageWarning;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -132,11 +146,29 @@ export const LeadModelRow = ({
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{warningText ? (
|
||||
{hasWarnings ? (
|
||||
<div className="md:col-span-3">
|
||||
<div className="bg-amber-500/8 ml-3 flex items-start gap-2 rounded-md border border-amber-500/25 px-3 py-2 text-[11px] leading-relaxed text-amber-200">
|
||||
<Info className="mt-0.5 size-3.5 shrink-0 text-amber-300" />
|
||||
<p>{warningText}</p>
|
||||
<div className="space-y-1">
|
||||
{warningMessages.map((message) => (
|
||||
<p key={message}>{message}</p>
|
||||
))}
|
||||
{showSonnetExtraUsageWarning ? (
|
||||
<p>
|
||||
{ANTHROPIC_SONNET_EXTRA_USAGE_WARNING}{' '}
|
||||
<a
|
||||
href={ANTHROPIC_LONG_CONTEXT_PRICING_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-amber-100 underline underline-offset-2 hover:text-white"
|
||||
>
|
||||
Read Anthropic pricing docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -250,10 +250,10 @@
|
|||
width: 100%;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
#splash-status {
|
||||
min-height: 16px;
|
||||
min-height: 8px;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
|
|
@ -261,21 +261,21 @@
|
|||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
color: rgba(212, 212, 216, 0.72);
|
||||
line-height: 1.35;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
#splash-elapsed::before {
|
||||
content: '·';
|
||||
margin-right: 6px;
|
||||
margin-right: 4px;
|
||||
color: rgba(212, 212, 216, 0.34);
|
||||
}
|
||||
#splash-elapsed {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
color: rgba(212, 212, 216, 0.5);
|
||||
white-space: nowrap;
|
||||
|
|
@ -295,14 +295,18 @@
|
|||
line-height: 1.35;
|
||||
}
|
||||
#splash-timeline {
|
||||
display: flex;
|
||||
display: none;
|
||||
width: min(320px, 78vw);
|
||||
max-height: 128px;
|
||||
margin-top: 14px;
|
||||
max-height: 58px;
|
||||
margin-top: 8px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#splash.splash-status-error #splash-timeline,
|
||||
#splash.splash-status-slow #splash-timeline {
|
||||
display: flex;
|
||||
}
|
||||
.splash-step {
|
||||
display: grid;
|
||||
grid-template-columns: 8px minmax(0, 1fr) auto;
|
||||
|
|
@ -494,6 +498,18 @@
|
|||
(function () {
|
||||
try {
|
||||
window.__claudeTeamsSplashStartedAt = performance.now();
|
||||
window.__claudeTeamsSplashStaticTimer = window.setInterval(function () {
|
||||
var elapsed = document.getElementById('splash-elapsed');
|
||||
var splash = document.getElementById('splash');
|
||||
if (!elapsed || !splash) {
|
||||
window.clearInterval(window.__claudeTeamsSplashStaticTimer);
|
||||
window.__claudeTeamsSplashStaticTimer = undefined;
|
||||
return;
|
||||
}
|
||||
var startedAt = window.__claudeTeamsSplashStartedAt || performance.now();
|
||||
var seconds = Math.max(0, Math.floor((performance.now() - startedAt) / 1000));
|
||||
elapsed.textContent = seconds + 's';
|
||||
}, 1000);
|
||||
var theme =
|
||||
localStorage.getItem('agent-teams-theme-cache') ||
|
||||
localStorage.getItem('claude-devtools-theme-cache');
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { AppStartupStatus, AppStartupStep } from '@shared/types/api';
|
|||
declare global {
|
||||
interface Window {
|
||||
__claudeTeamsUiDidInit?: boolean;
|
||||
__claudeTeamsSplashStaticTimer?: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ let startupTicker: number | undefined;
|
|||
|
||||
const SLOW_STEP_MS = 7_000;
|
||||
const VERY_SLOW_STEP_MS = 14_000;
|
||||
const TIMELINE_STEP_LIMIT = 6;
|
||||
const TIMELINE_STEP_LIMIT = 3;
|
||||
|
||||
function getStartupErrorText(status: AppStartupStatus): string {
|
||||
return status.error ? `Startup failed: ${status.error}` : 'Startup failed. Please restart.';
|
||||
|
|
@ -142,6 +143,12 @@ function updateStartupSplash(status: AppStartupStatus): void {
|
|||
renderStartupTimeline(status);
|
||||
}
|
||||
|
||||
function stopStaticSplashTimer(): void {
|
||||
if (window.__claudeTeamsSplashStaticTimer === undefined) return;
|
||||
window.clearInterval(window.__claudeTeamsSplashStaticTimer);
|
||||
window.__claudeTeamsSplashStaticTimer = undefined;
|
||||
}
|
||||
|
||||
function startStartupTicker(): void {
|
||||
if (startupTicker !== undefined) return;
|
||||
startupTicker = window.setInterval(() => {
|
||||
|
|
@ -194,13 +201,16 @@ async function bootstrapRenderer(): Promise<void> {
|
|||
if (nextStatus.ready) {
|
||||
finished = true;
|
||||
cleanup();
|
||||
stopStaticSplashTimer();
|
||||
stopStartupTicker();
|
||||
mountApp();
|
||||
} else if (nextStatus.error) {
|
||||
finished = true;
|
||||
cleanup();
|
||||
stopStaticSplashTimer();
|
||||
stopStartupTicker();
|
||||
} else {
|
||||
stopStaticSplashTimer();
|
||||
startStartupTicker();
|
||||
}
|
||||
};
|
||||
|
|
@ -210,6 +220,7 @@ async function bootstrapRenderer(): Promise<void> {
|
|||
} catch (error) {
|
||||
console.warn(`[startup] status bridge unavailable: ${String(error)}`);
|
||||
cleanup();
|
||||
stopStaticSplashTimer();
|
||||
stopStartupTicker();
|
||||
mountApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,16 @@ export function isAnthropicHaikuTeamModel(model: string | undefined): boolean {
|
|||
return baseModel === 'haiku' || baseModel.startsWith('claude-haiku-');
|
||||
}
|
||||
|
||||
export function isAnthropicSonnetTeamModel(model: string | undefined): boolean {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { baseModel } = splitOneMillionContextSuffix(trimmed);
|
||||
return baseModel === 'sonnet' || baseModel.startsWith('claude-sonnet-');
|
||||
}
|
||||
|
||||
export function getTeamProviderLabel(
|
||||
providerId: SupportedProviderId | undefined
|
||||
): string | undefined {
|
||||
|
|
|
|||
|
|
@ -2005,7 +2005,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('keeps missing models compatible when the runtime catalog is dynamic', async () => {
|
||||
it('treats missing Codex models as launchable when the runtime catalog is dynamic', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||
env: {
|
||||
|
|
@ -2033,13 +2033,77 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
|||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
details: ['Selected model future-model is compatible. Deep verification pending.'],
|
||||
details: ['Selected model future-model is available for launch.'],
|
||||
warnings: [],
|
||||
blockingMessages: [],
|
||||
});
|
||||
expect(spawnProbe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('treats explicit Codex models as launchable when the runtime model list is unparsable', async () => {
|
||||
execCliMock.mockImplementation(async (_binaryPath: string | null, args: string[]) => {
|
||||
if (args[0] === 'model' && args[1] === 'list' && args.includes('codex')) {
|
||||
return {
|
||||
stdout: 'Codex model list is temporarily unavailable',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
if (args[0] === 'runtime' && args[1] === 'status' && args.includes('codex')) {
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
providers: {
|
||||
codex: {
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: { dynamic: false, source: 'runtime' },
|
||||
reasoningEffort: {
|
||||
supported: true,
|
||||
values: ['low', 'medium', 'high'],
|
||||
configPassthrough: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
};
|
||||
}
|
||||
return defaultExecCliMockImplementation(_binaryPath, args);
|
||||
});
|
||||
|
||||
const svc = new TeamProvisioningService();
|
||||
vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({
|
||||
claudePath: '/fake/claude',
|
||||
authSource: 'codex_runtime',
|
||||
});
|
||||
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||
env: {
|
||||
PATH: '/usr/bin',
|
||||
SHELL: '/bin/zsh',
|
||||
},
|
||||
authSource: 'codex_runtime',
|
||||
geminiRuntimeAuth: null,
|
||||
});
|
||||
const spawnProbe = vi.spyOn(svc as any, 'spawnProbe');
|
||||
|
||||
const result = await svc.prepareForProvisioning(tempRoot, {
|
||||
forceFresh: true,
|
||||
providerId: 'codex',
|
||||
modelIds: ['gpt-5.5'],
|
||||
modelVerificationMode: 'compatibility',
|
||||
});
|
||||
|
||||
expect(result.ready).toBe(true);
|
||||
expect(result.details).toEqual(['Selected model gpt-5.5 is available for launch.']);
|
||||
expect(result.message).toBe('CLI is warmed up and ready to launch');
|
||||
expect(spawnProbe).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
||||
'[Service:TeamProvisioning] [codex] Failed to parse runtime model list for launch validation: No JSON object found in CLI output',
|
||||
]);
|
||||
vi.mocked(console.warn).mockClear();
|
||||
});
|
||||
|
||||
it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -469,6 +469,45 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
|||
expect(spawnCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows explicit Codex models when launch model list parsing is degraded', async () => {
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
|
||||
const { child } = createFakeChild();
|
||||
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||
env: {},
|
||||
authSource: 'codex_runtime',
|
||||
providerArgs: [],
|
||||
}));
|
||||
(svc as any).readRuntimeProviderLaunchFacts = vi.fn(async () => ({
|
||||
defaultModel: null,
|
||||
modelIds: new Set(),
|
||||
modelListParsed: false,
|
||||
modelCatalog: null,
|
||||
runtimeCapabilities: { modelCatalog: { dynamic: false, source: 'runtime' } },
|
||||
providerStatus: null,
|
||||
}));
|
||||
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||
(svc as any).startFilesystemMonitor = vi.fn();
|
||||
(svc as any).pathExists = vi.fn(async () => false);
|
||||
|
||||
const { runId } = await svc.createTeam(
|
||||
{
|
||||
teamName: 'codex-future-model-launchable',
|
||||
cwd: process.cwd(),
|
||||
members: [],
|
||||
providerId: 'codex',
|
||||
model: 'gpt-5.5',
|
||||
effort: 'medium',
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
|
||||
expect(spawnCli).toHaveBeenCalled();
|
||||
await svc.cancelProvisioning(runId);
|
||||
});
|
||||
|
||||
it('restart teammate message keeps the exact teammate identity and avoids duplicate semantics', () => {
|
||||
const message = buildRestartMemberSpawnMessage('forge-labs', 'Forge Labs', 'lead', {
|
||||
name: 'alice',
|
||||
|
|
|
|||
|
|
@ -266,6 +266,33 @@ describe('ProvisioningProviderStatusList', () => {
|
|||
).toBe('Codex native');
|
||||
});
|
||||
|
||||
it('does not show non-blocking Codex degraded backend state in provisioning summaries', () => {
|
||||
expect(
|
||||
getProvisioningProviderBackendSummary({
|
||||
providerId: 'codex',
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
backend: {
|
||||
kind: 'codex-native',
|
||||
label: 'Codex native',
|
||||
},
|
||||
availableBackends: [
|
||||
{
|
||||
id: 'codex-native',
|
||||
label: 'Codex native',
|
||||
description: 'Use codex exec JSON mode.',
|
||||
selectable: false,
|
||||
recommended: false,
|
||||
available: true,
|
||||
state: 'degraded',
|
||||
audience: 'general',
|
||||
statusMessage: 'Ready with degraded account verification.',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe('Codex native');
|
||||
});
|
||||
|
||||
it('normalizes persisted legacy codex fallback summaries to Codex native', () => {
|
||||
expect(
|
||||
getProvisioningProviderBackendSummary({
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
expect(result.details).toEqual(['5.4 Mini - verified', '5.4 - verified']);
|
||||
});
|
||||
|
||||
it('does not synthesize verified from a generic runtime preflight note alone', async () => {
|
||||
it('treats launchable Codex compatibility as ready and suppresses generic preflight notes', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
|
|
@ -593,16 +593,13 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('notes');
|
||||
expect(result.warnings).toEqual(['orchestrator-cli preflight check failed (exit code 1).']);
|
||||
expect(result.details).toEqual([
|
||||
'orchestrator-cli preflight check failed (exit code 1).',
|
||||
'5.4 - compatible, deep verification pending...',
|
||||
]);
|
||||
expect(result.status).toBe('ready');
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.details).toEqual(['5.4 - available for launch']);
|
||||
expect(result.modelResultsById).toEqual({
|
||||
'gpt-5.4': {
|
||||
status: 'notes',
|
||||
line: '5.4 - compatible, deep verification pending...',
|
||||
status: 'ready',
|
||||
line: '5.4 - available for launch',
|
||||
warningLine: null,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog';
|
||||
import {
|
||||
getVisibleTeamProviderModels,
|
||||
isAnthropicSonnetTeamModel,
|
||||
} from '@renderer/utils/teamModelCatalog';
|
||||
|
||||
describe('teamModelCatalog', () => {
|
||||
it('filters UI-disabled Codex models from provider badge lists', () => {
|
||||
|
|
@ -43,4 +46,13 @@ describe('teamModelCatalog', () => {
|
|||
'claude-sonnet-4-6[1m]',
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects Sonnet aliases with or without 1M suffix', () => {
|
||||
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
||||
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
||||
expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6')).toBe(true);
|
||||
expect(isAnthropicSonnetTeamModel('claude-sonnet-4-6[1m]')).toBe(true);
|
||||
expect(isAnthropicSonnetTeamModel('opus')).toBe(false);
|
||||
expect(isAnthropicSonnetTeamModel('haiku')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue