fix(extensions): harden mcp diagnostics runtime guards

This commit is contained in:
777genius 2026-04-17 21:03:12 +03:00
parent c17a08c2d3
commit f8c11af5b9
2 changed files with 105 additions and 5 deletions

View file

@ -100,6 +100,7 @@ export const McpServersPanel = ({
mcpDiagnosticsLastCheckedAtFallback, mcpDiagnosticsLastCheckedAtFallback,
runMcpDiagnostics, runMcpDiagnostics,
cliStatus, cliStatus,
cliStatusLoading,
} = useStore( } = useStore(
useShallow((s) => ({ useShallow((s) => ({
browseCatalog: s.mcpBrowseCatalog, browseCatalog: s.mcpBrowseCatalog,
@ -120,6 +121,7 @@ export const McpServersPanel = ({
mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt, mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt,
runMcpDiagnostics: s.runMcpDiagnostics, runMcpDiagnostics: s.runMcpDiagnostics,
cliStatus: s.cliStatus, cliStatus: s.cliStatus,
cliStatusLoading: s.cliStatusLoading,
})) }))
); );
const installedServers = const installedServers =
@ -144,9 +146,27 @@ export const McpServersPanel = ({
} }
}, [browseCatalog.length, browseError, browseLoading, mcpBrowse]); }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]);
const diagnosticsDisableReason = useMemo(() => {
if (cliStatusLoading) {
return 'Checking runtime status...';
}
if (cliStatus?.installed === false) {
if (cliStatus.binaryPath && cliStatus.launchError) {
return 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.';
}
return 'The configured runtime is required. Install or repair it from the Dashboard.';
}
return null;
}, [cliStatus, cliStatusLoading]);
useEffect(() => { useEffect(() => {
if (diagnosticsDisableReason) {
return;
}
void runMcpDiagnostics(projectPath ?? undefined); void runMcpDiagnostics(projectPath ?? undefined);
}, [projectPath, runMcpDiagnostics]); }, [diagnosticsDisableReason, projectPath, runMcpDiagnostics]);
// Fetch GitHub stars after catalog loads (fire-and-forget) // Fetch GitHub stars after catalog loads (fire-and-forget)
useEffect(() => { useEffect(() => {
@ -240,6 +260,8 @@ export const McpServersPanel = ({
<p className="text-xs text-text-muted"> <p className="text-xs text-text-muted">
{mcpDiagnosticsLoading ? ( {mcpDiagnosticsLoading ? (
<>Checking installed MCP servers via {runtimeLabel} ...</> <>Checking installed MCP servers via {runtimeLabel} ...</>
) : diagnosticsDisableReason ? (
diagnosticsDisableReason
) : mcpDiagnosticsLastCheckedAt ? ( ) : mcpDiagnosticsLastCheckedAt ? (
`Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}` `Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}`
) : ( ) : (
@ -251,7 +273,7 @@ export const McpServersPanel = ({
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => void runMcpDiagnostics(projectPath ?? undefined)} onClick={() => void runMcpDiagnostics(projectPath ?? undefined)}
disabled={mcpDiagnosticsLoading} disabled={mcpDiagnosticsLoading || Boolean(diagnosticsDisableReason)}
className="whitespace-nowrap" className="whitespace-nowrap"
> >
<RefreshCw <RefreshCw
@ -299,9 +321,7 @@ export const McpServersPanel = ({
))} ))}
</div> </div>
) : ( ) : (
<p className="text-xs text-text-muted"> <p className="text-xs text-text-muted">Waiting for diagnostics results...</p>
Waiting for <code>{diagnosticsCommand}</code> results...
</p>
)} )}
</div> </div>
)} )}

View file

@ -34,8 +34,12 @@ interface StoreState {
mcpDiagnosticsLastCheckedAt: number | null; mcpDiagnosticsLastCheckedAt: number | null;
mcpDiagnosticsLastCheckedAtByProjectPath?: Record<string, number | null>; mcpDiagnosticsLastCheckedAtByProjectPath?: Record<string, number | null>;
runMcpDiagnostics: ReturnType<typeof vi.fn>; runMcpDiagnostics: ReturnType<typeof vi.fn>;
cliStatusLoading: boolean;
cliStatus?: { cliStatus?: {
flavor?: 'claude' | 'agent_teams_orchestrator'; flavor?: 'claude' | 'agent_teams_orchestrator';
installed?: boolean;
binaryPath?: string | null;
launchError?: string | null;
}; };
} }
@ -144,6 +148,7 @@ describe('McpServersPanel initial browse loading', () => {
storeState.mcpDiagnosticsLastCheckedAt = null; storeState.mcpDiagnosticsLastCheckedAt = null;
storeState.mcpDiagnosticsLastCheckedAtByProjectPath = undefined; storeState.mcpDiagnosticsLastCheckedAtByProjectPath = undefined;
storeState.runMcpDiagnostics = vi.fn(); storeState.runMcpDiagnostics = vi.fn();
storeState.cliStatusLoading = false;
storeState.cliStatus = undefined; storeState.cliStatus = undefined;
}); });
@ -309,4 +314,79 @@ describe('McpServersPanel initial browse loading', () => {
await Promise.resolve(); await Promise.resolve();
}); });
}); });
it('does not auto-run diagnostics when the configured runtime is unavailable', async () => {
storeState.cliStatus = {
flavor: 'agent_teams_orchestrator',
installed: false,
binaryPath: null,
launchError: null,
};
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(McpServersPanel, {
projectPath: null,
mcpSearchQuery: '',
mcpSearch: vi.fn(),
mcpSearchResults: [],
mcpSearchLoading: false,
mcpSearchWarnings: [],
selectedMcpServerId: null,
setSelectedMcpServerId: vi.fn(),
})
);
await Promise.resolve();
});
expect(storeState.runMcpDiagnostics).not.toHaveBeenCalled();
expect(host.textContent).toContain(
'The configured runtime is required. Install or repair it from the Dashboard.'
);
const checkStatusButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Check Status')
);
expect(checkStatusButton).toBeDefined();
expect((checkStatusButton as HTMLButtonElement).disabled).toBe(true);
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('renders provider-neutral waiting copy while diagnostics are still running', async () => {
storeState.mcpDiagnosticsLoading = true;
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(McpServersPanel, {
projectPath: null,
mcpSearchQuery: '',
mcpSearch: vi.fn(),
mcpSearchResults: [],
mcpSearchLoading: false,
mcpSearchWarnings: [],
selectedMcpServerId: null,
setSelectedMcpServerId: vi.fn(),
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('Waiting for diagnostics results...');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
}); });