fix(team): scope OpenCode prepare runtime failures

This commit is contained in:
777genius 2026-05-14 12:01:05 +03:00
parent 8ab190bad8
commit 874123c773
4 changed files with 165 additions and 0 deletions

View file

@ -847,6 +847,34 @@ const PREFLIGHT_BINARY_TIMEOUT_MS = 8000;
const PREFLIGHT_AUTH_RETRY_DELAY_MS = 2000;
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
const OPENCODE_PREFLIGHT_MODEL_PROBE_CONCURRENCY = 2;
const OPENCODE_PROVIDER_SCOPED_PREPARE_FAILURE_REASONS = new Set([
'not_installed',
'not_authenticated',
'unsupported_version',
'capabilities_missing',
'runtime_store_blocked',
'mcp_unavailable',
'adapter_disabled',
]);
function pushUniqueLine(lines: string[], line: string): void {
const trimmed = line.trim();
if (trimmed.length > 0 && !lines.includes(trimmed)) {
lines.push(trimmed);
}
}
function looksLikeOpenCodeProviderPrepareDiagnostic(value: string): boolean {
const lower = value.trim().toLowerCase();
return (
lower.includes('opencode /experimental/tool') ||
lower.includes('/experimental/tool') ||
lower.includes('mcp_unavailable') ||
lower.includes('runtime store') ||
lower.includes('opencode cli') ||
lower.includes('unable to connect')
);
}
function applyDistinctProvisioningMemberColors<
T extends { name: string; color?: string; removedAt?: number },
@ -17698,6 +17726,30 @@ export class TeamProvisioningService {
const primaryReason =
prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason;
if (this.isProviderScopedOpenCodePrepareFailure(prepare, primaryReason)) {
pushUniqueLine(details, primaryReason);
pushUniqueLine(blockingMessages, primaryReason);
if (
!issues.some(
(issue) =>
issue.providerId === 'opencode' &&
issue.scope === 'provider' &&
issue.severity === 'blocking' &&
issue.code === prepare.reason &&
issue.message === primaryReason
)
) {
issues.push({
providerId: 'opencode',
scope: 'provider',
severity: 'blocking',
code: prepare.reason,
message: primaryReason,
});
}
continue;
}
const unavailableLine = `Selected model ${modelId} is unavailable. ${primaryReason}`;
const verificationWarningLine = `Selected model ${modelId} could not be verified. ${primaryReason}`;
const issueSeverity =
@ -17736,6 +17788,19 @@ export class TeamProvisioningService {
return { details, warnings, blockingMessages, issues };
}
private isProviderScopedOpenCodePrepareFailure(
prepare: Extract<TeamRuntimePrepareResult, { ok: false }>,
primaryReason: string
): boolean {
if (OPENCODE_PROVIDER_SCOPED_PREPARE_FAILURE_REASONS.has(prepare.reason)) {
return true;
}
return (
prepare.reason === 'unknown_error' &&
[primaryReason, ...prepare.diagnostics].some(looksLikeOpenCodeProviderPrepareDiagnostic)
);
}
private async prepareSelectedOpenCodeModelsCompatibilityBatch({
adapter,
cwd,

View file

@ -9488,6 +9488,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_manifest_fallback',
diagnostics: [],
}));
const registry = new TeamRuntimeAdapterRegistry([

View file

@ -1052,6 +1052,51 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
expect(prepare).toHaveBeenCalledTimes(1);
});
it('keeps deep OpenCode runtime failures provider-scoped instead of model-scoped', async () => {
const runtimeFailure =
'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?';
const prepare = vi.fn(async () => ({
ok: false as const,
providerId: 'opencode' as const,
reason: 'mcp_unavailable',
retryable: true,
diagnostics: [runtimeFailure],
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: 'deep',
});
expect(result.ready).toBe(false);
expect(result.message).toBe(runtimeFailure);
expect(result.details).toEqual([runtimeFailure]);
expect(result.warnings).toEqual([runtimeFailure]);
expect(result.issues).toEqual([
{
providerId: 'opencode',
scope: 'provider',
severity: 'blocking',
code: 'mcp_unavailable',
message: runtimeFailure,
},
]);
});
it('keeps shared OpenCode auth compatibility failures provider-scoped', async () => {
const prepare = vi.fn(async () => ({
ok: false as const,

View file

@ -572,6 +572,60 @@ describe('runProviderPrepareDiagnostics', () => {
expect(prepareProvisioning).toHaveBeenCalledTimes(2);
});
it('uses structured provider-scoped issues from OpenCode deep verification', 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: 'Future OpenCode runtime health failed',
details: ['Future OpenCode runtime health failed'],
issues: [
{
providerId: 'opencode',
scope: 'provider',
severity: 'blocking',
code: 'future_runtime_failure',
message: 'Future OpenCode runtime health failed',
},
],
});
});
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 runtime health failed']);
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<
(