Conformance kits
Prove a custom adapter, store, or trace sink implements its seam — from stitchapi/testing, in your own CI.
Every pluggable seam in StitchAPI is a contract, not a dependency: the
transport (Adapter), the state store (StitchStore), and the trace sink
(TraceSink) each have a small documented surface, core ships only platform
defaults for them (fetchAdapter, memoryStore, createTrace), and vendor
implementations — a Redis store, an axios transport, a Datadog sink — live in
their own packages.
The stitchapi/testing subpath makes that contract enforceable. It ships one
verify*Contract function per seam; any implementation that passes is a valid
seam implementation, and third-party authors can prove it in their own CI
without depending on StitchAPI internals.
The kit is framework-agnostic and browser-safe: every verifier is a plain
async function that returns a report (no vitest/jest imports, no node:*
modules), so it runs inside any test runner — or a browser.
The report
Each verifier resolves to a ContractReport:
interface ContractReport {
seam: string; // 'store' | 'adapter' | 'sink'
ok: boolean; // true when every rule passed
passed: string[]; // names of the rules that passed
violations: { rule: string; detail: string }[];
}Rules are checked independently — one violation never masks another — so a failing report lists every broken rule at once.
assertConformance
assertConformance(report) throws one readable Error listing every
violation (and is a no-op on a clean report). It is the one-liner for a test
body:
import { assertConformance, verifyStoreContract } from 'stitchapi/testing';
test('myStore implements the StitchStore contract', async () => {
assertConformance(await verifyStoreContract(() => myStore()));
});verifyStoreContract
verifyStoreContract(makeStore, opts?) verifies a pluggable
store — the get/set/incr + TTL
surface the engine uses for throttle counters and auth/session state:
set/getround-trips a value; a missing key resolves toundefined; a secondsetoverwrites; writes are isolated by key.- A
setwithttlMsexpires; asetwithoutttlMsdoes not. incrinitializes a missing key to 1, increments an existing counter, restarts at 1 once its TTL window lapses, and is atomic within a process: 20 concurrent calls must return 1..20 exactly.
TTL rules use real timers with a small window (default 60ms). Pass
{ ttlMs } to give a backend with coarser expiry more room. Keys are
namespaced per run, so rerunning against a persistent backend never collides.
verifyAdapterContract
verifyAdapterContract(adapter, { baseUrl }) verifies a custom transport
against a documented echo contract served at baseUrl:
- Status passthrough: 200, 404, and 500 all resolve — an adapter never throws on a non-2xx status; only network and abort errors reject.
- Request delivery: method, headers, and the JSON-encoded body reach the server intact.
- Response headers are readable using lowercased names (the
AdapterResponsecase convention). - A
text/plainbody round-trips as a string; anapplication/jsonbody round-trips as parsed data. - A pre-aborted
AbortSignalrejects, and an in-flight abort rejects promptly instead of waiting out the response.
adapterContractFixture
The echo contract itself ships as adapterContractFixture — a pure
function (request in, response out, no server), so you can mount it on
anything: node:http, hono, a service worker. The host lowercases request
header names, hands over the raw body text, and honors the fixture's
delayMs (the in-flight abort rule depends on it):
import { createServer } from 'node:http';
import { adapterContractFixture } from 'stitchapi/testing';
const server = createServer((req, res) => {
let raw = '';
req.setEncoding('utf8');
req.on('data', (chunk) => (raw += chunk));
req.on('end', () => {
const out = adapterContractFixture({
method: req.method ?? 'GET',
path: req.url ?? '/',
headers: Object.fromEntries(
Object.entries(req.headers).map(([k, v]) => [
k.toLowerCase(),
Array.isArray(v) ? v.join(', ') : (v ?? ''),
]),
),
...(raw === '' ? {} : { body: raw }),
});
const respond = () => {
res.writeHead(out.status, out.headers);
res.end(out.body);
};
if (out.delayMs) setTimeout(respond, out.delayMs);
else respond();
});
});The routes are documented on the function's JSDoc (/status/{code}, /echo,
/text, /json, /slow).
verifySinkContract
verifySinkContract(makeSink) feeds one sink instance a canonical fixture
sequence covering all StitchEvent variants — start, progress,
drift, delta, result, error, done — and asserts handle accepts
each without throwing, then exercises the optional flush() when present.
The sequence includes delta, which the type declares ahead of engine
support — a conforming sink must already tolerate events it does not
recognize.
Worked example: a community Redis store
A third-party author publishing @acme/stitch-redis-store proves compliance
in their own CI — no StitchAPI internals, just the kit:
import { redisStore } from '../src/redis-store';
import { createClient } from 'redis';
import { assertConformance, verifyStoreContract } from 'stitchapi/testing';
import { test } from 'vitest';
test('redisStore implements the StitchStore contract', async () => {
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
try {
assertConformance(
await verifyStoreContract(() => redisStore(client), {
// Give Redis expiry more room than the 60ms default.
ttlMs: 250,
}),
);
} finally {
await client.quit();
}
});If the store cuts a corner — a non-atomic INCR, a dropped TTL — the test
fails with every broken rule named:
Error: StitchAPI store contract: 2 violation(s), 8 rule(s) passed
- set: a ttlMs entry expires: value survived its 250ms TTL: got "soon-gone"
- incr: 20 concurrent calls net exactly +20: sorted results of 20 concurrent incrs ...Passing the kit is the compatibility claim: a store that passes can back distributed throttle and shared sessions with no change at the call site.