From 7c5832bd7e65b0711ee2699414cdfa64da08cb8e Mon Sep 17 00:00:00 2001 From: 777genius Date: Mon, 18 May 2026 15:50:38 +0300 Subject: [PATCH] fix(opencode): avoid busy preflight warnings after compatibility --- .../services/team/TeamProvisioningService.ts | 134 ++++++++++++------ .../dialogs/providerPrepareDiagnostics.ts | 31 +++- .../providerPrepareRequestSignature.ts | 11 +- .../TeamProvisioningServicePrepare.test.ts | 115 ++++++++++++++- .../team/dialogs/LaunchTeamDialog.test.ts | 26 ++-- .../providerPrepareDiagnostics.test.ts | 95 +++++++++++++ .../providerPrepareRequestSignature.test.ts | 112 +++++++++++++++ 7 files changed, 463 insertions(+), 61 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index d13581ad..e9d0a0e9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -903,13 +903,6 @@ const PROBE_CACHE_TTL_MS = 36 * 60 * 60 * 1000; const PREFLIGHT_BINARY_TIMEOUT_MS = 8000; const PREFLIGHT_AUTH_RETRY_DELAY_MS = 2000; const PREFLIGHT_AUTH_MAX_RETRIES = 2; -// Facts: -// - Deep OpenCode preflight below calls adapter.prepare({ runtimeOnly: false }). -// - OpenCodeTeamRuntimeAdapter maps runtimeOnly:false to requireExecutionProbe:true. -// - The readiness bridge then runs modelExecution.verify against the OpenCode host. -// - The host reports "session status busy" when another foreground probe/request is active. -// Keep probes serial so preflight does not create its own busy failures. -const OPENCODE_PREFLIGHT_MODEL_PROBE_CONCURRENCY = 1; const OPENCODE_PROVIDER_SCOPED_PREPARE_FAILURE_REASONS = new Set([ 'not_installed', 'not_authenticated', @@ -989,11 +982,38 @@ function isRetryableOpenCodePreflightBusyDiagnostic(value: string | null | undef ); } -function buildOpenCodeModelVerificationDeferredLine(modelId: string, reason: string): string { +function isOpenCodeModelVerificationTimeoutDiagnostic(value: string | null | undefined): boolean { + const lower = value?.trim().toLowerCase() ?? ''; + return lower.includes('model verification timed out'); +} + +function selectOpenCodeModelPreparePrimaryReason( + prepare: Extract +): string { + const candidates = [...prepare.diagnostics, prepare.reason] + .map((entry) => entry?.trim() ?? '') + .filter(Boolean); + const timeoutReason = candidates.find(isOpenCodeModelVerificationTimeoutDiagnostic); + return timeoutReason ?? candidates[0] ?? prepare.reason; +} + +function isOpenCodeModelPrepareBusyDeferred( + prepare: Extract, + primaryReason: string +): boolean { + const candidates = [primaryReason, prepare.reason, ...prepare.diagnostics]; + return ( + prepare.retryable && + !candidates.some(isOpenCodeModelVerificationTimeoutDiagnostic) && + candidates.some(isRetryableOpenCodePreflightBusyDiagnostic) + ); +} + +function buildOpenCodeProviderVerificationDeferredLine(reason: string): string { const normalizedReason = isRetryableOpenCodePreflightBusyDiagnostic(reason) - ? 'OpenCode session is busy; retry when idle.' + ? 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.' : reason; - return `Selected model ${modelId} verification deferred. ${normalizedReason}`; + return normalizedReason; } function applyDistinctProvisioningMemberColors< @@ -19227,11 +19247,14 @@ export class TeamProvisioningService { } } - const results = new Array<{ modelId: string; prepare: TeamRuntimePrepareResult }>( + const results = new Array<{ modelId: string; prepare: TeamRuntimePrepareResult } | undefined>( modelIds.length ); - const workerCount = Math.min(OPENCODE_PREFLIGHT_MODEL_PROBE_CONCURRENCY, modelIds.length); - let nextIndex = 0; + let providerBusyDeferred: { + modelId: string; + reason: string; + code: string; + } | null = null; const prepareModel = async (modelId: string): Promise => { const startedAt = Date.now(); @@ -19281,26 +19304,45 @@ export class TeamProvisioningService { } }; - await Promise.all( - Array.from({ length: workerCount }, async () => { - while (true) { - const currentIndex = nextIndex; - nextIndex += 1; - if (currentIndex >= modelIds.length) { - return; - } + // Facts: + // - Deep OpenCode preflight maps to a real foreground execution probe. + // - The host reports "session status busy" while another probe/member turn is active. + // - Once busy is observed, probing more selected models only repeats the same host state. + for (let index = 0; index < modelIds.length; index += 1) { + const modelId = modelIds[index]; + const prepare = await prepareModel(modelId); + results[index] = { modelId, prepare }; - const modelId = modelIds[currentIndex]; - results[currentIndex] = { - modelId, - prepare: await prepareModel(modelId), - }; - } - }) - ); + if (verificationMode === 'compatibility' || prepare.ok) { + continue; + } + + const primaryReason = normalizeOpenCodePrepareDiagnostic( + selectOpenCodeModelPreparePrimaryReason(prepare), + prepare.reason + ); + if (isOpenCodeModelPrepareBusyDeferred(prepare, primaryReason)) { + providerBusyDeferred = { + modelId, + reason: primaryReason, + code: prepare.reason, + }; + appendPreflightDebugLog('opencode_model_prepare_batch_busy_deferred', { + cwd, + modelId, + verificationMode, + skippedModelIds: modelIds.slice(index + 1), + reason: primaryReason, + }); + break; + } + } for (const result of results) { if (!result) { + if (providerBusyDeferred) { + continue; + } blockingMessages.push( 'OpenCode preflight could not collect model verification results for all selected models.' ); @@ -19324,25 +19366,15 @@ export class TeamProvisioningService { } const primaryReason = normalizeOpenCodePrepareDiagnostic( - prepare.diagnostics.find((entry) => entry.trim().length > 0) ?? prepare.reason, + selectOpenCodeModelPreparePrimaryReason(prepare), prepare.reason ); - if ( - prepare.retryable && - [primaryReason, prepare.reason, ...prepare.diagnostics].some( - isRetryableOpenCodePreflightBusyDiagnostic - ) - ) { - const deferredLine = buildOpenCodeModelVerificationDeferredLine(modelId, primaryReason); - warnings.push(deferredLine); - issues.push({ - providerId: 'opencode', + if (isOpenCodeModelPrepareBusyDeferred(prepare, primaryReason)) { + providerBusyDeferred ??= { modelId, - scope: 'model', - severity: 'warning', + reason: primaryReason, code: prepare.reason, - message: primaryReason, - }); + }; continue; } if (this.isProviderScopedOpenCodePrepareFailure(prepare, primaryReason)) { @@ -19394,6 +19426,20 @@ export class TeamProvisioningService { } } + if (providerBusyDeferred) { + const providerBusyLine = buildOpenCodeProviderVerificationDeferredLine( + providerBusyDeferred.reason + ); + pushUniqueLine(warnings, providerBusyLine); + issues.push({ + providerId: 'opencode', + scope: 'provider', + severity: 'warning', + code: providerBusyDeferred.code, + message: providerBusyLine, + }); + } + appendPreflightDebugLog('opencode_model_prepare_batch_complete', { cwd, modelIds, diff --git a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts index 96ca9e72..58636bcd 100644 --- a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts +++ b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts @@ -323,6 +323,15 @@ function buildOpenCodeAdvisoryDeepVerificationWarning(reason: string | null | un return `OpenCode model ping was not confirmed. ${normalizedReason}`; } +function isProviderLevelOpenCodeBusyDeepVerificationWarning(value: string): boolean { + const lower = value.trim().toLowerCase(); + return ( + lower.includes('opencode is currently busy') && + lower.includes('deep model verification') && + lower.includes('idle') + ); +} + function createOpenCodeAdvisoryDeepVerificationModelResult( providerId: TeamProviderId, modelId: string @@ -1196,6 +1205,7 @@ export async function runProviderPrepareDiagnostics({ const structuredProviderScopedFailure = structuredProviderScopedIssue?.message.trim() ?? null; let handledAdvisoryDeepFailure = false; + let handledBusyDeepDeferral = false; if (structuredProviderScopedFailure || providerScopedFailure) { const failureReason = structuredProviderScopedFailure ?? providerScopedFailure ?? 'OpenCode failed'; @@ -1231,6 +1241,24 @@ export async function runProviderPrepareDiagnostics({ } if ( !handledAdvisoryDeepFailure && + batchedModelResult.ready && + !hasModelScopedEntries && + runtimeWarnings.some(isProviderLevelOpenCodeBusyDeepVerificationWarning) + ) { + runtimeDetailLines = []; + runtimeWarnings = []; + for (const modelId of compatibilityPassedModelIds) { + recordTerminalModelResult(modelId, { + status: 'ready', + line: buildModelAvailableLine(providerId, modelId), + warningLine: null, + }); + } + handledBusyDeepDeferral = true; + } + if ( + !handledAdvisoryDeepFailure && + !handledBusyDeepDeferral && (shouldSurfaceProviderRuntimeFailureInsteadOfModelFailure({ result: batchedModelResult, modelIds: compatibilityPassedModelIds, @@ -1255,6 +1283,7 @@ export async function runProviderPrepareDiagnostics({ } if ( !handledAdvisoryDeepFailure && + !handledBusyDeepDeferral && !hasModelScopedEntries && compatibilityPassedModelIds.length === 1 ) { @@ -1262,7 +1291,7 @@ export async function runProviderPrepareDiagnostics({ runtimeWarnings = []; } - if (!handledAdvisoryDeepFailure) { + if (!handledAdvisoryDeepFailure && !handledBusyDeepDeferral) { for (const modelId of compatibilityPassedModelIds) { recordTerminalModelResult( modelId, diff --git a/src/renderer/components/team/dialogs/providerPrepareRequestSignature.ts b/src/renderer/components/team/dialogs/providerPrepareRequestSignature.ts index 0e820eff..b681ddda 100644 --- a/src/renderer/components/team/dialogs/providerPrepareRequestSignature.ts +++ b/src/renderer/components/team/dialogs/providerPrepareRequestSignature.ts @@ -51,12 +51,11 @@ export function buildProviderPrepareRuntimeStatusSignature( authMethod: provider?.authMethod ?? null, selectedBackendId: provider?.selectedBackendId ?? null, resolvedBackendId: provider?.resolvedBackendId ?? null, - models: normalizeModelIds(provider?.models), - modelCatalogSource: provider?.modelCatalog?.source ?? null, - modelCatalogStatus: provider?.modelCatalog?.status ?? null, - modelCatalogModels: normalizeModelIds( - provider?.modelCatalog?.models?.map((model) => model.id) - ), + // Facts: + // - Selected models are already represented by modelChecksSignature. + // - OpenCode/Codex live catalogs can expand while preflight is running. + // - Including catalog contents here retriggers duplicate preflights and can + // make still-running OpenCode PONG probes look like persistent busy. connection: provider?.connection ? { supportsOAuth: provider.connection.supportsOAuth, diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 71726eee..62dca346 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -1083,7 +1083,120 @@ describe('TeamProvisioningService prepare/auth behavior', () => { 'Selected model opencode/nemotron-3-super-free verified for launch.', ]); expect(result.warnings).toEqual([ - 'Selected model opencode/big-pickle verification deferred. OpenCode session is busy; retry when idle.', + 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.', + ]); + expect(result.issues).toEqual([ + { + providerId: 'opencode', + scope: 'provider', + severity: 'warning', + code: 'provider_busy', + message: + 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.', + }, + ]); + }); + + it('stops OpenCode deep model verification after the first busy host result', async () => { + const prepare = vi.fn(async (input: { model?: string }) => { + if (input.model === 'opencode/minimax-m2.5-free') { + return { + ok: false as const, + providerId: 'opencode' as const, + reason: 'provider_busy', + retryable: true, + diagnostics: ['OpenCode session status busy'], + warnings: [], + }; + } + + return { + ok: true as const, + providerId: 'opencode' as const, + modelId: input.model ?? null, + diagnostics: [], + 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/minimax-m2.5-free', + 'opencode/nemotron-3-super-free', + 'opencode/big-pickle', + ], + modelVerificationMode: 'deep', + }); + + expect(prepare).toHaveBeenCalledTimes(1); + expect(prepare).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'opencode/minimax-m2.5-free', + runtimeOnly: false, + }) + ); + expect(result.ready).toBe(true); + expect(result.details).toBeUndefined(); + expect(result.warnings).toEqual([ + 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.', + ]); + }); + + it('does not mask OpenCode model verification timeouts as busy deferred checks', async () => { + const prepare = vi.fn(async () => ({ + ok: false as const, + providerId: 'opencode' as const, + reason: 'model_unavailable' as const, + retryable: true, + diagnostics: ['OpenCode session status busy', 'Model verification timed out'], + 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(true); + expect(result.warnings).toEqual([ + 'Selected model opencode/big-pickle could not be verified. Model verification timed out', + ]); + expect(result.warnings?.join('\n')).not.toContain('verification deferred'); + expect(result.issues).toEqual([ + { + providerId: 'opencode', + modelId: 'opencode/big-pickle', + scope: 'model', + severity: 'warning', + code: 'model_unavailable', + message: 'Model verification timed out', + }, ]); }); diff --git a/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts b/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts index fc73b7e7..0a57af52 100644 --- a/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts +++ b/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts @@ -1751,7 +1751,7 @@ describe('LaunchTeamDialog', () => { }); }); - it('keeps the in-flight preflight result after a same-signature rerender', async () => { + it('keeps the in-flight OpenCode preflight result when live catalog expands during rerender', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliStatus = { flavor: 'agent_teams_orchestrator', @@ -1764,11 +1764,11 @@ describe('LaunchTeamDialog', () => { verificationState: 'verified', modelVerificationState: 'verified', statusMessage: 'warming up', - detailMessage: 'first render', + detailMessage: 'catalog still loading', models: ['opencode/minimax-m2.5-free'], modelCatalog: { - source: 'app-server', - status: 'ready', + source: 'live', + status: 'checking', models: [{ id: 'opencode/minimax-m2.5-free' }], }, capabilities: { @@ -1838,13 +1838,21 @@ describe('LaunchTeamDialog', () => { authMethod: 'opencode_managed', verificationState: 'verified', modelVerificationState: 'verified', - statusMessage: 'still warming', - detailMessage: 'same semantic status', - models: ['opencode/minimax-m2.5-free'], + statusMessage: 'healthy', + detailMessage: 'catalog ready', + models: [ + 'opencode/minimax-m2.5-free', + 'opencode/qwen3.6-plus-free', + 'openrouter/google/gemma-4-26b-a4b-it', + ], modelCatalog: { - source: 'app-server', + source: 'live', status: 'ready', - models: [{ id: 'opencode/minimax-m2.5-free' }], + models: [ + { id: 'opencode/minimax-m2.5-free' }, + { id: 'opencode/qwen3.6-plus-free' }, + { id: 'openrouter/google/gemma-4-26b-a4b-it' }, + ], }, capabilities: { teamLaunch: true, diff --git a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts index 865a7d88..95080c26 100644 --- a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts +++ b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts @@ -543,6 +543,101 @@ describe('runProviderPrepareDiagnostics', () => { 'big-pickle - verification deferred - OpenCode session is busy; retry when idle.', }, }); + expect(prepareProvisioning).toHaveBeenCalledTimes(2); + }); + + it('treats provider-level OpenCode busy after compatibility as launch-ready', async () => { + const prepareProvisioning = vi.fn< + ( + cwd?: string, + providerId?: TeamProviderId, + providerIds?: TeamProviderId[], + selectedModels?: string[], + limitContext?: boolean, + modelVerificationMode?: 'compatibility' | 'deep' + ) => Promise + >((_cwd, _providerId, _providerIds, selectedModels, _limitContext, modelVerificationMode) => + Promise.resolve( + modelVerificationMode === 'compatibility' + ? { + ready: true, + message: 'CLI is ready to launch', + details: (selectedModels ?? []).map( + (modelId) => `Selected model ${modelId} is compatible. Deep verification pending.` + ), + warnings: [], + } + : { + ready: true, + message: 'CLI is ready to launch', + warnings: [ + 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.', + ], + } + ) + ); + + const result = await runProviderPrepareDiagnostics({ + cwd: '/tmp/project', + providerId: 'opencode', + selectedModelIds: ['opencode/kimi-k2.6', 'openrouter/google/gemma-4-26b-a4b-it'], + prepareProvisioning, + }); + + expect(result.status).toBe('ready'); + expect(result.details).toEqual([ + 'kimi-k2.6 - available for launch', + 'google/gemma-4-26b-a4b-it - available for launch', + ]); + expect(result.warnings).toEqual([]); + expect(result.details.join('\n')).not.toContain('verification deferred - OpenCode session is busy'); + expect(prepareProvisioning).toHaveBeenCalledTimes(2); + }); + + it('treats provider-level OpenCode busy after compatibility as launch-ready for one selected model', async () => { + const prepareProvisioning = vi.fn< + ( + cwd?: string, + providerId?: TeamProviderId, + providerIds?: TeamProviderId[], + selectedModels?: string[], + limitContext?: boolean, + modelVerificationMode?: 'compatibility' | 'deep' + ) => Promise + >((_cwd, _providerId, _providerIds, selectedModels, _limitContext, modelVerificationMode) => + Promise.resolve( + modelVerificationMode === 'compatibility' + ? { + ready: true, + message: 'CLI is ready to launch', + details: (selectedModels ?? []).map( + (modelId) => `Selected model ${modelId} is compatible. Deep verification pending.` + ), + warnings: [], + } + : { + ready: true, + message: 'CLI is ready to launch', + warnings: [ + 'OpenCode is currently busy with another session. Deep model verification will retry when OpenCode is idle.', + ], + } + ) + ); + + const result = await runProviderPrepareDiagnostics({ + cwd: '/tmp/project', + providerId: 'opencode', + selectedModelIds: ['opencode/kimi-k2.6'], + prepareProvisioning, + }); + + expect(result.status).toBe('ready'); + expect(result.details).toEqual([ + 'kimi-k2.6 - available for launch', + ]); + expect(result.warnings).toEqual([]); + expect(prepareProvisioning).toHaveBeenCalledTimes(2); }); it('keeps stale OpenCode model-scoped runtime failures provider-scoped', async () => { diff --git a/test/renderer/components/team/dialogs/providerPrepareRequestSignature.test.ts b/test/renderer/components/team/dialogs/providerPrepareRequestSignature.test.ts index e220ee5b..3df7d5ad 100644 --- a/test/renderer/components/team/dialogs/providerPrepareRequestSignature.test.ts +++ b/test/renderer/components/team/dialogs/providerPrepareRequestSignature.test.ts @@ -349,6 +349,118 @@ describe('providerPrepareRequestSignature', () => { expect(first).toBe(second); }); + it('ignores OpenCode catalog expansion that can happen while preflight is already running', () => { + const providerIds = ['opencode'] as const; + const first = buildProviderPrepareRuntimeStatusSignature( + providerIds, + new Map([ + [ + 'opencode', + { + providerId: 'opencode', + supported: true, + authenticated: true, + authMethod: 'oauth', + selectedBackendId: 'opencode-cli', + resolvedBackendId: 'opencode-cli', + models: ['opencode/minimax-m2.5-free'], + modelCatalog: { + source: 'live', + status: 'checking', + models: [{ id: 'opencode/minimax-m2.5-free' }], + }, + }, + ], + ]) as any + ); + const second = buildProviderPrepareRuntimeStatusSignature( + providerIds, + new Map([ + [ + 'opencode', + { + providerId: 'opencode', + supported: true, + authenticated: true, + authMethod: 'oauth', + selectedBackendId: 'opencode-cli', + resolvedBackendId: 'opencode-cli', + models: [ + 'opencode/minimax-m2.5-free', + 'opencode/qwen3.6-plus-free', + 'openrouter/google/gemma-4-26b-a4b-it', + ], + modelCatalog: { + source: 'live', + status: 'ready', + models: [ + { id: 'opencode/minimax-m2.5-free' }, + { id: 'opencode/qwen3.6-plus-free' }, + { id: 'openrouter/google/gemma-4-26b-a4b-it' }, + ], + }, + }, + ], + ]) as any + ); + + expect(first).toBe(second); + }); + + it('still changes the full request signature when selected OpenCode model checks change', () => { + const runtimeStatusSignature = buildProviderPrepareRuntimeStatusSignature( + ['opencode'], + new Map([ + [ + 'opencode', + { + providerId: 'opencode', + supported: true, + authenticated: true, + authMethod: 'oauth', + selectedBackendId: 'opencode-cli', + resolvedBackendId: 'opencode-cli', + models: [ + 'opencode/minimax-m2.5-free', + 'opencode/qwen3.6-plus-free', + ], + modelCatalog: { + source: 'live', + status: 'ready', + models: [ + { id: 'opencode/minimax-m2.5-free' }, + { id: 'opencode/qwen3.6-plus-free' }, + ], + }, + }, + ], + ]) as any + ); + + const first = buildProviderPrepareRequestSignature({ + cwd: '/tmp/project', + selectedProviderId: 'opencode', + selectedModel: 'opencode/minimax-m2.5-free', + selectedMemberProviders: ['opencode'], + runtimeStatusSignature, + modelChecksSignature: buildProviderPrepareModelChecksSignature( + new Map([['opencode', ['opencode/minimax-m2.5-free']]]) + ), + }); + const second = buildProviderPrepareRequestSignature({ + cwd: '/tmp/project', + selectedProviderId: 'opencode', + selectedModel: 'opencode/qwen3.6-plus-free', + selectedMemberProviders: ['opencode'], + runtimeStatusSignature, + modelChecksSignature: buildProviderPrepareModelChecksSignature( + new Map([['opencode', ['opencode/qwen3.6-plus-free']]]) + ), + }); + + expect(first).not.toBe(second); + }); + it('ignores live verification fields that can drift while preflight is already running', () => { const providerIds = ['opencode'] as const; const first = buildProviderPrepareRuntimeStatusSignature(