|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { |
| 3 | + serialiseMollifierSnapshot, |
| 4 | + deserialiseMollifierSnapshot, |
| 5 | +} from "~/v3/mollifier/mollifierSnapshot.server"; |
| 6 | +import { prettyPrintPacket } from "@trigger.dev/core/v3"; |
| 7 | + |
| 8 | +// Regression test for the Devin "Buffered replay loader passes |
| 9 | +// non-string payload to prettyPrintPacket" finding on PR #3757. |
| 10 | +// |
| 11 | +// Devin's claim is that the snapshot codec double-unwraps the |
| 12 | +// payload: `engine.trigger` carries it pre-serialised, then the |
| 13 | +// snapshot serialise/deserialise round-trip would JSON.parse it a |
| 14 | +// second time, leaving `buffered.payload` as a *parsed* object — |
| 15 | +// which `prettyPrintPacket` then mis-handles, producing malformed |
| 16 | +// payload display in the Replay dialog. |
| 17 | +// |
| 18 | +// This test pins the actual contract: the snapshot codec is a single |
| 19 | +// JSON.stringify / JSON.parse layer. The payload field stored on the |
| 20 | +// engine trigger input is a string (the SDK-serialised payload from |
| 21 | +// `payloadPacket.data`). A string round-trips through |
| 22 | +// JSON.stringify/JSON.parse unchanged — it does NOT get a second |
| 23 | +// unwrap. Therefore `buffered.payload` reaches the replay loader as |
| 24 | +// a string, exactly the shape `prettyPrintPacket` expects. |
| 25 | +describe("mollifier replay payload shape", () => { |
| 26 | + it("serialise/deserialise preserves the payload as a string", () => { |
| 27 | + // Shape mirrors what `triggerTask.server.ts:#buildEngineTriggerInput` |
| 28 | + // produces — `payload` is `args.payloadPacket.data`, already a JSON |
| 29 | + // string from the SDK's packet serialisation. |
| 30 | + const triggerInput = { |
| 31 | + friendlyId: "run_x", |
| 32 | + taskIdentifier: "hello-world", |
| 33 | + payload: JSON.stringify({ hello: "world", n: 42 }), |
| 34 | + payloadType: "application/json", |
| 35 | + traceId: "trace_x", |
| 36 | + spanId: "span_x", |
| 37 | + }; |
| 38 | + |
| 39 | + const serialised = serialiseMollifierSnapshot(triggerInput); |
| 40 | + const roundTripped = deserialiseMollifierSnapshot(serialised); |
| 41 | + |
| 42 | + expect(typeof roundTripped.payload).toBe("string"); |
| 43 | + expect(roundTripped.payload).toBe(triggerInput.payload); |
| 44 | + expect(roundTripped.payloadType).toBe("application/json"); |
| 45 | + }); |
| 46 | + |
| 47 | + it("prettyPrintPacket on the round-tripped payload produces the expected pretty JSON", async () => { |
| 48 | + const original = { hello: "world", nested: { count: 3 } }; |
| 49 | + const triggerInput = { |
| 50 | + payload: JSON.stringify(original), |
| 51 | + payloadType: "application/json", |
| 52 | + }; |
| 53 | + |
| 54 | + const roundTripped = deserialiseMollifierSnapshot( |
| 55 | + serialiseMollifierSnapshot(triggerInput), |
| 56 | + ); |
| 57 | + |
| 58 | + // This is exactly the call the replay loader makes: |
| 59 | + // prettyPrintPacket(run.payload, run.payloadType) |
| 60 | + // If Devin were right, the payload here would be a parsed object |
| 61 | + // and prettyPrintPacket would either double-encode or skip |
| 62 | + // formatting. In reality it's a string, so we get correct pretty |
| 63 | + // JSON. |
| 64 | + const pretty = await prettyPrintPacket( |
| 65 | + roundTripped.payload, |
| 66 | + roundTripped.payloadType as string, |
| 67 | + ); |
| 68 | + |
| 69 | + expect(pretty).toBe(JSON.stringify(original, null, 2)); |
| 70 | + }); |
| 71 | + |
| 72 | + it("string payload survives the buffer-codec round-trip even with snapshot fields around it", () => { |
| 73 | + // Replicate the realistic snapshot shape (the engine.trigger input |
| 74 | + // has many sibling fields). Confirms there's no field-shape |
| 75 | + // interaction that would mutate payload. |
| 76 | + const triggerInput = { |
| 77 | + friendlyId: "run_x", |
| 78 | + environment: { |
| 79 | + id: "env", |
| 80 | + type: "DEVELOPMENT", |
| 81 | + project: { id: "p" }, |
| 82 | + organization: { id: "o" }, |
| 83 | + }, |
| 84 | + taskIdentifier: "t", |
| 85 | + payload: '{"a":1}', |
| 86 | + payloadType: "application/json", |
| 87 | + context: { run: { id: "x" } }, |
| 88 | + traceContext: { traceparent: "00-...-..." }, |
| 89 | + traceId: "abc", |
| 90 | + spanId: "def", |
| 91 | + tags: ["one", "two"], |
| 92 | + depth: 2, |
| 93 | + isTest: false, |
| 94 | + }; |
| 95 | + const out = deserialiseMollifierSnapshot(serialiseMollifierSnapshot(triggerInput)); |
| 96 | + expect(typeof out.payload).toBe("string"); |
| 97 | + expect(out.payload).toBe('{"a":1}'); |
| 98 | + }); |
| 99 | +}); |
0 commit comments