Shadowdiff Mock Upstreams
Shadowdiff runs are most useful when the Go gateway is wired to predictable upstream behavior.
The repository provides a small Node script that starts two local HTTP servers:
- trade mock on
127.0.0.1:4001 - task mock on
127.0.0.1:4002
These mocks are used by scripts/shadowdiff-run.sh by default.
See also:
Source: shadowdiff/mock-upstreams.mjs
shadowdiff/mock-upstreams.mjs
#!/usr/bin/env node
import http from 'node:http';
function createServer(port, name) {
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
if (req.url === '/health') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', upstream: name }));
return;
}
if (name === 'trade' && url.pathname.startsWith('/v1/trade')) {
if (url.searchParams.get('simulate') === 'error') {
res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ status: 'error', message: 'trade upstream failure' }));
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
orderId: '42',
status: 'confirmed',
}),
);
return;
}
if (name === 'task' && url.pathname.startsWith('/v1/task')) {
if (url.searchParams.get('simulate') === 'timeout') {
setTimeout(() => {
res.writeHead(504, { 'content-type': 'application/json' });
res.end(JSON.stringify({ status: 'timeout' }));
}, 200);
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({
jobId: 'a1b2',
state: 'synced',
}),
);
return;
}
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ status: 'unknown' }));
});
server.listen(port, '127.0.0.1', () => {
console.log(`[mock-upstream] ${name} listening on port ${port}`);
});
return server;
}
const servers = [
createServer(4001, 'trade'),
createServer(4002, 'task'),
];
function shutdown() {
console.log('[mock-upstream] shutting down');
for (const server of servers) {
server.close();
}
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
Walkthrough (line-by-line intent)
Imports and server factory
import http from 'node:http':- What: uses the built-in Node HTTP server module.
- Why: avoids dependencies for a small, predictable mock.
- How: creates a plain HTTP server with a request handler closure.
createServer(port, name):- What: creates and starts one HTTP server instance bound to a fixed port.
- Why: the script needs one server for trade and one for task, with slightly different routing behavior.
- How: wires a request handler that pattern-matches
req.urlandurl.pathname.
Routing behavior
GET /health:- What: returns
200JSON with{ status: "ok", upstream: <name> }. - Why: the Go gateway readiness checks (
/readyz) probe upstream health endpoints. - How: uses
res.writeHead(200, ...)thenres.end(JSON.stringify(...)).
- What: returns
- Trade routes (
/v1/trade...):- Success path: returns
200JSON with a fixedorderIdand status. - Error simulation: when
?simulate=error, returns502JSON{ status: "error", message: ... }.
- Success path: returns
- Task routes (
/v1/task...):- Success path: returns
200JSON with a fixedjobIdand state. - Timeout simulation: when
?simulate=timeout, delays and returns504JSON{ status: "timeout" }.
- Success path: returns
- Unknown routes:
- returns
404JSON{ status: "unknown" }to make mistakes obvious during fixture development.
- returns
Lifecycle management
- The
serversarray keeps track of both running server instances so they can be shut down together. shutdown():- What: closes all servers and exits the process.
- Why: ensures clean teardown when the wrapper script kills the process or you press Ctrl-C.
- How: calls
server.close()for each server and thenprocess.exit(0).
process.on('SIGINT' | 'SIGTERM', shutdown)installs signal handlers so cleanup runs on interrupts/termination.