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_RETRY_DELAY_MS = 2000;
|
||||||
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
|
const PREFLIGHT_AUTH_MAX_RETRIES = 2;
|
||||||
const OPENCODE_PREFLIGHT_MODEL_PROBE_CONCURRENCY = 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<
|
function applyDistinctProvisioningMemberColors<
|
||||||
T extends { name: string; color?: string; removedAt?: number },
|
T extends { name: string; color?: string; removedAt?: number },
|
||||||
|
|
@ -17698,6 +17726,30 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
const primaryReason =
|
const primaryReason =
|
||||||
prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason;
|
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 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 =
|
const issueSeverity =
|
||||||
|
|
@ -17736,6 +17788,19 @@ export class TeamProvisioningService {
|
||||||
return { details, warnings, blockingMessages, issues };
|
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({
|
private async prepareSelectedOpenCodeModelsCompatibilityBatch({
|
||||||
adapter,
|
adapter,
|
||||||
cwd,
|
cwd,
|
||||||
|
|
|
||||||
|
|
@ -9488,6 +9488,7 @@ describe('TeamProvisioningService', () => {
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
memberName: String(input.memberName),
|
memberName: String(input.memberName),
|
||||||
sessionId: 'oc-session-bob',
|
sessionId: 'oc-session-bob',
|
||||||
|
runtimePromptMessageId: 'msg_prompt_manifest_fallback',
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
}));
|
}));
|
||||||
const registry = new TeamRuntimeAdapterRegistry([
|
const registry = new TeamRuntimeAdapterRegistry([
|
||||||
|
|
|
||||||
|
|
@ -1052,6 +1052,51 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(prepare).toHaveBeenCalledTimes(1);
|
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 () => {
|
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,
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,60 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
expect(prepareProvisioning).toHaveBeenCalledTimes(2);
|
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 () => {
|
it('keeps OpenCode deep selected-model failures scoped to the selected model', async () => {
|
||||||
const prepareProvisioning = vi.fn<
|
const prepareProvisioning = vi.fn<
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue