Skip to content

Commit 5bc96f7

Browse files
d-csclaude
andcommitted
test(webapp): pin string-payload contract through mollifier snapshot codec
Regression test against the claim that the snapshot codec double-unwraps `payload`. `serialiseSnapshot` / `deserialiseSnapshot` are single-layer JSON.stringify / JSON.parse on the snapshot wrapper; the pre-serialised payload string survives unchanged. The replay loader's `prettyPrintPacket(run.payload, run.payloadType)` call receives a string at runtime, exactly the shape it expects. Locks the contract so a future change that accidentally introduced a second deserialisation layer (or stored payload as an object) would fail this test and surface the regression before reaching the replay dialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c265cc7 commit 5bc96f7

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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

Comments
 (0)