Files
gemini-cli/packages/core/src/agents/a2a-errors.test.ts
Shyam Raghuwanshi 7c4570339e fix: robust UX for remote agent errors (#20307)
Co-authored-by: Adam Weidman <adamfweidman@google.com>
2026-03-10 23:50:25 +00:00

299 lines
9.9 KiB
TypeScript

/**
* @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);
});
});
});
});