From 9975bd23cdba7e196ca97c3fcec41a6eee925e57 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:02:47 -0400 Subject: [PATCH] fix(replay): preserve Date call and explicit constructor semantics (#512) Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/replay/JavaScriptReplayWorker.ts | 16 +++++++-- .../replay/javascriptReplayWorker.test.ts | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/replay/JavaScriptReplayWorker.ts b/src/replay/JavaScriptReplayWorker.ts index 2af759e8d..900f78f90 100644 --- a/src/replay/JavaScriptReplayWorker.ts +++ b/src/replay/JavaScriptReplayWorker.ts @@ -276,8 +276,13 @@ const deterministicContext = (request: WorkerRequest, caseIndex: number) => { }; const epoch = Date.parse(request.determinism.clockIso); class ReplayDate extends Date { - constructor(...arguments_: [] | [string | number]) { - super(arguments_.length === 0 ? epoch : arguments_[0]); + constructor(...arguments_: unknown[]) { + super(epoch); + if (arguments_.length > 0) { + const date: unknown = Reflect.construct(Date, arguments_, new.target); + if (!types.isDate(date)) throw new TypeError("Invalid Date instance"); + return date; + } } static override now(): number { return epoch; @@ -286,7 +291,12 @@ const deterministicContext = (request: WorkerRequest, caseIndex: number) => { const replayMath: Math = Object.create(Math); Object.defineProperty(replayMath, "random", { value: random }); return createContext( - { Date: ReplayDate, Math: replayMath }, + { + Date: new Proxy(ReplayDate, { + apply: () => new ReplayDate().toString(), + }), + Math: replayMath, + }, { name: "rea-controlled-replay", codeGeneration: { strings: false, wasm: false }, diff --git a/tests/boundary/providers/replay/javascriptReplayWorker.test.ts b/tests/boundary/providers/replay/javascriptReplayWorker.test.ts index dce65a6d3..c74ef282e 100644 --- a/tests/boundary/providers/replay/javascriptReplayWorker.test.ts +++ b/tests/boundary/providers/replay/javascriptReplayWorker.test.ts @@ -226,6 +226,41 @@ describe("disposable JavaScript replay worker", () => { }); }); +describe("deterministic replay Date compatibility", () => { + it.each([ + ["new Date(2020, 0, 2).getFullYear()", 2020], + ["new Date(2020, 0, 2, 3, 4, 5, 6).getMilliseconds()", 6], + ["typeof Date()", "string"], + ["Date() === new Date().toString()", true], + ["new Date().toISOString()", "2000-01-01T00:00:00.000Z"], + ["Date.now()", 946684800000], + ["new Date(0).getTime()", 0], + [ + "new Date('2020-01-02T00:00:00Z').toISOString()", + "2020-01-02T00:00:00.000Z", + ], + ["new Date() instanceof Date", true], + [ + "(() => { class ChildDate extends Date {} const date = new ChildDate(2020, 0, 2); return [date instanceof ChildDate, date instanceof Date, date.getFullYear(), date.constructor === ChildDate]; })()", + [true, true, 2020, true], + ], + ["Date.UTC(2020, 0, 2)", 1577923200000], + ])("preserves %s", async (expression, expected) => { + const workerRequest = await request("parser.mjs", "esm", "default", []); + workerRequest.left.modules[0] = { + alias: "entry", + format: "esm", + dependencies: {}, + source: `export default function () { return ${expression}; }`, + }; + const result = await runWorker(workerRequest); + expect(result.code).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + left: [{ outcome: "return", value: expected }], + }); + }); +}); + describe("replay array projection", () => { it.each([ ["Array(3)", [null, null, null]],