Replay runner
The replay Seamward runs for a repair proposal executes Seamward's bounded mapping in an isolated sandbox. It never runs your application. The replay runner closes that gap: it feeds the same fixtures to the code you will ship, on your machine or in your pipeline, and reports which fixtures passed.
Use it to answer one question before you approve or merge a fix: does my handler accept the payload shape that caused this incident?
Node.js only
The runner is Node.js only; a PHP runner is planned.
Before you start
- A replay dataset. One is built when a repair proposal is requested for an incident. List an incident's datasets with
seamward replays datasets INCIDENT_ID --connection CONNECTION_ID. - A signed-in CLI with the report scope.
seamward loginrequestsreplays:reportunless you pass--read-only. A CLI authorized before the runner existed does not hold it; sign in again. Owners and admins can report a run; viewers cannot. - Node.js 22 or later, and a handler module Node can import.
Declare the handler
The handler is one small module you write once per connection. It exports a function, as the default export or as handle, that takes a payload and says what your code did with it. Have it call the same function your webhook route or API client calls, so the replay exercises your real code.
Code example
// replay-handler.mjsimport { handleCandidateWebhook } from "./src/webhooks/candidate.js";
export default async function handle(payload) { const candidate = await handleCandidateWebhook(payload); return { accepted: candidate !== null, businessObjectType: "candidate", output: candidate, };}| Returned field | Type | Meaning |
|---|---|---|
accepted | boolean, required | Whether your code accepted the payload |
businessObjectType | string, optional | The record your code produced, such as candidate |
output | JSON, optional | Compared with the fixture's expected output when the dataset defines one |
statusCode | 100 to 599, optional | Defaults to 200 when accepted and 422 when not |
assertions | object of booleans | Named checks compared with the fixture's expected assertions |
The function may be synchronous or return a promise. It receives a fresh copy of the fixture input each time, and a second argument { fixtureId }. Keep it free of side effects you would not want repeated: a replay calls it once per fixture, and a fixture is not a real event.
If your handler is TypeScript, point --handler at the compiled module, or at a .ts file on a Node.js version that runs TypeScript directly.
Pull the dataset
Code example
seamward replays pull rds_example --connection int_example --workspace acme --out dataset.jsonThe file is a seamward.replay-dataset/1 document: the dataset's manifest hash, its fixture source, and for each fixture an input and the expected outcome. The command recomputes the manifest hash over the fixtures and writes the file only when it matches. Each download is recorded in the workspace audit log.
Fixture inputs are generated, never copied from your traffic. A shape dataset holds a placeholder for each field's type. A profile dataset holds a value in each field's recorded format and size, which is what lets a date parser or an email check run meaningfully. Replay explains the two sources.
Run it twice
Code example
git checkout mainseamward replays run --dataset dataset.json --handler ./replay-handler.mjs --label current --out current.json --deny-network
git checkout fix/provider-renameseamward replays run --dataset dataset.json --handler ./replay-handler.mjs --label candidate --out candidate.json --deny-networkrun is local. It needs no sign-in and sends nothing anywhere. It runs your handler in a child process, compares each returned outcome with the fixture's expected outcome, and writes a seamward.replay-result/1 file.
--label currentis the code deployed now. On an incident's fixtures you expect it to fail, which shows the fixtures exercise the problem.--label candidateis your fix. You expect it to pass.--timeout-msis the time allowed per fixture, from 100 to 120000 (default 5000).
The command exits 0 whenever the run completed, pass or fail. Read status and summary in its output:
Code example
{ "datasetId": "rds_example", "label": "candidate", "commitSha": "0b3f5a9c1d2e4f60718293a4b5c6d7e8f9012345", "workingTreeDirty": false, "networkDenied": true, "status": "passed", "summary": { "passed": 40, "failed": 0, "errors": 0 }, "file": "candidate.json"}commitSha is the commit of the checkout the handler module is in, and workingTreeDirty is true when that checkout had uncommitted changes. Outside a git checkout commitSha is null.
What the result contains
For each fixture: its ID, an outcome of pass, fail or error, and for a failure up to ten mismatch paths with their kind, such as $.accepted and value. For an error: one of handler_threw, handler_timeout, or invalid_handler_output.
It contains nothing else about your data. The result format has no field for your handler's input, its output, its logs, or the text of an error it threw. Whatever your handler prints goes to your terminal and is not recorded.
Deny network access
A replay should not call a provider or write to a remote system. --deny-network runs the handler process with network access denied by the operating system:
| Platform | How |
|---|---|
| macOS | sandbox-exec with a profile that denies all network operations |
| Linux | unshare into a new, empty network namespace |
Before any fixture runs, the handler process tries to connect to a local probe the runner listens on. If the connection succeeds, the command stops with replay_network_denial_failed and runs nothing. If the platform has no supported wrapper, or the wrapper cannot start (for example where unprivileged user namespaces are disabled), it stops with replay_network_denial_unavailable. It never falls back to running with network access.
Without the flag your handler has whatever access your shell has, and the result records networkDenied: false. Only network access is restricted: the handler can still read and write files, so point any database or queue it uses at a disposable target.
Submit the result
Code example
seamward replays submit --connection int_example --workspace acme --file current.jsonseamward replays submit --connection int_example --workspace acme --file candidate.jsonSeamward checks that the result names this dataset and its manifest hash, that its digest matches, and that it reports every fixture exactly once. Submitting the same file again returns the first run.
In the incident's Repair tab, the runs appear under Your own handler, next to the sandbox validation, labelled Reported by your runner, with the commit and whether the runner denied network access.
What a reported run means
Seamward cannot rerun your handler, so it cannot confirm the outcomes. A reported run is evidence you supply for whoever reviews the repair:
- It is always shown as reported by your runner, never as verified by Seamward.
- It never changes a repair proposal's status and never approves one. Approval stays a decision your team records.
- It is bound to one dataset. A run on an older dataset is not shown against a newer proposal.
- A dataset holds at most 50 reported runs.
Troubleshooting
| Code | What it means | What to do |
|---|---|---|
replay_dataset_tampered | The fixtures do not match the manifest hash | Pull the dataset again; do not edit the file |
invalid_replay_dataset | The file is not a seamward.replay-dataset/1 document | Pass the file replays pull wrote |
replay_handler_unreadable | Node could not import the handler module | Check the path, and that the module and its imports load with node |
replay_handler_not_found | The module exports no function as default or handle | Export the handler function |
replay_network_denial_unavailable | No supported wrapper, or it could not start | Run on macOS or Linux with unprivileged user namespaces, or drop the flag |
replay_network_denial_failed | The handler process could still reach the network | Nothing ran. Report it; do not treat a later run as network-denied |
invalid_replay_result | The file is not a seamward.replay-result/1 result | Submit the file replays run wrote, unedited |
replay_result_manifest_mismatch | The result was produced from other fixtures than the dataset now holds | Pull the dataset again and rerun |
replay_result_fixture_coverage | The result does not report every fixture once | Rerun; do not edit the result |
replay_result_limit_reached | The dataset already holds 50 reported runs | Request a new proposal to build a fresh dataset |
insufficient_scope | The CLI grant lacks replays:report, or you are a viewer | Sign in again without --read-only, as an owner or admin |
Next steps
- Replay: what the sandbox runs, the two fixture sources, and what a pass does not establish.
- CLI reference: every replay command and flag.
- Example application: a consumer with a declared replay handler.
