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,
|
TeamMember,
|
||||||
TeamProviderBackendId,
|
TeamProviderBackendId,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
|
TeamProvisioningPrepareIssue,
|
||||||
TeamProvisioningModelVerificationMode,
|
TeamProvisioningModelVerificationMode,
|
||||||
TeamProvisioningPrepareResult,
|
TeamProvisioningPrepareResult,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
|
|
@ -17306,6 +17307,7 @@ export class TeamProvisioningService {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
const blockingMessages: string[] = [];
|
const blockingMessages: string[] = [];
|
||||||
|
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||||
const selectedModelIds = Array.from(
|
const selectedModelIds = Array.from(
|
||||||
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
new Set((opts?.modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
||||||
);
|
);
|
||||||
|
|
@ -17349,6 +17351,7 @@ export class TeamProvisioningService {
|
||||||
details.push(...openCodeModelPrepare.details);
|
details.push(...openCodeModelPrepare.details);
|
||||||
warnings.push(...openCodeModelPrepare.warnings);
|
warnings.push(...openCodeModelPrepare.warnings);
|
||||||
blockingMessages.push(...openCodeModelPrepare.blockingMessages);
|
blockingMessages.push(...openCodeModelPrepare.blockingMessages);
|
||||||
|
issues.push(...openCodeModelPrepare.issues);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17512,6 +17515,7 @@ export class TeamProvisioningService {
|
||||||
? blockingMessages[0]
|
? blockingMessages[0]
|
||||||
: 'Some provider runtimes are not ready',
|
: 'Some provider runtimes are not ready',
|
||||||
warnings: failureWarnings.length > 0 ? failureWarnings : undefined,
|
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 ready to launch (see notes)'
|
||||||
: 'CLI is warmed up and ready to launch',
|
: 'CLI is warmed up and ready to launch',
|
||||||
warnings: warnings.length > 0 ? warnings : undefined,
|
warnings: warnings.length > 0 ? warnings : undefined,
|
||||||
|
issues: issues.length > 0 ? issues : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17544,14 +17549,16 @@ export class TeamProvisioningService {
|
||||||
details: string[];
|
details: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
blockingMessages: string[];
|
blockingMessages: string[];
|
||||||
|
issues: TeamProvisioningPrepareIssue[];
|
||||||
}> {
|
}> {
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const blockingMessages: string[] = [];
|
const blockingMessages: string[] = [];
|
||||||
|
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
||||||
if (modelIds.length === 0) {
|
if (modelIds.length === 0) {
|
||||||
return { details, warnings, blockingMessages };
|
return { details, warnings, blockingMessages, issues };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verificationMode === 'compatibility') {
|
if (verificationMode === 'compatibility') {
|
||||||
|
|
@ -17662,6 +17669,16 @@ export class TeamProvisioningService {
|
||||||
prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason;
|
prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason;
|
||||||
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
|
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
|
||||||
const verificationWarningLine = `Selected model ${modelId} could not be verified. ${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) {
|
if (prepare.retryable) {
|
||||||
warnings.push(verificationWarningLine);
|
warnings.push(verificationWarningLine);
|
||||||
if (verificationMode === 'compatibility') {
|
if (verificationMode === 'compatibility') {
|
||||||
|
|
@ -17685,7 +17702,7 @@ export class TeamProvisioningService {
|
||||||
blockingMessages,
|
blockingMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { details, warnings, blockingMessages };
|
return { details, warnings, blockingMessages, issues };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async prepareSelectedOpenCodeModelsCompatibilityBatch({
|
private async prepareSelectedOpenCodeModelsCompatibilityBatch({
|
||||||
|
|
@ -17700,10 +17717,12 @@ export class TeamProvisioningService {
|
||||||
details: string[];
|
details: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
blockingMessages: string[];
|
blockingMessages: string[];
|
||||||
|
issues: TeamProvisioningPrepareIssue[];
|
||||||
} | null> {
|
} | null> {
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const blockingMessages: string[] = [];
|
const blockingMessages: string[] = [];
|
||||||
|
const issues: TeamProvisioningPrepareIssue[] = [];
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
||||||
appendPreflightDebugLog('opencode_compatibility_batch_start', {
|
appendPreflightDebugLog('opencode_compatibility_batch_start', {
|
||||||
|
|
@ -17749,18 +17768,20 @@ export class TeamProvisioningService {
|
||||||
if (!sharedPrepare.ok) {
|
if (!sharedPrepare.ok) {
|
||||||
const primaryReason =
|
const primaryReason =
|
||||||
sharedPrepare.diagnostics.find((entry) => entry.trim().length > 0) ?? sharedPrepare.reason;
|
sharedPrepare.diagnostics.find((entry) => entry.trim().length > 0) ?? sharedPrepare.reason;
|
||||||
for (const modelId of modelIds) {
|
if (primaryReason.trim().length > 0) {
|
||||||
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
|
details.push(primaryReason);
|
||||||
const verificationWarningLine = `Selected model ${modelId} could not be verified. ${primaryReason}`;
|
blockingMessages.push(primaryReason);
|
||||||
if (sharedPrepare.retryable) {
|
} else {
|
||||||
warnings.push(verificationWarningLine);
|
blockingMessages.push(`OpenCode: ${sharedPrepare.reason}`);
|
||||||
blockingMessages.push(verificationWarningLine);
|
|
||||||
} else {
|
|
||||||
details.push(unavailableLine);
|
|
||||||
blockingMessages.push(unavailableLine);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 =
|
const latestReadiness =
|
||||||
|
|
@ -17798,6 +17819,14 @@ export class TeamProvisioningService {
|
||||||
const unavailableLine = `Selected model ${modelId} is unavailable. ${resolvedModel.reason}`;
|
const unavailableLine = `Selected model ${modelId} is unavailable. ${resolvedModel.reason}`;
|
||||||
details.push(unavailableLine);
|
details.push(unavailableLine);
|
||||||
blockingMessages.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', {
|
appendPreflightDebugLog('opencode_compatibility_batch_complete', {
|
||||||
|
|
@ -17808,7 +17837,7 @@ export class TeamProvisioningService {
|
||||||
details,
|
details,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { details, warnings, blockingMessages };
|
return { details, warnings, blockingMessages, issues };
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveOpenCodeCompatibilityModel(
|
private resolveOpenCodeCompatibilityModel(
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,20 @@ function escapeRegExp(value: string): string {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
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 {
|
function getModelLabel(providerId: TeamProviderId, modelId: string): string {
|
||||||
if (isDefaultProviderModelSelection(modelId)) {
|
if (isDefaultProviderModelSelection(modelId)) {
|
||||||
return 'Default';
|
return 'Default';
|
||||||
|
|
@ -205,6 +219,36 @@ function normalizeModelReason(rawReason: string | null | undefined): string | nu
|
||||||
return trimmed;
|
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 {
|
function getResultReason(modelId: string, result: TeamProvisioningPrepareResult): string | null {
|
||||||
const candidates = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
|
const candidates = [...(result.details ?? []), ...(result.warnings ?? []), result.message]
|
||||||
.map((entry) => entry?.trim() ?? '')
|
.map((entry) => entry?.trim() ?? '')
|
||||||
|
|
@ -286,7 +330,18 @@ function buildModelFailureLine(
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string[] {
|
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 {
|
function extractTimedOutPreflightProbeModelId(detail: string): string | null {
|
||||||
|
|
@ -545,7 +600,10 @@ function resolveModelResultFromCompatibilityBatch(
|
||||||
const hasUnavailableLine = modelScopedEntries.some((entry) =>
|
const hasUnavailableLine = modelScopedEntries.some((entry) =>
|
||||||
/selected model .* is unavailable\./i.test(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 {
|
return {
|
||||||
kind: 'terminal',
|
kind: 'terminal',
|
||||||
result: {
|
result: {
|
||||||
|
|
@ -561,9 +619,6 @@ function resolveModelResultFromCompatibilityBatch(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasVerificationWarningLine = modelScopedEntries.some((entry) =>
|
|
||||||
/selected model .* could not be verified\./i.test(entry)
|
|
||||||
);
|
|
||||||
if (hasVerificationWarningLine) {
|
if (hasVerificationWarningLine) {
|
||||||
const line = buildModelFailureLine(
|
const line = buildModelFailureLine(
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -574,9 +629,9 @@ function resolveModelResultFromCompatibilityBatch(
|
||||||
return {
|
return {
|
||||||
kind: 'terminal',
|
kind: 'terminal',
|
||||||
result: {
|
result: {
|
||||||
status: 'notes',
|
status: result.ready ? 'notes' : 'failed',
|
||||||
line,
|
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({
|
export async function runProviderPrepareDiagnostics({
|
||||||
cwd,
|
cwd,
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -627,12 +760,12 @@ export async function runProviderPrepareDiagnostics({
|
||||||
limitContext
|
limitContext
|
||||||
);
|
);
|
||||||
const runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
const runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
||||||
const runtimeWarnings = [...(runtimeResult.warnings ?? [])];
|
const runtimeWarnings = createRuntimeWarningLines(runtimeResult);
|
||||||
|
|
||||||
if (!runtimeResult.ready) {
|
if (!runtimeResult.ready) {
|
||||||
return {
|
return {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
details: [...runtimeDetailLines, ...(runtimeResult.message ? [runtimeResult.message] : [])],
|
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
|
||||||
warnings: runtimeWarnings,
|
warnings: runtimeWarnings,
|
||||||
modelResultsById: {},
|
modelResultsById: {},
|
||||||
};
|
};
|
||||||
|
|
@ -712,12 +845,12 @@ export async function runProviderPrepareDiagnostics({
|
||||||
limitContext
|
limitContext
|
||||||
);
|
);
|
||||||
runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
runtimeDetailLines = createRuntimeDetailLines(runtimeResult);
|
||||||
runtimeWarnings = [...(runtimeResult.warnings ?? [])];
|
runtimeWarnings = createRuntimeWarningLines(runtimeResult);
|
||||||
|
|
||||||
if (!runtimeResult.ready) {
|
if (!runtimeResult.ready) {
|
||||||
return {
|
return {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
details: [...runtimeDetailLines, ...(runtimeResult.message ? [runtimeResult.message] : [])],
|
details: createRuntimeFailureDetailLines(runtimeDetailLines, runtimeResult.message),
|
||||||
warnings: runtimeWarnings,
|
warnings: runtimeWarnings,
|
||||||
modelResultsById: {},
|
modelResultsById: {},
|
||||||
};
|
};
|
||||||
|
|
@ -754,7 +887,7 @@ export async function runProviderPrepareDiagnostics({
|
||||||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
runtimeWarnings = [...(compatibilityResult.warnings ?? [])].filter(
|
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -766,18 +899,43 @@ export async function runProviderPrepareDiagnostics({
|
||||||
const hasSingleModelFallbackReason =
|
const hasSingleModelFallbackReason =
|
||||||
uncachedModelIds.length === 1 &&
|
uncachedModelIds.length === 1 &&
|
||||||
looksLikeSingleModelBatchFailure(uncachedModelIds[0], compatibilityResult);
|
looksLikeSingleModelBatchFailure(uncachedModelIds[0], compatibilityResult);
|
||||||
if (
|
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
|
||||||
!compatibilityResult.ready &&
|
uncachedModelIds,
|
||||||
!hasModelScopedEntries &&
|
compatibilityResult
|
||||||
(uncachedModelIds.length > 1 ||
|
);
|
||||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))
|
const structuredProviderScopedFailure = getBlockingProviderIssueMessage(
|
||||||
) {
|
providerId,
|
||||||
|
compatibilityResult
|
||||||
|
);
|
||||||
|
if (structuredProviderScopedFailure || providerScopedFailure) {
|
||||||
return {
|
return {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
details: [
|
details: [
|
||||||
...runtimeDetailLines,
|
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed',
|
||||||
...(compatibilityResult.message ? [compatibilityResult.message] : []),
|
|
||||||
],
|
],
|
||||||
|
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,
|
warnings: runtimeWarnings,
|
||||||
modelResultsById: {},
|
modelResultsById: {},
|
||||||
};
|
};
|
||||||
|
|
@ -862,7 +1020,7 @@ export async function runProviderPrepareDiagnostics({
|
||||||
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(batchedModelResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
||||||
);
|
);
|
||||||
runtimeWarnings = [...(batchedModelResult.warnings ?? [])].filter(
|
runtimeWarnings = createRuntimeWarningLines(batchedModelResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(compatibilityPassedModelIds, entry)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -874,18 +1032,43 @@ export async function runProviderPrepareDiagnostics({
|
||||||
const hasSingleModelFallbackReason =
|
const hasSingleModelFallbackReason =
|
||||||
compatibilityPassedModelIds.length === 1 &&
|
compatibilityPassedModelIds.length === 1 &&
|
||||||
looksLikeSingleModelBatchFailure(compatibilityPassedModelIds[0], batchedModelResult);
|
looksLikeSingleModelBatchFailure(compatibilityPassedModelIds[0], batchedModelResult);
|
||||||
if (
|
const providerScopedFailure = getOpenCodeProviderScopedFailureFromModelScopedEntries(
|
||||||
!batchedModelResult.ready &&
|
compatibilityPassedModelIds,
|
||||||
!hasModelScopedEntries &&
|
batchedModelResult
|
||||||
(compatibilityPassedModelIds.length > 1 ||
|
);
|
||||||
(!hasNonModelScopedDiagnostics && !hasSingleModelFallbackReason))
|
const structuredProviderScopedFailure = getBlockingProviderIssueMessage(
|
||||||
) {
|
providerId,
|
||||||
|
batchedModelResult
|
||||||
|
);
|
||||||
|
if (structuredProviderScopedFailure || providerScopedFailure) {
|
||||||
return {
|
return {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
details: [
|
details: [
|
||||||
...runtimeDetailLines,
|
structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed',
|
||||||
...(batchedModelResult.message ? [batchedModelResult.message] : []),
|
|
||||||
],
|
],
|
||||||
|
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,
|
warnings: runtimeWarnings,
|
||||||
modelResultsById: {},
|
modelResultsById: {},
|
||||||
};
|
};
|
||||||
|
|
@ -935,7 +1118,7 @@ export async function runProviderPrepareDiagnostics({
|
||||||
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
runtimeWarnings = [...(compatibilityResult.warnings ?? [])].filter(
|
runtimeWarnings = createRuntimeWarningLines(compatibilityResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -955,10 +1138,10 @@ export async function runProviderPrepareDiagnostics({
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
details: [
|
details: createRuntimeFailureDetailLines(
|
||||||
...runtimeDetailLines,
|
runtimeDetailLines,
|
||||||
...(compatibilityResult.message ? [compatibilityResult.message] : []),
|
compatibilityResult.message
|
||||||
],
|
),
|
||||||
warnings: runtimeWarnings,
|
warnings: runtimeWarnings,
|
||||||
modelResultsById: {},
|
modelResultsById: {},
|
||||||
};
|
};
|
||||||
|
|
@ -995,7 +1178,7 @@ export async function runProviderPrepareDiagnostics({
|
||||||
runtimeDetailLines = createRuntimeDetailLines(deepResult).filter(
|
runtimeDetailLines = createRuntimeDetailLines(deepResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
runtimeWarnings = [...(deepResult.warnings ?? [])].filter(
|
runtimeWarnings = createRuntimeWarningLines(deepResult).filter(
|
||||||
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
(entry) => !isModelScopedEntryForAnyModel(uncachedModelIds, entry)
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
|
|
@ -1003,15 +1186,15 @@ export async function runProviderPrepareDiagnostics({
|
||||||
runtimeDetailLines.length === 0 &&
|
runtimeDetailLines.length === 0 &&
|
||||||
runtimeWarnings.length === 0
|
runtimeWarnings.length === 0
|
||||||
) {
|
) {
|
||||||
runtimeWarnings = deepResult.message ? [deepResult.message] : [];
|
runtimeWarnings = uniquePrepareLines([deepResult.message]);
|
||||||
}
|
}
|
||||||
} catch (deepError) {
|
} catch (deepError) {
|
||||||
hasNotes = true;
|
hasNotes = true;
|
||||||
runtimeWarnings = [
|
runtimeWarnings = uniquePrepareLines([
|
||||||
normalizeModelReason(
|
normalizeModelReason(
|
||||||
deepError instanceof Error ? deepError.message.trim() : String(deepError).trim()
|
deepError instanceof Error ? deepError.message.trim() : String(deepError).trim()
|
||||||
) ?? 'One-shot diagnostic failed',
|
) ?? 'One-shot diagnostic failed',
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1433,11 +1433,24 @@ export interface TeamCreateResponse {
|
||||||
|
|
||||||
export type TeamProvisioningModelVerificationMode = 'compatibility' | 'deep';
|
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 {
|
export interface TeamProvisioningPrepareResult {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
details?: string[];
|
details?: string[];
|
||||||
warnings?: string[];
|
warnings?: string[];
|
||||||
|
issues?: TeamProvisioningPrepareIssue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamProvisioningProgress {
|
export interface TeamProvisioningProgress {
|
||||||
|
|
|
||||||
|
|
@ -1052,7 +1052,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(prepare).toHaveBeenCalledTimes(1);
|
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 () => ({
|
const prepare = vi.fn(async () => ({
|
||||||
ok: false as const,
|
ok: false as const,
|
||||||
providerId: 'opencode' as const,
|
providerId: 'opencode' as const,
|
||||||
|
|
@ -1080,12 +1080,70 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
modelVerificationMode: 'compatibility',
|
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.ready).toBe(false);
|
||||||
expect(result.message).toBe(
|
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([
|
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', () => {
|
it('picks the first real failure detail instead of a verified line', () => {
|
||||||
expect(
|
expect(
|
||||||
getPrimaryProvisioningFailureDetail([
|
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 () => {
|
it('normalizes raw Codex API error envelopes into a clean model reason', async () => {
|
||||||
const prepareProvisioning = vi.fn<
|
const prepareProvisioning = vi.fn<
|
||||||
(
|
(
|
||||||
|
|
@ -707,7 +1073,7 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
expect(result.details).toEqual(['Default - verified', '5.4 - verified']);
|
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<
|
const prepareProvisioning = vi.fn<
|
||||||
(
|
(
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
|
|
@ -720,6 +1086,15 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
ready: false,
|
ready: false,
|
||||||
message: 'OpenCode: not_authenticated',
|
message: 'OpenCode: not_authenticated',
|
||||||
details: ['Token refresh failed: 401'],
|
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.status).toBe('failed');
|
||||||
expect(result.details).toEqual([
|
expect(result.details).toEqual(['Token refresh failed: 401']);
|
||||||
'GPT-5.2 Codex - unavailable - OpenCode provider authentication failed (token refresh 401)',
|
expect(result.modelResultsById).toEqual({});
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue