1import { deepEqual } from "@/Diff";
2import {
3 DATE_MARKER,
4 decodeDuration,
5 DURATION_MARKER,
6 encodeState,
7 reviveState,
8 reviveStateRecursive,
9} from "@/State/StateEncoding";
10import { describe, expect, test } from "alchemy-test";
11import * as Duration from "effect/Duration";
12
13// Dates in persisted props must ROUND-TRIP: a provider's `diff`/`delete`/
14// `read` receive `olds` from the state store on a later run, and a
15// Date-typed prop must come back as a real Date — not `{}` (the old
16// structural-walk bug, which churned a phantom update on every plan) and
17// not a bare ISO string (which lies about the declared prop type).
18describe(
19 "StateEncoding Duration round-trip",
20 { tags: ["unit", "local"] },
21 () => {
22 // The revived value is untyped JSON, but a Duration in = a Duration out is
23 // exactly what these tests assert; the cast lets `Duration.equals` (which
24 // no longer accepts `unknown`) verify it.
25 const roundTrip = (value: Duration.Duration) =>
26 JSON.parse(
27 JSON.stringify(encodeState(value)),
28 reviveState,
29 ) as Duration.Duration;
30
31 test("finite, infinity, and negative infinity survive encode → JSON → revive", () => {
32 expect(
33 Duration.equals(roundTrip(Duration.seconds(15)), Duration.seconds(15)),
34 ).toBe(true);
35 expect(
36 Duration.equals(roundTrip(Duration.infinity), Duration.infinity),
37 ).toBe(true);
38 expect(
39 Duration.equals(
40 roundTrip(Duration.negativeInfinity),
41 Duration.negativeInfinity,
42 ),
43 ).toBe(true);
44 });
45
46 test("decodeDuration rebuilds Duration.toJSON without clamping negatives", () => {
47 expect(
48 Duration.equals(
49 decodeDuration(Duration.negativeInfinity.toJSON())!,
50 Duration.negativeInfinity,
51 ),
52 ).toBe(true);
53 expect(encodeState(Duration.millis(15000))).toEqual({
54 [DURATION_MARKER]: Duration.millis(15000).toJSON(),
55 });
56 });
57 },
58);
59
60describe("StateEncoding Date round-trip", { tags: ["unit", "local"] }, () => {
61 const value = {
62 expires: new Date("2027-01-01T00:00:00.000Z"),
63 nested: { dates: [new Date("2028-06-15T12:30:00.000Z")] },
64 text: "unrelated",
65 };
66
67 test("encode → JSON → reviveState rebuilds Date instances (local store path)", () => {
68 const revived = JSON.parse(
69 JSON.stringify(encodeState(value)),
70 reviveState,
71 ) as typeof value;
72 expect(revived.expires).toBeInstanceOf(Date);
73 expect(revived.expires.toISOString()).toBe("2027-01-01T00:00:00.000Z");
74 expect(revived.nested.dates[0]).toBeInstanceOf(Date);
75 expect(revived.nested.dates[0]!.toISOString()).toBe(
76 "2028-06-15T12:30:00.000Z",
77 );
78 expect(revived.text).toBe("unrelated");
79 });
80
81 test("encode → transport JSON → reviveStateRecursive rebuilds Date instances (HTTP store path)", () => {
82 // The HTTP state store sends `encodeState(...)` as a JSON payload and
83 // revives the pre-parsed value on the way back out.
84 const transported = JSON.parse(JSON.stringify(encodeState(value)));
85 const revived = reviveStateRecursive(transported) as typeof value;
86 expect(revived.expires).toBeInstanceOf(Date);
87 expect(revived.expires.toISOString()).toBe("2027-01-01T00:00:00.000Z");
88 expect(revived.nested.dates[0]).toBeInstanceOf(Date);
89 });
90
91 test("the persisted envelope is the marker, not a structural walk", () => {
92 expect(encodeState(new Date("2027-01-01T00:00:00.000Z"))).toEqual({
93 [DATE_MARKER]: "2027-01-01T00:00:00.000Z",
94 });
95 });
96
97 test("legacy rows with bare ISO strings stay strings and never churn", () => {
98 // Rows written before DATE_MARKER persisted plain ISO strings. The
99 // reviver must not promote arbitrary strings to Dates — and the diff
100 // must still treat them as equal to the program's live Date, so legacy
101 // rows re-deploy without a phantom update.
102 const legacy = JSON.parse(
103 JSON.stringify({ expires: "2027-01-01T00:00:00.000Z" }),
104 reviveState,
105 ) as { expires: unknown };
106 expect(typeof legacy.expires).toBe("string");
107 expect(
108 deepEqual(
109 { expires: new Date("2027-01-01T00:00:00.000Z") },
110 { expires: "2027-01-01T00:00:00.000Z" },
111 ),
112 ).toBe(true);
113 });
114
115 test("a user object that happens to carry the marker key is not corrupted into a Date", () => {
116 // Both revivers only rewrite EXACT single-key envelopes, so a user prop
117 // shaped `{ __date__: ..., other: ... }` survives untouched through the
118 // HTTP path and the local JSON.parse path alike.
119 const suspicious = { [DATE_MARKER]: "2027-01-01T00:00:00.000Z", n: 1 };
120 const viaHttp = reviveStateRecursive(
121 JSON.parse(JSON.stringify(suspicious)),
122 ) as typeof suspicious;
123 expect(viaHttp.n).toBe(1);
124 expect(typeof viaHttp[DATE_MARKER]).toBe("string");
125 const viaLocal = JSON.parse(
126 JSON.stringify(suspicious),
127 reviveState,
128 ) as typeof suspicious;
129 expect(viaLocal.n).toBe(1);
130 expect(typeof viaLocal[DATE_MARKER]).toBe("string");
131 });
132});