Generate a Jest replay session (the default happy path)
For Jest projects, softprobe generate jest-session is the recommended way to turn a captured case into a runnable replay test. It emits one TypeScript module that:
- creates a
replay-mode session, - loads the case file,
- registers one
findInCase+mockOutboundpair per outbound HTTP hop in the case, - returns
{ sessionId, close }for your test to consume.
You never hand-write the mock list. You never call the runtime's JSON API. You commit the generated module alongside the case file and regenerate after every capture refresh.
This is the flow docs/design.md §3.2 calls the "default happy path" — the 80/20 choice.
Prerequisites
- A Softprobe runtime running somewhere (local:
docker compose upine2e/; CI: see CI integration). - A case file — produced by Capture your first session or shipped with the repo.
@softprobe/softprobe-jsinstalled:npm install --save-dev @softprobe/softprobe-js.- The CLI on
PATH:softprobe doctorprints version info.
One command
softprobe generate jest-session \
--case cases/checkout.case.json \
--out test/generated/checkout.replay.session.ts| Flag | Purpose |
|---|---|
--case | Path to the *.case.json file to replay. |
--out | Where to write the generated TypeScript module. Convention: test/generated/<scenario>.replay.session.ts. |
The CLI reads the case, walks its traces[], deduplicates (direction, method, path) outbound hops, and emits one module:
// Generated by `softprobe generate jest-session` - do not edit by hand.
import path from 'path';
import { Softprobe } from '@softprobe/softprobe-js';
const generatedDir = path.dirname(__filename);
export async function startReplaySession(): Promise<{ sessionId: string; close: () => Promise<void> }> {
const softprobe = new Softprobe();
const session = await softprobe.startSession({ mode: 'replay' });
await session.loadCaseFromFile(path.join(generatedDir, '../../cases/checkout.case.json'));
const hit0 = session.findInCase({
direction: 'outbound',
method: 'POST',
path: '/v1/payment_intents',
});
await session.mockOutbound({
direction: 'outbound',
method: 'POST',
path: '/v1/payment_intents',
response: hit0.response,
});
const hit1 = session.findInCase({
direction: 'outbound',
method: 'GET',
path: '/v1/customers/cus_abc',
});
await session.mockOutbound({
direction: 'outbound',
method: 'GET',
path: '/v1/customers/cus_abc',
response: hit1.response,
});
return {
sessionId: session.id,
close: () => session.close(),
};
}Anatomy:
- Imports — only
@softprobe/softprobe-js. No hand-rolledfetchto the runtime. startReplaySession()— starts the session, loads the case (path is resolved relative to the generated module, so moving the file doesn't break it), and registers onefindInCase→mockOutboundpair per unique outbound hop.- Return value —
{ sessionId, close }. AttachsessionIdto test requests viax-softprobe-session-id; callclose()inafterAll.
Use it in a Jest test
// test/checkout.replay.test.ts
import fetch from 'node-fetch';
import { startReplaySession } from './generated/checkout.replay.session';
describe('checkout replay', () => {
let sessionId: string;
let close: () => Promise<void>;
beforeAll(async () => {
const session = await startReplaySession();
sessionId = session.sessionId;
close = session.close;
});
afterAll(async () => {
await close();
});
it('returns a successful checkout response', async () => {
const res = await fetch('http://127.0.0.1:8082/checkout', {
method: 'POST',
headers: {
'x-softprobe-session-id': sessionId,
'content-type': 'application/json',
},
body: JSON.stringify({ cartId: 'c_test' }),
});
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({ status: 'succeeded' });
});
});That's the entire test. No mocking library, no HTTP interception setup, no jest.mock(...).
Regeneration workflow
The generated module is a build artifact of the case file. Every time you re-capture (for instance, because an upstream API changed), regenerate:
# 1. Re-capture
softprobe capture run --out cases/checkout.case.json -- npm run test:integration
# 2. Regenerate
softprobe generate jest-session \
--case cases/checkout.case.json \
--out test/generated/checkout.replay.session.ts
# 3. Review the diff
git diff cases/checkout.case.json test/generated/checkout.replay.session.tsA Makefile target or an npm script keeps this one step:
{
"scripts": {
"replay:regen": "softprobe generate jest-session --case cases/checkout.case.json --out test/generated/checkout.replay.session.ts"
}
}Sidecar YAML (optional policy and fixtures)
The generator emits plain SDK calls. To layer policy or auth fixtures on top without editing the generated module, add a tiny wrapper next to it:
// test/generated/checkout.replay.session.wrapper.ts
import { startReplaySession } from './checkout.replay.session';
export async function startHardenedReplaySession() {
const session = await startReplaySession();
// Post-generation setup:
// - strict policy
// - additional fixtures
// - one-off overrides
return session;
}The wrapper is yours to edit and commit; the generated module is regenerated.
Diff-review tips
When reviewing a regenerated module in code review:
pathchanges usually mean the upstream API path or URL structure changed — investigate before merging.- New
findInCase/mockOutboundpairs mean the capture added a new outbound dependency — confirm that's expected. - Removed pairs mean a call was removed — confirm your app no longer makes it, or a capture ran under a different scenario.
- Reorderings are harmless: the generator sorts by
(path, method, direction)for stable diffs.
The case file diff is usually more informative than the module diff.
What the generator is not
- It does not generate a Jest test wrapper — only the session helper. You still write the
describe/ityourself (usually once, then never touch). - It does not emit rules for inbound traffic. If you need to mock inbound (rare — you're testing the real app), add a
mockOutbound({ direction: 'inbound', ... })call in your wrapper. - It does not handle
consume: oncesemantics automatically. If your capture contains the same path multiple times with different responses, the generator keeps the first and skips duplicates; use hand-writtenmockOutboundfor ordered response sequences.
When to drop down to ad-hoc findInCase + mockOutbound
Reach for Path B (ad-hoc) when you:
- need to mutate a captured response (edit a timestamp, swap a test card, zero a field),
- need ordered responses (first call 503, second call 200),
- need predicate-based matching (
hostSuffix,bodyJsonPath) beyond(direction, method, path)triples, - are writing a test that mixes real and mocked upstream calls.
See Replay in Jest for the ad-hoc pattern.
See also
- Quickstart — end-to-end walk from capture to replay.
- CLI reference →
generate jest-session— all flags and machine output. - TypeScript SDK — what
Softprobe,findInCase,mockOutboundreturn. - Capture your first session — producing the input case.