fix(runtime): improve opencode diagnostics
This commit is contained in:
parent
d25c65381f
commit
88e01ae87d
15 changed files with 194 additions and 26 deletions
|
|
@ -44,8 +44,11 @@ const bubblePath = computed(() => {
|
||||||
position: var(--robot-bubble-position, relative);
|
position: var(--robot-bubble-position, relative);
|
||||||
z-index: var(--robot-bubble-z-index, auto);
|
z-index: var(--robot-bubble-z-index, auto);
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
|
width: max-content;
|
||||||
|
inline-size: max-content;
|
||||||
min-width: var(--robot-bubble-min-width, 86px);
|
min-width: var(--robot-bubble-min-width, 86px);
|
||||||
max-width: var(--robot-bubble-max-width, 184px);
|
max-width: var(--robot-bubble-max-width, 184px);
|
||||||
|
max-inline-size: var(--robot-bubble-max-width, 184px);
|
||||||
min-height: var(--robot-bubble-min-height, 42px);
|
min-height: var(--robot-bubble-min-height, 42px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--robot-bubble-color, #07111d);
|
color: var(--robot-bubble-color, #07111d);
|
||||||
|
|
@ -85,12 +88,15 @@ const bubblePath = computed(() => {
|
||||||
justify-self: stretch;
|
justify-self: stretch;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
padding: var(--robot-bubble-padding, 8px 16px 16px);
|
padding: var(--robot-bubble-padding, 8px 16px 16px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow-wrap: anywhere;
|
word-break: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
text-wrap: balance;
|
text-wrap: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.robot-speech-bubble--tail-right .robot-speech-bubble__text {
|
.robot-speech-bubble--tail-right .robot-speech-bubble__text {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const EXACT_STRIP_ENV_KEYS = new Set([
|
||||||
'CLAUDE_CODE_GEMINI_BACKEND',
|
'CLAUDE_CODE_GEMINI_BACKEND',
|
||||||
'CLAUDE_CODE_CODEX_BACKEND',
|
'CLAUDE_CODE_CODEX_BACKEND',
|
||||||
'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH',
|
'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH',
|
||||||
|
'OPENCODE_BIN_PATH',
|
||||||
'CODEX_HOME',
|
'CODEX_HOME',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,25 @@ function describeMemberWorkSyncReviewPickupEscalationReason(reason: string): str
|
||||||
return 'The current review request is still waiting for explicit review pickup.';
|
return 'The current review request is still waiting for explicit review pickup.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveOpenCodeRuntimeBinaryForBridgeEnv(): Promise<string | null> {
|
||||||
|
const manifestBinaryPath = await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath();
|
||||||
|
if (manifestBinaryPath) {
|
||||||
|
return manifestBinaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await openCodeRuntimeInstallerService?.getStatus();
|
||||||
|
return status?.installed === true && status.binaryPath ? status.binaryPath : null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`[OpenCode] Runtime installer status unavailable while resolving bridge binary: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createOpenCodeRuntimeAdapterRegistry(
|
async function createOpenCodeRuntimeAdapterRegistry(
|
||||||
reportProgress: (phase: string, message: string) => void = () => undefined
|
reportProgress: (phase: string, message: string) => void = () => undefined
|
||||||
): Promise<TeamRuntimeAdapterRegistry> {
|
): Promise<TeamRuntimeAdapterRegistry> {
|
||||||
|
|
@ -416,7 +435,7 @@ async function createOpenCodeRuntimeAdapterRegistry(
|
||||||
await ensureOpenCodeBridgeRuntimeBinaryEnv({
|
await ensureOpenCodeBridgeRuntimeBinaryEnv({
|
||||||
targetEnv,
|
targetEnv,
|
||||||
bridgeEnv,
|
bridgeEnv,
|
||||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath,
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: resolveOpenCodeRuntimeBinaryForBridgeEnv,
|
||||||
onWarning: (message) => logger.warn(message),
|
onWarning: (message) => logger.warn(message),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { getErrorMessage } from '@shared/utils/errorHandling';
|
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||||
|
|
||||||
import { applyOpenCodeRuntimeBinaryEnv } from './openCodeRuntimeBinaryEnv';
|
import {
|
||||||
|
applyOpenCodeRuntimeBinaryEnv,
|
||||||
|
OPENCODE_LEGACY_BINARY_PATH_ENV,
|
||||||
|
OPENCODE_RUNTIME_BINARY_PATH_ENV,
|
||||||
|
} from './openCodeRuntimeBinaryEnv';
|
||||||
|
|
||||||
export interface EnsureOpenCodeBridgeRuntimeBinaryEnvOptions {
|
export interface EnsureOpenCodeBridgeRuntimeBinaryEnvOptions {
|
||||||
targetEnv: NodeJS.ProcessEnv;
|
targetEnv: NodeJS.ProcessEnv;
|
||||||
|
|
@ -15,7 +19,10 @@ export async function ensureOpenCodeBridgeRuntimeBinaryEnv({
|
||||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath,
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath,
|
||||||
onWarning,
|
onWarning,
|
||||||
}: EnsureOpenCodeBridgeRuntimeBinaryEnvOptions): Promise<void> {
|
}: EnsureOpenCodeBridgeRuntimeBinaryEnvOptions): Promise<void> {
|
||||||
if (targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH?.trim()) {
|
if (
|
||||||
|
targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV]?.trim() ||
|
||||||
|
targetEnv[OPENCODE_LEGACY_BINARY_PATH_ENV]?.trim()
|
||||||
|
) {
|
||||||
applyOpenCodeRuntimeBinaryEnv(targetEnv, null);
|
applyOpenCodeRuntimeBinaryEnv(targetEnv, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -25,10 +32,10 @@ export async function ensureOpenCodeBridgeRuntimeBinaryEnv({
|
||||||
applyOpenCodeRuntimeBinaryEnv(targetEnv, appManagedOpenCodeBinary);
|
applyOpenCodeRuntimeBinaryEnv(targetEnv, appManagedOpenCodeBinary);
|
||||||
if (
|
if (
|
||||||
targetEnv !== bridgeEnv &&
|
targetEnv !== bridgeEnv &&
|
||||||
targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH &&
|
targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV] &&
|
||||||
!bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH
|
!bridgeEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV]
|
||||||
) {
|
) {
|
||||||
applyOpenCodeRuntimeBinaryEnv(bridgeEnv, targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH);
|
applyOpenCodeRuntimeBinaryEnv(bridgeEnv, targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onWarning?.(
|
onWarning?.(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
export const OPENCODE_RUNTIME_BINARY_PATH_ENV = 'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH';
|
export const OPENCODE_RUNTIME_BINARY_PATH_ENV = 'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH';
|
||||||
|
export const OPENCODE_LEGACY_BINARY_PATH_ENV = 'OPENCODE_BIN_PATH';
|
||||||
|
|
||||||
function normalizePathEntryForCompare(value: string): string {
|
function normalizePathEntryForCompare(value: string): string {
|
||||||
const normalized = path.resolve(value.trim());
|
const normalized = path.resolve(value.trim());
|
||||||
|
|
@ -33,7 +34,9 @@ export function applyOpenCodeRuntimeBinaryEnv(
|
||||||
discoveredBinaryPath: string | null | undefined
|
discoveredBinaryPath: string | null | undefined
|
||||||
): void {
|
): void {
|
||||||
const existingBinaryPath = env[OPENCODE_RUNTIME_BINARY_PATH_ENV]?.trim();
|
const existingBinaryPath = env[OPENCODE_RUNTIME_BINARY_PATH_ENV]?.trim();
|
||||||
const nextBinaryPath = existingBinaryPath || discoveredBinaryPath?.trim() || '';
|
const existingLegacyBinaryPath = env[OPENCODE_LEGACY_BINARY_PATH_ENV]?.trim();
|
||||||
|
const nextBinaryPath =
|
||||||
|
existingBinaryPath || existingLegacyBinaryPath || discoveredBinaryPath?.trim() || '';
|
||||||
if (!nextBinaryPath) {
|
if (!nextBinaryPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +44,9 @@ export function applyOpenCodeRuntimeBinaryEnv(
|
||||||
if (!existingBinaryPath) {
|
if (!existingBinaryPath) {
|
||||||
env[OPENCODE_RUNTIME_BINARY_PATH_ENV] = nextBinaryPath;
|
env[OPENCODE_RUNTIME_BINARY_PATH_ENV] = nextBinaryPath;
|
||||||
}
|
}
|
||||||
|
if (!existingLegacyBinaryPath) {
|
||||||
|
env[OPENCODE_LEGACY_BINARY_PATH_ENV] = nextBinaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
if (!path.isAbsolute(nextBinaryPath)) {
|
if (!path.isAbsolute(nextBinaryPath)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -48,6 +54,8 @@ export function applyOpenCodeRuntimeBinaryEnv(
|
||||||
|
|
||||||
// Facts:
|
// Facts:
|
||||||
// - The app-managed OpenCode status is resolved from the app runtime manifest.
|
// - The app-managed OpenCode status is resolved from the app runtime manifest.
|
||||||
|
// - Released claude-multimodel builds have used both OPENCODE_BIN_PATH and
|
||||||
|
// CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH while the managed runtime path evolved.
|
||||||
// - Older claude-multimodel readiness inventory still resolves "opencode" through PATH.
|
// - Older claude-multimodel readiness inventory still resolves "opencode" through PATH.
|
||||||
// - Exposing the selected binary directory keeps both checks on the same runtime.
|
// - Exposing the selected binary directory keeps both checks on the same runtime.
|
||||||
prependPathEntry(env, path.dirname(nextBinaryPath));
|
prependPathEntry(env, path.dirname(nextBinaryPath));
|
||||||
|
|
|
||||||
|
|
@ -588,15 +588,13 @@ function shouldShowOpenCodeInstallAction(
|
||||||
showSkeleton: boolean,
|
showSkeleton: boolean,
|
||||||
openCodeRuntimeStatus: OpenCodeRuntimeStatus | null
|
openCodeRuntimeStatus: OpenCodeRuntimeStatus | null
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
const runtimeReady =
|
||||||
provider.providerId === 'opencode' &&
|
openCodeRuntimeStatus?.installed === true &&
|
||||||
!showSkeleton &&
|
(openCodeRuntimeStatus.source === 'path' ||
|
||||||
!provider.supported &&
|
(openCodeRuntimeStatus.source === 'app-managed' && openCodeRuntimeStatus.state !== 'failed'));
|
||||||
!provider.authenticated &&
|
const runtimeNeedsInstall = !runtimeReady;
|
||||||
provider.backend == null &&
|
|
||||||
openCodeRuntimeStatus?.source !== 'path' &&
|
return provider.providerId === 'opencode' && !showSkeleton && runtimeNeedsInstall;
|
||||||
!(openCodeRuntimeStatus?.source === 'app-managed' && openCodeRuntimeStatus.state !== 'failed')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowCodexInstallAction(
|
function shouldShowCodexInstallAction(
|
||||||
|
|
@ -1054,7 +1052,7 @@ const InstalledBanner = ({
|
||||||
title={
|
title={
|
||||||
openCodeRuntimeStatus?.error ??
|
openCodeRuntimeStatus?.error ??
|
||||||
openCodeRuntimeStatus?.progress?.detail ??
|
openCodeRuntimeStatus?.progress?.detail ??
|
||||||
'Install OpenCode CLI into app data'
|
'Install OpenCode runtime into app data'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isRuntimeInstalling(
|
{isRuntimeInstalling(
|
||||||
|
|
|
||||||
|
|
@ -690,6 +690,13 @@ export function getProvisioningFailureHint(
|
||||||
if (combined.includes('provider is not configured for runtime use')) {
|
if (combined.includes('provider is not configured for runtime use')) {
|
||||||
return 'Configure the selected provider runtime, then reopen this dialog.';
|
return 'Configure the selected provider runtime, then reopen this dialog.';
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
combined.includes('opencode cli not detected on path') ||
|
||||||
|
combined.includes('opencode cli not found') ||
|
||||||
|
combined.includes('opencode runtime binary is not installed')
|
||||||
|
) {
|
||||||
|
return 'Install or retry OpenCode runtime from the provider status card, then reopen this dialog.';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
combined.includes('spawn ') ||
|
combined.includes('spawn ') ||
|
||||||
combined.includes(' enoent') ||
|
combined.includes(' enoent') ||
|
||||||
|
|
|
||||||
|
|
@ -640,7 +640,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
return (
|
return (
|
||||||
providerStatus.detailMessage ??
|
providerStatus.detailMessage ??
|
||||||
providerStatus.statusMessage ??
|
providerStatus.statusMessage ??
|
||||||
'OpenCode CLI is not installed.'
|
'OpenCode runtime is not installed.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!providerStatus.authenticated) {
|
if (!providerStatus.authenticated) {
|
||||||
|
|
|
||||||
|
|
@ -440,11 +440,26 @@ function createRuntimeWarningLines(result: TeamProvisioningPrepareResult): strin
|
||||||
return uniquePrepareLines(result.warnings ?? []);
|
return uniquePrepareLines(result.warnings ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRuntimeFailureDetailLine(detail: string | null | undefined): string | null {
|
||||||
|
const trimmed = detail?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/opencode cli (?:not detected on path|not found)/i.test(trimmed)) {
|
||||||
|
return 'OpenCode runtime binary is not installed or not reachable by launch preflight.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
function createRuntimeFailureDetailLines(
|
function createRuntimeFailureDetailLines(
|
||||||
runtimeDetailLines: readonly string[],
|
runtimeDetailLines: readonly string[],
|
||||||
message: string | null | undefined
|
message: string | null | undefined
|
||||||
): string[] {
|
): string[] {
|
||||||
return uniquePrepareLines([...runtimeDetailLines, message]);
|
return uniquePrepareLines(
|
||||||
|
[...runtimeDetailLines, message].map(normalizeRuntimeFailureDetailLine).filter(Boolean)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTimedOutPreflightProbeModelId(detail: string): string | null {
|
function extractTimedOutPreflightProbeModelId(detail: string): string | null {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ describe('workspaceTrustPreflightEnv', () => {
|
||||||
CLAUDE_TEAM_CONTROL_URL: 'http://127.0.0.1:1234',
|
CLAUDE_TEAM_CONTROL_URL: 'http://127.0.0.1:1234',
|
||||||
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE: 'api_key_helper',
|
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE: 'api_key_helper',
|
||||||
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER: '1',
|
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER: '1',
|
||||||
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH: '/tmp/helper-settings.json',
|
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH:
|
||||||
|
'/Users/tester/helper-settings.json',
|
||||||
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1',
|
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1',
|
||||||
CLAUDE_CODE_ENTRY_PROVIDER: 'codex',
|
CLAUDE_CODE_ENTRY_PROVIDER: 'codex',
|
||||||
CLAUDE_CODE_USE_OPENAI: '1',
|
CLAUDE_CODE_USE_OPENAI: '1',
|
||||||
|
|
@ -25,10 +26,11 @@ describe('workspaceTrustPreflightEnv', () => {
|
||||||
CLAUDE_CODE_USE_GEMINI: '1',
|
CLAUDE_CODE_USE_GEMINI: '1',
|
||||||
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
CLAUDE_CODE_GEMINI_BACKEND: 'api',
|
CLAUDE_CODE_GEMINI_BACKEND: 'api',
|
||||||
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: '/tmp/opencode',
|
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: '/Users/tester/bin/opencode',
|
||||||
CODEX_HOME: '/tmp/codex-home',
|
OPENCODE_BIN_PATH: '/Users/tester/bin/opencode',
|
||||||
AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT: '/tmp/spool',
|
CODEX_HOME: '/Users/tester/codex-home',
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: '/tmp/claude-dir',
|
AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT: '/Users/tester/spool',
|
||||||
|
AGENT_TEAMS_MCP_CLAUDE_DIR: '/Users/tester/claude-dir',
|
||||||
CLAUDE_TEAM_BOOTSTRAP_TOKEN: 'bootstrap-token',
|
CLAUDE_TEAM_BOOTSTRAP_TOKEN: 'bootstrap-token',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,6 +57,7 @@ describe('workspaceTrustPreflightEnv', () => {
|
||||||
expect(env.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined();
|
expect(env.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined();
|
||||||
expect(env.CLAUDE_CODE_GEMINI_BACKEND).toBeUndefined();
|
expect(env.CLAUDE_CODE_GEMINI_BACKEND).toBeUndefined();
|
||||||
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
expect(env.CODEX_HOME).toBeUndefined();
|
expect(env.CODEX_HOME).toBeUndefined();
|
||||||
expect(env.AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT).toBeUndefined();
|
expect(env.AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT).toBeUndefined();
|
||||||
expect(env.AGENT_TEAMS_MCP_CLAUDE_DIR).toBeUndefined();
|
expect(env.AGENT_TEAMS_MCP_CLAUDE_DIR).toBeUndefined();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
expect(env.PATH?.split(path.delimiter)).toEqual([
|
expect(env.PATH?.split(path.delimiter)).toEqual([
|
||||||
path.dirname(binaryPath),
|
path.dirname(binaryPath),
|
||||||
'/usr/bin',
|
'/usr/bin',
|
||||||
|
|
@ -48,11 +49,32 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(commandEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
expect(commandEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(commandEnv.OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
expect(commandEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath));
|
expect(commandEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath));
|
||||||
expect(bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
expect(bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(bridgeEnv.OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
expect(bridgeEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath));
|
expect(bridgeEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('honors a legacy OpenCode binary override already present in the command env', async () => {
|
||||||
|
const binaryPath = path.join(process.cwd(), 'legacy opencode', 'opencode');
|
||||||
|
const env: NodeJS.ProcessEnv = {
|
||||||
|
OPENCODE_BIN_PATH: binaryPath,
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
};
|
||||||
|
const resolver = vi.fn<() => Promise<string | null>>();
|
||||||
|
|
||||||
|
await ensureOpenCodeBridgeRuntimeBinaryEnv({
|
||||||
|
targetEnv: env,
|
||||||
|
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: resolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolver).not.toHaveBeenCalled();
|
||||||
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath));
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps bridge startup non-fatal when the managed resolver fails', async () => {
|
it('keeps bridge startup non-fatal when the managed resolver fails', async () => {
|
||||||
const onWarning = vi.fn();
|
const onWarning = vi.fn();
|
||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
|
|
@ -72,6 +94,7 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => {
|
||||||
'[OpenCode] Runtime adapter bundled OpenCode binary unresolved: manifest unreadable'
|
'[OpenCode] Runtime adapter bundled OpenCode binary unresolved: manifest unreadable'
|
||||||
);
|
);
|
||||||
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
expect(env.PATH).toBe('/usr/bin');
|
expect(env.PATH).toBe('/usr/bin');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ describe('applyOpenCodeRuntimeBinaryEnv', () => {
|
||||||
applyOpenCodeRuntimeBinaryEnv(env, binaryPath);
|
applyOpenCodeRuntimeBinaryEnv(env, binaryPath);
|
||||||
|
|
||||||
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBe(binaryPath);
|
||||||
expect(env.PATH?.split(path.delimiter)).toEqual([
|
expect(env.PATH?.split(path.delimiter)).toEqual([
|
||||||
path.dirname(binaryPath),
|
path.dirname(binaryPath),
|
||||||
'/usr/bin',
|
'/usr/bin',
|
||||||
|
|
@ -32,6 +33,21 @@ describe('applyOpenCodeRuntimeBinaryEnv', () => {
|
||||||
applyOpenCodeRuntimeBinaryEnv(env, discoveredBinaryPath);
|
applyOpenCodeRuntimeBinaryEnv(env, discoveredBinaryPath);
|
||||||
|
|
||||||
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
|
expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mirrors a legacy OpenCode binary override into the managed env var', () => {
|
||||||
|
const explicitBinaryPath = path.join(process.cwd(), 'legacy opencode', 'opencode');
|
||||||
|
const env: NodeJS.ProcessEnv = {
|
||||||
|
OPENCODE_BIN_PATH: explicitBinaryPath,
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
};
|
||||||
|
|
||||||
|
applyOpenCodeRuntimeBinaryEnv(env, null);
|
||||||
|
|
||||||
|
expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
|
expect(env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath));
|
expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -371,11 +371,13 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: appManagedBinaryPath,
|
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: appManagedBinaryPath,
|
||||||
|
OPENCODE_BIN_PATH: appManagedBinaryPath,
|
||||||
}),
|
}),
|
||||||
'opencode',
|
'opencode',
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(appManagedBinaryPath);
|
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(appManagedBinaryPath);
|
||||||
|
expect(result.env.OPENCODE_BIN_PATH).toBe(appManagedBinaryPath);
|
||||||
expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(appManagedBinaryPath));
|
expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(appManagedBinaryPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -392,6 +394,7 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
|
expect(result.env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath);
|
||||||
expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath));
|
expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -407,6 +410,7 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
|
expect(result.env.OPENCODE_BIN_PATH).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('injects the verified app-managed Codex binary for Codex launches', async () => {
|
it('injects the verified app-managed Codex binary for Codex launches', async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
deriveEffectiveProvisioningPrepareState,
|
deriveEffectiveProvisioningPrepareState,
|
||||||
getPrimaryProvisioningFailureDetail,
|
getPrimaryProvisioningFailureDetail,
|
||||||
|
getProvisioningFailureHint,
|
||||||
getProvisioningProviderBackendSummary,
|
getProvisioningProviderBackendSummary,
|
||||||
ProvisioningProviderStatusList,
|
ProvisioningProviderStatusList,
|
||||||
createInitialProviderChecks,
|
createInitialProviderChecks,
|
||||||
|
|
@ -116,6 +117,21 @@ describe('ProvisioningProviderStatusList', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('gives a concrete hint for missing OpenCode runtime binary failures', () => {
|
||||||
|
expect(
|
||||||
|
getProvisioningFailureHint('CLI environment is not available - launch is blocked', [
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
status: 'failed',
|
||||||
|
backendSummary: null,
|
||||||
|
details: ['OpenCode runtime binary is not installed or not reachable by launch preflight.'],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).toBe(
|
||||||
|
'Install or retry OpenCode runtime from the provider status card, then reopen this dialog.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
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([
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { runProviderPrepareDiagnostics } from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
|
||||||
|
|
||||||
|
import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/types';
|
||||||
|
|
||||||
|
type PrepareProvisioningFn = (
|
||||||
|
cwd?: string,
|
||||||
|
providerId?: TeamProviderId,
|
||||||
|
providerIds?: TeamProviderId[],
|
||||||
|
selectedModels?: string[],
|
||||||
|
limitContext?: boolean,
|
||||||
|
modelVerificationMode?: 'compatibility' | 'deep'
|
||||||
|
) => Promise<TeamProvisioningPrepareResult>;
|
||||||
|
|
||||||
|
describe('runProviderPrepareDiagnostics OpenCode runtime failures', () => {
|
||||||
|
it('normalizes missing OpenCode binary diagnostics for packaged launch preflight', async () => {
|
||||||
|
const prepareProvisioning = vi.fn<PrepareProvisioningFn>().mockResolvedValue({
|
||||||
|
ready: false,
|
||||||
|
message: 'OpenCode CLI not detected on PATH',
|
||||||
|
details: ['OpenCode CLI not found'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runProviderPrepareDiagnostics({
|
||||||
|
cwd: '/Users/tester/project',
|
||||||
|
providerId: 'opencode',
|
||||||
|
selectedModelIds: ['opencode/big-pickle'],
|
||||||
|
prepareProvisioning,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe('failed');
|
||||||
|
expect(result.details).toEqual([
|
||||||
|
'OpenCode runtime binary is not installed or not reachable by launch preflight.',
|
||||||
|
]);
|
||||||
|
expect(result.modelResultsById).toEqual({});
|
||||||
|
expect(prepareProvisioning).toHaveBeenCalledWith(
|
||||||
|
'/Users/tester/project',
|
||||||
|
'opencode',
|
||||||
|
['opencode'],
|
||||||
|
['opencode/big-pickle'],
|
||||||
|
undefined,
|
||||||
|
'compatibility'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue