fix(team): scope OpenCode prepare runtime failures
This commit is contained in:
parent
8ab190bad8
commit
874123c773
4 changed files with 165 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<
|
||||
(
|
||||
|
|
|
|||
Loading…
Reference in a new issue