diff --git a/tests/unit/a2a-v1-compat-10839.test.ts b/tests/unit/a2a-v1-compat-10839.test.ts index 075a3e2936..7aeea86542 100644 --- a/tests/unit/a2a-v1-compat-10839.test.ts +++ b/tests/unit/a2a-v1-compat-10839.test.ts @@ -14,6 +14,19 @@ const settingsDb = await import("../../src/lib/db/settings.ts"); const a2aRoute = await import("../../src/app/a2a/route.ts"); const agentCardRoute = await import("../../src/app/.well-known/agent-card.json/route.ts"); +/** + * The agent-card routes derive their base URL from `request.nextUrl.origin` + * (S2 topology sanitisation, #11418), so the handler must be invoked with a + * request the way Next.js does — a bare `GET()` throws on `nextUrl`. + */ +function makeCardRequest( + url = "https://gateway.example.com/.well-known/agent-card.json" +): NextRequest { + const request = new Request(url) as unknown as NextRequest; + Object.defineProperty(request, "nextUrl", { value: new URL(url), configurable: true }); + return request; +} + function makeJsonRpcRequest(body: unknown): NextRequest { return new Request("http://localhost/a2a", { method: "POST", @@ -48,7 +61,13 @@ test("#10839: v1.0 SendMessage is aliased to message/send and reshapes the respo ); assert.equal(res.status, 200); const body = (await res.json()) as { - result?: { task?: { id: string; status?: { message?: { parts?: { text?: string }[] } }; artifacts?: unknown } }; + result?: { + task?: { + id: string; + status?: { message?: { parts?: { text?: string }[] } }; + artifacts?: unknown; + }; + }; error?: unknown; }; assert.equal(body.error, undefined, JSON.stringify(body)); @@ -102,7 +121,7 @@ test("#10839: SendStreamingMessage no longer 404s (aliased to message/stream)", }); test("#10839: GET /.well-known/agent-card.json serves a v1.0 card declaring both interfaces", async () => { - const res = await agentCardRoute.GET(); + const res = await agentCardRoute.GET(makeCardRequest()); assert.equal(res.status, 200); const card = (await res.json()) as { supportedInterfaces?: { protocolVersion?: string }[] }; assert.ok(Array.isArray(card.supportedInterfaces)); diff --git a/tests/unit/agent-card-route.test.ts b/tests/unit/agent-card-route.test.ts index 2dc714eeac..a9ba0d8dca 100644 --- a/tests/unit/agent-card-route.test.ts +++ b/tests/unit/agent-card-route.test.ts @@ -8,9 +8,21 @@ import test from "node:test"; import assert from "node:assert/strict"; +import type { NextRequest } from "next/server"; const { GET } = await import("../../src/app/.well-known/agent.json/route.js"); +/** + * The agent-card routes derive their base URL from `request.nextUrl.origin` + * (S2 topology sanitisation, #11418), so the handler must be invoked with a + * request the way Next.js does — a bare `GET()` throws on `nextUrl`. + */ +function makeCardRequest(url = "https://gateway.example.com/.well-known/agent.json"): NextRequest { + const request = new Request(url) as unknown as NextRequest; + Object.defineProperty(request, "nextUrl", { value: new URL(url), configurable: true }); + return request; +} + interface AgentSkillEntry { id: string; name: string; @@ -26,7 +38,7 @@ interface AgentCard { } test("GET /.well-known/agent.json returns 6 skills", async () => { - const response = await GET(); + const response = await GET(makeCardRequest()); assert.equal(response.status, 200, "Expected HTTP 200"); const body = (await response.json()) as AgentCard; @@ -35,7 +47,7 @@ test("GET /.well-known/agent.json returns 6 skills", async () => { }); test("Agent Card includes list-capabilities skill entry", async () => { - const response = await GET(); + const response = await GET(makeCardRequest()); const body = (await response.json()) as AgentCard; const skill = body.skills.find((s) => s.id === "list-capabilities"); @@ -43,7 +55,7 @@ test("Agent Card includes list-capabilities skill entry", async () => { }); test("list-capabilities entry has required tags [discovery, capabilities]", async () => { - const response = await GET(); + const response = await GET(makeCardRequest()); const body = (await response.json()) as AgentCard; const skill = body.skills.find((s) => s.id === "list-capabilities"); @@ -54,7 +66,7 @@ test("list-capabilities entry has required tags [discovery, capabilities]", asyn }); test("list-capabilities entry has at least one example question", async () => { - const response = await GET(); + const response = await GET(makeCardRequest()); const body = (await response.json()) as AgentCard; const skill = body.skills.find((s) => s.id === "list-capabilities"); @@ -64,7 +76,7 @@ test("list-capabilities entry has at least one example question", async () => { }); test("Agent Card includes all 5 original skills", async () => { - const response = await GET(); + const response = await GET(makeCardRequest()); const body = (await response.json()) as AgentCard; const originalIds = [ @@ -78,7 +90,7 @@ test("Agent Card includes all 5 original skills", async () => { for (const id of originalIds) { assert.ok( body.skills.some((s) => s.id === id), - `Original skill '${id}' must be present in Agent Card`, + `Original skill '${id}' must be present in Agent Card` ); } }); diff --git a/tests/unit/conductor-agent-card.test.ts b/tests/unit/conductor-agent-card.test.ts index 079efff9e8..d078493a82 100644 --- a/tests/unit/conductor-agent-card.test.ts +++ b/tests/unit/conductor-agent-card.test.ts @@ -2,9 +2,22 @@ import test from "node:test"; import assert from "node:assert/strict"; import { createServer, type Server } from "node:http"; +import type { NextRequest } from "next/server"; + import { GET } from "../../src/app/.well-known/agent.json/route.ts"; import { clearFleetSkillsCache } from "../../src/lib/conductor/fleetSkills.ts"; +/** + * The agent-card routes derive their base URL from `request.nextUrl.origin` + * (S2 topology sanitisation, #11418), so the handler must be invoked with a + * request the way Next.js does — a bare `GET()` throws on `nextUrl`. + */ +function makeCardRequest(url = "https://gateway.example.com/.well-known/agent.json"): NextRequest { + const request = new Request(url) as unknown as NextRequest; + Object.defineProperty(request, "nextUrl", { value: new URL(url), configurable: true }); + return request; +} + const servers: Server[] = []; test.beforeEach(() => { @@ -22,7 +35,7 @@ test.after(async () => { }); test("sem CONDUCTOR_HUB_URL o card continua válido, com as skills estáticas e zero conductor-*", async () => { - const res = await GET(); + const res = await GET(makeCardRequest()); const card = await res.json(); assert.equal(typeof card.name, "string"); assert.ok(Array.isArray(card.skills) && card.skills.length >= 6, "skills estáticas presentes"); @@ -34,7 +47,11 @@ test("com hub de pé o card anuncia as skills da frota SEM perder as estáticas" res.writeHead(200, { "content-type": "application/json" }); res.end( JSON.stringify([ - { id: "r_1", online: true, capabilities: { name: "devbox", clis: [{ profile: "claude" }], skills: [] } }, + { + id: "r_1", + online: true, + capabilities: { name: "devbox", clis: [{ profile: "claude" }], skills: [] }, + }, ]) ); }); @@ -44,7 +61,7 @@ test("com hub de pé o card anuncia as skills da frota SEM perder as estáticas" process.env.CONDUCTOR_HUB_URL = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`; process.env.CONDUCTOR_HUB_TOKEN = "tok"; - const res = await GET(); + const res = await GET(makeCardRequest()); const card = await res.json(); const ids = card.skills.map((s: { id: string }) => s.id); assert.ok(ids.includes("conductor-cli-claude"), `frota anunciada (ids: ${ids.join(",")})`);