OSS-first docs
These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.
TestSpec & TestRunner
Use `TestSpec` to describe end-to-end scenarios for contracts and workflows. Specs live alongside your contracts and exercise the same OperationSpecRegistry handlers the app uses.
•
Types & registry: `packages/libs/contracts/src/tests/spec.ts`
•
Runtime runner: `packages/libs/contracts/src/tests/runner.ts`
•
CLI: `contractspec test`
Structure
export interface TestSpec {
meta: TestSpecMeta;
target: TestTarget; // contract or workflow
fixtures?: Fixture[]; // optional shared setup before each scenario
scenarios: TestScenario[];
coverage?: CoverageRequirement;
}•
`Fixture`: run an operation before the scenario (`operation`, optional `input`)
•
`Action`: operation input that the scenario exercises
•
`Assertion`:
•
`expectOutput` `{ match }` deep-equals the handler output
•
`expectError` `{ messageIncludes? }` ensures an error was thrown
•
`expectEvents` `{ events: [{ name, version, min?, max? }] }` checks emitted events
Example
import { defineCommand, type TestSpec } from '@lssm-tech/lib.contracts-spec';
export const AddNumbersSpec = defineCommand({
meta: { name: 'math.add', version: '1.0.0', /* … */ },
io: {
input: AddNumbersInput,
output: AddNumbersOutput,
},
policy: { auth: 'user' },
});
export const MathAddTests: TestSpec = {
meta: {
name: 'math.add.tests',
version: '1.0.0',
title: 'Math add scenarios',
owners: ['@team.math'],
tags: ['math'],
stability: StabilityEnum.Experimental,
},
target: { type: 'contract', operation: { name: 'math.add' } },
scenarios: [
{
name: 'adds positive numbers',
when: {
operation: { name: 'math.add' },
input: { a: 2, b: 3 },
},
then: [
{ type: 'expectOutput', match: { sum: 5 } },
{
type: 'expectEvents',
events: [{ name: 'math.sum_calculated', version: '1.0.0', min: 1 }],
},
],
},
],
};Running tests
1.
Register the contract handlers in a `OperationSpecRegistry`:
export function createRegistry() {
const registry = new OperationSpecRegistry();
registry.register(AddNumbersSpec);
registry.bind(AddNumbersSpec, addNumbersHandler);
return registry;
}1.
Run the CLI:
contractspec test apps/math/tests/math.add.tests.ts \
--registry apps/math/tests/registry.ts•
The CLI loads the TestSpec, instantiates the registry (via the provided module), and executes each scenario via `TestRunner`.
•
`--json` outputs machine-readable results.
Programmatic usage
const runner = new TestRunner({
registry,
createContext: () => ({ actor: 'user', organizationId: 'tenant-1' }),
});
const result = await runner.run(MathAddTests);
console.log(result.passed, result.failed);•
`createContext` can supply default `HandlerCtx` values.
•
`beforeEach` / `afterEach` hooks let you seed databases or reset state.
Best practices
•
Keep fixtures idempotent so scenarios can run in parallel in the future.
•
Use `expectEvents` to guard analytics/telemetry expectations.
•
Add specs to `TestRegistry` for discovery and documentation.
•
`coverage` captures desired coverage metrics (enforced by future tooling).
•
Pair TestSpec files with CI using `contractspec test --json` and fail builds when `failed > 0`.
Mocking with Bun's `vi`
•
Pass a single function type to `vi.fn<TFunction>()` so calls retain typed arguments:
const handler = vi.fn<typeof fetch>();
const fetchImpl: typeof fetch = ((...args) => handler(...args)) as typeof fetch;
Object.defineProperty(fetchImpl, 'preconnect', {
value: vi.fn<typeof fetch.preconnect>(),
});•
When you need to inspect calls, use the typed mock (`handler.mock.calls`) rather than casting to `any`.
•
Narrow optional request data defensively (e.g., check for headers before reading them) so tests remain type-safe under strict `tsconfig` settings.