Skip to content

Commit 3fa9984

Browse files
d-csclaude
andcommitted
test(scripts): mollifier SDK response shape audit (5.6)
Phase 4's audit found two Zod drifts reactively (idempotencyKey: null and parentId: undefined). This script proactively sweeps every public SDK method with a buffered branch by calling them through the real @trigger.dev/core apiClient — zodfetch's schemas execute against each response, so any drift now fails the audit. The existing mollifier-challenge shell scripts only do jq structural checks, which miss schema-level drift like null-vs-undefined or optional-vs-nullable mismatches. Covers nine methods against a fresh buffered run each (separate runs for destructive ones so they don't interfere): retrieveRun, retrieveRunTrace, retrieveSpan, listRunEvents, addTags, updateRunMetadata, replayRun, rescheduleRun, cancelRun. Manually verified against the live local webapp — all nine pass with no drift surfaced. The audit is reusable as a smoke-check before each prod rollout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 97018b1 commit 3fa9984

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# 25 — SDK response shape audit. Hits each public apiClient method
3+
# against a buffered run via the actual SDK so zodfetch's Zod schemas
4+
# execute against the response. Catches schema drift between
5+
# server-side synthesised responses and client-side parsers.
6+
#
7+
# Required: drainer OFF, gate tripped (TRIP_THRESHOLD=0 or burst-first).
8+
#
9+
# Pre-reqs: TRIGGER_API_URL + TRIGGER_SECRET_KEY env vars
10+
# (defaults assume local dev: http://localhost:3030 with the seeded
11+
# personal access token).
12+
13+
set -euo pipefail
14+
15+
REPO_ROOT=$(cd "$(dirname "$0")/../.." && pwd)
16+
exec pnpm --filter references-hello-world exec tsx \
17+
"$REPO_ROOT/scripts/mollifier-challenge/25-sdk-response-shape-audit.ts" "$@"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Phase 5.6 — SDK response shape audit.
2+
//
3+
// Each method below has a buffered branch on the server. The audit
4+
// hits the real local webapp via the actual SDK so the response Zod
5+
// schemas execute against a buffered-run response. zodfetch throws on
6+
// a schema mismatch — a thrown error here is the regression signal
7+
// the Phase 4 audit's two known drifts (idempotencyKey: null →
8+
// undefined, parentId: undefined → null) would have surfaced if this
9+
// script had existed earlier.
10+
//
11+
// Usage (from references/hello-world to get the workspace SDK):
12+
// cd references/hello-world
13+
// pnpm exec tsx ../../scripts/mollifier-challenge/25-sdk-response-shape-audit.ts
14+
//
15+
// Pre-reqs:
16+
// • Webapp running at TRIGGER_API_URL (default http://localhost:3030)
17+
// • Mollifier configured to buffer every trigger (e.g. TRIP_THRESHOLD=0)
18+
// • Drainer OFF so the buffered runs stay buffered
19+
//
20+
// Exits 1 on any Zod or HTTP failure.
21+
22+
import { ApiClient } from "@trigger.dev/core/v3";
23+
24+
const apiUrl = process.env.TRIGGER_API_URL ?? "http://localhost:3030";
25+
const secretKey = process.env.TRIGGER_SECRET_KEY ?? "tr_dev_XVYfgsDzhCZRt2dgcbmN";
26+
const taskId = process.env.TASK_ID ?? "hello-world";
27+
28+
const apiClient = new ApiClient(apiUrl, secretKey);
29+
30+
type Result = { name: string; ok: boolean; err?: string };
31+
const results: Result[] = [];
32+
33+
async function check<T>(name: string, fn: () => Promise<T>): Promise<T | undefined> {
34+
try {
35+
const out = await fn();
36+
results.push({ name, ok: true });
37+
return out;
38+
} catch (err) {
39+
const msg = err instanceof Error ? err.message : String(err);
40+
results.push({ name, ok: false, err: msg });
41+
return undefined;
42+
}
43+
}
44+
45+
async function triggerBuffered(label: string): Promise<{ runId: string }> {
46+
// SDK trigger via apiClient — exercises triggerTask's response shape
47+
// as a side benefit. The shape includes the synthesised result for
48+
// buffered triggers (mollifier.queued notice, isCached, etc.).
49+
const handle = await apiClient.triggerTask(taskId, {
50+
payload: { message: `phase5-6-audit-${label}` },
51+
});
52+
return { runId: handle.id };
53+
}
54+
55+
async function main() {
56+
console.log(`audit target: ${apiUrl}`);
57+
58+
// Single buffered run for the non-destructive reads + metadata/tags mutations.
59+
const reads = await triggerBuffered("reads");
60+
console.log(`buffered run for reads: ${reads.runId}`);
61+
62+
await check("retrieveRun", () => apiClient.retrieveRun(reads.runId));
63+
// Capture the run's root spanId from the trace response — it's not
64+
// on RetrieveRunResponse by design, so we have to walk the trace
65+
// tree. The audit also catches Zod drift on the trace response by
66+
// making the call.
67+
const trace = await check("retrieveRunTrace", () =>
68+
apiClient.retrieveRunTrace(reads.runId),
69+
);
70+
// RetrieveRunTraceSpan exposes the span identifier as `id` (not
71+
// `spanId`); the retrieveSpan endpoint takes it as `spanId` in the
72+
// URL path.
73+
const rootSpanId = trace?.trace.rootSpan.id;
74+
if (rootSpanId) {
75+
await check("retrieveSpan", () => apiClient.retrieveSpan(reads.runId, rootSpanId));
76+
} else {
77+
results.push({
78+
name: "retrieveSpan",
79+
ok: false,
80+
err: "trace.rootSpan.id missing from retrieveRunTrace response",
81+
});
82+
}
83+
await check("listRunEvents", () => apiClient.listRunEvents(reads.runId));
84+
await check("addTags", () =>
85+
apiClient.addTags(reads.runId, { tags: ["phase5-6-audit"] }),
86+
);
87+
await check("updateRunMetadata", () =>
88+
apiClient.updateRunMetadata(reads.runId, { metadata: { audit: true } }),
89+
);
90+
91+
// Destructive paths need fresh buffered runs.
92+
const replayRun = await triggerBuffered("replay");
93+
console.log(`buffered run for replay: ${replayRun.runId}`);
94+
await check("replayRun", () => apiClient.replayRun(replayRun.runId));
95+
96+
const rescheduleRunHandle = await triggerBuffered("reschedule");
97+
console.log(`buffered run for reschedule: ${rescheduleRunHandle.runId}`);
98+
const futureIso = new Date(Date.now() + 5 * 60 * 1000).toISOString();
99+
await check("rescheduleRun", () =>
100+
apiClient.rescheduleRun(rescheduleRunHandle.runId, { delay: futureIso }),
101+
);
102+
103+
const cancelRun = await triggerBuffered("cancel");
104+
console.log(`buffered run for cancel: ${cancelRun.runId}`);
105+
await check("cancelRun", () => apiClient.cancelRun(cancelRun.runId));
106+
107+
console.log("");
108+
let failed = 0;
109+
for (const r of results) {
110+
if (r.ok) {
111+
console.log(` ✓ ${r.name}`);
112+
} else {
113+
console.log(` ✗ ${r.name}: ${r.err}`);
114+
failed += 1;
115+
}
116+
}
117+
console.log("");
118+
if (failed > 0) {
119+
console.log(`${failed} of ${results.length} failed`);
120+
process.exit(1);
121+
}
122+
console.log(`all ${results.length} pass`);
123+
}
124+
125+
main().catch((err) => {
126+
console.error("audit harness threw:", err);
127+
process.exit(1);
128+
});

0 commit comments

Comments
 (0)