@mcpose/testing

Note · documents 3.0.0

This page documents @mcpose/testing 3.0.0, which is on main but not yet published. The current npm release is 2.0.3, and its assertions are materially weaker. assertAuditChainIntegrity returns cleanly on an empty chain instead of throwing. assertPiiRedacted does nothing at all for sensitivityTier: 'high'. assertDelegationHonored takes an Identity[] rather than an AuditEvent, so passing an event to it silently passes no matter what the event contains. @mcpose/audit 2.0.3 also does not export verifyAuditChain or verifyManifestSignature. Pin @mcpose/testing@2 only if you are verifying v1-format archives.

Compliance assertions for @mcpose/audit audit chains. A small set of assertion functions that verify the tamper-evidence guarantees of an mcpose audit trail: chain integrity, Merkle-proof validity, PII redaction, and delegation handling. Each throws a descriptive Error on failure and returns void on success.

Keyless by design

These assertions are deliberately keyless: the signing secret is not available to tests. They prove the artifact is internally consistent. They do not prove it is authentic, and the gap between those is wider than it sounds: a forger who rewrites every event and regenerates the Merkle root and proofs produces a document these assertions accept, and does not need the signing secret to do it. Authenticity is a keyed question, answered by verifyAuditChain and verifyManifestSignature in @mcpose/audit. The split is deliberate: a test suite holding the production signing secret would be a larger liability than the tampering it was checking for. See ADR-0003 and ADR-0004 for the signing and canonical-serialization rationale.

Runner-agnostic

These are plain functions with no test-framework dependency. Use them with Vitest, Jest, node:test, or any runner.

Install

terminal
npm install --save-dev @mcpose/testing

mcpose and @mcpose/audit are peer dependencies.

Quick start

testing.test.ts
import { expect, test } from 'vitest'; // or jest, node:test, your choice
import {
  assertAuditChainIntegrity,
  assertReplayManifestValid,
  assertPiiRedacted,
  assertDelegationHonored,
} from '@mcpose/testing';

// Supplied by your test setup:
//   captureAuditEvents: collects the AuditEvents emitted via AuditOptions.onEvent
//   auditHandle: the handle returned by createAuditMiddleware()

test('transfer flow produces a verifiable audit trail', async () => {
  const events = await captureAuditEvents(/* run your scenario */);
  const manifest = await auditHandle.closeSession('session-123');

  assertAuditChainIntegrity(events);            // positions sequential, hashes distinct, non-empty
  assertReplayManifestValid(events, manifest!); // every Merkle proof verifies
  assertPiiRedacted(events[0], [/\d{16}/]);     // no card numbers in plaintext
  assertDelegationHonored(events[0]);           // delegated call carries its chain
});

API

FunctionProvesDoes NOT prove
assertAuditChainIntegrity(events)Positions sequential; chainHashes distinct and non-empty; non-empty logAuthenticity. No HMAC is recomputed, so any self-consistent rewrite passes, as does truncation of the tail
assertReplayManifestValid(events, manifest)Root recomputes from the events; one proof per event; every proof verifies at its indexThe manifest signature: use verifyManifestSignature
assertPiiRedacted(event, patterns)low/medium: no pattern matches plaintext; high: no plaintext fields, encrypted payloads presentAnything about what is inside high-tier ciphertext
assertDelegationHonored(event)delegatedFrom present, non-empty, entries have a subDelegation signatures or chain continuity (v3)

Re-exports AuditEvent and ReplayManifest types from @mcpose/audit for convenience.

Empty chain

assertAuditChainIntegrity throws on an empty chain. A log truncated to zero events must not pass.

What truncation this does and does not catch

Deleting from the head or the middle renumbers everything after it, so replayManifestPosition stops matching the array index and the assertion throws. Deleting from the tail leaves a valid prefix: positions 0…n-1 are still sequential, so assertAuditChainIntegrity on its own accepts it.

What catches tail truncation is the manifest. assertReplayManifestValid(events, manifest) compares manifest.eventCount against the event list and recomputes the Merkle root from the events under test, so a truncated log fails against the manifest that was signed at session close. Keep the manifest: it is the record of how long the log was supposed to be.

mcpose/testing vs @mcpose/testing

Not to be confused with mcpose/testing: the subpath export of the core mcpose package, which provides proxy/middleware mocks (createMockBackendClient, runToolMiddleware). This package is about asserting the audit chain.

References

  • @mcpose/audit: the package these helpers verify
  • ADR-0003: audit subkeys derived from the signing secret via the oracle, not from keyId
  • ADR-0004: audit format v2, canonical serialization, and the full-manifest signature

Next steps