mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-13 06:40:33 -07:00
fix: robust UX for remote agent errors (#20307)
Co-authored-by: Adam Weidman <adamfweidman@google.com>
This commit is contained in:
committed by
GitHub
parent
e22d9917b7
commit
7c4570339e
298
packages/core/src/agents/a2a-errors.test.ts
Normal file
298
packages/core/src/agents/a2a-errors.test.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
A2AAgentError,
|
||||
AgentCardNotFoundError,
|
||||
AgentCardAuthError,
|
||||
AgentAuthConfigMissingError,
|
||||
AgentConnectionError,
|
||||
classifyAgentError,
|
||||
} from './a2a-errors.js';
|
||||
|
||||
describe('A2A Error Types', () => {
|
||||
describe('A2AAgentError', () => {
|
||||
it('should set name, agentName, and userMessage', () => {
|
||||
const error = new A2AAgentError('my-agent', 'internal msg', 'user msg');
|
||||
expect(error.name).toBe('A2AAgentError');
|
||||
expect(error.agentName).toBe('my-agent');
|
||||
expect(error.message).toBe('internal msg');
|
||||
expect(error.userMessage).toBe('user msg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentCardNotFoundError', () => {
|
||||
it('should produce a user-friendly 404 message', () => {
|
||||
const error = new AgentCardNotFoundError(
|
||||
'my-agent',
|
||||
'https://example.com/card',
|
||||
);
|
||||
expect(error.name).toBe('AgentCardNotFoundError');
|
||||
expect(error.agentName).toBe('my-agent');
|
||||
expect(error.userMessage).toContain('404');
|
||||
expect(error.userMessage).toContain('https://example.com/card');
|
||||
expect(error.userMessage).toContain('agent_card_url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentCardAuthError', () => {
|
||||
it('should produce a user-friendly 401 message', () => {
|
||||
const error = new AgentCardAuthError(
|
||||
'secure-agent',
|
||||
'https://example.com/card',
|
||||
401,
|
||||
);
|
||||
expect(error.name).toBe('AgentCardAuthError');
|
||||
expect(error.statusCode).toBe(401);
|
||||
expect(error.userMessage).toContain('401');
|
||||
expect(error.userMessage).toContain('Unauthorized');
|
||||
expect(error.userMessage).toContain('"auth" configuration');
|
||||
});
|
||||
|
||||
it('should produce a user-friendly 403 message', () => {
|
||||
const error = new AgentCardAuthError(
|
||||
'secure-agent',
|
||||
'https://example.com/card',
|
||||
403,
|
||||
);
|
||||
expect(error.statusCode).toBe(403);
|
||||
expect(error.userMessage).toContain('403');
|
||||
expect(error.userMessage).toContain('Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentAuthConfigMissingError', () => {
|
||||
it('should list missing config fields', () => {
|
||||
const error = new AgentAuthConfigMissingError(
|
||||
'api-agent',
|
||||
'API Key (x-api-key): Send x-api-key in header',
|
||||
[
|
||||
'Authentication is required but not configured',
|
||||
"Scheme 'api_key' requires apiKey authentication",
|
||||
],
|
||||
);
|
||||
expect(error.name).toBe('AgentAuthConfigMissingError');
|
||||
expect(error.requiredAuth).toContain('API Key');
|
||||
expect(error.missingFields).toHaveLength(2);
|
||||
expect(error.userMessage).toContain('API Key');
|
||||
expect(error.userMessage).toContain('no auth is configured');
|
||||
expect(error.userMessage).toContain('Missing:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentConnectionError', () => {
|
||||
it('should wrap the original error cause', () => {
|
||||
const cause = new Error('ECONNREFUSED');
|
||||
const error = new AgentConnectionError(
|
||||
'my-agent',
|
||||
'https://example.com/card',
|
||||
cause,
|
||||
);
|
||||
expect(error.name).toBe('AgentConnectionError');
|
||||
expect(error.userMessage).toContain('ECONNREFUSED');
|
||||
expect(error.userMessage).toContain('https://example.com/card');
|
||||
});
|
||||
|
||||
it('should handle non-Error causes', () => {
|
||||
const error = new AgentConnectionError(
|
||||
'my-agent',
|
||||
'https://example.com/card',
|
||||
'raw string error',
|
||||
);
|
||||
expect(error.userMessage).toContain('raw string error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyAgentError', () => {
|
||||
it('should classify a 404 error message', () => {
|
||||
const raw = new Error('HTTP 404: Not Found');
|
||||
const result = classifyAgentError(
|
||||
'agent-a',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardNotFoundError);
|
||||
expect(result.agentName).toBe('agent-a');
|
||||
});
|
||||
|
||||
it('should classify a "not found" error message (case-insensitive)', () => {
|
||||
const raw = new Error('Agent card not found at the given URL');
|
||||
const result = classifyAgentError(
|
||||
'agent-a',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardNotFoundError);
|
||||
});
|
||||
|
||||
it('should classify a 401 error message', () => {
|
||||
const raw = new Error('Request failed with status 401');
|
||||
const result = classifyAgentError(
|
||||
'agent-b',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should classify an "unauthorized" error message', () => {
|
||||
const raw = new Error('Unauthorized access to agent card');
|
||||
const result = classifyAgentError(
|
||||
'agent-b',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
});
|
||||
|
||||
it('should classify a 403 error message', () => {
|
||||
const raw = new Error('HTTP 403 Forbidden');
|
||||
const result = classifyAgentError(
|
||||
'agent-c',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('should fall back to AgentConnectionError for unknown errors', () => {
|
||||
const raw = new Error('Something completely unexpected');
|
||||
const result = classifyAgentError(
|
||||
'agent-d',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentConnectionError);
|
||||
});
|
||||
|
||||
it('should classify ECONNREFUSED as AgentConnectionError', () => {
|
||||
const raw = new Error('ECONNREFUSED 127.0.0.1:8080');
|
||||
const result = classifyAgentError(
|
||||
'agent-d',
|
||||
'https://example.com/card',
|
||||
raw,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentConnectionError);
|
||||
});
|
||||
|
||||
it('should handle non-Error values', () => {
|
||||
const result = classifyAgentError(
|
||||
'agent-e',
|
||||
'https://example.com/card',
|
||||
'some string error',
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentConnectionError);
|
||||
});
|
||||
|
||||
describe('cause chain inspection', () => {
|
||||
it('should detect 404 in a nested cause', () => {
|
||||
const inner = new Error('HTTP 404 Not Found');
|
||||
const outer = new Error('fetch failed', { cause: inner });
|
||||
const result = classifyAgentError(
|
||||
'agent-nested',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardNotFoundError);
|
||||
});
|
||||
|
||||
it('should detect 401 in a deeply nested cause', () => {
|
||||
const innermost = new Error('Server returned 401');
|
||||
const middle = new Error('Request error', { cause: innermost });
|
||||
const outer = new Error('fetch failed', { cause: middle });
|
||||
const result = classifyAgentError(
|
||||
'agent-deep',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should detect ECONNREFUSED error code in cause chain', () => {
|
||||
const inner = Object.assign(new Error('connect failed'), {
|
||||
code: 'ECONNREFUSED',
|
||||
});
|
||||
const outer = new Error('fetch failed', { cause: inner });
|
||||
const result = classifyAgentError(
|
||||
'agent-conn',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentConnectionError);
|
||||
});
|
||||
|
||||
it('should detect status property on error objects in cause chain', () => {
|
||||
const inner = Object.assign(new Error('Bad response'), {
|
||||
status: 403,
|
||||
});
|
||||
const outer = new Error('agent card resolution failed', {
|
||||
cause: inner,
|
||||
});
|
||||
const result = classifyAgentError(
|
||||
'agent-status',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('should detect status on a plain-object cause (non-Error)', () => {
|
||||
const outer = new Error('fetch failed');
|
||||
// Some HTTP libs set cause to a plain object, not an Error instance
|
||||
(outer as unknown as { cause: unknown }).cause = {
|
||||
message: 'Unauthorized',
|
||||
status: 401,
|
||||
};
|
||||
const result = classifyAgentError(
|
||||
'agent-plain-cause',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should detect statusCode on a plain-object cause (non-Error)', () => {
|
||||
const outer = new Error('fetch failed');
|
||||
(outer as unknown as { cause: unknown }).cause = {
|
||||
message: 'Forbidden',
|
||||
statusCode: 403,
|
||||
};
|
||||
const result = classifyAgentError(
|
||||
'agent-plain-cause-403',
|
||||
'https://example.com/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentCardAuthError);
|
||||
expect((result as AgentCardAuthError).statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('should classify ENOTFOUND as AgentConnectionError, not 404', () => {
|
||||
// ENOTFOUND (DNS resolution failure) should NOT be misclassified
|
||||
// as a 404 despite containing "NOTFOUND" in the error code.
|
||||
const inner = Object.assign(
|
||||
new Error('getaddrinfo ENOTFOUND example.invalid'),
|
||||
{
|
||||
code: 'ENOTFOUND',
|
||||
},
|
||||
);
|
||||
const outer = new Error('fetch failed', { cause: inner });
|
||||
const result = classifyAgentError(
|
||||
'agent-dns',
|
||||
'https://example.invalid/card',
|
||||
outer,
|
||||
);
|
||||
expect(result).toBeInstanceOf(AgentConnectionError);
|
||||
expect(result).not.toBeInstanceOf(AgentCardNotFoundError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user