Files
triggerdotdev--trigger.dev/apps/webapp/test/mollifierReplayPayloadShape.test.ts
2026-07-13 13:32:57 +08:00

98 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
serialiseMollifierSnapshot,
deserialiseMollifierSnapshot,
} from "~/v3/mollifier/mollifierSnapshot.server";
import { prettyPrintPacket } from "@trigger.dev/core/v3";
// Regression test for the Devin "Buffered replay loader passes
// non-string payload to prettyPrintPacket" finding on PR #3757.
//
// Devin's claim is that the snapshot codec double-unwraps the
// payload: `engine.trigger` carries it pre-serialised, then the
// snapshot serialise/deserialise round-trip would JSON.parse it a
// second time, leaving `buffered.payload` as a *parsed* object —
// which `prettyPrintPacket` then mis-handles, producing malformed
// payload display in the Replay dialog.
//
// This test pins the actual contract: the snapshot codec is a single
// JSON.stringify / JSON.parse layer. The payload field stored on the
// engine trigger input is a string (the SDK-serialised payload from
// `payloadPacket.data`). A string round-trips through
// JSON.stringify/JSON.parse unchanged — it does NOT get a second
// unwrap. Therefore `buffered.payload` reaches the replay loader as
// a string, exactly the shape `prettyPrintPacket` expects.
describe("mollifier replay payload shape", () => {
it("serialise/deserialise preserves the payload as a string", () => {
// Shape mirrors what `triggerTask.server.ts:#buildEngineTriggerInput`
// produces — `payload` is `args.payloadPacket.data`, already a JSON
// string from the SDK's packet serialisation.
const triggerInput = {
friendlyId: "run_x",
taskIdentifier: "hello-world",
payload: JSON.stringify({ hello: "world", n: 42 }),
payloadType: "application/json",
traceId: "trace_x",
spanId: "span_x",
};
const serialised = serialiseMollifierSnapshot(triggerInput);
const roundTripped = deserialiseMollifierSnapshot(serialised);
expect(typeof roundTripped.payload).toBe("string");
expect(roundTripped.payload).toBe(triggerInput.payload);
expect(roundTripped.payloadType).toBe("application/json");
});
it("prettyPrintPacket on the round-tripped payload produces the expected pretty JSON", async () => {
const original = { hello: "world", nested: { count: 3 } };
const triggerInput = {
payload: JSON.stringify(original),
payloadType: "application/json",
};
const roundTripped = deserialiseMollifierSnapshot(serialiseMollifierSnapshot(triggerInput));
// This is exactly the call the replay loader makes:
// prettyPrintPacket(run.payload, run.payloadType)
// If Devin were right, the payload here would be a parsed object
// and prettyPrintPacket would either double-encode or skip
// formatting. In reality it's a string, so we get correct pretty
// JSON.
const pretty = await prettyPrintPacket(
roundTripped.payload,
roundTripped.payloadType as string
);
expect(pretty).toBe(JSON.stringify(original, null, 2));
});
it("string payload survives the buffer-codec round-trip even with snapshot fields around it", () => {
// Replicate the realistic snapshot shape (the engine.trigger input
// has many sibling fields). Confirms there's no field-shape
// interaction that would mutate payload.
const triggerInput = {
friendlyId: "run_x",
environment: {
id: "env",
type: "DEVELOPMENT",
project: { id: "p" },
organization: { id: "o" },
},
taskIdentifier: "t",
payload: '{"a":1}',
payloadType: "application/json",
context: { run: { id: "x" } },
traceContext: { traceparent: "00-...-..." },
traceId: "abc",
spanId: "def",
tags: ["one", "two"],
depth: 2,
isTest: false,
};
const out = deserialiseMollifierSnapshot(serialiseMollifierSnapshot(triggerInput));
expect(typeof out.payload).toBe("string");
expect(out.payload).toBe('{"a":1}');
});
});