-
Notifications
You must be signed in to change notification settings - Fork 100
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
Open
ak68a
wants to merge
3
commits into
agentcommercekit:main
Choose a base branch
from
ak68a:test/a2a-module
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 } }), | ||
| ) | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.