Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/ack-id/src/a2a/random.test.ts
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())
})
})
27 changes: 27 additions & 0 deletions packages/ack-id/src/a2a/service-endpoints.test.ts
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")
})
Comment on lines +19 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

})
152 changes: 152 additions & 0 deletions packages/ack-id/src/a2a/sign-message.test.ts
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

})
101 changes: 101 additions & 0 deletions packages/ack-id/src/a2a/test-fixtures.ts
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",
}
}
Loading