Skip to content

TypeScript SDK Testing Strategy

RunTimeAdmin edited this page Nov 3, 2026 · 23 revisions

TypeScript SDK Testing Strategy

**Referenced Files in This Document** - [packages/sdk/package.json](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/package.json) - [packages/sdk/vitest.config.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/vitest.config.ts) - [packages/sdk/src/index.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/index.ts) - [packages/sdk/src/agent.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/agent.ts) - [packages/sdk/src/challenge.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/challenge.ts) - [packages/sdk/src/verifier.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/src/verifier.ts) - [packages/sdk/test/agent.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/agent.test.ts) - [packages/sdk/test/challenge.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/challenge.test.ts) - [packages/sdk/test/verifier.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/verifier.test.ts) - [packages/sdk/test/integration.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/integration.test.ts) - [packages/sdk/test/did.test.ts](https://github.com/RunTimeAdmin/Sigvara/blob/main/packages/sdk/test/did.test.ts)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion

Introduction

This document explains the testing strategy for the Sigvara TypeScript SDK using Vitest. It covers unit tests for agent operations, cryptographic challenge-response flows, and verifier utilities; mocking strategies for blockchain interactions; integration test patterns for end-to-end workflows; fixtures and environment configuration; and guidance for edge cases, error handling, and performance validation. The goal is to make the testing approach clear and reproducible for both new and experienced contributors.

Project Structure

The SDK lives under packages/sdk with a focused set of source modules and corresponding test files:

  • Source modules include agent identity, challenge generation/signing/verification, DID helpers, and a verifier that interacts with on-chain contracts.
  • Tests are organized by feature: agent behavior, challenge protocol, verifier pre-chain guards, DID utilities, and an integration suite that runs against a live EVM when environment variables are provided.

The suite is 300 tests in 16 files, run with npx vitest run in packages/sdk. Eight of them are the integration tests, which skip without a live deployment. Beside the five files in the diagram below, gate.test.ts covers the gate, verifier-reads.test.ts the verifier's reads (including getScoreUpdatedAt, which reads when the score was written and not the total) and verifySignature against a registered key, rpc-timeout.test.ts the read timeout, register-agent.test.ts, stake.test.ts and registration-signature.test.ts the registration and bonding helpers, keys.test.ts the base58, hex and key conversions, outcome.test.ts the payer-signed outcome against the vectors the oracle and test/PaymentOutcome.t.sol share, mcp.test.ts the trust server, including its answer matching SigvaraGate's over 96 chain states, evidence.test.ts the evidence-root functions against the contract test's fixture, and built-package.test.ts the compiled package: it loads dist/ the way a consumer does and looks for the agent's secret key in JSON.stringify, spread and Object.assign copies, a structured clone, util.inspect with hidden properties, util.format and an error message. TypeScript's private keyword is erased when the package is built, so 1.0.0-alpha.9 shipped with the key in an ordinary property while the tests of the source passed. prepublishOnly now builds the package and runs the tests, so a release cannot go out without this one. It holds the audience cases too: a JavaScript call to verifySignature without the expected audience throws, which the compiler no longer lets a TypeScript caller write. It holds the removal of the agent's signChallenge too: the compiled method throws and names signChallengeFor, and signChallengeFor signs for the verifier it means and refuses to sign for another. outcome.test.ts (from 1.0.0-alpha.13) checks signPaymentOutcome and verifyPaymentOutcome against the repository's test/vectors/payment-outcome.json, the cases the oracle and a Foundry test are held to as well.

graph TB
subgraph "SDK Package"
A["src/index.ts"]
B["src/agent.ts"]
C["src/challenge.ts"]
D["src/verifier.ts"]
E["src/did.ts"]
T1["test/agent.test.ts"]
T2["test/challenge.test.ts"]
T3["test/verifier.test.ts"]
T4["test/integration.test.ts"]
T5["test/did.test.ts"]
end
A --> B
A --> C
A --> D
T1 --> B
T1 --> C
T2 --> C
T3 --> D
T4 --> A
T5 --> E
Loading

Diagram sources

Section sources

Core Components

  • SigvaraAgent: Encapsulates Ed25519 key material, DID derivation, challenge issuance, and signing. Provides a static factory to generate agents without exposing low-level crypto directly.
  • Challenge module: Implements v1 and v2 challenge payloads, signature creation/verification, parsing, and expiration checks. v2 binds the audience to prevent relay attacks.
  • SigvaraVerifier: Interacts with Identity, Reputation, and Staking contracts via ethers. Validates signatures, enforces pre-chain guards (payload format, DID binding, audience binding, TTL, signature width), and builds W3C-compliant DID Documents.
  • Integration harness: Optional end-to-end tests that register an agent and exercise identity, reputation, and challenge-response flows against a live network when environment variables are present. Registration moves no tokens, so the new agent waits in PendingBond until it is bonded, and the harness expects that.
  • Type checking: Vitest strips types without checking them, so npm run typecheck checks the tests against the SDK's types as well as its sources. CI runs it between the build and the tests.

Key responsibilities and relationships:

  • Agent uses challenge and keys utilities to issue and sign challenges.
  • Verifier composes challenge verification with on-chain reads to resolve public keys and reputation data.
  • Index re-exports core APIs for consumers and tests.

Section sources

Architecture Overview

The SDK’s testing architecture separates concerns into unit tests for pure logic and optional integration tests for on-chain behavior.

sequenceDiagram
participant Test as "Vitest Test"
participant Agent as "SigvaraAgent"
participant Challenge as "challenge.ts"
participant Verifier as "SigvaraVerifier"
participant Chain as "EVM Contracts"
Test->>Agent : issueChallenge(peerDid)
Agent->>Challenge : generateChallenge(proverDid, audience, ttl)
Challenge-->>Agent : { payload, nonce, timestamp }
Test->>Agent : signChallengeFor(payload, audience)
Agent->>Challenge : signChallenge(payload, secretKey)
Challenge-->>Agent : base58(signature)
Test->>Verifier : verifySignature(did, payload, signature, maxAge, audience)
Verifier->>Verifier : parseChallengePayload()
Verifier->>Verifier : isChallengeExpired()
Verifier->>Chain : getIdentity(didHash)
Chain-->>Verifier : { ed25519PubKey, status }
Verifier->>Challenge : verifyChallenge(payload, signature, publicKey)
Challenge-->>Verifier : boolean
Verifier-->>Test : boolean
Loading

Diagram sources

Detailed Component Analysis

Agent Operations Testing

  • Identity assertions: Tests validate DID derivation, didHash computation, and public key bytes32 formatting.
  • Challenge issuance: Ensures peer DID inclusion and unique nonces per issued challenge.
  • A2A sign/verify flow: Demonstrates full round-trip where one agent issues a challenge to another, the target signs it, and the signature verifies against the correct public key. Also validates failure paths with wrong keys and freshness checks.
  • Static factory: Confirms deterministic keypair reconstruction, uniqueness across calls, and successful challenge signing from generated agents.
  • signChallengeFor: Signs a v2 challenge addressed to the audience it expects, and the signature verifies. It refuses, and signs nothing, for a different audience (a difference of case or a trailing slash counts), a different prover, a v1 challenge, or something that is not a challenge. It accepts the prover DID however the address is cased.
  • Secret key: A test checks that the key is not in the agent's JSON, its console output or its enumerable properties, and that signing still works through the private field.

Mocking strategy:

  • Uses tweetnacl directly within tests to construct deterministic or random keypairs for predictable outcomes.
  • No blockchain dependencies are required for these unit tests.
flowchart TD
Start(["Agent Unit Tests"]) --> MakeAgent["Create SigvaraAgent with seed/keypair"]
MakeAgent --> Issue["issueChallenge(peerDid)"]
Issue --> Sign["signChallengeFor(payload, audience)"]
Sign --> Verify["verifyChallenge(payload, signature, publicKey)"]
Verify --> CheckFreshness{"isChallengeExpired(payload)?"}
CheckFreshness --> |No| Pass["Pass"]
CheckFreshness --> |Yes| Fail["Fail"]
Loading

Diagram sources

Section sources

Cryptographic Functions and Challenge Protocol

  • Payload structure: v2 uses newline-delimited fields including prover DID, audience, nonce, and timestamp; v1 remains supported for backward compatibility.
  • Signature encoding: Base58-encoded Ed25519 signatures; length and alphabet constraints are enforced.
  • Audience binding: Prevents relay attacks by ensuring the signed payload names the intended verifier or agent.
  • Expiration: Parses timestamps and treats malformed payloads as expired to fail safely.

Testing highlights:

  • Valid signature verification and rejection with wrong keys or tampered payloads.
  • Audience binding scenarios, including DID audiences and colon-bearing URLs.
  • Backward compatibility with v1 payloads and safe handling of unparseable inputs.
  • A timestamp more than 60 seconds ahead of the clock is not fresh, and one within that allowance is.
flowchart TD
Gen["generateChallenge(proverDid, audience, ttl)"] --> Validate["Validate audience and no newlines"]
Validate --> Build["Build newline-delimited payload"]
Build --> Sign["signChallenge(payload, secretKey)"]
Sign --> Verify["verifyChallenge(payload, signature, publicKey)"]
Verify --> Parse["parseChallengePayload(payload)"]
Parse --> Expire["isChallengeExpired(payload, maxAgeSeconds)"]
Loading

Diagram sources

Section sources

Verifier Utilities and Pre-Chain Guards

  • Pre-chain validation: Rejects malformed payloads, mismatched prover DID, expired challenges, and out-of-range signature lengths before any RPC call.
  • On-chain resolution: Reads identity to obtain Ed25519 public key and status, then performs cryptographic verification.
  • Error handling: Converts invalid input errors (e.g., bad base58 alphabet) into false results rather than throwing, preventing gate crashes.

Testing highlights:

  • Short-circuit behavior verified by pointing at an unreachable RPC; failures occur before chain access.
  • Width guard boundaries tested with minimal and maximal base58 lengths.
  • Audience binding enforcement: a call without expectedAudience throws, and a payload for another audience or a v1 payload is refused.

verifier-reads.test.ts stubs the contract handles on the verifier instead of the RPC layer, because what it tests is which didHash the verifier asks about and how it turns the contracts' answers into SDK types. It also runs verifySignature against a registered key: a signature by that key is accepted, and any other key, a different payload, an unregistered agent, a zero key, a different audience and a v1 payload are refused. A non-base58 signature of legal width returns false instead of throwing. A payload dated in the future is refused however well it is signed, and one a little ahead of the clock is accepted.

sequenceDiagram
participant Test as "verifier.test.ts"
participant V as "SigvaraVerifier.verifySignature"
participant P as "parseChallengePayload"
participant E as "isChallengeExpired"
participant I as "getIdentity"
participant C as "verifyChallenge"
Test->>V : verifySignature(did, payload, sig, maxAge, audience)
V->>P : parse(payload)
P-->>V : parsed
V->>E : isChallengeExpired(payload)
E-->>V : boolean
V->>I : getIdentity(did)
I-->>V : identity
V->>C : verifyChallenge(payload, sig, publicKey)
C-->>V : boolean
V-->>Test : boolean
Loading

Diagram sources

Section sources

DID Utilities

  • Formatting and parsing: Ensures canonical DID strings and robust parsing with meaningful errors.
  • Hashing: Deterministic bytes32 hashing dependent on address and chain ID.

Tests cover:

  • Lowercasing normalization.
  • Invalid format exceptions.
  • Determinism and cross-chain differentiation.

Section sources

Gate, Registration and Timeout Tests

  • Gate: gate.test.ts stubs the verifier's methods, because the gate's logic is the order it asks questions in and the reason it gives when it refuses. It checks that each refusal reason comes back for the right cause (stale_score only when a limit is set), that an inactive agent is refused on standing and not on score, that an agent whose bond is under the minimum in force now is refused on standing even at a threshold of zero, that a score older than maxScoreAgeSeconds, or never written, is refused as stale_score after standing and before the number, that a gate without the limit makes no extra read, that a bad proof reads neither the status nor the score, that a failed proof does not burn the nonce, that the nonce and the expiry come from the signed payload and not the wrapper, that two concurrent admits of one nonce succeed once, that a store's atomic consume() is preferred, that two gates sharing a store with consume() admit a response once between them (also when both receive it at the same moment) while two gates on their own default stores, or on a store with only seen() and add() hit at the same moment, admit it twice, that a store that cannot be reached makes admit throw before the agent is read, and how pruning behaves. It also checks that the status, the bond and the score (and the write time, when a limit is set) are requested together, and that a threshold, ttlSeconds or maxScoreAgeSeconds that is not a usable number throws at construction. It checks the fleet settings too: two gates sharing an atomicNonceStore admit a response once, instances: 'multiple' without a consume() and an unknown instances value both throw, and atomicNonceStore prefixes the key, passes the expiry and refuses to run seen() and add(). With profile: 'production', a missing instances or maxScoreAgeSeconds throws, and the message names both when both are missing; any other profile value is refused; 'multiple' still needs a consume(); and the gate admits a response once across a fleet sharing an atomic store and refuses a stale score.
  • Registration and bonding: register-agent.test.ts and stake.test.ts replace ethers.Contract with a fake that records calls. registerAgent signs the contract's digest once as raw bytes, or passes a ready signature straight through, and finds the AgentRegistered log among others. depositStake approves only when the allowance is short and rejects a zero amount before touching the chain. registration-signature.test.ts shows that a signature made over the raw digest recovers to the agent address and a double-prefixed one does not.
  • Timeouts: rpc-timeout.test.ts reads the timeout back from the provider (30 seconds by default, the caller's timeoutMs on a verifier and through a gate, a positive number required) and checks that a read is abandoned against a local HTTP server that accepts the request and never answers.

Integration Test Patterns

  • Environment gating: Tests skip automatically unless all required environment variables are set.
  • Setup: Creates a verifier connected to a live RPC, registers an agent if not already registered, and derives DID and public key.
  • Scenarios:
    • Identity queries return active status and registration metadata.
    • Challenge-response authentication succeeds for valid signatures and fails for tampered payloads.
    • DID Document construction yields W3C-compliant structures.
    • Reputation checks return bounded scores and threshold evaluation.

Setup and teardown:

  • Uses beforeAll to initialize shared state and perform one-time registration.
  • Each scenario sets explicit timeouts appropriate for network latency.
flowchart TD
EnvCheck{"Env vars present?"}
EnvCheck --> |No| Skip["Skip integration suite"]
EnvCheck --> |Yes| Init["Init verifier + operator wallet"]
Init --> Register["registerAgent(operator, agentAddress, pubKey, identityAddr)"]
Register --> Run["Run identity, challenge-response, DID doc, reputation tests"]
Loading

Diagram sources

Section sources

Dependency Analysis

The SDK’s internal dependencies and their test coverage are summarized below.

graph LR
Index["src/index.ts"] --> Agent["src/agent.ts"]
Index --> Challenge["src/challenge.ts"]
Index --> Verifier["src/verifier.ts"]
Agent --> Challenge
Verifier --> Challenge
Verifier --> Did["did helpers (via index)"]
Verifier --> Abi["abis (ethers contracts)"]
T_Agent["test/agent.test.ts"] --> Agent
T_Challenge["test/challenge.test.ts"] --> Challenge
T_Verifier["test/verifier.test.ts"] --> Verifier
T_Integration["test/integration.test.ts"] --> Index
Loading

Diagram sources

Section sources

Performance Considerations

  • Avoid unnecessary RPC calls: Verifier.pre-chain guards reject invalid inputs early, preventing expensive contract reads. Tests confirm this short-circuit behavior by targeting an unreachable RPC.
  • Reuse provider and contracts: Verifier lazily caches contract instances to avoid repeated ABI parsing and object creation.
  • Efficient reputation queries: getTotalScore provides a single RPC read when only the total score is needed, avoiding tuple decoding overhead.
  • Deterministic randomness: Use fixed seeds in tests to ensure repeatable outcomes and reduce variance in timing-sensitive checks.

Troubleshooting Guide

Common issues and resolutions:

  • Malformed challenge payload: Ensure payloads start with the correct version header and contain required fields. Tests assert strict prefix validation and field parsing.
  • Wrong audience binding: expectedAudience is required, and only v2 payloads with a matching audience pass. For agent-to-agent auth, use the issuer’s DID as the audience.
  • Expired challenges: Verify timestamps and TTL settings. Unparseable payloads are treated as expired to fail safely, and so is a payload dated more than 60 seconds ahead of the clock.
  • Signature width errors: Base58-encoded signatures must be between 64 and 88 characters. Out-of-range values are rejected before chain access.
  • Network connectivity: Integration tests require valid environment variables; otherwise, they are skipped. Ensure RPC URL, addresses, and operator private key are set correctly.

Section sources

Conclusion

The SDK’s testing strategy combines isolated unit tests for cryptographic and identity logic with optional integration tests for end-to-end workflows. By enforcing pre-chain guards, audience binding, and strict payload formats, the verifier minimizes attack surface and reduces unnecessary blockchain calls. Tests leverage deterministic keypairs, environment-gated integration suites, and targeted mocks to provide reliable, fast feedback while covering critical security properties such as replay protection and signature validity.

Overview

Clone this wiki locally