From 754806e0f8a89cb073ff8713b9de0075e93492e4 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 21:49:03 -0300 Subject: [PATCH] test(a2a): unit tests for listCapabilities + Agent Card route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 6 tests for executeListCapabilities (§3.7 shape, 42-skill IDs in markdown, coverage bounds, ISO datetime, handler registration) and 5 tests for the Agent Card route (6 skills total, list-capabilities presence + tags + examples, all original 5 skills preserved). All 11 pass. --- tests/unit/agent-card-route.test.ts | 84 +++++++++++++++++++++++++ tests/unit/listCapabilities-a2a.test.ts | 83 ++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 tests/unit/agent-card-route.test.ts create mode 100644 tests/unit/listCapabilities-a2a.test.ts diff --git a/tests/unit/agent-card-route.test.ts b/tests/unit/agent-card-route.test.ts new file mode 100644 index 0000000000..2dc714eeac --- /dev/null +++ b/tests/unit/agent-card-route.test.ts @@ -0,0 +1,84 @@ +/** + * Unit tests for GET /.well-known/agent.json (Agent Card endpoint). + * + * Verifies: + * - Response includes 6 skills after the list-capabilities addition + * - list-capabilities entry has the required id, tags, and examples + */ + +import test from "node:test"; +import assert from "node:assert/strict"; + +const { GET } = await import("../../src/app/.well-known/agent.json/route.js"); + +interface AgentSkillEntry { + id: string; + name: string; + description: string; + tags: string[]; + examples: string[]; +} + +interface AgentCard { + name: string; + version: string; + skills: AgentSkillEntry[]; +} + +test("GET /.well-known/agent.json returns 6 skills", async () => { + const response = await GET(); + assert.equal(response.status, 200, "Expected HTTP 200"); + + const body = (await response.json()) as AgentCard; + assert.ok(Array.isArray(body.skills), "skills is an array"); + assert.equal(body.skills.length, 6, "Expected exactly 6 skills"); +}); + +test("Agent Card includes list-capabilities skill entry", async () => { + const response = await GET(); + const body = (await response.json()) as AgentCard; + + const skill = body.skills.find((s) => s.id === "list-capabilities"); + assert.ok(skill, "list-capabilities skill must be present in Agent Card"); +}); + +test("list-capabilities entry has required tags [discovery, capabilities]", async () => { + const response = await GET(); + const body = (await response.json()) as AgentCard; + + const skill = body.skills.find((s) => s.id === "list-capabilities"); + assert.ok(skill, "list-capabilities skill must be present"); + assert.ok(Array.isArray(skill.tags), "tags is an array"); + assert.ok(skill.tags.includes("discovery"), "tags includes 'discovery'"); + assert.ok(skill.tags.includes("capabilities"), "tags includes 'capabilities'"); +}); + +test("list-capabilities entry has at least one example question", async () => { + const response = await GET(); + const body = (await response.json()) as AgentCard; + + const skill = body.skills.find((s) => s.id === "list-capabilities"); + assert.ok(skill, "list-capabilities skill must be present"); + assert.ok(Array.isArray(skill.examples), "examples is an array"); + assert.ok(skill.examples.length > 0, "examples has at least one entry"); +}); + +test("Agent Card includes all 5 original skills", async () => { + const response = await GET(); + const body = (await response.json()) as AgentCard; + + const originalIds = [ + "smart-routing", + "quota-management", + "provider-discovery", + "cost-analysis", + "health-report", + ]; + + for (const id of originalIds) { + assert.ok( + body.skills.some((s) => s.id === id), + `Original skill '${id}' must be present in Agent Card`, + ); + } +}); diff --git a/tests/unit/listCapabilities-a2a.test.ts b/tests/unit/listCapabilities-a2a.test.ts new file mode 100644 index 0000000000..16892070f6 --- /dev/null +++ b/tests/unit/listCapabilities-a2a.test.ts @@ -0,0 +1,83 @@ +/** + * Unit tests for the A2A list-capabilities skill. + * + * Verifies: + * - Return shape matches §3.7 contract + * - Markdown table contains all 42 skill IDs + * - Coverage bounds are within declared totals + * - metadata.source === "agent-skills-catalog" + * - metadata.generatedAt is an ISO datetime string + */ + +import assert from "node:assert/strict"; +import { test } from "node:test"; +import type { A2ATask } from "../../src/lib/a2a/taskManager.js"; +import { executeListCapabilities } from "../../src/lib/a2a/skills/listCapabilities.js"; +import { API_SKILL_IDS, CLI_SKILL_IDS } from "../../src/lib/agentSkills/catalog.js"; + +// Minimal stub — executeListCapabilities only receives the task arg but does not use it +const stubTask = {} as A2ATask; + +test("executeListCapabilities returns shape matching §3.7 contract", async () => { + const result = await executeListCapabilities(stubTask); + + // artifacts array with exactly 1 text artifact + assert.ok(Array.isArray(result.artifacts), "artifacts is an array"); + assert.equal(result.artifacts.length, 1, "exactly 1 artifact"); + assert.equal(result.artifacts[0].type, "text", "artifact type is 'text'"); + assert.ok(typeof result.artifacts[0].content === "string", "artifact content is a string"); + + // metadata shape + const { metadata } = result; + assert.ok(metadata, "metadata exists"); + assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches"); + assert.equal(metadata.totalSkills, 42, "metadata.totalSkills === 42"); + assert.ok(metadata.coverage, "metadata.coverage exists"); + assert.ok(metadata.coverage.api, "metadata.coverage.api exists"); + assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists"); + assert.equal(metadata.coverage.api.total, 22, "api.total === 22"); + assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20"); +}); + +test("executeListCapabilities markdown table contains all 42 skill IDs", async () => { + const result = await executeListCapabilities(stubTask); + const content = result.artifacts[0].content; + + const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; + assert.equal(allIds.length, 42, "catalog declares 42 skill IDs"); + + for (const id of allIds) { + assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`); + } +}); + +test("metadata.coverage.api.have is within [0, 22]", async () => { + const result = await executeListCapabilities(stubTask); + const { api } = result.metadata.coverage; + assert.ok(api.have >= 0, "api.have >= 0"); + assert.ok(api.have <= 22, "api.have <= 22"); +}); + +test("metadata.coverage.cli.have is within [0, 20]", async () => { + const result = await executeListCapabilities(stubTask); + const { cli } = result.metadata.coverage; + assert.ok(cli.have >= 0, "cli.have >= 0"); + assert.ok(cli.have <= 20, "cli.have <= 20"); +}); + +test("metadata.generatedAt is a valid ISO datetime", async () => { + const result = await executeListCapabilities(stubTask); + const { generatedAt } = result.metadata; + assert.ok(typeof generatedAt === "string", "generatedAt is a string"); + const parsed = new Date(generatedAt); + assert.ok(!isNaN(parsed.getTime()), `generatedAt is not a valid date: ${generatedAt}`); + assert.ok(generatedAt.includes("T"), "generatedAt contains 'T' (ISO format)"); +}); + +test("list-capabilities is registered in A2A_SKILL_HANDLERS", async () => { + const { A2A_SKILL_HANDLERS } = await import("../../src/lib/a2a/taskExecution.js"); + assert.ok( + "list-capabilities" in A2A_SKILL_HANDLERS, + "list-capabilities must be in A2A_SKILL_HANDLERS", + ); +});