client.test
Tests for client.test functionality in the Artinet SDK.
Test Suites
- A2AClient
Source Code
import {
A2AClient,
SystemError,
AgentCard,
Message,
Task,
TaskState,
TaskSendParams,
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
TaskPushNotificationConfig,
TaskIdParams,
TaskQueryParams,
PushNotificationConfig,
configureLogger,
} from "../src/index.js";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
configureLogger({ level: "silent" });
const MOCK_AGENT_CARD: AgentCard = {
name: "Test Agent",
description: "A test agent for unit tests",
url: "https://test-agent.example.com/api",
version: "1.0.0",
capabilities: {
streaming: true,
pushNotifications: true,
stateTransitionHistory: false,
},
skills: [
{
id: "test-skill",
name: "Test Skill",
description: "A test skill for unit tests",
},
],
};
const MOCK_TASK: Task = {
id: "test-task-123",
status: {
state: "completed" as TaskState,
message: {
role: "agent",
parts: [
{
type: "text",
text: "This is a test response",
},
],
},
timestamp: new Date().toISOString(),
},
artifacts: [
{
name: "test-artifact",
parts: [
{
type: "text",
text: "Artifact content",
},
],
},
],
};
// Task update events for streaming
const STATUS_UPDATE_EVENT: TaskStatusUpdateEvent = {
id: "test-task-123",
status: {
state: "in_progress" as TaskState,
timestamp: new Date().toISOString(),
},
};
const ARTIFACT_UPDATE_EVENT: TaskArtifactUpdateEvent = {
id: "test-task-123",
artifact: {
name: "new-artifact",
parts: [
{
type: "text",
text: "New artifact content",
},
],
},
};
const MOCK_NOTIFICATION_CONFIG: PushNotificationConfig = {
url: "https://notification-endpoint.example.com",
token: "test-notification-token",
};
const MOCK_PUSH_NOTIFICATION_CONFIG: TaskPushNotificationConfig = {
id: "test-task-123",
pushNotificationConfig: MOCK_NOTIFICATION_CONFIG,
};
// Setup MSW server for mocking HTTP requests
const server = setupServer(
// Mock agent card endpoint
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return HttpResponse.json(MOCK_AGENT_CARD);
}),
// Mock fallback agent card endpoint
http.get("https://test-agent.example.com/agent-card", () => {
return HttpResponse.json(MOCK_AGENT_CARD);
}),
// Mock tasks/send endpoint
http.post("https://test-agent.example.com", async ({ request }) => {
const body = (await request.json()) as {
method: string;
id: string | number;
params?: Record<string, any>;
};
if (body.method === "tasks/send") {
return HttpResponse.json({
jsonrpc: "2.0",
id: body.id,
result: MOCK_TASK,
});
}
if (body.method === "tasks/get") {
return HttpResponse.json({
jsonrpc: "2.0",
id: body.id,
result: MOCK_TASK,
});
}
if (body.method === "tasks/cancel") {
const canceledTask = {
...MOCK_TASK,
status: {
...MOCK_TASK.status,
state: "canceled",
},
};
return HttpResponse.json({
jsonrpc: "2.0",
id: body.id,
result: canceledTask,
});
}
if (body.method === "tasks/pushNotification/set") {
return HttpResponse.json({
jsonrpc: "2.0",
id: body.id,
result: body.params,
});
}
if (body.method === "tasks/pushNotification/get") {
return HttpResponse.json({
jsonrpc: "2.0",
id: body.id,
result: MOCK_PUSH_NOTIFICATION_CONFIG,
});
}
if (
body.method === "tasks/sendSubscribe" ||
body.method === "tasks/resubscribe"
) {
// For streaming endpoints, create a mock SSE response
// This is a simplified implementation since MSW doesn't handle SSE natively
// We'll create a text response with the correct format
const eventData1 = JSON.stringify({
jsonrpc: "2.0",
id: body.id,
result: STATUS_UPDATE_EVENT,
});
const eventData2 = JSON.stringify({
jsonrpc: "2.0",
id: body.id,
result: ARTIFACT_UPDATE_EVENT,
});
// Create a text response that mimics SSE format
const responseText =
`event: event\ndata: ${eventData1}\n\n` +
`event: event\ndata: ${eventData2}\n\n`;
return new HttpResponse(responseText, {
headers: {
"Content-Type": "text/event-stream",
},
});
}
// Default case for unhandled methods
return HttpResponse.json(
{
jsonrpc: "2.0",
id: body.id,
error: {
code: -32601,
message: "Method not found",
},
},
{ status: 400 }
);
})
);
describe("A2AClient", () => {
let client: A2AClient;
beforeAll(() => {
server.listen();
});
afterAll(() => {
server.close();
});
beforeEach(() => {
client = new A2AClient("https://test-agent.example.com");
server.resetHandlers();
});
// Test agent card retrieval
test("should fetch an agent card", async () => {
const card = await client.agentCard();
expect(card).toEqual(MOCK_AGENT_CARD);
// Test the cached card path (line 51 in client.ts)
// This second call should use the cached card without making a network request
// Override the server to return a different card,
// if the cache is used, we'll still get the original
server.use(
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return HttpResponse.json({
...MOCK_AGENT_CARD,
version: "2.0.0", // Changed version
});
})
);
const cachedCard = await client.agentCard();
// Verify we got the original card (from cache) not the new one
expect(cachedCard).toEqual(MOCK_AGENT_CARD);
expect(cachedCard.version).toBe("1.0.0"); // Original version, not 2.0.0
});
// Test agent card refreshing
test("should refresh an agent card", async () => {
// First get the card to cache it
await client.agentCard();
// Mock a change to the agent card on the server
server.use(
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return HttpResponse.json({
...MOCK_AGENT_CARD,
version: "1.1.0",
});
})
);
// Refresh the card
const updatedCard = await client.refreshAgentCard();
expect(updatedCard.version).toBe("1.1.0");
});
// Test fallback to secondary card URL
test("should fetch agent card from fallback URL when primary fails", async () => {
server.use(
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return new HttpResponse(null, { status: 404 });
})
);
const card = await client.agentCard();
expect(card).toEqual(MOCK_AGENT_CARD);
});
// Test agent card fetching error
test("should throw when both agent card endpoints fail", async () => {
server.use(
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return new HttpResponse("Not found", {
status: 404,
headers: { "Content-Type": "text/plain" },
});
}),
http.get("https://test-agent.example.com/agent-card", () => {
return new HttpResponse("Server error", {
status: 500,
headers: { "Content-Type": "text/plain" },
});
})
);
await expect(client.agentCard()).rejects.toThrow();
});
// Test constructor with string URL and headers
test("should construct client with string URL and headers", () => {
const testClient = new A2AClient("https://example.com", {
Authorization: "Bearer test-token",
});
// Check internal state
expect((testClient as any).baseUrl.href).toBe("https://example.com/");
expect((testClient as any).customHeaders["Authorization"]).toBe(
"Bearer test-token"
);
});
// Test sending a task
test("should send a task and receive a response", async () => {
const message: Message = {
role: "user",
parts: [
{
type: "text",
text: "Hello, this is a test message",
},
],
};
const params: TaskSendParams = {
id: "test-task-123",
message,
};
const task = await client.sendTask(params);
expect(task).toEqual(MOCK_TASK);
});
// Test getting a task
test("should get a task by ID", async () => {
const task = await client.getTask({ id: "test-task-123" });
expect(task).toEqual(MOCK_TASK);
});
// Test canceling a task
test("should cancel a task", async () => {
const task = await client.cancelTask({ id: "test-task-123" });
expect(task).toMatchObject({
id: "test-task-123",
status: {
state: "canceled",
},
});
});
// Test push notification config setting
test("should set task push notification config", async () => {
const config: TaskPushNotificationConfig = {
id: "test-task-123",
pushNotificationConfig: {
url: "https://notification-endpoint.example.com",
token: "test-notification-token",
},
};
const result = await client.setTaskPushNotification(config);
expect(result).toEqual(config);
});
// Test push notification config getting
test("should get task push notification config", async () => {
const params: TaskIdParams = {
id: "test-task-123",
};
const config = await client.getTaskPushNotification(params);
expect(config).toEqual(MOCK_PUSH_NOTIFICATION_CONFIG);
});
// Test streaming task updates
test("should stream task updates", async () => {
const message: Message = {
role: "user",
parts: [
{
type: "text",
text: "Hello, this is a test message",
},
],
};
const params: TaskSendParams = {
id: "test-task-123",
message,
};
const events: (TaskStatusUpdateEvent | TaskArtifactUpdateEvent)[] = [];
const stream = client.sendTaskSubscribe(params);
for await (const event of stream) {
events.push(event);
}
expect(events.length).toBe(2);
// Check event types without explicit type property references
expect(events[0]).toHaveProperty("status"); // It's a status update
expect(events[1]).toHaveProperty("artifact"); // It's an artifact update
});
// Test resubscribe task updates
test("should resubscribe to task updates", async () => {
const params: TaskQueryParams = {
id: "test-task-123",
};
const events: (TaskStatusUpdateEvent | TaskArtifactUpdateEvent)[] = [];
for await (const event of client.resubscribeTask(params)) {
events.push(event);
}
expect(events.length).toBe(2);
// Check event types without explicit type property references
expect(events[0]).toHaveProperty("status"); // It's a status update
expect(events[1]).toHaveProperty("artifact"); // It's an artifact update
});
// Test error handling - network errors
test("should handle network errors during HTTP request", async () => {
// Mock a network error by using a response that forces a fetch error
server.use(
http.post("https://test-agent.example.com", () => {
return new HttpResponse(null, { status: 500 });
})
);
await expect(client.getTask({ id: "test-task-123" })).rejects.toThrow();
});
// Test error handling - invalid JSON response
test("should handle invalid JSON response", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return new HttpResponse("This is not JSON", {
headers: { "Content-Type": "text/plain" },
});
})
);
await expect(client.getTask({ id: "test-task-123" })).rejects.toThrow();
});
// Test error handling - invalid JSON-RPC structure
test("should handle invalid JSON-RPC structure", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return HttpResponse.json({ not: "valid-jsonrpc" });
})
);
await expect(client.getTask({ id: "test-task-123" })).rejects.toThrow();
});
// Test error handling - HTTP error with JSON-RPC error
test("should handle HTTP error with JSON-RPC error", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return HttpResponse.json(
{
jsonrpc: "2.0",
id: "123",
error: {
code: -32000,
message: "Task not found",
},
},
{ status: 400 }
);
})
);
await expect(client.getTask({ id: "test-task-123" })).rejects.toThrow();
});
// Test error handling - HTTP error with non-JSON response
test("should handle HTTP error with non-JSON response", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return new HttpResponse("Internal Server Error", {
status: 500,
headers: { "Content-Type": "text/plain" },
});
})
);
await expect(client.getTask({ id: "test-task-123" })).rejects.toThrow();
});
// Test error handling for streaming - response not OK
test("should handle streaming error when response is not OK", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return new HttpResponse("Bad Request", {
status: 400,
headers: { "Content-Type": "text/plain" },
});
})
);
const stream = client.sendTaskSubscribe({
id: "test-task-123",
message: { role: "user", parts: [{ type: "text", text: "Test" }] },
});
await expect(async () => {
for await (const event of stream) {
// This should not execute
}
}).rejects.toThrow();
});
// Test capability check - edge case with no capabilities
test("should handle agent card with no capabilities", async () => {
// First ensure the agent card is cached with valid data
await client.agentCard();
server.use(
http.get("https://test-agent.example.com/.well-known/agent.json", () => {
return HttpResponse.json({
...MOCK_AGENT_CARD,
capabilities: undefined,
});
})
);
// Force refresh to clear the cache
await client.refreshAgentCard();
const hasStreaming = await client.supports("streaming");
expect(hasStreaming).toBe(false);
});
// Test capability check error handling
test("should handle error during capability check", async () => {
// Re-use the server default handlers which have valid responses
// This ensures the test doesn't fail when trying to fetch the agent card
// Create a client with a known invalid URL to simulate error
const badClient = new A2AClient("https://invalid-url.example.com");
// Mock a failed request for the invalid URL
server.use(
http.get("https://invalid-url.example.com/.well-known/agent.json", () => {
return new HttpResponse(null, { status: 404 });
}),
http.get("https://invalid-url.example.com/agent-card", () => {
return new HttpResponse(null, { status: 404 });
})
);
const hasStreaming = await badClient.supports("streaming");
expect(hasStreaming).toBe(false);
});
// Test error handling
test("should handle JSON-RPC errors", async () => {
server.use(
http.post("https://test-agent.example.com", () => {
return HttpResponse.json({
jsonrpc: "2.0",
id: "123",
error: {
code: -32000,
message: "Task not found",
},
});
})
);
await expect(client.getTask({ id: "nonexistent-task" })).rejects.toThrow(
SystemError
);
});
// Test capability check
test("should check if a capability is supported", async () => {
// First, ensure the agent card is cached
await client.agentCard();
const hasStreaming = await client.supports("streaming");
expect(hasStreaming).toBe(true);
const hasPushNotifications = await client.supports("pushNotifications");
expect(hasPushNotifications).toBe(true);
const hasStateTransitionHistory = await client.supports(
"stateTransitionHistory"
);
expect(hasStateTransitionHistory).toBe(false);
// Test the default case in the switch statement for uncovered branch
const hasUnsupportedCapability = await client.supports(
"unknownCapability" as any
);
expect(hasUnsupportedCapability).toBe(false);
});
// Test header management
test("should manage custom headers", () => {
// Add a header
client.addHeader("Authorization", "Bearer test-token");
// Check internal state (this is a private test hack, normally wouldn't test private members)
const headers = (client as any).customHeaders;
expect(headers["Authorization"]).toBe("Bearer test-token");
// Add another header
client.addHeader("X-Custom-Header", "test-value");
expect((client as any).customHeaders["X-Custom-Header"]).toBe("test-value");
// Replace all headers
client.setHeaders({
"Content-Type": "application/json",
"Accept-Language": "en-US",
});
const newHeaders = (client as any).customHeaders;
expect(newHeaders["Authorization"]).toBeUndefined();
expect(newHeaders["Content-Type"]).toBe("application/json");
expect(newHeaders["Accept-Language"]).toBe("en-US");
// Remove a header
client.removeHeader("Content-Type");
expect((client as any).customHeaders["Content-Type"]).toBeUndefined();
// Clear all headers
client.clearHeaders();
expect(Object.keys((client as any).customHeaders).length).toBe(0);
});
});
Running the Tests
To run these tests:
- Clone the Artinet SDK repository
- Install dependencies with
npm install
- Run the tests with
npm test
or specifically withnpx jest client.test.ts