test(ack-id): add comprehensive A2A module test suite#69
test(ack-id): add comprehensive A2A module test suite#69ak68a wants to merge 2 commits intoagentcommercekit:mainfrom
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add test-fixtures.ts with shared identity constants, message builders, and mock factories - Replace inline message construction with typed builder functions - Use it.each for schema rejection cases - Derive expected signed payload from signedMessage() to prevent fixture drift - Add unsignedMessage() builder for explicit unsigned test cases - Add comments explaining why (contextId stripping, schema-first validation, nonce correlation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WalkthroughThis PR adds comprehensive Vitest test coverage for A2A (Agent-to-Agent) functionality across five new files, including unit tests for random generation, service endpoints, signing, verification, and a shared test-fixtures module that centralizes test setup and message builders. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/ack-id/src/a2a/verify.test.ts (1)
84-112: Enforce schema-first behavior with an explicitverifyJwtnon-call assertion.The table test currently checks rejection only; it should also prove malformed input is rejected before JWT verification.
Suggested update
])("rejects $name", async ({ message }) => { + vi.mocked(verifyJwt).mockClear() await expect( verifyA2AHandshakeMessage(message as never, { did: agentDid }), ).rejects.toThrow() + expect(verifyJwt).not.toHaveBeenCalled() })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ack-id/src/a2a/verify.test.ts` around lines 84 - 112, The tests in verify.test.ts currently only assert that malformed messages cause verifyA2AHandshakeMessage to reject, but they don’t assert that JWT verification (verifyJwt) is not invoked; update the table-driven test to mock or spy on the verifyJwt function used by verifyA2AHandshakeMessage and add a non-call assertion (e.g., expect(verifyJwt).not.toHaveBeenCalled()) after the await expect(...).rejects.toThrow() so the test proves schema validation occurs before any JWT verification. Make sure the mock/spy targets the same symbol imported/used by verifyA2AHandshakeMessage (verifyJwt) so the assertion reliably detects no calls.packages/ack-id/src/a2a/random.test.ts (1)
5-6: Consider asserting UUIDv4-specific bits in the regex.Current pattern validates generic UUID shape only. If
generateRandomJti/generateRandomNonceare expected to be UUIDv4, tightening this improves mutation resistance.Suggested tweak
-const uuidPattern = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ +const uuidPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ack-id/src/a2a/random.test.ts` around lines 5 - 6, The current uuidPattern only checks generic UUID shape; tighten it to assert UUIDv4 version and RFC-variant bits so tests for generateRandomJti and generateRandomNonce validate v4 UUIDs. Replace the pattern used in uuidPattern with one that enforces a '4' in the version position and one of [89ab] in the variant position (e.g. pattern matching ...-4...-[89ab]...-...), keeping case handling consistent with the rest of the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ack-id/src/a2a/service-endpoints.test.ts`:
- Around line 19-26: Rename the test title to use the approved assertive
pattern: change the it(...) description for the test that calls
createAgentCardServiceEndpoint with DID "did:web:example.com:agents:my-agent" to
an assertive form such as "returns an id with `#agent-card` for colon-separated
DID paths" (or another "creates/returns/throws/requires" phrasing); keep the
test body and expectations
(expect(endpoint.id).toBe("did:web:example.com:agents:my-agent#agent-card"))
unchanged so only the test name is updated.
In `@packages/ack-id/src/a2a/sign-message.test.ts`:
- Around line 45-151: Rename the test titles to use assertive verbs
(creates/returns/throws/requires) so they follow the project's naming
convention: update the it(...) strings in tests referencing
createA2AHandshakePayload ("echoes the request nonce..." → "returns the request
nonce and creates a new reply nonce"), createA2AHandshakeMessageFromJwt ("wraps
a JWT..." → "returns an A2A data-part message containing a JWT" and "respects
the role parameter" → "returns the provided role"), createSignedA2AMessage
("produces a JWT signature..." → "creates a JWT signature and attaches it to
message metadata", "preserves original message content..." → "returns original
message content alongside the signature", "merges the signature..." → "creates
merged metadata without clobbering existing fields"), and
createA2AHandshakeMessage ("signs a credential handshake and returns the
nonce..." → "creates a signed credential handshake and returns the nonce",
"embeds the signed JWT..." → "returns a message embedding the signed JWT"); keep
the test logic and assertions unchanged, only change the human-readable it(...)
description strings.
In `@packages/ack-id/src/a2a/verify.test.ts`:
- Around line 40-250: The tests use non-assertive "it(...)" titles; rename each
test title in this suite to use the mandated assertive prefixes (creates...,
throws..., requires..., or returns...) so they conform to the guideline.
Specifically update the it(...) descriptions in the verifyA2AHandshakeMessage
and verifyA2ASignedMessage blocks (tests referencing verifyA2AHandshakeMessage,
verifyA2ASignedMessage, mockValidSignature, handshakeMessage, signedMessage,
unsignedMessage) — e.g. change "extracts issuer, nonce, and credential from a
valid handshake" to "returns issuer, nonce, and credential from a valid
handshake", "verifies the JWT with audience=self and issuer=counterparty" to
"requires JWT verification with audience=self and issuer=counterparty" or
similar, and similarly convert "rejects ..." to "throws ..." where appropriate;
apply consistent pattern across all test titles.
---
Nitpick comments:
In `@packages/ack-id/src/a2a/random.test.ts`:
- Around line 5-6: The current uuidPattern only checks generic UUID shape;
tighten it to assert UUIDv4 version and RFC-variant bits so tests for
generateRandomJti and generateRandomNonce validate v4 UUIDs. Replace the pattern
used in uuidPattern with one that enforces a '4' in the version position and one
of [89ab] in the variant position (e.g. pattern matching
...-4...-[89ab]...-...), keeping case handling consistent with the rest of the
tests.
In `@packages/ack-id/src/a2a/verify.test.ts`:
- Around line 84-112: The tests in verify.test.ts currently only assert that
malformed messages cause verifyA2AHandshakeMessage to reject, but they don’t
assert that JWT verification (verifyJwt) is not invoked; update the table-driven
test to mock or spy on the verifyJwt function used by verifyA2AHandshakeMessage
and add a non-call assertion (e.g., expect(verifyJwt).not.toHaveBeenCalled())
after the await expect(...).rejects.toThrow() so the test proves schema
validation occurs before any JWT verification. Make sure the mock/spy targets
the same symbol imported/used by verifyA2AHandshakeMessage (verifyJwt) so the
assertion reliably detects no calls.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4865f10f-67ef-40dd-9209-a61e6396a477
📒 Files selected for processing (5)
packages/ack-id/src/a2a/random.test.tspackages/ack-id/src/a2a/service-endpoints.test.tspackages/ack-id/src/a2a/sign-message.test.tspackages/ack-id/src/a2a/test-fixtures.tspackages/ack-id/src/a2a/verify.test.ts
| 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") | ||
| }) |
There was a problem hiding this comment.
Rename test title to the approved assertive pattern.
Use creates..., throws..., requires..., or returns... for this case (e.g., returns an id with #agent-card for 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
Verify each finding against the current code and only fix it if needed.
In `@packages/ack-id/src/a2a/service-endpoints.test.ts` around lines 19 - 26,
Rename the test title to use the approved assertive pattern: change the it(...)
description for the test that calls createAgentCardServiceEndpoint with DID
"did:web:example.com:agents:my-agent" to an assertive form such as "returns an
id with `#agent-card` for colon-separated DID paths" (or another
"creates/returns/throws/requires" phrasing); keep the test body and expectations
(expect(endpoint.id).toBe("did:web:example.com:agents:my-agent#agent-card"))
unchanged so only the test name is updated.
| 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 } }), | ||
| ) | ||
| }) |
There was a problem hiding this comment.
Normalize it(...) titles to the required assertive verbs.
Several cases use verbs outside the allowed naming pattern; please rename to creates..., throws..., requires..., or returns....
As per coding guidelines, **/*.test.ts: Use assertive test names with patterns: "it("creates...")" , "it("throws...")" , "it("requires...")" , "it("returns...")".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ack-id/src/a2a/sign-message.test.ts` around lines 45 - 151, Rename
the test titles to use assertive verbs (creates/returns/throws/requires) so they
follow the project's naming convention: update the it(...) strings in tests
referencing createA2AHandshakePayload ("echoes the request nonce..." → "returns
the request nonce and creates a new reply nonce"),
createA2AHandshakeMessageFromJwt ("wraps a JWT..." → "returns an A2A data-part
message containing a JWT" and "respects the role parameter" → "returns the
provided role"), createSignedA2AMessage ("produces a JWT signature..." →
"creates a JWT signature and attaches it to message metadata", "preserves
original message content..." → "returns original message content alongside the
signature", "merges the signature..." → "creates merged metadata without
clobbering existing fields"), and createA2AHandshakeMessage ("signs a credential
handshake and returns the nonce..." → "creates a signed credential handshake and
returns the nonce", "embeds the signed JWT..." → "returns a message embedding
the signed JWT"); keep the test logic and assertions unchanged, only change the
human-readable it(...) description strings.
| it("extracts issuer, nonce, and credential from a valid handshake", async () => { | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt({ | ||
| iss: userDid, | ||
| nonce: "challenge-nonce", | ||
| vc: testCredential, | ||
| }), | ||
| ) | ||
|
|
||
| const result = await verifyA2AHandshakeMessage(handshakeMessage(), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }) | ||
|
|
||
| expect(result.iss).toBe(userDid) | ||
| expect(result.nonce).toBe("challenge-nonce") | ||
| expect(result.vc).toEqual(testCredential) | ||
| }) | ||
|
|
||
| it("verifies the JWT with audience=self and issuer=counterparty", async () => { | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt({ | ||
| iss: userDid, | ||
| nonce: "n", | ||
| vc: testCredential, | ||
| }), | ||
| ) | ||
|
|
||
| await verifyA2AHandshakeMessage(handshakeMessage("the.jwt"), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }) | ||
|
|
||
| expect(verifyJwt).toHaveBeenCalledWith("the.jwt", { | ||
| audience: agentDid, | ||
| issuer: userDid, | ||
| resolver: expect.anything(), | ||
| }) | ||
| }) | ||
|
|
||
| // The message schema uses valibot's v.parse(), which rejects structurally | ||
| // invalid input before any JWT verification happens. This matters because | ||
| // a malformed message should fail fast, not trigger a network call to | ||
| // resolve a DID. | ||
| it.each([ | ||
| { name: "null message", message: null }, | ||
| { | ||
| name: "empty parts array", | ||
| message: { kind: "message", messageId: "m", role: "user", parts: [] }, | ||
| }, | ||
| { | ||
| name: "text part instead of data part", | ||
| message: { | ||
| kind: "message", | ||
| messageId: "m", | ||
| role: "user", | ||
| parts: [{ kind: "text", text: "x" }], | ||
| }, | ||
| }, | ||
| { | ||
| name: "data part without jwt field", | ||
| message: { | ||
| kind: "message", | ||
| messageId: "m", | ||
| role: "user", | ||
| parts: [{ kind: "data", data: { notJwt: "x" } }], | ||
| }, | ||
| }, | ||
| ])("rejects $name", async ({ message }) => { | ||
| await expect( | ||
| verifyA2AHandshakeMessage(message as never, { did: agentDid }), | ||
| ).rejects.toThrow() | ||
| }) | ||
|
|
||
| it("rejects when JWT verification fails", async () => { | ||
| vi.mocked(verifyJwt).mockRejectedValueOnce( | ||
| new Error("JWT verification failed"), | ||
| ) | ||
|
|
||
| await expect( | ||
| verifyA2AHandshakeMessage(handshakeMessage(), { did: agentDid }), | ||
| ).rejects.toThrow("JWT verification failed") | ||
| }) | ||
|
|
||
| it("rejects when the verified payload is missing required handshake fields", async () => { | ||
| // JWT is valid but payload lacks nonce and vc — not a proper handshake | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt({ iss: userDid }), | ||
| ) | ||
|
|
||
| await expect( | ||
| verifyA2AHandshakeMessage(handshakeMessage(), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }), | ||
| ).rejects.toThrow() | ||
| }) | ||
|
|
||
| it("rejects when issuer is not a valid DID URI", async () => { | ||
| // The handshake payload schema requires iss to be a did: URI. | ||
| // A compromised or misconfigured peer might send a plain string. | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt({ | ||
| iss: "not-a-did", | ||
| nonce: "n", | ||
| vc: testCredential, | ||
| }), | ||
| ) | ||
|
|
||
| await expect( | ||
| verifyA2AHandshakeMessage(handshakeMessage(), { did: agentDid }), | ||
| ).rejects.toThrow() | ||
| }) | ||
| }) | ||
|
|
||
| // --- Signed message verification --- | ||
|
|
||
| describe("verifyA2ASignedMessage", () => { | ||
| function mockValidSignature() { | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt(expectedSignedPayload()), | ||
| ) | ||
| } | ||
|
|
||
| it("verifies that a message's content matches its JWT signature", async () => { | ||
| mockValidSignature() | ||
|
|
||
| const result = await verifyA2ASignedMessage(signedMessage(), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }) | ||
|
|
||
| expect(result.verified).toBe(true) | ||
| }) | ||
|
|
||
| it("verifies the signature JWT with audience=self and issuer=counterparty", async () => { | ||
| mockValidSignature() | ||
|
|
||
| await verifyA2ASignedMessage(signedMessage("hello", "the.sig"), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }) | ||
|
|
||
| expect(verifyJwt).toHaveBeenCalledWith("the.sig", { | ||
| audience: agentDid, | ||
| issuer: userDid, | ||
| resolver: expect.anything(), | ||
| }) | ||
| }) | ||
|
|
||
| it("rejects unsigned messages (no metadata at all)", async () => { | ||
| await expect( | ||
| verifyA2ASignedMessage(unsignedMessage(), { did: agentDid }), | ||
| ).rejects.toThrow() | ||
| }) | ||
|
|
||
| it("rejects messages with metadata but no sig field", async () => { | ||
| const noSig = { ...unsignedMessage(), metadata: { traceId: "abc" } } | ||
|
|
||
| await expect( | ||
| verifyA2ASignedMessage(noSig as never, { did: agentDid }), | ||
| ).rejects.toThrow() | ||
| }) | ||
|
|
||
| it("detects tampering: rejects when message content diverges from signed payload", async () => { | ||
| // Signature covers "original content" but the message body says "tampered" | ||
| vi.mocked(verifyJwt).mockResolvedValueOnce( | ||
| mockVerifiedJwt({ | ||
| message: { | ||
| kind: "message", | ||
| messageId: "msg-1", | ||
| role: "user", | ||
| parts: [{ kind: "text", text: "original content" }], | ||
| }, | ||
| }), | ||
| ) | ||
|
|
||
| await expect( | ||
| verifyA2ASignedMessage(signedMessage("tampered"), { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }), | ||
| ).rejects.toThrow("Message parts do not match") | ||
| }) | ||
|
|
||
| it("rejects when the underlying JWT signature is invalid", async () => { | ||
| vi.mocked(verifyJwt).mockRejectedValueOnce(new Error("Signature invalid")) | ||
|
|
||
| await expect( | ||
| verifyA2ASignedMessage(signedMessage(), { did: agentDid }), | ||
| ).rejects.toThrow("Signature invalid") | ||
| }) | ||
|
|
||
| it("ignores server-injected contextId when comparing content", async () => { | ||
| // A2A servers may auto-assign a contextId after the client signs the | ||
| // message. The verification must strip it before comparing, otherwise | ||
| // every message routed through a server would fail validation. | ||
| mockValidSignature() | ||
|
|
||
| const messageWithContextId = { | ||
| ...signedMessage(), | ||
| contextId: "ctx-server-assigned", | ||
| } | ||
|
|
||
| const result = await verifyA2ASignedMessage(messageWithContextId, { | ||
| did: agentDid, | ||
| counterparty: userDid, | ||
| }) | ||
|
|
||
| expect(result.verified).toBe(true) | ||
| }) |
There was a problem hiding this comment.
Rename tests to match the required assertive naming convention.
Please standardize it(...) titles to creates..., throws..., requires..., or returns... across this suite.
As per coding guidelines, **/*.test.ts: Use assertive test names with patterns: "it("creates...")" , "it("throws...")" , "it("requires...")" , "it("returns...")".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ack-id/src/a2a/verify.test.ts` around lines 40 - 250, The tests use
non-assertive "it(...)" titles; rename each test title in this suite to use the
mandated assertive prefixes (creates..., throws..., requires..., or returns...)
so they conform to the guideline. Specifically update the it(...) descriptions
in the verifyA2AHandshakeMessage and verifyA2ASignedMessage blocks (tests
referencing verifyA2AHandshakeMessage, verifyA2ASignedMessage,
mockValidSignature, handshakeMessage, signedMessage, unsignedMessage) — e.g.
change "extracts issuer, nonce, and credential from a valid handshake" to
"returns issuer, nonce, and credential from a valid handshake", "verifies the
JWT with audience=self and issuer=counterparty" to "requires JWT verification
with audience=self and issuer=counterparty" or similar, and similarly convert
"rejects ..." to "throws ..." where appropriate; apply consistent pattern across
all test titles.
Summary
packages/ack-id/src/a2a/module which previously had zero test coveragerandom.ts,service-endpoints.ts,sign-message.ts,verify.tsWhat's tested
Handshake signing — payload construction with nonce correlation for challenge-response flows, JWT wrapping into A2A data-part messages, metadata merge behavior (signature doesn't mess up existing metadata)
Handshake verification — JWT audience/issuer delegation to verifyJwt, schema rejection of malformed messages before any crypto/network calls, payload validation (missing fields, invalid DID URIs)
Signed message verification — content integrity check (signed payload must match message body), rejection of unsigned and improperly signed messages, contextId stripping (A2A servers inject this post-signing, verification must ignore it)
Utilities — UUID format and uniqueness for JTI/nonce generation, service endpoint construction with DID fragment assembly
Test plan
pnpm --filter ./packages/ack-id test— 39 tests passpnpm run check— format, types, lint all cleanSummary by CodeRabbit