@company-semantics/contracts 51.0.0 → 51.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The published anchor size bounds, and the proof that `CommentAnchorSchema`
3
+ * actually reads them.
4
+ *
5
+ * WHY THIS SUITE EXISTS. The bounds used to be inline literals in `../anchor`,
6
+ * duplicated by hand in the app's constructor. Naming them removes the
7
+ * duplication only if two things stay true, and neither is mechanically
8
+ * enforced: the VALUES must not have moved during the extraction, and the
9
+ * schema must be reading the named constant rather than a leftover literal
10
+ * that happens to agree with it today.
11
+ *
12
+ * The mirrored corpus in `./fixtures/comment-anchors.json` proves the 64
13
+ * affix bound from both sides (its `oversized-prefix`/`oversized-suffix` rows
14
+ * are 65 chars), but NOTHING anywhere exercised the quote bound at 2000 or a
15
+ * relative position at 512 — a typo in either would have shipped green. Each
16
+ * boundary case below is that missing tripwire: accept at exactly the bound,
17
+ * refuse one code unit past it, phrased in terms of the constant so the test
18
+ * moves with the value it is protecting rather than re-pinning it a third time.
19
+ *
20
+ * The bare value assertions are the other half: they are what catches a bound
21
+ * being CHANGED (as opposed to being read from the wrong place), which no
22
+ * schema-relative assertion can see.
23
+ */
24
+ import { describe, expect, it } from "vitest";
25
+
26
+ import {
27
+ ANCHOR_AFFIX_MAX_CHARS,
28
+ ANCHOR_CONTEXT_CHARS,
29
+ QUOTE_MAX_CHARS,
30
+ RELATIVE_POSITION_MAX_CHARS,
31
+ } from "../anchor-constants.js";
32
+ import { CommentAnchorSchema } from "../anchor.js";
33
+
34
+ /** A well-formed text anchor; override one field per boundary test. */
35
+ const makeTextAnchor = (over: Record<string, unknown> = {}) => ({
36
+ type: "text",
37
+ v: 1,
38
+ quote: "the quarterly goals",
39
+ prefix: "we restated ",
40
+ suffix: " last week",
41
+ ...over,
42
+ });
43
+
44
+ /** A well-formed insertion anchor; override one field per boundary test. */
45
+ const makeInsertionAnchor = (over: Record<string, unknown> = {}) => ({
46
+ type: "text-insertion",
47
+ v: 1,
48
+ leftContext: "the quarterly ",
49
+ rightContext: " goals",
50
+ ...over,
51
+ });
52
+
53
+ describe("the published anchor size bounds", () => {
54
+ it("holds the values the wire contract was published with", () => {
55
+ // These are durable promises, not knobs: anchors already in storage were
56
+ // written against them, and the backend route boundary declares the same
57
+ // numbers independently. Changing one here is a wire change.
58
+ expect(ANCHOR_CONTEXT_CHARS).toBe(32);
59
+ expect(ANCHOR_AFFIX_MAX_CHARS).toBe(64);
60
+ expect(QUOTE_MAX_CHARS).toBe(2000);
61
+ expect(RELATIVE_POSITION_MAX_CHARS).toBe(512);
62
+ });
63
+
64
+ it("keeps capture strictly narrower than accept", () => {
65
+ // The two "context" numbers are NOT a duplication waiting to be collapsed.
66
+ // A constructor captures ANCHOR_CONTEXT_CHARS; the schema accepts up to
67
+ // ANCHOR_AFFIX_MAX_CHARS, so an anchor written by a client with a wider
68
+ // capture still parses. Narrowing accept to capture would refuse durable
69
+ // jsonb the server never re-validates.
70
+ expect(ANCHOR_CONTEXT_CHARS).toBeLessThan(ANCHOR_AFFIX_MAX_CHARS);
71
+ });
72
+ });
73
+
74
+ describe("CommentAnchorSchema reads the published bounds", () => {
75
+ it("accepts a quote of exactly QUOTE_MAX_CHARS and refuses one more", () => {
76
+ // The quote is the durable fallback the passage is re-found by, so it is
77
+ // bounded generously — but bounded, or an anchor becomes a second copy of
78
+ // the document in a column nothing on the server reads.
79
+ const atBound = "q".repeat(QUOTE_MAX_CHARS);
80
+ expect(
81
+ CommentAnchorSchema.safeParse(makeTextAnchor({ quote: atBound })).success,
82
+ ).toBe(true);
83
+ expect(
84
+ CommentAnchorSchema.safeParse(makeTextAnchor({ quote: `${atBound}q` }))
85
+ .success,
86
+ ).toBe(false);
87
+ });
88
+
89
+ it("refuses an empty quote", () => {
90
+ // `quote` is required AND non-empty: an anchor with no quote is unplaceable
91
+ // the first time the document is edited.
92
+ expect(
93
+ CommentAnchorSchema.safeParse(makeTextAnchor({ quote: "" })).success,
94
+ ).toBe(false);
95
+ });
96
+
97
+ it("bounds prefix and suffix at ANCHOR_AFFIX_MAX_CHARS", () => {
98
+ const atBound = "x".repeat(ANCHOR_AFFIX_MAX_CHARS);
99
+ expect(
100
+ CommentAnchorSchema.safeParse(
101
+ makeTextAnchor({ prefix: atBound, suffix: atBound }),
102
+ ).success,
103
+ ).toBe(true);
104
+ expect(
105
+ CommentAnchorSchema.safeParse(makeTextAnchor({ prefix: `${atBound}x` }))
106
+ .success,
107
+ ).toBe(false);
108
+ expect(
109
+ CommentAnchorSchema.safeParse(makeTextAnchor({ suffix: `${atBound}x` }))
110
+ .success,
111
+ ).toBe(false);
112
+ });
113
+
114
+ it("bounds leftContext and rightContext at the SAME affix bound", () => {
115
+ // One constant, four fields: the insertion seam is context in the same
116
+ // sense prefix/suffix are, and a reader must not have to check whether the
117
+ // two variants drifted apart.
118
+ const atBound = "x".repeat(ANCHOR_AFFIX_MAX_CHARS);
119
+ expect(
120
+ CommentAnchorSchema.safeParse(
121
+ makeInsertionAnchor({ leftContext: atBound, rightContext: atBound }),
122
+ ).success,
123
+ ).toBe(true);
124
+ expect(
125
+ CommentAnchorSchema.safeParse(
126
+ makeInsertionAnchor({ rightContext: `${atBound}x` }),
127
+ ).success,
128
+ ).toBe(false);
129
+ });
130
+
131
+ it("bounds every relative position at RELATIVE_POSITION_MAX_CHARS", () => {
132
+ // relStart/relEnd/relPos are opaque base64 the backend never decodes, so
133
+ // the length and the charset are the ONLY things anyone checks. Applied to
134
+ // all three fields because a position that is bounded on one path and
135
+ // unbounded on another is not bounded.
136
+ const atBound = "A".repeat(RELATIVE_POSITION_MAX_CHARS);
137
+ const overBound = `${atBound}A`;
138
+
139
+ expect(
140
+ CommentAnchorSchema.safeParse(
141
+ makeTextAnchor({ relStart: atBound, relEnd: atBound }),
142
+ ).success,
143
+ ).toBe(true);
144
+ expect(
145
+ CommentAnchorSchema.safeParse(makeTextAnchor({ relStart: overBound }))
146
+ .success,
147
+ ).toBe(false);
148
+ expect(
149
+ CommentAnchorSchema.safeParse(makeTextAnchor({ relEnd: overBound }))
150
+ .success,
151
+ ).toBe(false);
152
+ expect(
153
+ CommentAnchorSchema.safeParse(makeInsertionAnchor({ relPos: atBound }))
154
+ .success,
155
+ ).toBe(true);
156
+ expect(
157
+ CommentAnchorSchema.safeParse(makeInsertionAnchor({ relPos: overBound }))
158
+ .success,
159
+ ).toBe(false);
160
+ });
161
+ });
@@ -0,0 +1,300 @@
1
+ /**
2
+ * The construction half — range → anchor, and back again.
3
+ *
4
+ * WHAT THIS SUITE EXISTS TO KILL. The plausible-looking wrong constructor
5
+ * TRUNCATES an over-long quote instead of refusing it. Its output is a
6
+ * well-formed anchor: it parses, it passes the route boundary, and it resolves
7
+ * — onto a range the caller never selected. Nothing throws, and the comment
8
+ * reads as a considered remark about the first `QUOTE_MAX_CHARS` characters of
9
+ * a passage its author selected all of. `null` means "compose no comment",
10
+ * which a caller can report; a truncated quote means "compose a comment about
11
+ * something else", which nobody can. Every `toBeNull()` below is a case where
12
+ * that implementation returns an object and looks entirely healthy.
13
+ *
14
+ * The second wrong implementation returns an unvalidated object literal that
15
+ * the route boundary later rejects — a comment lost AFTER it was composed,
16
+ * which is the worst moment to lose one. `every constructed anchor parses
17
+ * through the published schema` is the standing assertion against it.
18
+ *
19
+ * The third silently promotes an empty range to a text anchor with an empty
20
+ * quote, rather than refusing it. That anchor is unplaceable the first time the
21
+ * document is edited, and the zero-width case has its own constructor.
22
+ *
23
+ * The round-trip cases run the construction half against the SHIPPED resolver
24
+ * rather than against a restatement of it. That is the property that actually
25
+ * matters: the two halves have to agree about the same geometry, and a suite
26
+ * asserting only the anchor's FIELDS would sit green over a capture width that
27
+ * disagreed with what the resolver searches for.
28
+ */
29
+ import { describe, expect, it } from "vitest";
30
+
31
+ import { CommentAnchorSchema, type CommentAnchor } from "../anchor.js";
32
+ import { ANCHOR_CONTEXT_CHARS, QUOTE_MAX_CHARS } from "../anchor-constants.js";
33
+ import {
34
+ createTextAnchorFromRange,
35
+ createTextInsertionAnchorAt,
36
+ type TextRange,
37
+ } from "../create.js";
38
+ import { resolveAnchorFromText } from "../resolve.js";
39
+
40
+ /** A range named by its quote, so no offset in this file is hand-counted. */
41
+ const rangeOf = (text: string, quote: string): TextRange => {
42
+ const start = text.indexOf(quote);
43
+ if (start === -1) throw new Error("fixture quote absent from fixture text");
44
+ return { text, start, end: start + quote.length };
45
+ };
46
+
47
+ /** Construct, refusing to continue on a refusal — a `null` here is a broken
48
+ * fixture, not the behaviour under test, and `expect(x).not.toBeNull()` would
49
+ * leave the rest of the case running against `null`. */
50
+ const constructed = (range: TextRange): CommentAnchor => {
51
+ const anchor = createTextAnchorFromRange(range);
52
+ if (anchor === null) throw new Error("fixture range refused");
53
+ return anchor;
54
+ };
55
+
56
+ const insertionAt = (text: string, point: number): CommentAnchor => {
57
+ const anchor = createTextInsertionAnchorAt(text, point);
58
+ if (anchor === null) throw new Error("fixture point refused");
59
+ return anchor;
60
+ };
61
+
62
+ const doc = "The board approved the revised plan on Friday, then adjourned.";
63
+
64
+ describe("createTextAnchorFromRange — range to anchor", () => {
65
+ it("captures the quote and both contexts from the surrounding text", () => {
66
+ const anchor = constructed(rangeOf(doc, "revised plan"));
67
+
68
+ expect(anchor).toEqual({
69
+ type: "text",
70
+ v: 1,
71
+ quote: "revised plan",
72
+ prefix: "The board approved the ",
73
+ suffix: " on Friday, then adjourned.",
74
+ });
75
+ });
76
+
77
+ it("produces no relative positions — that seam stays in consumers", () => {
78
+ const anchor = constructed(rangeOf(doc, "revised plan"));
79
+
80
+ // `yjs` cannot be imported here (vocabulary-guard), and a consumer holding
81
+ // a Y.Text merges the encoded pair on. Absent must mean "this side could
82
+ // not produce one", never "this side produced an empty one".
83
+ expect(anchor).not.toHaveProperty("relStart");
84
+ expect(anchor).not.toHaveProperty("relEnd");
85
+ });
86
+
87
+ it("captures at most ANCHOR_CONTEXT_CHARS of context on each side", () => {
88
+ const long = "x".repeat(200) + "MIDDLE" + "y".repeat(200);
89
+ const anchor = constructed(rangeOf(long, "MIDDLE"));
90
+
91
+ expect(anchor).toMatchObject({
92
+ prefix: "x".repeat(ANCHOR_CONTEXT_CHARS),
93
+ suffix: "y".repeat(ANCHOR_CONTEXT_CHARS),
94
+ });
95
+ });
96
+
97
+ it("refuses an EMPTY range — the zero-width case has its own constructor", () => {
98
+ const start = doc.indexOf("revised");
99
+
100
+ expect(
101
+ createTextAnchorFromRange({ text: doc, start, end: start }),
102
+ ).toBeNull();
103
+ });
104
+
105
+ it("refuses an out-of-order range", () => {
106
+ expect(
107
+ createTextAnchorFromRange({ text: doc, start: 12, end: 4 }),
108
+ ).toBeNull();
109
+ });
110
+
111
+ it("refuses a range reaching past the end of the text", () => {
112
+ expect(
113
+ createTextAnchorFromRange({ text: doc, start: 4, end: doc.length + 1 }),
114
+ ).toBeNull();
115
+ });
116
+
117
+ it("refuses a negative start", () => {
118
+ expect(
119
+ createTextAnchorFromRange({ text: doc, start: -1, end: 4 }),
120
+ ).toBeNull();
121
+ });
122
+
123
+ it("refuses non-integer offsets", () => {
124
+ expect(
125
+ createTextAnchorFromRange({ text: doc, start: 1.5, end: 6 }),
126
+ ).toBeNull();
127
+ expect(
128
+ createTextAnchorFromRange({ text: doc, start: 1, end: 6.5 }),
129
+ ).toBeNull();
130
+ expect(
131
+ createTextAnchorFromRange({ text: doc, start: 1, end: NaN }),
132
+ ).toBeNull();
133
+ });
134
+
135
+ it("accepts a quote of exactly QUOTE_MAX_CHARS", () => {
136
+ const text = "z".repeat(QUOTE_MAX_CHARS);
137
+
138
+ expect(
139
+ createTextAnchorFromRange({ text, start: 0, end: text.length }),
140
+ ).toMatchObject({ type: "text", quote: text });
141
+ });
142
+
143
+ it("refuses an over-long quote rather than truncating it", () => {
144
+ const text = "z".repeat(QUOTE_MAX_CHARS + 1);
145
+
146
+ // Truncating here yields an anchor that parses, resolves, and highlights a
147
+ // range the caller never selected. Refusal is the only legible outcome.
148
+ expect(
149
+ createTextAnchorFromRange({ text, start: 0, end: text.length }),
150
+ ).toBeNull();
151
+ });
152
+ });
153
+
154
+ describe("createTextInsertionAnchorAt — the zero-width point", () => {
155
+ const text = "Hello world";
156
+
157
+ it("captures a bilateral context at a point between two characters", () => {
158
+ expect(insertionAt(text, 5)).toEqual({
159
+ type: "text-insertion",
160
+ v: 1,
161
+ leftContext: "Hello",
162
+ rightContext: " world",
163
+ });
164
+ });
165
+
166
+ it("allows position 0 — the left context is legitimately empty there", () => {
167
+ expect(insertionAt(text, 0)).toEqual({
168
+ type: "text-insertion",
169
+ v: 1,
170
+ leftContext: "",
171
+ rightContext: "Hello world",
172
+ });
173
+ });
174
+
175
+ it("allows the document end — the right context is legitimately empty", () => {
176
+ expect(insertionAt(text, text.length)).toEqual({
177
+ type: "text-insertion",
178
+ v: 1,
179
+ leftContext: "Hello world",
180
+ rightContext: "",
181
+ });
182
+ });
183
+
184
+ it("allows position 0 of an EMPTY document — the one point there is", () => {
185
+ expect(insertionAt("", 0)).toEqual({
186
+ type: "text-insertion",
187
+ v: 1,
188
+ leftContext: "",
189
+ rightContext: "",
190
+ });
191
+ });
192
+
193
+ it("refuses a point outside the text", () => {
194
+ expect(createTextInsertionAnchorAt(text, -1)).toBeNull();
195
+ expect(createTextInsertionAnchorAt(text, text.length + 1)).toBeNull();
196
+ });
197
+
198
+ it("refuses a non-integer point", () => {
199
+ expect(createTextInsertionAnchorAt(text, 2.5)).toBeNull();
200
+ expect(createTextInsertionAnchorAt(text, NaN)).toBeNull();
201
+ });
202
+
203
+ it("produces no relative position — that seam stays in consumers", () => {
204
+ expect(insertionAt(text, 5)).not.toHaveProperty("relPos");
205
+ });
206
+ });
207
+
208
+ describe("the two halves agree about the same geometry", () => {
209
+ it("constructed anchors round-trip through resolveAnchorFromText", () => {
210
+ for (const quote of ["The board", "revised plan", "adjourned."]) {
211
+ const range = rangeOf(doc, quote);
212
+
213
+ expect(resolveAnchorFromText(constructed(range), doc)).toEqual({
214
+ status: "resolved",
215
+ start: range.start,
216
+ end: range.end,
217
+ rung: 2,
218
+ });
219
+ }
220
+ });
221
+
222
+ it("round-trips a range flanked by surrogate pairs on both sides", () => {
223
+ // Both context windows land INSIDE an astral character: the prefix window
224
+ // opens on the low half of 🎯 and the suffix window closes on the high half
225
+ // of 🚀. A constructor that sliced blindly would emit a lone surrogate,
226
+ // which is ill-formed once the anchor is encoded for the wire.
227
+ const text =
228
+ "🎯" +
229
+ "a".repeat(ANCHOR_CONTEXT_CHARS - 1) +
230
+ "SELECTED" +
231
+ "b".repeat(ANCHOR_CONTEXT_CHARS - 1) +
232
+ "🚀tail";
233
+ const range = rangeOf(text, "SELECTED");
234
+ const anchor = constructed(range);
235
+
236
+ expect(anchor).toMatchObject({
237
+ prefix: "a".repeat(ANCHOR_CONTEXT_CHARS - 1),
238
+ suffix: "b".repeat(ANCHOR_CONTEXT_CHARS - 1),
239
+ });
240
+ expect(resolveAnchorFromText(anchor, text)).toEqual({
241
+ status: "resolved",
242
+ start: range.start,
243
+ end: range.end,
244
+ rung: 2,
245
+ });
246
+ });
247
+
248
+ it("round-trips an insertion point through the seam rung", () => {
249
+ const text = "Ship it on Friday.";
250
+
251
+ for (const point of [0, 4, text.length]) {
252
+ expect(resolveAnchorFromText(insertionAt(text, point), text)).toEqual({
253
+ status: "resolved",
254
+ start: point,
255
+ end: point,
256
+ rung: 2,
257
+ });
258
+ }
259
+ });
260
+
261
+ it("round-trips the sole point of an empty document", () => {
262
+ expect(resolveAnchorFromText(insertionAt("", 0), "")).toEqual({
263
+ status: "resolved",
264
+ start: 0,
265
+ end: 0,
266
+ rung: 2,
267
+ });
268
+ });
269
+
270
+ it("captured context disambiguates a quote that repeats", () => {
271
+ // The bare quote is ambiguous here; the constructor's prefix is what makes
272
+ // the SECOND occurrence identifiable. This is the capture width earning its
273
+ // keep, not an incidental pass.
274
+ const rows = "item done\nitem done";
275
+ const second = rows.lastIndexOf("item done");
276
+
277
+ expect(
278
+ resolveAnchorFromText(
279
+ constructed({ text: rows, start: second, end: rows.length }),
280
+ rows,
281
+ ),
282
+ ).toEqual({ status: "resolved", start: second, end: rows.length, rung: 2 });
283
+ });
284
+
285
+ it("every constructed anchor parses through the published schema", () => {
286
+ const anchors = [
287
+ constructed(rangeOf(doc, "revised plan")),
288
+ insertionAt(doc, 0),
289
+ insertionAt(doc, doc.length),
290
+ insertionAt("", 0),
291
+ ];
292
+
293
+ // An anchor the route boundary rejects is a comment lost after it was
294
+ // composed, so the constructors validate before returning rather than
295
+ // asserting the shape they just built.
296
+ for (const anchor of anchors) {
297
+ expect(() => CommentAnchorSchema.parse(anchor)).not.toThrow();
298
+ }
299
+ });
300
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The application receipt: one published key, one closed shape, one parser.
3
+ *
4
+ * WHAT THIS SUITE EXISTS TO KILL. The first wrong implementation returns the
5
+ * raw map value unvalidated — `readSuggestionReceipt = (v) => v as
6
+ * SuggestionReceipt`. It compiles, it is green against every well-formed row,
7
+ * and it never throws. Its symptom appears only when a peer writes something
8
+ * else into the map: a receipt whose `claimToken` is `undefined` is still
9
+ * TRUTHY-SHAPED enough to read as "this edit already landed", so the accept
10
+ * control is withdrawn from a live suggestion with no way back, and the token
11
+ * that gets posted is garbage. Every `toBeNull()` below is a row that
12
+ * implementation hands back as an object.
13
+ *
14
+ * The second is a parser that accepts an EMPTY `claimToken` or an empty
15
+ * `byUserId` — "it is a string, so it parsed". An empty token is not a lease;
16
+ * it is the absence of one wearing the right type, and it completes nothing.
17
+ *
18
+ * The third accepts a stringified `appliedAt`, or a `NaN` one. This is not a
19
+ * theoretical row: a CRDT map replicates raw JavaScript values with no schema
20
+ * of its own, so a peer can literally put `NaN` there.
21
+ *
22
+ * The fourth is a rename. `SUGGESTION_RECEIPTS_MAP` is the wire name of a root
23
+ * map, so a receipt written under any other spelling lands where nobody reads
24
+ * and the reader watches where nobody writes — with nothing anywhere throwing.
25
+ * The constant is therefore tested by restating its literal, so a rename is a
26
+ * red test rather than a silent second name.
27
+ */
28
+ import { describe, expect, it } from "vitest";
29
+
30
+ import {
31
+ SUGGESTION_RECEIPTS_MAP,
32
+ SuggestionReceiptSchema,
33
+ readSuggestionReceipt,
34
+ } from "../receipt.js";
35
+
36
+ /**
37
+ * A well-formed row. Every negative case below mutates exactly ONE field of
38
+ * this, because a hand-built broken object can pass for the wrong reason — it
39
+ * fails because of the field nobody was testing.
40
+ */
41
+ const makeReceipt = (
42
+ overrides: Record<string, unknown> = {},
43
+ ): Record<string, unknown> => ({
44
+ claimToken: "a".repeat(64),
45
+ appliedAt: 1_760_000_000_000,
46
+ byUserId: "user_01JQ",
47
+ ...overrides,
48
+ });
49
+
50
+ /** The same row with one field removed rather than replaced. */
51
+ const withoutField = (field: string): Record<string, unknown> => {
52
+ const row = makeReceipt();
53
+ delete row[field];
54
+ return row;
55
+ };
56
+
57
+ describe("SUGGESTION_RECEIPTS_MAP — the key is the contract", () => {
58
+ it("names the root map receipts are written into", () => {
59
+ expect(SUGGESTION_RECEIPTS_MAP).toBe("suggestionReceipts");
60
+ });
61
+ });
62
+
63
+ describe("a receipt is parsed, not trusted", () => {
64
+ it("accepts a well-formed row and returns it field for field", () => {
65
+ const row = makeReceipt();
66
+
67
+ expect(readSuggestionReceipt(row)).toEqual({
68
+ claimToken: "a".repeat(64),
69
+ appliedAt: 1_760_000_000_000,
70
+ byUserId: "user_01JQ",
71
+ });
72
+ });
73
+
74
+ it("returns a parsed value, not the caller's object", () => {
75
+ // The pass-through implementation returns the input by reference. It is
76
+ // green on the assertion above and wrong on every assertion below it.
77
+ const row = makeReceipt();
78
+
79
+ expect(readSuggestionReceipt(row)).not.toBe(row);
80
+ });
81
+
82
+ it.each(["claimToken", "appliedAt", "byUserId"])(
83
+ "drops a row missing %s rather than returning a partial receipt",
84
+ (field) => {
85
+ expect(readSuggestionReceipt(withoutField(field))).toBeNull();
86
+ },
87
+ );
88
+
89
+ it("drops a row whose claimToken is empty", () => {
90
+ // An empty token is the absence of a lease wearing the right type. It
91
+ // completes nothing, and a receipt carrying one proves nothing landed.
92
+ expect(readSuggestionReceipt(makeReceipt({ claimToken: "" }))).toBeNull();
93
+ });
94
+
95
+ it("drops a row whose byUserId is empty", () => {
96
+ expect(readSuggestionReceipt(makeReceipt({ byUserId: "" }))).toBeNull();
97
+ });
98
+
99
+ it("drops a row whose appliedAt is a string", () => {
100
+ expect(
101
+ readSuggestionReceipt(makeReceipt({ appliedAt: "1760000000000" })),
102
+ ).toBeNull();
103
+ });
104
+
105
+ it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])(
106
+ "drops a row whose appliedAt is %p — a CRDT replicates raw values",
107
+ (appliedAt) => {
108
+ expect(readSuggestionReceipt(makeReceipt({ appliedAt }))).toBeNull();
109
+ },
110
+ );
111
+
112
+ it("drops a row carrying a field outside the shape", () => {
113
+ // Strict, and the drop direction is why that is safe: an unrecognised row
114
+ // reads as "no receipt is known here", which every consumer must already
115
+ // handle, because a reader with no replica sees exactly the same thing.
116
+ expect(
117
+ readSuggestionReceipt(makeReceipt({ appliedBySomethingElse: true })),
118
+ ).toBeNull();
119
+ });
120
+
121
+ it.each([null, undefined, "suggestionReceipts", 7, []])(
122
+ "drops %p, which is not a row at all",
123
+ (value) => {
124
+ expect(readSuggestionReceipt(value)).toBeNull();
125
+ },
126
+ );
127
+
128
+ it("never throws, whatever the peer wrote", () => {
129
+ expect(() => readSuggestionReceipt(Symbol("nonsense"))).not.toThrow();
130
+ });
131
+ });
132
+
133
+ describe("SuggestionReceiptSchema", () => {
134
+ it("is the schema the parser reads, not a second description of it", () => {
135
+ // A parser hand-rolling its own checks beside the schema is how the two
136
+ // drift; this asserts they agree on a row the schema accepts.
137
+ const row = makeReceipt();
138
+
139
+ expect(readSuggestionReceipt(row)).toEqual(
140
+ SuggestionReceiptSchema.parse(row),
141
+ );
142
+ });
143
+ });