fix(opencode): detect managed node_modules symlink permission failures
Распознаём отдельную диагностику для EPERM на создании managed node_modules symlink под Windows и подсказываем пользователю запустить приложение от имени Administrator. UI-подсказка и provisioning hint показываются только для этого случая, обычный Windows access-denied flow не затрагивается.
This commit is contained in:
parent
961770b7a7
commit
90fe3c9107
8 changed files with 215 additions and 2 deletions
|
|
@ -26,6 +26,7 @@ import {
|
||||||
getOpenCodeTeamModelRecommendation,
|
getOpenCodeTeamModelRecommendation,
|
||||||
isOpenCodeTeamModelRecommended,
|
isOpenCodeTeamModelRecommended,
|
||||||
} from '@renderer/utils/openCodeModelRecommendations';
|
} from '@renderer/utils/openCodeModelRecommendations';
|
||||||
|
import { isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic } from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Check,
|
Check,
|
||||||
|
|
@ -785,6 +786,20 @@ function getRuntimeProviderDiagnosticRows(
|
||||||
.map(([label, value]) => [label, String(value)]);
|
.map(([label, value]) => [label, String(value)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOpenCodeWindowsNodeModulesSymlinkPermissionError(
|
||||||
|
message: string,
|
||||||
|
diagnostics: RuntimeProviderManagementErrorDiagnosticsDto | null | undefined
|
||||||
|
): boolean {
|
||||||
|
const value = [
|
||||||
|
message,
|
||||||
|
diagnostics?.stderrPreview ?? '',
|
||||||
|
diagnostics?.stdoutPreview ?? '',
|
||||||
|
diagnostics?.likelyCause ?? '',
|
||||||
|
...(diagnostics?.hints ?? []),
|
||||||
|
].join('\n');
|
||||||
|
return isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(value);
|
||||||
|
}
|
||||||
|
|
||||||
async function writeRuntimeProviderDiagnosticsToClipboard(text: string): Promise<boolean> {
|
async function writeRuntimeProviderDiagnosticsToClipboard(text: string): Promise<boolean> {
|
||||||
if (navigator.clipboard?.writeText) {
|
if (navigator.clipboard?.writeText) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -826,6 +841,10 @@ const RuntimeProviderErrorAlert = ({
|
||||||
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
|
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
|
||||||
const fallbackDetails = detailLines.join('\n').trim();
|
const fallbackDetails = detailLines.join('\n').trim();
|
||||||
const hints = diagnostics?.hints ?? [];
|
const hints = diagnostics?.hints ?? [];
|
||||||
|
const showWindowsSymlinkPermissionHint = isOpenCodeWindowsNodeModulesSymlinkPermissionError(
|
||||||
|
message,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
const copyText = useMemo(
|
const copyText = useMemo(
|
||||||
() => formatRuntimeProviderDiagnosticsCopyText(message, diagnostics),
|
() => formatRuntimeProviderDiagnosticsCopyText(message, diagnostics),
|
||||||
[diagnostics, message]
|
[diagnostics, message]
|
||||||
|
|
@ -859,6 +878,11 @@ const RuntimeProviderErrorAlert = ({
|
||||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
||||||
<div className="min-w-0 whitespace-pre-wrap break-words font-medium leading-5">
|
<div className="min-w-0 whitespace-pre-wrap break-words font-medium leading-5">
|
||||||
{headline || message}
|
{headline || message}
|
||||||
|
{showWindowsSymlinkPermissionHint ? (
|
||||||
|
<span className="ml-2 inline-flex rounded border border-red-200/30 bg-red-500/10 px-1.5 py-0.5 text-[11px] font-semibold leading-4 text-red-50">
|
||||||
|
{t('runtimeProvider.diagnostics.windowsSymlinkAdminHint')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,24 @@
|
||||||
import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
|
import {
|
||||||
|
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
|
||||||
|
} from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { getProvisioningFailureHint } from './ProvisioningProviderStatusList';
|
import { getProvisioningFailureHint } from './ProvisioningProviderStatusList';
|
||||||
|
|
||||||
describe('getProvisioningFailureHint', () => {
|
describe('getProvisioningFailureHint', () => {
|
||||||
|
it('returns the administrator hint for the exact OpenCode node_modules symlink permission failure', () => {
|
||||||
|
expect(
|
||||||
|
getProvisioningFailureHint(null, [
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
status: 'failed',
|
||||||
|
details: [OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).toBe('Run Agent Teams AI as Administrator, then retry launch.');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the OpenCode Windows permissions hint for OpenCode access-denied details', () => {
|
it('returns the OpenCode Windows permissions hint for OpenCode access-denied details', () => {
|
||||||
expect(
|
expect(
|
||||||
getProvisioningFailureHint(null, [
|
getProvisioningFailureHint(null, [
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdent
|
||||||
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
||||||
import {
|
import {
|
||||||
isOpenCodeWindowsAccessDeniedDiagnostic,
|
isOpenCodeWindowsAccessDeniedDiagnostic,
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic,
|
||||||
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
|
||||||
} from '@shared/utils/openCodeWindowsAccessDenied';
|
} from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { AlertTriangle, Check, CheckCircle2, Copy, Loader2, SlidersHorizontal } from 'lucide-react';
|
import { AlertTriangle, Check, CheckCircle2, Copy, Loader2, SlidersHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -1042,14 +1044,31 @@ export function getProvisioningFailureHint(
|
||||||
const hasOpenCodeAccessDeniedDetail = failedOpenCodeChecks.some((check) =>
|
const hasOpenCodeAccessDeniedDetail = failedOpenCodeChecks.some((check) =>
|
||||||
check.details.some(isOpenCodeWindowsAccessDeniedDiagnostic)
|
check.details.some(isOpenCodeWindowsAccessDeniedDiagnostic)
|
||||||
);
|
);
|
||||||
|
const hasOpenCodeNodeModulesSymlinkPermissionDetail = failedOpenCodeChecks.some((check) =>
|
||||||
|
check.details.some(isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic)
|
||||||
|
);
|
||||||
const hasOpenCodeBridgeNoOutputDetail = failedOpenCodeChecks.some((check) =>
|
const hasOpenCodeBridgeNoOutputDetail = failedOpenCodeChecks.some((check) =>
|
||||||
check.details.some(isOpenCodeBridgeNoOutputDiagnostic)
|
check.details.some(isOpenCodeBridgeNoOutputDiagnostic)
|
||||||
);
|
);
|
||||||
const normalizedMessage = message?.trim() ?? '';
|
const normalizedMessage = message?.trim() ?? '';
|
||||||
|
const hasOpenCodeNodeModulesSymlinkPermissionMessage =
|
||||||
|
failedOpenCodeChecks.length > 0 &&
|
||||||
|
(normalizedMessage === OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE ||
|
||||||
|
(!hasFailedNonOpenCodeCheck &&
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(normalizedMessage)));
|
||||||
const hasOpenCodeAccessDeniedMessage =
|
const hasOpenCodeAccessDeniedMessage =
|
||||||
failedOpenCodeChecks.length > 0 &&
|
failedOpenCodeChecks.length > 0 &&
|
||||||
(normalizedMessage === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
(normalizedMessage === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
||||||
(!hasFailedNonOpenCodeCheck && isOpenCodeWindowsAccessDeniedDiagnostic(normalizedMessage)));
|
(!hasFailedNonOpenCodeCheck && isOpenCodeWindowsAccessDeniedDiagnostic(normalizedMessage)));
|
||||||
|
if (
|
||||||
|
hasOpenCodeNodeModulesSymlinkPermissionDetail ||
|
||||||
|
hasOpenCodeNodeModulesSymlinkPermissionMessage
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
t?.('provisioning.providerStatus.failureHints.openCodeNodeModulesSymlinkPermission') ??
|
||||||
|
'Run Agent Teams AI as Administrator, then retry launch.'
|
||||||
|
);
|
||||||
|
}
|
||||||
if (hasOpenCodeAccessDeniedDetail || hasOpenCodeAccessDeniedMessage) {
|
if (hasOpenCodeAccessDeniedDetail || hasOpenCodeAccessDeniedMessage) {
|
||||||
return (
|
return (
|
||||||
t?.('provisioning.providerStatus.failureHints.openCodeAccessDenied') ??
|
t?.('provisioning.providerStatus.failureHints.openCodeAccessDenied') ??
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isOpenCodeWindowsAccessDeniedDiagnostic,
|
isOpenCodeWindowsAccessDeniedDiagnostic,
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic,
|
||||||
normalizeOpenCodeWindowsAccessDeniedDiagnostic,
|
normalizeOpenCodeWindowsAccessDeniedDiagnostic,
|
||||||
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
|
||||||
} from '../openCodeWindowsAccessDenied';
|
} from '../openCodeWindowsAccessDenied';
|
||||||
|
|
||||||
describe('OpenCode Windows access-denied diagnostics', () => {
|
describe('OpenCode Windows access-denied diagnostics', () => {
|
||||||
|
|
@ -24,4 +26,18 @@ describe('OpenCode Windows access-denied diagnostics', () => {
|
||||||
expect(isOpenCodeWindowsAccessDeniedDiagnostic('OpenCode app MCP is unreachable')).toBe(false);
|
expect(isOpenCodeWindowsAccessDeniedDiagnostic('OpenCode app MCP is unreachable')).toBe(false);
|
||||||
expect(normalizeOpenCodeWindowsAccessDeniedDiagnostic('OpenCode CLI not found')).toBeNull();
|
expect(normalizeOpenCodeWindowsAccessDeniedDiagnostic('OpenCode CLI not found')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects the managed OpenCode node_modules symlink permission failure separately', () => {
|
||||||
|
const message = [
|
||||||
|
'Runtime provider management command failed unexpectedly:',
|
||||||
|
"EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
||||||
|
"-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
expect(isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(message)).toBe(true);
|
||||||
|
expect(isOpenCodeWindowsAccessDeniedDiagnostic(message)).toBe(true);
|
||||||
|
expect(normalizeOpenCodeWindowsAccessDeniedDiagnostic(message)).toBe(
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,35 @@
|
||||||
export const OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE =
|
export const OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE =
|
||||||
'Windows blocked OpenCode from accessing project or runtime files. Fix folder permissions or move the project to a user-writable folder. Running as administrator is only a temporary workaround.';
|
'Windows blocked OpenCode from accessing project or runtime files. Fix folder permissions or move the project to a user-writable folder. Running as administrator is only a temporary workaround.';
|
||||||
|
|
||||||
|
export const OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE =
|
||||||
|
'Windows blocked OpenCode from creating the managed node_modules symlink. Run Agent Teams AI as Administrator, then retry launch.';
|
||||||
|
|
||||||
const OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN =
|
const OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN =
|
||||||
/\b(?:EPERM|EACCES)\b|access is denied|permission denied|operation not permitted/i;
|
/\b(?:EPERM|EACCES)\b|access is denied|permission denied|operation not permitted/i;
|
||||||
|
|
||||||
|
const OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_PATTERN =
|
||||||
|
/(?=[\s\S]*\bEPERM\b)(?=[\s\S]*operation not permitted)(?=[\s\S]*\bsymlink\b)(?=[\s\S]*opencode)(?=[\s\S]*node_modules)(?=[\s\S]*(?:[A-Z]:\\|AppData\\Local\\claude-multimodel-nodejs))/i;
|
||||||
|
|
||||||
|
export function isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(
|
||||||
|
value: string | null | undefined
|
||||||
|
): boolean {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
trimmed === OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE ||
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_PATTERN.test(trimmed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isOpenCodeWindowsAccessDeniedDiagnostic(value: string | null | undefined): boolean {
|
export function isOpenCodeWindowsAccessDeniedDiagnostic(value: string | null | undefined): boolean {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(trimmed) ||
|
||||||
trimmed === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
trimmed === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
||||||
OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN.test(trimmed)
|
OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN.test(trimmed)
|
||||||
);
|
);
|
||||||
|
|
@ -18,6 +38,9 @@ export function isOpenCodeWindowsAccessDeniedDiagnostic(value: string | null | u
|
||||||
export function normalizeOpenCodeWindowsAccessDeniedDiagnostic(
|
export function normalizeOpenCodeWindowsAccessDeniedDiagnostic(
|
||||||
value: string | null | undefined
|
value: string | null | undefined
|
||||||
): string | null {
|
): string | null {
|
||||||
|
if (isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(value)) {
|
||||||
|
return OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE;
|
||||||
|
}
|
||||||
return isOpenCodeWindowsAccessDeniedDiagnostic(value)
|
return isOpenCodeWindowsAccessDeniedDiagnostic(value)
|
||||||
? OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE
|
? OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE
|
||||||
: null;
|
: null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { buildCodexWorkspaceTrustSettingsArgs } from '@features/workspace-trust/core/domain';
|
import { buildCodexWorkspaceTrustSettingsArgs } from '@features/workspace-trust/core/domain';
|
||||||
import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
|
import {
|
||||||
|
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
|
||||||
|
} from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
@ -1026,6 +1029,42 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(result.warnings).toEqual([OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE]);
|
expect(result.warnings).toEqual([OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps OpenCode managed node_modules symlink EPERM diagnostics specific during prepareForProvisioning', async () => {
|
||||||
|
const prepare = vi.fn(async () => ({
|
||||||
|
ok: false as const,
|
||||||
|
providerId: 'opencode' as const,
|
||||||
|
reason: 'unknown_error',
|
||||||
|
retryable: false,
|
||||||
|
diagnostics: [],
|
||||||
|
warnings: [
|
||||||
|
[
|
||||||
|
'Runtime provider management command failed unexpectedly:',
|
||||||
|
"EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
||||||
|
"-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
||||||
|
].join(' '),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
const adapter: TeamLaunchRuntimeAdapter = {
|
||||||
|
providerId: 'opencode',
|
||||||
|
prepare,
|
||||||
|
launch: vi.fn(),
|
||||||
|
reconcile: vi.fn(),
|
||||||
|
stop: vi.fn(),
|
||||||
|
};
|
||||||
|
const registry = new TeamRuntimeAdapterRegistry([adapter]);
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
svc.setRuntimeAdapterRegistry(registry);
|
||||||
|
|
||||||
|
const result = await svc.prepareForProvisioning(tempRoot, {
|
||||||
|
providerId: 'opencode',
|
||||||
|
forceFresh: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ready).toBe(false);
|
||||||
|
expect(result.message).toBe(OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE);
|
||||||
|
expect(result.warnings).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps OpenCode access-denied selected-model failures provider-scoped', async () => {
|
it('keeps OpenCode access-denied selected-model failures provider-scoped', async () => {
|
||||||
const prepare = vi.fn(async () => ({
|
const prepare = vi.fn(async () => ({
|
||||||
ok: false as const,
|
ok: false as const,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
mergeReusableProviderPrepareModelResults,
|
mergeReusableProviderPrepareModelResults,
|
||||||
runProviderPrepareDiagnostics,
|
runProviderPrepareDiagnostics,
|
||||||
} from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
|
} from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
|
||||||
|
import { OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
|
@ -488,6 +489,43 @@ describe('runProviderPrepareDiagnostics', () => {
|
||||||
expect(result.modelResultsById).toEqual({});
|
expect(result.modelResultsById).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the OpenCode node_modules symlink EPERM failure on the administrator hint path', async () => {
|
||||||
|
const symlinkError = [
|
||||||
|
'Runtime provider management command failed unexpectedly:',
|
||||||
|
"EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
||||||
|
"-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
||||||
|
].join(' ');
|
||||||
|
const prepareProvisioning = vi.fn<
|
||||||
|
(
|
||||||
|
cwd?: string,
|
||||||
|
providerId?: TeamProviderId,
|
||||||
|
providerIds?: TeamProviderId[],
|
||||||
|
selectedModels?: string[],
|
||||||
|
limitContext?: boolean,
|
||||||
|
modelVerificationMode?: 'compatibility' | 'deep'
|
||||||
|
) => Promise<TeamProvisioningPrepareResult>
|
||||||
|
>(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ready: false,
|
||||||
|
message: symlinkError,
|
||||||
|
details: [symlinkError],
|
||||||
|
warnings: [symlinkError],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await runProviderPrepareDiagnostics({
|
||||||
|
cwd: '/tmp/project',
|
||||||
|
providerId: 'opencode',
|
||||||
|
selectedModelIds: ['opencode/big-pickle'],
|
||||||
|
prepareProvisioning,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe('failed');
|
||||||
|
expect(result.details).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
|
||||||
|
expect(result.warnings).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
|
||||||
|
expect(result.modelResultsById).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it('treats OpenCode compatibility verification warnings as blocking when the batch failed', async () => {
|
it('treats OpenCode compatibility verification warnings as blocking when the batch failed', async () => {
|
||||||
const prepareProvisioning = vi.fn<
|
const prepareProvisioning = vi.fn<
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,45 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
expect(details?.className).toContain('font-mono');
|
expect(details?.className).toContain('font-mono');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the Windows administrator hint only for OpenCode node_modules symlink EPERM errors', async () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const symlinkError = [
|
||||||
|
'Runtime provider management command failed unexpectedly:',
|
||||||
|
"EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
||||||
|
"-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(RuntimeProviderManagementPanelView, {
|
||||||
|
state: createState({ error: symlinkError }),
|
||||||
|
actions: createActions(),
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('Windows: run Agent Teams AI as Administrator');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(RuntimeProviderManagementPanelView, {
|
||||||
|
state: createState({
|
||||||
|
error: 'EPERM: operation not permitted, mkdir C:\\Program Files\\locked-project',
|
||||||
|
}),
|
||||||
|
actions: createActions(),
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).not.toContain('Windows: run Agent Teams AI as Administrator');
|
||||||
|
});
|
||||||
|
|
||||||
it('copies fallback error text when structured diagnostics are unavailable', async () => {
|
it('copies fallback error text when structured diagnostics are unavailable', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue