|
| 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