-
Notifications
You must be signed in to change notification settings - Fork 99
test(ack-id): add comprehensive A2A module test suite #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { describe, expect, it } from "vitest" | ||
|
|
||
| import { generateRandomJti, generateRandomNonce } from "./random" | ||
|
|
||
| const uuidPattern = | ||
| /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ | ||
|
|
||
| describe("generateRandomJti", () => { | ||
| it("returns a UUID", () => { | ||
| expect(generateRandomJti()).toMatch(uuidPattern) | ||
| }) | ||
|
|
||
| it("returns unique values", () => { | ||
| expect(generateRandomJti()).not.toBe(generateRandomJti()) | ||
| }) | ||
| }) | ||
|
|
||
| describe("generateRandomNonce", () => { | ||
| it("returns a UUID", () => { | ||
| expect(generateRandomNonce()).toMatch(uuidPattern) | ||
| }) | ||
|
|
||
| it("returns unique values", () => { | ||
| expect(generateRandomNonce()).not.toBe(generateRandomNonce()) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { describe, expect, it } from "vitest" | ||
|
|
||
| import { createAgentCardServiceEndpoint } from "./service-endpoints" | ||
|
|
||
| describe("createAgentCardServiceEndpoint", () => { | ||
| it("creates a service endpoint linking a DID to its agent card URL", () => { | ||
| const endpoint = createAgentCardServiceEndpoint( | ||
| "did:web:example.com", | ||
| "https://example.com/.well-known/agent.json", | ||
| ) | ||
|
|
||
| expect(endpoint).toEqual({ | ||
| id: "did:web:example.com#agent-card", | ||
| type: "AgentCard", | ||
| serviceEndpoint: "https://example.com/.well-known/agent.json", | ||
| }) | ||
| }) | ||
|
|
||
| it("handles DIDs with colon-separated path components", () => { | ||
| const endpoint = createAgentCardServiceEndpoint( | ||
| "did:web:example.com:agents:my-agent", | ||
| "https://example.com/agents/my-agent/agent.json", | ||
| ) | ||
|
|
||
| expect(endpoint.id).toBe("did:web:example.com:agents:my-agent#agent-card") | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import type { JwtSigner } from "@agentcommercekit/jwt" | ||
| import { createJwtSigner } from "@agentcommercekit/jwt" | ||
| import { generateKeypair } from "@agentcommercekit/keys" | ||
| import { beforeEach, describe, expect, it, vi } from "vitest" | ||
|
|
||
| import { | ||
| createA2AHandshakeMessage, | ||
| createA2AHandshakeMessageFromJwt, | ||
| createA2AHandshakePayload, | ||
| createSignedA2AMessage, | ||
| } from "./sign-message" | ||
| import { | ||
| agentDid, | ||
| makeTextMessage, | ||
| testCredential, | ||
| userDid, | ||
| } from "./test-fixtures" | ||
|
|
||
| vi.mock("uuid", () => ({ | ||
| v4: vi.fn(() => "test-uuid-1234"), | ||
| })) | ||
|
|
||
| vi.mock("./random", async () => { | ||
| const actual = await vi.importActual<typeof import("./random")>("./random") | ||
| return { | ||
| ...actual, | ||
| generateRandomJti: vi.fn(() => "test-jti-1234"), | ||
| generateRandomNonce: vi.fn(() => "test-nonce-1234"), | ||
| } | ||
| }) | ||
|
|
||
| describe("createA2AHandshakePayload", () => { | ||
| it("creates a payload addressed to the recipient with a fresh nonce", () => { | ||
| const payload = createA2AHandshakePayload({ | ||
| recipient: userDid, | ||
| vc: testCredential, | ||
| }) | ||
|
|
||
| expect(payload.aud).toBe(userDid) | ||
| expect(payload.nonce).toBe("test-nonce-1234") | ||
| expect(payload.vc).toBe(testCredential) | ||
| expect(payload).not.toHaveProperty("replyNonce") | ||
| }) | ||
|
|
||
| it("echoes the request nonce and generates a new reply nonce for responses", () => { | ||
| const payload = createA2AHandshakePayload({ | ||
| recipient: userDid, | ||
| vc: testCredential, | ||
| requestNonce: "original-nonce", | ||
| }) | ||
|
|
||
| // The initiator's nonce becomes ours so they can correlate the reply | ||
| expect(payload.nonce).toBe("original-nonce") | ||
| // We generate a fresh nonce for the next leg of the handshake | ||
| expect(payload.replyNonce).toBe("test-nonce-1234") | ||
| }) | ||
| }) | ||
|
|
||
| describe("createA2AHandshakeMessageFromJwt", () => { | ||
| it("wraps a JWT in an A2A data-part message", () => { | ||
| const jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.test.sig" | ||
|
|
||
| expect(createA2AHandshakeMessageFromJwt("agent", jwt)).toEqual({ | ||
| kind: "message", | ||
| messageId: "test-uuid-1234", | ||
| role: "agent", | ||
| parts: [{ kind: "data", data: { jwt } }], | ||
| }) | ||
| }) | ||
|
|
||
| it("respects the role parameter", () => { | ||
| const message = createA2AHandshakeMessageFromJwt("user", "any.jwt") | ||
| expect(message.role).toBe("user") | ||
| }) | ||
| }) | ||
|
|
||
| describe("createSignedA2AMessage", () => { | ||
| let jwtSigner: JwtSigner | ||
|
|
||
| beforeEach(async () => { | ||
| const keypair = await generateKeypair("secp256k1") | ||
| jwtSigner = createJwtSigner(keypair) | ||
| }) | ||
|
|
||
| it("produces a JWT signature and attaches it to message metadata", async () => { | ||
| const result = await createSignedA2AMessage(makeTextMessage(), { | ||
| did: agentDid, | ||
| jwtSigner, | ||
| }) | ||
|
|
||
| expect(result.sig).toEqual(expect.any(String)) | ||
| expect(result.jti).toBe("test-jti-1234") | ||
| expect(result.message.metadata?.sig).toBe(result.sig) | ||
| }) | ||
|
|
||
| it("preserves original message content alongside the signature", async () => { | ||
| const original = makeTextMessage("agent") | ||
| const result = await createSignedA2AMessage(original, { | ||
| did: agentDid, | ||
| jwtSigner, | ||
| }) | ||
|
|
||
| expect(result.message.kind).toBe("message") | ||
| expect(result.message.role).toBe("agent") | ||
| expect(result.message.parts).toEqual(original.parts) | ||
| }) | ||
|
|
||
| it("merges the signature into existing metadata without clobbering", async () => { | ||
| const message = makeTextMessage("user", { traceId: "abc" }) | ||
| const result = await createSignedA2AMessage(message, { | ||
| did: agentDid, | ||
| jwtSigner, | ||
| }) | ||
|
|
||
| expect(result.message.metadata?.sig).toBe(result.sig) | ||
| expect(result.message.metadata?.traceId).toBe("abc") | ||
| }) | ||
| }) | ||
|
|
||
| describe("createA2AHandshakeMessage", () => { | ||
| let jwtSigner: JwtSigner | ||
|
|
||
| beforeEach(async () => { | ||
| const keypair = await generateKeypair("secp256k1") | ||
| jwtSigner = createJwtSigner(keypair) | ||
| }) | ||
|
|
||
| it("signs a credential handshake and returns the nonce for correlation", async () => { | ||
| const result = await createA2AHandshakeMessage( | ||
| "agent", | ||
| { recipient: userDid, vc: testCredential }, | ||
| { did: agentDid, jwtSigner }, | ||
| ) | ||
|
|
||
| expect(result.sig).toEqual(expect.any(String)) | ||
| expect(result.jti).toBe("test-jti-1234") | ||
| expect(result.nonce).toBe("test-nonce-1234") | ||
| expect(result.message.role).toBe("agent") | ||
| }) | ||
|
|
||
| it("embeds the signed JWT in the message data part", async () => { | ||
| const result = await createA2AHandshakeMessage( | ||
| "agent", | ||
| { recipient: userDid, vc: testCredential }, | ||
| { did: agentDid, jwtSigner }, | ||
| ) | ||
|
|
||
| expect(result.message.parts[0]).toEqual( | ||
| expect.objectContaining({ kind: "data", data: { jwt: result.sig } }), | ||
| ) | ||
| }) | ||
|
Comment on lines
+45
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize Several cases use verbs outside the allowed naming pattern; please rename to As per coding guidelines, 🤖 Prompt for AI Agents |
||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /** | ||
| * Shared fixtures for A2A test suites. | ||
| * | ||
| * Provides identity constants, message builders, and mock factories so | ||
| * individual test files can focus on behavior rather than setup. | ||
| */ | ||
| import type { DidUri } from "@agentcommercekit/did" | ||
| import type { JwtVerified } from "@agentcommercekit/jwt" | ||
| import type { W3CCredential } from "@agentcommercekit/vc" | ||
|
|
||
| // --- Identity constants --- | ||
|
|
||
| export const agentDid = "did:web:agent.example.com" as DidUri | ||
| export const userDid = "did:web:user.example.com" as DidUri | ||
|
|
||
| export const testCredential: W3CCredential = { | ||
| "@context": ["https://www.w3.org/2018/credentials/v1"], | ||
| type: ["VerifiableCredential", "ControllerCredential"], | ||
| issuer: { id: "did:web:issuer.example.com" }, | ||
| issuanceDate: "2025-01-01T00:00:00.000Z", | ||
| credentialSubject: { id: "did:web:subject.example.com" }, | ||
| } | ||
|
|
||
| // --- Message builders --- | ||
|
|
||
| /** A text message, optionally with a specific role or pre-existing metadata. */ | ||
| export function makeTextMessage( | ||
| role: "agent" | "user" = "user", | ||
| metadata?: Record<string, unknown>, | ||
| ) { | ||
| return { | ||
| kind: "message" as const, | ||
| messageId: "msg-1", | ||
| role, | ||
| parts: [{ kind: "text" as const, text: "hello" }], | ||
| ...(metadata && { metadata }), | ||
| } | ||
| } | ||
|
|
||
| /** A handshake message carrying a JWT in its data part. */ | ||
| export function handshakeMessage(jwt = "valid.jwt.token") { | ||
| return { | ||
| kind: "message" as const, | ||
| messageId: "msg-1", | ||
| role: "user" as const, | ||
| parts: [{ kind: "data" as const, data: { jwt } }], | ||
| } | ||
| } | ||
|
|
||
| /** A signed message with text content and a signature in metadata. */ | ||
| export function signedMessage(text = "hello", sig = "valid.jwt.signature") { | ||
| return { | ||
| kind: "message" as const, | ||
| messageId: "msg-1", | ||
| role: "user" as const, | ||
| parts: [{ kind: "text" as const, text }], | ||
| metadata: { sig }, | ||
| } | ||
| } | ||
|
|
||
| /** A message with no signature — for testing rejection of unsigned input. */ | ||
| export function unsignedMessage(text = "hello") { | ||
| return { | ||
| kind: "message" as const, | ||
| messageId: "msg-1", | ||
| role: "user" as const, | ||
| parts: [{ kind: "text" as const, text }], | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The expected JWT payload for a signed message with the given text. | ||
| * Derives from the same shape as signedMessage() so they can't drift apart. | ||
| */ | ||
| export function expectedSignedPayload(text = "hello") { | ||
| const { metadata: _, ...content } = signedMessage(text) | ||
| return { message: content } | ||
| } | ||
|
|
||
| // --- Mock factories --- | ||
|
|
||
| /** Builds a JwtVerified result with sensible defaults, overriding only the payload. */ | ||
| export function mockVerifiedJwt(payload: Record<string, unknown>): JwtVerified { | ||
| return { | ||
| verified: true, | ||
| payload: { iss: "did:web:issuer.example.com", ...payload }, | ||
| didResolutionResult: { | ||
| didResolutionMetadata: {}, | ||
| didDocument: null, | ||
| didDocumentMetadata: {}, | ||
| }, | ||
| issuer: "did:web:issuer.example.com", | ||
| signer: { | ||
| id: "did:web:issuer.example.com#key-1", | ||
| type: "Multikey", | ||
| controller: "did:web:issuer.example.com", | ||
| publicKeyHex: "02...", | ||
| }, | ||
| jwt: "mock.jwt.token", | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename test title to the approved assertive pattern.
Use
creates...,throws...,requires..., orreturns...for this case (e.g.,returns an id with#agent-cardfor colon-separated DID paths).As per coding guidelines,
**/*.test.ts: Use assertive test names with patterns: "it("creates...")" , "it("throws...")" , "it("requires...")" , "it("returns...")".🤖 Prompt for AI Agents