fix(team): scope OpenCode preflight runtime failures
This commit is contained in:
parent
9a8a59757c
commit
b77eaf2b26
6 changed files with 750 additions and 59 deletions
|
|
@ -556,6 +556,7 @@ import type {
|
|||
TeamMember,
|
||||
TeamProviderBackendId,
|
||||
TeamProviderId,
|
||||
TeamProvisioningPrepareIssue,
|
||||
TeamProvisioningModelVerificationMode,
|
||||
TeamProvisioningPrepareResult,
|
||||
TeamProvisioningProgress,
|
||||
|
|
@ -17306,6 +17307,7 @@ export class TeamProvisioningService {
|
|||
const warnings: string[] = [];
|
||||
const details: string[] = [];
|
||||
const blockingMessages: string[] = [];
|
||||
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||
const selectedModelIds = Array.from(
|
||||
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
||||
);
|
||||
|
|
@ -17349,6 +17351,7 @@ export class TeamProvisioningService {
|
|||
details.push(...openCodeModelPrepare.details);
|
||||
warnings.push(...openCodeModelPrepare.warnings);
|
||||
blockingMessages.push(...openCodeModelPrepare.blockingMessages);
|
||||
issues.push(...openCodeModelPrepare.issues);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -17512,6 +17515,7 @@ export class TeamProvisioningService {
|
|||
? blockingMessages[0]
|
||||
: 'Some provider runtimes are not ready',
|
||||
warnings: failureWarnings.length > 0 ? failureWarnings : undefined,
|
||||
issues: issues.length > 0 ? issues : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -17527,6 +17531,7 @@ export class TeamProvisioningService {
|
|||
? 'CLI is ready to launch (see notes)'
|
||||
: 'CLI is warmed up and ready to launch',
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
issues: issues.length > 0 ? issues : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -17544,14 +17549,16 @@ export class TeamProvisioningService {
|
|||
details: string[];
|
||||
warnings: string[];
|
||||
blockingMessages: string[];
|
||||
issues: TeamProvisioningPrepareIssue[];
|
||||
}> {
|
||||
const details: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const blockingMessages: string[] = [];
|
||||
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||
const startedAt = Date.now();
|
||||
|
||||
if (modelIds.length === 0) {
|
||||
return { details, warnings, blockingMessages };
|
||||
return { details, warnings, blockingMessages, issues };
|
||||
}
|
||||
|
||||
if (verificationMode === 'compatibility') {
|
||||
|
|
@ -17662,6 +17669,16 @@ export class TeamProvisioningService {
|
|||
prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason;
|
||||
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
|
||||
const verificationWarningLine = `Selected model ${modelId} could not be verified. ${primaryReason}`;
|
||||
const issueSeverity =
|
||||
prepare.retryable && verificationMode !== 'compatibility' ? 'warning' : 'blocking';
|
||||
issues.push({
|
||||
providerId: 'opencode',
|
||||
modelId,
|
||||
scope: 'model',
|
||||
severity: issueSeverity,
|
||||
code: prepare.reason,
|
||||
message: primaryReason,
|
||||
});
|
||||
if (prepare.retryable) {
|
||||
warnings.push(verificationWarningLine);
|
||||
if (verificationMode === 'compatibility') {
|
||||
|
|
@ -17685,7 +17702,7 @@ export class TeamProvisioningService {
|
|||
blockingMessages,
|
||||
});
|
||||
|
||||
return { details, warnings, blockingMessages };
|
||||
return { details, warnings, blockingMessages, issues };
|
||||
}
|
||||
|
||||
private async prepareSelectedOpenCodeModelsCompatibilityBatch({
|
||||
|
|
@ -17700,10 +17717,12 @@ export class TeamProvisioningService {
|
|||
details: string[];
|
||||
warnings: string[];
|
||||
blockingMessages: string[];
|
||||
issues: TeamProvisioningPrepareIssue[];
|
||||
} | null> {
|
||||
const details: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const blockingMessages: string[] = [];
|
||||
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||
const startedAt = Date.now();
|
||||
|
||||
appendPreflightDebugLog('opencode_compatibility_batch_start', {
|
||||
|
|
@ -17749,18 +17768,20 @@ export class TeamProvisioningService {
|
|||
if (!sharedPrepare.ok) {
|
||||
const primaryReason =
|
||||
sharedPrepare.diagnostics.find((entry) => entry.trim().length > 0) ?? sharedPrepare.reason;
|
||||
for (const modelId of modelIds) {
|
||||
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
|
||||
const verificationWarningLine = `Selected model ${modelId} could not be verified. ${primaryReason}`;
|
||||
if (sharedPrepare.retryable) {
|
||||
warnings.push(verificationWarningLine);
|
||||
blockingMessages.push(verificationWarningLine);
|
||||
} else {
|
||||
details.push(unavailableLine);
|
||||
blockingMessages.push(unavailableLine);
|
||||
}
|
||||
if (primaryReason.trim().length > 0) {
|
||||
details.push(primaryReason);
|
||||
blockingMessages.push(primaryReason);
|
||||
} else {
|
||||
blockingMessages.push(`OpenCode: ${sharedPrepare.reason}`);
|
||||
}
|
||||
return { details, warnings, blockingMessages };
|
||||
issues.push({
|
||||
providerId: 'opencode',
|
||||
scope: 'provider',
|
||||
severity: 'blocking',
|
||||
code: sharedPrepare.reason,
|
||||
message: primaryReason.trim() || `OpenCode: ${sharedPrepare.reason}`,
|
||||
});
|
||||
return { details, warnings, blockingMessages, issues };
|
||||
}
|
||||
|
||||
const latestReadiness =
|
||||
|
|
@ -17798,6 +17819,14 @@ export class TeamProvisioningService {
|
|||
const unavailableLine = `Selected model ${modelId} is unavailable. ${resolvedModel.reason}`;
|
||||
details.push(unavailableLine);
|
||||
blockingMessages.push(unavailableLine);
|
||||
issues.push({
|
||||
providerId: 'opencode',
|
||||
modelId,
|
||||
scope: 'model',
|
||||
severity: 'blocking',
|
||||
code: 'model_unavailable',
|
||||
message: resolvedModel.reason,
|
||||
});
|
||||
}
|
||||
|
||||
appendPreflightDebugLog('opencode_compatibility_batch_complete', {
|
||||
|
|
@ -17808,7 +17837,7 @@ export class TeamProvisioningService {
|
|||
details,
|
||||
});
|
||||
|
||||
return { details, warnings, blockingMessages };
|
||||
return { details, warnings, blockingMessages, issues };
|
||||
}
|
||||
|
||||
private resolveOpenCodeCompatibilityModel(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ function escapeRegExp(value: string): string {
|
|||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function uniquePrepareLines(lines: Array<string | null | undefined>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const uniqueLines: string[] = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line?.trim() ?? '';
|
||||
if (!trimmed || seen.has(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
uniqueLines.push(trimmed);
|
||||
}
|
||||
return uniqueLines;
|
||||
}
|
||||
|
||||
function getModelLabel(providerId: TeamProviderId, modelId: string): string {
|
||||
if (isDefaultProviderModelSelection(modelId)) {
|
||||
return 'Default';
|
||||
|
|
@ -205,6 +219,36 @@ function normalizeModelReason(rawReason: string | null | undefined): string | nu
|
|||
return trimmed;
|
||||
}
|
||||
|
||||
function looksLikeOpenCodeRuntimeFailureReason(reason: string | null | undefined): boolean {
|
||||
const lower = reason?.trim().toLowerCase() ?? '';
|
||||
if (!lower) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
lower.includes('opencode /experimental/tool') ||
|
||||
lower.includes('/experimental/tool') ||
|
||||
lower.includes('mcp_unavailable') ||
|
||||
lower.includes('unable to connect') ||
|
||||
lower.includes('runtime store') ||
|
||||
lower.includes('opencode cli')
|
||||
);
|
||||
}
|
||||
|
||||
function getBlockingProviderIssueMessage(
|
||||
providerId: TeamProviderId,
|
||||
result: TeamProvisioningPrepareResult
|
||||
): string | null {
|
||||
const issue = result.issues?.find(
|
||||
(entry) =>
|
||||
entry.scope === 'provider' &&
|
||||
entry.severity === 'blocking' &&
|
||||
(!entry.providerId || entry.providerId === providerId) &&
|
||||
entry.message.trim().length > 0
|
||||
);
|
||||
return issue?.message.trim() ?? null;
|
||||
}
|
||||
|
||||
function getResultReason(modelId: string, result: TeamProvisioningPrepareResult): string | null {
|
||||
const candidates = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
|
||||
.map((entry) => entry?.trim() ?? '')
|
||||
|
|
@ -286,7 +330,18 @@ function buildModelFailureLine(
|
|||
}
|
||||
|
||||
function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string[] {
|
||||
return [...(result.details ?? []), ...(result.warnings ?? [])];
|
||||
return uniquePrepareLines([...(result.details ?? []), ...(result.warnings ?? [])]);
|
||||
}
|
||||
|
||||
function createRuntimeWarningLines(result: TeamProvisioningPrepareResult): string[] {
|
||||
return uniquePrepareLines(result.warnings ?? []);
|
||||
}
|
||||
|
||||
function createRuntimeFailureDetailLines(
|
||||
runtimeDetailLines: readonly string[],
|
||||
message: string | null | undefined
|
||||
): string[] {
|
||||
return uniquePrepareLines([...runtimeDetailLines, message]);
|
||||
}
|
||||
|
||||
function extractTimedOutPreflightProbeModelId(detail: string): string | null {
|
||||
|
|
@ -545,7 +600,10 @@ function resolveModelResultFromCompatibilityBatch(
|
|||
const hasUnavailableLine = modelScopedEntries.some((entry) =>
|
||||
/selected model .* is unavailable\./i.test(entry)
|
||||
);
|
||||
if (hasUnavailableLine || (!result.ready && isOnlyModel)) {
|
||||
const hasVerificationWarningLine = modelScopedEntries.some((entry) =>
|
||||
/selected model .* could not be verified\./i.test(entry)
|
||||
);
|
||||
if (hasUnavailableLine || (!result.ready && isOnlyModel && !hasVerificationWarningLine)) {
|
||||
return {
|
||||
kind: 'terminal',
|
||||
result: {
|
||||
|
|
@ -561,9 +619,6 @@ function resolveModelResultFromCompatibilityBatch(
|
|||
};
|
||||
}
|
||||
|
||||
const hasVerificationWarningLine = modelScopedEntries.some((entry) =>
|
||||
/selected model .* could not be verified\./i.test(entry)
|
||||
);
|
||||
if (hasVerificationWarningLine) {
|
||||
const line = buildModelFailureLine(
|
||||
providerId,
|
||||
|
|
@ -574,9 +629,9 @@ function resolveModelResultFromCompatibilityBatch(
|
|||
return {
|
||||
kind: 'terminal',
|
||||
result: {
|
||||
status: 'notes',
|
||||
status: result.ready ? 'notes' : 'failed',
|
||||
line,
|
||||
warningLine: line,
|
||||
warningLine: result.ready ? line : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -601,6 +656,84 @@ function resolveModelResultFromCompatibilityBatch(
|
|||
};
|
||||
}
|
||||
|
||||
function looksLikeSingleModelCredentialFailure(result: TeamProvisioningPrepareResult): boolean {
|
||||
const combined = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
|
||||
.map((entry) => entry?.trim() ?? '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.toLowerCase();
|
||||
|
||||
const hasCredentialContext =
|
||||
combined.includes('token') ||
|
||||
combined.includes('authenticat') ||
|
||||
combined.includes('credential') ||
|
||||
combined.includes('api key') ||
|
||||
combined.includes('apikey') ||
|
||||
combined.includes('not_authenticated');
|
||||
const hasAuthStatus =
|
||||
combined.includes('unauthorized') ||
|
||||
combined.includes('forbidden') ||
|
||||
/\b401\b/.test(combined) ||
|
||||
/\b403\b/.test(combined);
|
||||
|
||||
return (
|
||||
combined.includes('token refresh failed') ||
|
||||
combined.includes('authentication failed') ||
|
||||
combined.includes('not_authenticated') ||
|
||||
(hasCredentialContext && hasAuthStatus)
|
||||
);
|
||||
}
|
||||
|
||||
function getOpenCodeProviderScopedFailureFromModelScopedEntries(
|
||||
modelIds: readonly string[],
|
||||
result: TeamProvisioningPrepareResult
|
||||
): string | null {
|
||||
if (result.ready) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const modelId of modelIds) {
|
||||
const scopedEntries = getModelScopedEntries(modelId, result);
|
||||
for (const entry of scopedEntries) {
|
||||
const stripped = stripSelectedModelPrefix(modelId, entry);
|
||||
if (looksLikeOpenCodeRuntimeFailureReason(stripped)) {
|
||||
return stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure(params: {
|
||||
result: TeamProvisioningPrepareResult;
|
||||
modelIds: readonly string[];
|
||||
modelScopedEntriesPresent: boolean;
|
||||
runtimeDetailLines: readonly string[];
|
||||
runtimeWarnings: readonly string[];
|
||||
}): boolean {
|
||||
if (params.result.ready || params.modelScopedEntriesPresent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasRuntimeScopedDiagnostics =
|
||||
params.runtimeDetailLines.length > 0 ||
|
||||
params.runtimeWarnings.length > 0 ||
|
||||
Boolean(params.result.message?.trim());
|
||||
|
||||
if (!hasRuntimeScopedDiagnostics) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canTreatAsLegacySingleModelFailure =
|
||||
params.modelIds.length === 1 &&
|
||||
(looksLikeSingleModelBatchFailure(params.modelIds[0], params.result) ||
|
||||
looksLikeSingleModelCredentialFailure(params.result)) &&
|
||||
params.runtimeWarnings.length === 0;
|
||||
|
||||
return !canTreatAsLegacySingleModelFailure;
|
||||
}
|
||||
|
||||
export async function runProviderPrepareDiagnostics({
|
||||
cwd,
|
||||
providerId,
|
||||
|
|
@ -627,12 +760,12 @@ export async function runProviderPrepareDiagnostics({
|
|||
limitContext
|
||||
);
|
||||
const runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
||||
const runtimeWarnings = [...(runtimeResult.warnings ?? [])];
|
||||
const runtimeWarnings = createRuntimeWarningLines(runtimeResult);
|
||||
|
||||
if (!runtimeResult.ready) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: [...runtimeDetailLines, ...(runtimeResult.message ? [runtimeResult.message] : [])],
|
||||
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
|
||||
warnings: runtimeWarnings,
|
||||
modelResultsById: {},
|
||||
};
|
||||
|
|
@ -712,12 +845,12 @@ export async function runProviderPrepareDiagnostics({
|
|||
limitContext
|
||||
);
|
||||
runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
||||
runtimeWarnings = [...(runtimeResult.warnings ?? [])];
|
||||
runtimeWarnings = createRuntimeWarningLines(runtimeResult);
|
||||
|
||||
if (!runtimeResult.ready) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: [...runtimeDetailLines, ...(runtimeResult.message ? [runtimeResult.message] : [])],
|
||||
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
|
||||
warnings: runtimeWarnings,
|
||||
modelResultsById: {},
|
||||
};
|
||||
|
|
@ -754,7 +887,7 @@ export async function runProviderPrepareDiagnostics({
|
|||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
runtimeWarnings = [...(compatibilityResult.warnings ?? [])].filter(
|
||||
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
|
||||
|
|
@ -766,18 +899,43 @@ export async function runProviderPrepareDiagnostics({
|
|||
const hasSingleModelFallbackReason =
|
||||
uncachedModelIds.length === 1 &&
|
||||
looksLikeSingleModelBatchFailure(uncachedModelIds[0], compatibilityResult);
|
||||
if (
|
||||
!compatibilityResult.ready &&
|
||||
!hasModelScopedEntries &&
|
||||
(uncachedModelIds.length > 1 ||
|
||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))
|
||||
) {
|
||||
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
|
||||
uncachedModelIds,
|
||||
compatibilityResult
|
||||
);
|
||||
const structuredProviderScopedFailure = getBlockingProviderIssueMessage(
|
||||
providerId,
|
||||
compatibilityResult
|
||||
);
|
||||
if (structuredProviderScopedFailure || providerScopedFailure) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: [
|
||||
...runtimeDetailLines,
|
||||
...(compatibilityResult.message ? [compatibilityResult.message] : []),
|
||||
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed',
|
||||
],
|
||||
warnings: [],
|
||||
modelResultsById: {},
|
||||
};
|
||||
}
|
||||
if (
|
||||
shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure({
|
||||
result: compatibilityResult,
|
||||
modelIds: uncachedModelIds,
|
||||
modelScopedEntriesPresent: hasModelScopedEntries,
|
||||
runtimeDetailLines,
|
||||
runtimeWarnings,
|
||||
}) ||
|
||||
(!compatibilityResult.ready &&
|
||||
!hasModelScopedEntries &&
|
||||
(uncachedModelIds.length > 1 ||
|
||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason)))
|
||||
) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: createRuntimeFailureDetailLines(
|
||||
runtimeDetailLines,
|
||||
compatibilityResult.message
|
||||
),
|
||||
warnings: runtimeWarnings,
|
||||
modelResultsById: {},
|
||||
};
|
||||
|
|
@ -862,7 +1020,7 @@ export async function runProviderPrepareDiagnostics({
|
|||
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
||||
);
|
||||
runtimeWarnings = [...(batchedModelResult.warnings ?? [])].filter(
|
||||
runtimeWarnings = createRuntimeWarningLines(batchedModelResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
||||
);
|
||||
|
||||
|
|
@ -874,18 +1032,43 @@ export async function runProviderPrepareDiagnostics({
|
|||
const hasSingleModelFallbackReason =
|
||||
compatibilityPassedModelIds.length === 1 &&
|
||||
looksLikeSingleModelBatchFailure(compatibilityPassedModelIds[0], batchedModelResult);
|
||||
if (
|
||||
!batchedModelResult.ready &&
|
||||
!hasModelScopedEntries &&
|
||||
(compatibilityPassedModelIds.length > 1 ||
|
||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))
|
||||
) {
|
||||
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
|
||||
compatibilityPassedModelIds,
|
||||
batchedModelResult
|
||||
);
|
||||
const structuredProviderScopedFailure = getBlockingProviderIssueMessage(
|
||||
providerId,
|
||||
batchedModelResult
|
||||
);
|
||||
if (structuredProviderScopedFailure || providerScopedFailure) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: [
|
||||
...runtimeDetailLines,
|
||||
...(batchedModelResult.message ? [batchedModelResult.message] : []),
|
||||
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed',
|
||||
],
|
||||
warnings: [],
|
||||
modelResultsById: {},
|
||||
};
|
||||
}
|
||||
if (
|
||||
shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure({
|
||||
result: batchedModelResult,
|
||||
modelIds: compatibilityPassedModelIds,
|
||||
modelScopedEntriesPresent: hasModelScopedEntries,
|
||||
runtimeDetailLines,
|
||||
runtimeWarnings,
|
||||
}) ||
|
||||
(!batchedModelResult.ready &&
|
||||
!hasModelScopedEntries &&
|
||||
(compatibilityPassedModelIds.length > 1 ||
|
||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason)))
|
||||
) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: createRuntimeFailureDetailLines(
|
||||
runtimeDetailLines,
|
||||
batchedModelResult.message
|
||||
),
|
||||
warnings: runtimeWarnings,
|
||||
modelResultsById: {},
|
||||
};
|
||||
|
|
@ -935,7 +1118,7 @@ export async function runProviderPrepareDiagnostics({
|
|||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
runtimeWarnings = [...(compatibilityResult.warnings ?? [])].filter(
|
||||
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
|
||||
|
|
@ -955,10 +1138,10 @@ export async function runProviderPrepareDiagnostics({
|
|||
) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: [
|
||||
...runtimeDetailLines,
|
||||
...(compatibilityResult.message ? [compatibilityResult.message] : []),
|
||||
],
|
||||
details: createRuntimeFailureDetailLines(
|
||||
runtimeDetailLines,
|
||||
compatibilityResult.message
|
||||
),
|
||||
warnings: runtimeWarnings,
|
||||
modelResultsById: {},
|
||||
};
|
||||
|
|
@ -995,7 +1178,7 @@ export async function runProviderPrepareDiagnostics({
|
|||
runtimeDetailLines = createRuntimeDetailLines(deepResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
runtimeWarnings = [...(deepResult.warnings ?? [])].filter(
|
||||
runtimeWarnings = createRuntimeWarningLines(deepResult).filter(
|
||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||
);
|
||||
if (
|
||||
|
|
@ -1003,15 +1186,15 @@ export async function runProviderPrepareDiagnostics({
|
|||
runtimeDetailLines.length === 0 &&
|
||||
runtimeWarnings.length === 0
|
||||
) {
|
||||
runtimeWarnings = deepResult.message ? [deepResult.message] : [];
|
||||
runtimeWarnings = uniquePrepareLines([deepResult.message]);
|
||||
}
|
||||
} catch (deepError) {
|
||||
hasNotes = true;
|
||||
runtimeWarnings = [
|
||||
runtimeWarnings = uniquePrepareLines([
|
||||
normalizeModelReason(
|
||||
deepError instanceof Error ? deepError.message.trim() : String(deepError).trim()
|
||||
) ?? 'One-shot diagnostic failed',
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1433,11 +1433,24 @@ export interface TeamCreateResponse {
|
|||
|
||||
export type TeamProvisioningModelVerificationMode = 'compatibility' | 'deep';
|
||||
|
||||
export type TeamProvisioningPrepareIssueScope = 'provider' | 'model';
|
||||
export type TeamProvisioningPrepareIssueSeverity = 'blocking' | 'warning';
|
||||
|
||||
export interface TeamProvisioningPrepareIssue {
|
||||
providerId?: TeamProviderId;
|
||||
modelId?: string;
|
||||
scope: TeamProvisioningPrepareIssueScope;
|
||||
severity: TeamProvisioningPrepareIssueSeverity;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TeamProvisioningPrepareResult {
|
||||
ready: boolean;
|
||||
message: string;
|
||||
details?: string[];
|
||||
warnings?: string[];
|
||||
issues?: TeamProvisioningPrepareIssue[];
|
||||
}
|
||||
|
||||
export interface TeamProvisioningProgress {
|
||||
|
|
|
|||
|
|
@ -1052,7 +1052,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
|||
expect(prepare).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('treats retryable OpenCode compatibility failures as blocking selected-model diagnostics', async () => {
|
||||
it('keeps shared OpenCode auth compatibility failures provider-scoped', async () => {
|
||||
const prepare = vi.fn(async () => ({
|
||||
ok: false as const,
|
||||
providerId: 'opencode' as const,
|
||||
|
|
@ -1080,12 +1080,70 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
|||
modelVerificationMode: 'compatibility',
|
||||
});
|
||||
|
||||
expect(result.ready).toBe(false);
|
||||
expect(result.message).toBe('OpenCode provider authentication failed');
|
||||
expect(result.details).toEqual(['OpenCode provider authentication failed']);
|
||||
expect(result.warnings).toEqual(['OpenCode provider authentication failed']);
|
||||
expect(result.issues).toEqual([
|
||||
{
|
||||
providerId: 'opencode',
|
||||
scope: 'provider',
|
||||
severity: 'blocking',
|
||||
code: 'not_authenticated',
|
||||
message: 'OpenCode provider authentication failed',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps shared OpenCode MCP compatibility failures provider-scoped', async () => {
|
||||
const prepare = vi.fn(async () => ({
|
||||
ok: false as const,
|
||||
providerId: 'opencode' as const,
|
||||
reason: 'mcp_unavailable',
|
||||
retryable: true,
|
||||
diagnostics: [
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
],
|
||||
warnings: [],
|
||||
}));
|
||||
const registry = new TeamRuntimeAdapterRegistry([
|
||||
{
|
||||
providerId: 'opencode',
|
||||
prepare,
|
||||
launch: vi.fn(),
|
||||
reconcile: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
} as any,
|
||||
]);
|
||||
const svc = new TeamProvisioningService();
|
||||
svc.setRuntimeAdapterRegistry(registry);
|
||||
|
||||
const result = await svc.prepareForProvisioning(tempRoot, {
|
||||
providerId: 'opencode',
|
||||
forceFresh: true,
|
||||
modelIds: ['opencode/big-pickle'],
|
||||
modelVerificationMode: 'compatibility',
|
||||
});
|
||||
|
||||
expect(result.ready).toBe(false);
|
||||
expect(result.message).toBe(
|
||||
'Selected model opencode/minimax-m2.5-free could not be verified. OpenCode provider authentication failed'
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?'
|
||||
);
|
||||
expect(result.details).toEqual([
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
'Selected model opencode/minimax-m2.5-free could not be verified. OpenCode provider authentication failed',
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
]);
|
||||
expect(result.issues).toEqual([
|
||||
{
|
||||
providerId: 'opencode',
|
||||
scope: 'provider',
|
||||
severity: 'blocking',
|
||||
code: 'mcp_unavailable',
|
||||
message:
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,40 @@ describe('ProvisioningProviderStatusList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps OpenCode runtime connectivity failures out of selected-model summaries', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(ProvisioningProviderStatusList, {
|
||||
checks: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
status: 'failed',
|
||||
backendSummary: 'OpenCode CLI',
|
||||
details: [
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('OpenCode (OpenCode CLI): Needs attention');
|
||||
expect(host.textContent).not.toContain('Selected model checks');
|
||||
expect(host.textContent).not.toContain('model unavailable');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('picks the first real failure detail instead of a verified line', () => {
|
||||
expect(
|
||||
getPrimaryProvisioningFailureDetail([
|
||||
|
|
|
|||
|
|
@ -256,6 +256,372 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not mislabel OpenCode runtime connectivity failures as model unavailable', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message: 'OpenCode: mcp_unavailable',
|
||||
details: [
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
'OpenCode: mcp_unavailable',
|
||||
]);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
expect(result.details.join('\n')).not.toContain('big-pickle - unavailable');
|
||||
expect(prepareProvisioning).toHaveBeenCalledTimes(1);
|
||||
expect(prepareProvisioning).toHaveBeenCalledWith(
|
||||
'/tmp/project',
|
||||
'opencode',
|
||||
['opencode'],
|
||||
['opencode/big-pickle'],
|
||||
undefined,
|
||||
'compatibility'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses structured provider-scoped issues before OpenCode runtime text heuristics', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message: 'OpenCode runtime failed with a future diagnostic shape',
|
||||
details: ['Future OpenCode health check failed without known marker words'],
|
||||
issues: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
scope: 'provider',
|
||||
severity: 'blocking',
|
||||
code: 'future_runtime_failure',
|
||||
message: 'Future OpenCode health check failed without known marker words',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual(['Future OpenCode health check failed without known marker words']);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
expect(result.details.join('\n')).not.toContain('big-pickle - unavailable');
|
||||
});
|
||||
|
||||
it('deduplicates repeated OpenCode provider runtime failure details', async () => {
|
||||
const runtimeFailure =
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?';
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message: runtimeFailure,
|
||||
details: [runtimeFailure],
|
||||
warnings: [runtimeFailure],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([runtimeFailure]);
|
||||
expect(result.warnings).toEqual([runtimeFailure]);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
});
|
||||
|
||||
it('treats OpenCode compatibility verification warnings as blocking when the batch failed', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message:
|
||||
'Selected model opencode/big-pickle could not be verified. OpenCode provider authentication failed',
|
||||
warnings: [
|
||||
'Selected model opencode/big-pickle could not be verified. OpenCode provider authentication failed',
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'big-pickle - check failed - OpenCode provider authentication failed',
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.modelResultsById).toEqual({
|
||||
'opencode/big-pickle': {
|
||||
status: 'failed',
|
||||
line: 'big-pickle - check failed - OpenCode provider authentication failed',
|
||||
warningLine: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps stale OpenCode model-scoped runtime failures provider-scoped', async () => {
|
||||
const runtimeFailure =
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?';
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message: `Selected model opencode/big-pickle could not be verified. ${runtimeFailure}`,
|
||||
warnings: [`Selected model opencode/big-pickle could not be verified. ${runtimeFailure}`],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([runtimeFailure]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
});
|
||||
|
||||
it('does not mislabel OpenCode endpoint authorization failures as model unavailable', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message: 'OpenCode: mcp_unavailable',
|
||||
details: ['OpenCode /experimental/tool/ids unavailable - HTTP 403 Forbidden'],
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'OpenCode /experimental/tool/ids unavailable - HTTP 403 Forbidden',
|
||||
'OpenCode: mcp_unavailable',
|
||||
]);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
expect(result.details.join('\n')).not.toContain('big-pickle - unavailable');
|
||||
});
|
||||
|
||||
it('keeps OpenCode selected-model compatibility failures scoped to the selected model', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>(() =>
|
||||
Promise.resolve({
|
||||
ready: false,
|
||||
message:
|
||||
'Selected model opencode/not-real is unavailable. Selected model opencode/not-real is not available',
|
||||
})
|
||||
);
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/not-real'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'not-real - unavailable - Selected model opencode/not-real is not available',
|
||||
]);
|
||||
expect(result.modelResultsById).toEqual({
|
||||
'opencode/not-real': {
|
||||
status: 'failed',
|
||||
line: 'not-real - unavailable - Selected model opencode/not-real is not available',
|
||||
warningLine: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mislabel OpenCode deep runtime failures as model unavailable', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>((_cwd, _providerId, _providerIds, selectedModels, _limitContext, modelVerificationMode) => {
|
||||
if (modelVerificationMode === 'compatibility') {
|
||||
expect(selectedModels).toEqual(['opencode/big-pickle']);
|
||||
return Promise.resolve({
|
||||
ready: true,
|
||||
message: 'CLI is ready to launch',
|
||||
details: [
|
||||
'Selected model opencode/big-pickle is compatible. Deep verification pending.',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
expect(modelVerificationMode).toBe('deep');
|
||||
expect(selectedModels).toEqual(['opencode/big-pickle']);
|
||||
return Promise.resolve({
|
||||
ready: false,
|
||||
message: 'OpenCode: mcp_unavailable',
|
||||
details: [
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['opencode/big-pickle'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?',
|
||||
'OpenCode: mcp_unavailable',
|
||||
]);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
expect(result.details.join('\n')).not.toContain('big-pickle - unavailable');
|
||||
expect(prepareProvisioning).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('keeps OpenCode deep selected-model failures scoped to the selected model', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
providerId?: TeamProviderId,
|
||||
providerIds?: TeamProviderId[],
|
||||
selectedModels?: string[],
|
||||
limitContext?: boolean,
|
||||
modelVerificationMode?: 'compatibility' | 'deep'
|
||||
) => Promise<TeamProvisioningPrepareResult>
|
||||
>((_cwd, _providerId, _providerIds, selectedModels, _limitContext, modelVerificationMode) => {
|
||||
if (modelVerificationMode === 'compatibility') {
|
||||
expect(selectedModels).toEqual(['openrouter/example/not-available']);
|
||||
return Promise.resolve({
|
||||
ready: true,
|
||||
message: 'CLI is ready to launch',
|
||||
details: [
|
||||
'Selected model openrouter/example/not-available is compatible. Deep verification pending.',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
expect(modelVerificationMode).toBe('deep');
|
||||
expect(selectedModels).toEqual(['openrouter/example/not-available']);
|
||||
return Promise.resolve({
|
||||
ready: false,
|
||||
message: 'API Error: 400 {"detail":"The requested model is not available for your account."}',
|
||||
});
|
||||
});
|
||||
|
||||
const result = await runProviderPrepareDiagnostics({
|
||||
cwd: '/tmp/project',
|
||||
providerId: 'opencode',
|
||||
selectedModelIds: ['openrouter/example/not-available'],
|
||||
prepareProvisioning,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'example/not-available - unavailable - Not available for this account',
|
||||
]);
|
||||
expect(result.modelResultsById).toEqual({
|
||||
'openrouter/example/not-available': {
|
||||
status: 'failed',
|
||||
line: 'example/not-available - unavailable - Not available for this account',
|
||||
warningLine: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes raw Codex API error envelopes into a clean model reason', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
|
|
@ -707,7 +1073,7 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
expect(result.details).toEqual(['Default - verified', '5.4 - verified']);
|
||||
});
|
||||
|
||||
it('prefers detailed OpenCode auth diagnostics over a generic not_authenticated batch message', async () => {
|
||||
it('uses structured OpenCode auth diagnostics as provider-scoped failures', async () => {
|
||||
const prepareProvisioning = vi.fn<
|
||||
(
|
||||
cwd?: string,
|
||||
|
|
@ -720,6 +1086,15 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
ready: false,
|
||||
message: 'OpenCode: not_authenticated',
|
||||
details: ['Token refresh failed: 401'],
|
||||
issues: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
scope: 'provider',
|
||||
severity: 'blocking',
|
||||
code: 'not_authenticated',
|
||||
message: 'Token refresh failed: 401',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -731,8 +1106,7 @@ describe('runProviderPrepareDiagnostics', () => {
|
|||
});
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.details).toEqual([
|
||||
'GPT-5.2 Codex - unavailable - OpenCode provider authentication failed (token refresh 401)',
|
||||
]);
|
||||
expect(result.details).toEqual(['Token refresh failed: 401']);
|
||||
expect(result.modelResultsById).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue