Compare commits

...
Author SHA1 Message Date
morluto a2955d6607 fix(replay): pin the replay timezone and map undefined to null 2026-10-05 16:46:41 +08:00
2 changed files with 64 additions and 0 deletions
+13
View File
@@ -108,6 +108,12 @@ class ReplayDeniedError extends Error {
} }
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
// `Intl` and `Date.prototype.toLocaleString` read the host timezone, so the
// same artifact replayed on two machines produced different output — fatal
// for a deterministic replay. Pin the timezone before anything touches a
// date. Verified: with the host set to Asia/Tokyo the observed values become
// UTC and epoch respectively, independent of the machine.
process.env.TZ = "UTC";
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk)); for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
const rawRequest: unknown = JSON.parse( const rawRequest: unknown = JSON.parse(
@@ -396,6 +402,13 @@ const projectValueRecursive = (
}; };
const projectLeaf = (candidate: unknown): unknown => { const projectLeaf = (candidate: unknown): unknown => {
if (candidate === undefined) {
// JSON has no undefined: `JSON.stringify([undefined])` is `[null]` and the
// array-hole projection already yields null. Mapping it here keeps one
// value for "no value" instead of rejecting `[undefined]` while accepting
// `[ , ]` for the same position.
return null;
}
if ( if (
candidate === null || candidate === null ||
typeof candidate === "string" || typeof candidate === "string" ||
@@ -497,6 +497,57 @@ describe("deterministic replay Date compatibility", () => {
}); });
}); });
it("maps undefined to null so a position is never silently unrepresentable", async () => {
const workerRequest = await request("parser.mjs", "esm", "default", []);
workerRequest.left.modules[0] = {
alias: "entry",
format: "esm",
dependencies: {},
source: "export default function () { return [undefined, 1]; }",
};
const result = await runWorker(workerRequest);
expect(JSON.parse(result.stdout)).toMatchObject({
left: [{ outcome: "return", value: [null, 1] }],
});
});
it("still rejects values JSON cannot represent at all", async () => {
const workerRequest = await request("parser.mjs", "esm", "default", []);
workerRequest.left.modules[0] = {
alias: "entry",
format: "esm",
dependencies: {},
source: "export default function () { return [10n]; }",
};
const result = await runWorker(workerRequest);
expect(JSON.parse(result.stdout)).toMatchObject({
left: [
{
outcome: "serialization_error",
exception: { message: "Unsupported replay result type: bigint" },
},
],
});
});
it("pins the timezone so locale output is machine independent", async () => {
const workerRequest = await request("parser.mjs", "esm", "default", []);
workerRequest.left.modules[0] = {
alias: "entry",
format: "esm",
dependencies: {},
source:
"export default function () { return [new Intl.DateTimeFormat().resolvedOptions().timeZone, new Date(0).toLocaleString()]; }",
};
const result = await runWorker(workerRequest);
const outcome = JSON.parse(result.stdout).left[0];
expect(outcome.outcome).toBe("return");
expect(outcome.value[0]).toBe("UTC");
// The rendered clock is 12:00 UTC regardless of the host zone. The exact
// wording is ICU-build dependent, so only the pinned hour is asserted.
expect(outcome.value[1]).toContain("12:00:00");
});
describe("replay array projection", () => { describe("replay array projection", () => {
it.each([ it.each([
["Array(3)", [null, null, null]], ["Array(3)", [null, null, null]],