@domino-sdk/relay 1.11.1 → 1.14.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.
Files changed (50) hide show
  1. package/README.md +1 -0
  2. package/build.mjs +15 -0
  3. package/dist/accounts-5ciAkCMP.d.ts +147 -0
  4. package/dist/{activity-CNiPDPK6.d.ts → activity-D_R3J-gU.d.ts} +115 -115
  5. package/dist/attempt-captures-DZldPZJc.d.ts +285 -0
  6. package/dist/authoring.d.ts +49 -47
  7. package/dist/authoring.js +6 -4
  8. package/dist/{automatic-outcomes-kPIDv6X_.d.ts → automatic-outcomes-6lJGnSDe.d.ts} +1 -1
  9. package/dist/browser.d.ts +8 -7
  10. package/dist/browser.js +7 -6
  11. package/dist/capability-error-oTd3eYFH.d.ts +151 -0
  12. package/dist/chunk-2UAVZCMH.js +314 -0
  13. package/dist/chunk-5TS3OAWS.js +470 -0
  14. package/dist/{chunk-PUDL4JBY.js → chunk-6DD7DVNQ.js} +2 -2
  15. package/dist/{chunk-AGFUDCNM.js → chunk-C3CB4WIQ.js} +1 -1
  16. package/dist/{chunk-T657WO56.js → chunk-LLMJ2SUT.js} +6 -2
  17. package/dist/chunk-NIYCAW2I.js +79 -0
  18. package/dist/chunk-UJACD6K2.js +987 -0
  19. package/dist/{chunk-XK5E4HAS.js → chunk-VBHFJH3P.js} +80 -28
  20. package/dist/chunk-W335CWYJ.js +53 -0
  21. package/dist/{chunk-X7ECZ3AK.js → chunk-XMBM2XD5.js} +1 -1
  22. package/dist/{chunk-2K2QBPAD.js → chunk-ZLYBUM7K.js} +6 -4
  23. package/dist/{client-fUSERMAC.d.ts → client-BTd_CyX3.d.ts} +26 -6
  24. package/dist/{groups-BK8ts2yK.d.ts → groups-4Jb3uYFO.d.ts} +1 -1
  25. package/dist/{images-aw6W7vFZ.d.ts → images-D5kpqHxv.d.ts} +1 -1
  26. package/dist/index.d.ts +9 -8
  27. package/dist/index.js +8 -6
  28. package/dist/{capability-error-ty3SqD_b.d.ts → integrations-Cr9EhGI_.d.ts} +1 -149
  29. package/dist/internal.d.ts +32 -5
  30. package/dist/internal.js +8 -3
  31. package/dist/{offers-C2FANzrG.d.ts → offers-BFI5t-e-.d.ts} +2 -2
  32. package/dist/{pages-Chb6OZSB.d.ts → pages-nIoMT9J3.d.ts} +1 -1
  33. package/dist/{participant-activity-DkL3nY5a.d.ts → participant-activity-DYdUUhw2.d.ts} +1 -0
  34. package/dist/portal-proxy.js +1 -1
  35. package/dist/react/index.d.ts +246 -177
  36. package/dist/react/index.js +954 -383
  37. package/dist/{referral-reward-kinds-M3f0PdiR.d.ts → referral-reward-kinds-DnvQXL8L.d.ts} +100 -3
  38. package/dist/{schema-DEM8y8Fm.d.ts → schema-K-Pg5lPz.d.ts} +1 -1
  39. package/dist/schema.d.ts +316 -314
  40. package/dist/schema.js +16 -14
  41. package/dist/server/index.d.ts +2546 -377
  42. package/dist/server/index.js +594 -841
  43. package/dist/{sign-in-options-iWOp9puZ.d.ts → sign-in-options-DBjMzgnu.d.ts} +2 -2
  44. package/dist/submission-input-CPIsJt-_.d.ts +146 -0
  45. package/dist/testing.d.ts +641 -0
  46. package/dist/testing.js +1282 -0
  47. package/package.json +6 -2
  48. package/dist/chunk-OCXGG75K.js +0 -171
  49. package/dist/chunk-ZFABFKIY.js +0 -1295
  50. package/dist/submission-input-BbBJTVlT.d.ts +0 -290
@@ -0,0 +1,1282 @@
1
+ import {
2
+ attemptCaptureSchema
3
+ } from "./chunk-NIYCAW2I.js";
4
+ import {
5
+ CapabilityError
6
+ } from "./chunk-MC6V6VMG.js";
7
+ import {
8
+ parseImageResult
9
+ } from "./chunk-W335CWYJ.js";
10
+ import {
11
+ imageRequestSchema,
12
+ xConnectionSchema,
13
+ xPostSchema,
14
+ xProfileSchema,
15
+ xRequestSchema
16
+ } from "./chunk-ZGH6NEG6.js";
17
+ import {
18
+ cosmosNetworkSchema,
19
+ evmChainIdSchema,
20
+ linkedWalletSchema,
21
+ solanaMainnet,
22
+ walletBalanceSchema
23
+ } from "./chunk-2UAVZCMH.js";
24
+ import "./chunk-NPGENI7K.js";
25
+ import {
26
+ contentPerformanceSnapshotSchema,
27
+ discordMemberInputSchema,
28
+ discordMemberSchema,
29
+ discordMessageInputSchema,
30
+ discordMessageSchema,
31
+ identifier,
32
+ memberIdentitySchema,
33
+ photoResultSchema,
34
+ quizEvidenceSchema,
35
+ rewardKey,
36
+ textEvidenceSchema,
37
+ textResultSchema,
38
+ textReviewInputSchema,
39
+ trackerContentQuerySchema,
40
+ trackerContentSchema,
41
+ trackerPageSchema,
42
+ trackerQuerySchema,
43
+ trackerSummarySchema,
44
+ trackerTriggerContextSchema,
45
+ triggerSchema,
46
+ urlEvidenceSchema,
47
+ validateSubmissionValue
48
+ } from "./chunk-46C33ASD.js";
49
+
50
+ // src/testing/run.ts
51
+ import { z as z3 } from "zod";
52
+
53
+ // src/testing/calendar.ts
54
+ var dayMilliseconds = 24 * 60 * 60 * 1e3;
55
+ function evaluationCalendar(snapshot, eligibleAt) {
56
+ const formatters = /* @__PURE__ */ new Map();
57
+ const dateAt = (at, timeZone) => {
58
+ let formatter = formatters.get(timeZone);
59
+ if (!formatter) {
60
+ formatter = new Intl.DateTimeFormat("en", {
61
+ calendar: "iso8601",
62
+ numberingSystem: "latn",
63
+ timeZone,
64
+ year: "numeric",
65
+ month: "2-digit",
66
+ day: "2-digit"
67
+ });
68
+ formatters.set(timeZone, formatter);
69
+ }
70
+ const parts = Object.fromEntries(
71
+ formatter.formatToParts(at).filter(
72
+ ({ type }) => type === "year" || type === "month" || type === "day"
73
+ ).map(({ type, value }) => [type, value])
74
+ );
75
+ return `${parts.year}-${parts.month}-${parts.day}`;
76
+ };
77
+ const firstInstantOfDate = (at, date, timeZone) => {
78
+ let low = Math.max(0, at - 2 * dayMilliseconds);
79
+ let high = at;
80
+ while (low < high) {
81
+ const middle = Math.floor((low + high) / 2);
82
+ if (dateAt(middle, timeZone) < date) {
83
+ low = middle + 1;
84
+ } else {
85
+ high = middle;
86
+ }
87
+ }
88
+ return low;
89
+ };
90
+ const firstInstantAfterDate = (at, date, timeZone) => {
91
+ let low = at;
92
+ let high = at + 2 * dayMilliseconds;
93
+ while (dateAt(high, timeZone) <= date) {
94
+ high += dayMilliseconds;
95
+ }
96
+ while (low < high) {
97
+ const middle = Math.floor((low + high) / 2);
98
+ if (dateAt(middle, timeZone) <= date) {
99
+ low = middle + 1;
100
+ } else {
101
+ high = middle;
102
+ }
103
+ }
104
+ return low;
105
+ };
106
+ return {
107
+ day(at = eligibleAt) {
108
+ if (!Number.isSafeInteger(at) || at < 0) {
109
+ throw new TypeError("Calendar time must be a non-negative integer");
110
+ }
111
+ let index = snapshot.segments.length - 1;
112
+ while (index >= 0 && snapshot.segments[index].effectiveAt > at) {
113
+ index--;
114
+ }
115
+ const segment = snapshot.segments[index];
116
+ const timeZone = segment?.timeZone ?? "UTC";
117
+ const date = dateAt(at, timeZone);
118
+ const next = snapshot.segments[index + 1];
119
+ return {
120
+ key: `${date}@${timeZone}`,
121
+ date,
122
+ timeZone,
123
+ start: Math.max(
124
+ firstInstantOfDate(at, date, timeZone),
125
+ segment?.effectiveAt ?? 0
126
+ ),
127
+ end: Math.min(
128
+ firstInstantAfterDate(at, date, timeZone),
129
+ next?.effectiveAt ?? Number.POSITIVE_INFINITY
130
+ )
131
+ };
132
+ }
133
+ };
134
+ }
135
+
136
+ // src/testing/capabilities.ts
137
+ import { z } from "zod";
138
+ var trackerInput = {
139
+ reference: identifier,
140
+ query: trackerQuerySchema,
141
+ asOf: z.number().int().nonnegative().safe()
142
+ };
143
+ var capabilityRequestSchemas = {
144
+ "wallet-balance": z.strictObject({
145
+ kind: z.literal("wallet-balance"),
146
+ chain: z.union([
147
+ z.literal(solanaMainnet),
148
+ evmChainIdSchema,
149
+ cosmosNetworkSchema
150
+ ]),
151
+ token: z.string().min(1).max(128).nullable(),
152
+ address: z.string().min(1).max(100).nullable()
153
+ }),
154
+ "tracker-content": z.strictObject({
155
+ kind: z.literal("tracker-content"),
156
+ reference: identifier,
157
+ query: trackerContentQuerySchema
158
+ }),
159
+ x: z.strictObject({ kind: z.literal("x"), input: xRequestSchema }),
160
+ image: z.strictObject({
161
+ kind: z.literal("image"),
162
+ input: imageRequestSchema
163
+ }),
164
+ text: textReviewInputSchema.extend({ kind: z.literal("text") }).strict(),
165
+ "tracker-records": z.strictObject({
166
+ kind: z.literal("tracker-records"),
167
+ ...trackerInput
168
+ }),
169
+ "tracker-summary": z.strictObject({
170
+ kind: z.literal("tracker-summary"),
171
+ ...trackerInput
172
+ }),
173
+ photo: z.strictObject({
174
+ kind: z.literal("photo"),
175
+ criteria: z.string().min(1).max(2e3),
176
+ image: z.string().optional()
177
+ }),
178
+ discord: z.strictObject({
179
+ kind: z.literal("discord"),
180
+ input: z.union([discordMemberInputSchema, discordMessageInputSchema])
181
+ }),
182
+ "wallet-list": z.strictObject({ kind: z.literal("wallet-list") }),
183
+ "member-identities": z.strictObject({
184
+ kind: z.literal("member-identities")
185
+ })
186
+ };
187
+ var capabilityRequestSchema = z.discriminatedUnion("kind", [
188
+ capabilityRequestSchemas["wallet-balance"],
189
+ capabilityRequestSchemas["tracker-content"],
190
+ capabilityRequestSchemas.x,
191
+ capabilityRequestSchemas.image,
192
+ capabilityRequestSchemas.text,
193
+ capabilityRequestSchemas["tracker-records"],
194
+ capabilityRequestSchemas["tracker-summary"],
195
+ capabilityRequestSchemas.photo,
196
+ capabilityRequestSchemas.discord,
197
+ capabilityRequestSchemas["wallet-list"],
198
+ capabilityRequestSchemas["member-identities"]
199
+ ]);
200
+ function parseCapabilityRequest(raw) {
201
+ return capabilityRequestSchema.safeParse(raw);
202
+ }
203
+ function xResult(request, value) {
204
+ switch (request.input.operation) {
205
+ case "connection":
206
+ return xConnectionSchema.nullable().parse(value);
207
+ case "profile":
208
+ return xProfileSchema.nullable().parse(value);
209
+ case "post":
210
+ return xPostSchema.nullable().parse(value);
211
+ case "follows":
212
+ case "reposted":
213
+ return z.boolean().nullable().parse(value);
214
+ }
215
+ }
216
+ function parseCapabilityResult(request, value) {
217
+ switch (request.kind) {
218
+ case "wallet-balance":
219
+ return walletBalanceSchema.nullable().parse(value);
220
+ case "tracker-content":
221
+ return trackerContentSchema.nullable().parse(value);
222
+ case "x":
223
+ return xResult(request, value);
224
+ case "image":
225
+ return parseImageResult(request.input, value);
226
+ case "text":
227
+ return textResultSchema.strip().parse(value);
228
+ case "tracker-records":
229
+ return trackerPageSchema.parse(value);
230
+ case "tracker-summary":
231
+ return trackerSummarySchema.parse(value);
232
+ case "photo":
233
+ return photoResultSchema.strip().parse(value);
234
+ case "discord":
235
+ return ("channelId" in request.input ? discordMessageSchema : discordMemberSchema).nullable().parse(value);
236
+ case "wallet-list":
237
+ return z.array(linkedWalletSchema).min(1).nullable().parse(value);
238
+ case "member-identities":
239
+ return z.array(memberIdentitySchema).min(1).nullable().parse(value);
240
+ }
241
+ }
242
+ function imageReferences(input) {
243
+ return input.operation === "compare" || input.operation === "review" ? input.images : [input.image];
244
+ }
245
+ function capabilityIntegrations(request) {
246
+ switch (request.kind) {
247
+ case "x":
248
+ return ["x"];
249
+ case "discord":
250
+ return ["discord"];
251
+ case "image":
252
+ return imageReferences(request.input).some((image) => "x" in image) ? ["x"] : [];
253
+ default:
254
+ return [];
255
+ }
256
+ }
257
+ function capabilityEvidence(request) {
258
+ if (request.kind === "photo") {
259
+ return request.image === void 0 ? [] : [request.image];
260
+ }
261
+ if (request.kind !== "image") {
262
+ return [];
263
+ }
264
+ return imageReferences(request.input).flatMap(
265
+ (image) => "evidence" in image ? [image.evidence] : []
266
+ );
267
+ }
268
+ var readerFactories = {
269
+ member: (call, input) => ({
270
+ ...input.member,
271
+ identities: () => call({ kind: "member-identities" })
272
+ }),
273
+ discord: (call) => ({
274
+ member: () => call({ kind: "discord", input: {} }),
275
+ message: (input) => call({ kind: "discord", input })
276
+ }),
277
+ x: (call) => ({
278
+ connection: () => call({ kind: "x", input: { operation: "connection" } }),
279
+ profile: () => call({ kind: "x", input: { operation: "profile" } }),
280
+ post: (input) => call({ kind: "x", input: { ...input, operation: "post" } }),
281
+ follows: (input) => call({ kind: "x", input: { ...input, operation: "follows" } }),
282
+ reposted: (input) => call({ kind: "x", input: { ...input, operation: "reposted" } })
283
+ }),
284
+ trackers: (call, input) => {
285
+ const key = (reference) => typeof reference === "string" ? reference : reference.key;
286
+ return {
287
+ records: (reference, query = {}) => {
288
+ const { asOf = input.eligibleAt, ...rest } = query;
289
+ return call({
290
+ kind: "tracker-records",
291
+ reference: key(reference),
292
+ query: rest,
293
+ asOf
294
+ });
295
+ },
296
+ summary: (reference, query = {}) => {
297
+ const { asOf = input.eligibleAt, ...rest } = query;
298
+ return call({
299
+ kind: "tracker-summary",
300
+ reference: key(reference),
301
+ query: rest,
302
+ asOf
303
+ });
304
+ },
305
+ content: (reference, query) => call({ kind: "tracker-content", reference: key(reference), query })
306
+ };
307
+ },
308
+ images: (call) => ({
309
+ inspect: (image) => call({ kind: "image", input: { operation: "inspect", image } }),
310
+ compare: (a, b) => call({ kind: "image", input: { operation: "compare", images: [a, b] } }),
311
+ review: (options) => call({ kind: "image", input: { ...options, operation: "review" } }),
312
+ extract: (options) => call({
313
+ kind: "image",
314
+ input: { ...options, operation: "extract" }
315
+ }),
316
+ seen: (image, options = {}) => call({ kind: "image", input: { ...options, operation: "seen", image } })
317
+ }),
318
+ review: (call) => ({
319
+ text: (options) => call({ kind: "text", ...options }),
320
+ photo: (options) => call({ kind: "photo", ...options })
321
+ }),
322
+ wallet: (call) => ({
323
+ list: () => call({ kind: "wallet-list" }),
324
+ balance: (query) => call({
325
+ kind: "wallet-balance",
326
+ chain: query.chain === "solana" ? solanaMainnet : query.chain,
327
+ token: query.token ?? null,
328
+ address: query.address ?? null
329
+ })
330
+ })
331
+ };
332
+
333
+ // src/testing/determinism.ts
334
+ function seededStream(text) {
335
+ let h1 = 1779033703;
336
+ let h2 = 3144134277;
337
+ let h3 = 1013904242;
338
+ let h4 = 2773480762;
339
+ for (let i = 0; i < text.length; i++) {
340
+ const k = text.charCodeAt(i);
341
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
342
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
343
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
344
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044597);
345
+ }
346
+ h1 = Math.imul(h3 ^ h1 >>> 18, 597399067);
347
+ h2 = Math.imul(h4 ^ h2 >>> 22, 2869860233);
348
+ h3 = Math.imul(h1 ^ h3 >>> 17, 951274213);
349
+ h4 = Math.imul(h2 ^ h4 >>> 19, 2716044597);
350
+ h1 ^= h2 ^ h3 ^ h4;
351
+ h2 ^= h1;
352
+ h3 ^= h1;
353
+ h4 ^= h1;
354
+ let a = h1 | 0;
355
+ let b = h2 | 0;
356
+ let c = h3 | 0;
357
+ let d = h4 | 0;
358
+ return () => {
359
+ const t = (a + b | 0) + d | 0;
360
+ d = d + 1 | 0;
361
+ a = b ^ b >>> 9;
362
+ b = c + (c << 3) | 0;
363
+ c = c << 21 | c >>> 11;
364
+ c = c + t | 0;
365
+ return t >>> 0;
366
+ };
367
+ }
368
+ function replace(target, key, value) {
369
+ const original = Object.getOwnPropertyDescriptor(target, key);
370
+ Object.defineProperty(target, key, {
371
+ value,
372
+ writable: true,
373
+ configurable: true
374
+ });
375
+ return () => {
376
+ if (original) {
377
+ Object.defineProperty(target, key, original);
378
+ } else {
379
+ Reflect.deleteProperty(target, key);
380
+ }
381
+ };
382
+ }
383
+ var integerArrays = [
384
+ Int8Array,
385
+ Uint8Array,
386
+ Uint8ClampedArray,
387
+ Int16Array,
388
+ Uint16Array,
389
+ Int32Array,
390
+ Uint32Array,
391
+ BigInt64Array,
392
+ BigUint64Array
393
+ ];
394
+ function installDeterminism(now, seed) {
395
+ const restores = [];
396
+ const restore = () => {
397
+ for (const undo of restores.reverse()) {
398
+ undo();
399
+ }
400
+ };
401
+ try {
402
+ install(now, seed, restores);
403
+ } catch (error) {
404
+ restore();
405
+ throw error;
406
+ }
407
+ return restore;
408
+ }
409
+ function install(now, seed, restores) {
410
+ const RealDate = Date;
411
+ function FrozenDate(...args) {
412
+ return new.target ? Reflect.construct(
413
+ RealDate,
414
+ args.length === 0 ? [now] : args,
415
+ new.target
416
+ ) : new RealDate(now).toString();
417
+ }
418
+ Object.defineProperty(FrozenDate, "prototype", { value: RealDate.prototype });
419
+ Object.defineProperty(FrozenDate, "length", { value: 7 });
420
+ replace(FrozenDate, "now", () => now);
421
+ replace(FrozenDate, "parse", RealDate.parse);
422
+ replace(FrozenDate, "UTC", RealDate.UTC);
423
+ restores.push(replace(RealDate.prototype, "constructor", FrozenDate));
424
+ restores.push(replace(globalThis, "Date", FrozenDate));
425
+ if (globalThis.performance) {
426
+ restores.push(replace(globalThis.performance, "now", () => 0));
427
+ }
428
+ const temporal = globalThis.Temporal;
429
+ if (temporal?.Now) {
430
+ const instant = () => temporal.Instant.fromEpochMilliseconds(now);
431
+ const zoned = (zone = "UTC") => instant().toZonedDateTimeISO(zone);
432
+ restores.push(
433
+ replace(
434
+ temporal,
435
+ "Now",
436
+ Object.freeze({
437
+ instant,
438
+ timeZoneId: () => "UTC",
439
+ zonedDateTimeISO: zoned,
440
+ plainDateTimeISO: (zone) => zoned(zone).toPlainDateTime(),
441
+ plainDateISO: (zone) => zoned(zone).toPlainDate(),
442
+ plainTimeISO: (zone) => zoned(zone).toPlainTime()
443
+ })
444
+ )
445
+ );
446
+ }
447
+ const math = seededStream("math:" + seed);
448
+ restores.push(
449
+ replace(
450
+ Math,
451
+ "random",
452
+ () => ((math() >>> 5) * 67108864 + (math() >>> 6)) / 9007199254740992
453
+ )
454
+ );
455
+ const bytes = seededStream("crypto:" + seed);
456
+ const fill = (view) => {
457
+ for (let i = 0; i < view.length; i += 4) {
458
+ const word = bytes();
459
+ for (let j = 0; j < 4 && i + j < view.length; j++) {
460
+ view[i + j] = word >>> j * 8 & 255;
461
+ }
462
+ }
463
+ return view;
464
+ };
465
+ const crypto = globalThis.crypto;
466
+ if (crypto) {
467
+ restores.push(
468
+ replace(crypto, "getRandomValues", (array) => {
469
+ if (!integerArrays.some((type) => array instanceof type)) {
470
+ throw new DOMException(
471
+ "getRandomValues takes an integer typed array",
472
+ "TypeMismatchError"
473
+ );
474
+ }
475
+ if (array.byteLength > 65536) {
476
+ throw new DOMException(
477
+ "getRandomValues takes at most 65536 bytes",
478
+ "QuotaExceededError"
479
+ );
480
+ }
481
+ fill(new Uint8Array(array.buffer, array.byteOffset, array.byteLength));
482
+ return array;
483
+ })
484
+ );
485
+ restores.push(
486
+ replace(crypto, "randomUUID", () => {
487
+ const b = fill(new Uint8Array(16));
488
+ b[6] = b[6] & 15 | 64;
489
+ b[8] = b[8] & 63 | 128;
490
+ const hex = Array.from(
491
+ b,
492
+ (value) => value.toString(16).padStart(2, "0")
493
+ ).join("");
494
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
495
+ })
496
+ );
497
+ }
498
+ }
499
+
500
+ // src/testing/facts.ts
501
+ import { z as z2 } from "zod";
502
+ var TestSetupError = class extends Error {
503
+ constructor(message) {
504
+ super(message);
505
+ this.name = "TestSetupError";
506
+ }
507
+ };
508
+ function fakeFor(facts, request) {
509
+ switch (request.kind) {
510
+ case "member-identities":
511
+ return {
512
+ name: "member.identities",
513
+ answer: facts.member?.identities,
514
+ args: []
515
+ };
516
+ case "wallet-list":
517
+ return {
518
+ name: "wallet.list",
519
+ answer: facts.wallet?.list,
520
+ args: []
521
+ };
522
+ case "wallet-balance": {
523
+ const { chain, token, address } = request;
524
+ return {
525
+ name: "wallet.balance",
526
+ answer: facts.wallet?.balance,
527
+ args: [{ chain, token, address }]
528
+ };
529
+ }
530
+ case "x": {
531
+ const { operation, ...input } = request.input;
532
+ return {
533
+ name: `x.${operation}`,
534
+ answer: facts.x?.[operation],
535
+ args: operation === "connection" || operation === "profile" ? [] : [input]
536
+ };
537
+ }
538
+ case "discord": {
539
+ const message = "channelId" in request.input;
540
+ return {
541
+ name: message ? "discord.message" : "discord.member",
542
+ answer: message ? facts.discord?.message : facts.discord?.member,
543
+ args: message ? [request.input] : []
544
+ };
545
+ }
546
+ case "tracker-records":
547
+ case "tracker-summary": {
548
+ const method = request.kind === "tracker-records" ? "records" : "summary";
549
+ return {
550
+ name: `trackers["${request.reference}"].${method}`,
551
+ answer: facts.trackers?.[request.reference]?.[method],
552
+ args: [{ ...request.query, asOf: request.asOf }]
553
+ };
554
+ }
555
+ case "tracker-content":
556
+ return {
557
+ name: `trackers["${request.reference}"].content`,
558
+ answer: facts.trackers?.[request.reference]?.content,
559
+ args: [request.query]
560
+ };
561
+ case "image": {
562
+ const { input } = request;
563
+ const args = input.operation === "inspect" ? [input.image] : input.operation === "compare" ? [...input.images] : input.operation === "seen" ? [
564
+ input.image,
565
+ input.maxDistance === void 0 ? {} : { maxDistance: input.maxDistance }
566
+ ] : [
567
+ Object.fromEntries(
568
+ Object.entries(input).filter(([key]) => key !== "operation")
569
+ )
570
+ ];
571
+ return {
572
+ name: `images.${input.operation}`,
573
+ answer: facts.images?.[input.operation],
574
+ args
575
+ };
576
+ }
577
+ case "text": {
578
+ const { kind: _, ...options } = request;
579
+ return {
580
+ name: "review.text",
581
+ answer: facts.review?.text,
582
+ args: [options]
583
+ };
584
+ }
585
+ case "photo": {
586
+ const { kind: _, ...options } = request;
587
+ return {
588
+ name: "review.photo",
589
+ answer: facts.review?.photo,
590
+ args: [options]
591
+ };
592
+ }
593
+ }
594
+ }
595
+ var failure = (code, message) => ({
596
+ error: { code, message, retryable: false }
597
+ });
598
+ function covers(wallet, chain) {
599
+ if (chain.startsWith("solana:")) {
600
+ return wallet.chain === "solana";
601
+ }
602
+ if (chain.startsWith("eip155:")) {
603
+ return wallet.chain === "eip155";
604
+ }
605
+ return wallet.chain === "cosmos" && wallet.network === chain;
606
+ }
607
+ var sameAddress = (a, b, chain) => chain.startsWith("eip155:") ? a.toLowerCase() === b.toLowerCase() : a === b;
608
+ function factsAnswerer(facts) {
609
+ const pins = /* @__PURE__ */ new Map();
610
+ async function ask(fake) {
611
+ if (fake.answer === void 0) {
612
+ throw new TestSetupError(
613
+ `The quest called ctx.${fake.name}, but the test gave no answer: set facts.${fake.name}`
614
+ );
615
+ }
616
+ try {
617
+ return {
618
+ value: typeof fake.answer === "function" ? await fake.answer(
619
+ ...fake.args
620
+ ) : fake.answer
621
+ };
622
+ } catch (error) {
623
+ if (error instanceof CapabilityError) {
624
+ return {
625
+ error: {
626
+ code: error.code,
627
+ message: error.message,
628
+ retryable: error.retryable
629
+ }
630
+ };
631
+ }
632
+ throw error;
633
+ }
634
+ }
635
+ function pin(request) {
636
+ const key = JSON.stringify(request);
637
+ let outcome = pins.get(key);
638
+ if (!outcome) {
639
+ const fake = fakeFor(facts, request);
640
+ outcome = fake.answer === void 0 ? Promise.resolve({ value: null }) : ask(fake).then(
641
+ (answered) => "error" in answered ? answered : {
642
+ value: checked(request, answered.value, fake.name)
643
+ }
644
+ );
645
+ pins.set(key, outcome);
646
+ }
647
+ return outcome;
648
+ }
649
+ function own(fake, account, link) {
650
+ if (account !== null) {
651
+ return ask(fake);
652
+ }
653
+ if (fake.answer !== void 0) {
654
+ throw new TestSetupError(
655
+ `facts.${fake.name} answers a read of the member's own account, which the test did not link: set facts.${link}`
656
+ );
657
+ }
658
+ return Promise.resolve({ value: null });
659
+ }
660
+ return async (request) => {
661
+ const fake = fakeFor(facts, request);
662
+ switch (request.kind) {
663
+ case "member-identities":
664
+ case "wallet-list":
665
+ return pin(request);
666
+ case "x": {
667
+ const { operation } = request.input;
668
+ if (operation === "connection") {
669
+ return pin(request);
670
+ }
671
+ if (operation === "post") {
672
+ return ask(fake);
673
+ }
674
+ const x = await pin(xConnection);
675
+ return "error" in x ? x : own(fake, x.value, "x.connection");
676
+ }
677
+ case "wallet-balance": {
678
+ const pinned = await pin(walletList);
679
+ if ("error" in pinned) {
680
+ return pinned;
681
+ }
682
+ const wallets = (pinned.value ?? []).filter(
683
+ (wallet) => covers(wallet, request.chain)
684
+ );
685
+ const { address } = request;
686
+ if (address !== null && !wallets.some(
687
+ (wallet) => sameAddress(wallet.address, address, request.chain)
688
+ )) {
689
+ return failure(
690
+ "author-error",
691
+ "Read only the member's linked wallets"
692
+ );
693
+ }
694
+ if (pinned.value === null) {
695
+ return own(fake, null, "wallet.list");
696
+ }
697
+ return wallets.length ? ask(fake) : { value: null };
698
+ }
699
+ case "discord":
700
+ return "channelId" in request.input || fake.answer !== void 0 ? ask(fake) : { value: null };
701
+ case "image": {
702
+ if (imageReferences(request.input).some(
703
+ (image) => "x" in image && image.x === "profile"
704
+ )) {
705
+ const x = await pin(xConnection);
706
+ if ("error" in x) {
707
+ return x;
708
+ }
709
+ if (x.value === null) {
710
+ return failure(
711
+ "not-linked",
712
+ "Link an X account to check its profile picture."
713
+ );
714
+ }
715
+ }
716
+ return ask(fake);
717
+ }
718
+ default:
719
+ return ask(fake);
720
+ }
721
+ };
722
+ }
723
+ var xConnection = {
724
+ kind: "x",
725
+ input: { operation: "connection" }
726
+ };
727
+ var walletList = { kind: "wallet-list" };
728
+ function checked(request, value, name) {
729
+ try {
730
+ return parseCapabilityResult(request, value);
731
+ } catch (error) {
732
+ throw new TestSetupError(
733
+ `facts.${name} is not a result Relay delivers: ${error instanceof z2.ZodError ? z2.prettifyError(error) : String(error)}`
734
+ );
735
+ }
736
+ }
737
+
738
+ // src/testing/rewards.ts
739
+ function kindOf(reward) {
740
+ if (reward.kind === "integration") {
741
+ return `integration:${reward.provider}:${reward.action}`;
742
+ }
743
+ return reward.kind;
744
+ }
745
+ function within(selected, published) {
746
+ if (selected.kind === "points" && published.kind === "points") {
747
+ return selected.amount > published.amount ? "Evaluator exceeded published reward bounds" : null;
748
+ }
749
+ if (selected.kind === "tier" && published.kind === "tier") {
750
+ const rank = (reward) => reward.reward.tiers.findIndex((tier) => tier.id === reward.tier);
751
+ return JSON.stringify(selected.reward) !== JSON.stringify(published.reward) || rank(selected) > rank(published) ? "Evaluator exceeded published reward tier" : null;
752
+ }
753
+ return null;
754
+ }
755
+ function rewardSelectionIssue(selected, published) {
756
+ const seen = /* @__PURE__ */ new Set();
757
+ for (const reward of selected) {
758
+ const key = rewardKey(reward);
759
+ if (seen.has(key)) {
760
+ return "Duplicate reward selection";
761
+ }
762
+ seen.add(key);
763
+ const match = published.find((candidate) => rewardKey(candidate) === key);
764
+ if (!match || kindOf(match) !== kindOf(reward)) {
765
+ return "Evaluator selected an unapproved reward";
766
+ }
767
+ const refusal = within(reward, match);
768
+ if (refusal) {
769
+ return refusal;
770
+ }
771
+ }
772
+ return null;
773
+ }
774
+
775
+ // src/testing/run.ts
776
+ var defaultNow = Date.UTC(2026, 0, 1);
777
+ var defaultMember = "member_test";
778
+ var defaultSeed = "attempt_test";
779
+ var maxCapabilityCalls = 8;
780
+ var evaluationTimeoutMs = 15e3;
781
+ var time = (value) => value instanceof Date ? value.getTime() : value;
782
+ function submission(trigger, raw) {
783
+ if (trigger.kind !== "manual" || trigger.input === "none") {
784
+ if (raw !== void 0 && Object.keys(raw).length) {
785
+ throw new TestSetupError(
786
+ "This quest takes no evidence: Relay admits its attempts without input"
787
+ );
788
+ }
789
+ return {};
790
+ }
791
+ if (trigger.input === "photo") {
792
+ const photo = z3.object({ photo: z3.string().min(1) }).safeParse(raw);
793
+ if (!photo.success) {
794
+ throw new TestSetupError(
795
+ "This quest takes a photo: pass input: { photo: <evidence ID> }"
796
+ );
797
+ }
798
+ return { ...raw, evidence: photo.data.photo };
799
+ }
800
+ const refused = (issue2) => new TestSetupError(
801
+ `Relay refuses this ${trigger.input} submission: ${issue2}`
802
+ );
803
+ if (trigger.input === "quiz") {
804
+ const parsed2 = quizEvidenceSchema.safeParse({ kind: "quiz", ...raw });
805
+ if (!parsed2.success) {
806
+ throw refused(z3.prettifyError(parsed2.error));
807
+ }
808
+ return parsed2.data;
809
+ }
810
+ const parsed = (trigger.input === "text" ? textEvidenceSchema : urlEvidenceSchema).safeParse({ kind: trigger.input, ...raw });
811
+ if (!parsed.success) {
812
+ throw refused(z3.prettifyError(parsed.error));
813
+ }
814
+ const issue = validateSubmissionValue(
815
+ parsed.data,
816
+ trigger
817
+ );
818
+ if (issue) {
819
+ throw refused(issue);
820
+ }
821
+ return parsed.data;
822
+ }
823
+ function hostedRun(quest, options) {
824
+ const release = quest.describe(options.settings ?? {});
825
+ const trigger = triggerSchema.parse(release.trigger);
826
+ const tracked = trigger.kind === "automatic" && trigger.tracker !== void 0;
827
+ const now = time(options.now ?? defaultNow);
828
+ if (!Number.isSafeInteger(now) || now < 0) {
829
+ throw new TestSetupError("now must be a time at or after 1970-01-01");
830
+ }
831
+ if (tracked && options.tracker === void 0) {
832
+ throw new TestSetupError(
833
+ `Quest "${quest.key}" runs on tracker observations: pass tracker: trackerObservation(tracker, { data })`
834
+ );
835
+ }
836
+ if (!tracked && options.tracker !== void 0) {
837
+ throw new TestSetupError(
838
+ `Quest "${quest.key}" has no tracker trigger, so it reads no tracker observation`
839
+ );
840
+ }
841
+ const tracker = options.tracker === void 0 ? null : trackerTriggerContextSchema.parse(options.tracker);
842
+ const member = options.member ?? tracker?.attribution.member ?? defaultMember;
843
+ if (tracker && options.eligibleAt !== void 0) {
844
+ throw new TestSetupError(
845
+ "A tracker-triggered attempt is eligible when its record occurred: set occurredAt on the observation instead of eligibleAt"
846
+ );
847
+ }
848
+ const eligibleAt = time(
849
+ tracker?.record.occurredAt ?? options.eligibleAt ?? now
850
+ );
851
+ const input = submission(
852
+ trigger,
853
+ options.input
854
+ );
855
+ const performance = options.performance ? contentPerformanceSnapshotSchema.parse({
856
+ contentAttempt: "attempt_content",
857
+ observation: "observation_test",
858
+ observedAt: options.performance.observedAt ?? now,
859
+ metrics: options.performance.metrics,
860
+ rewards: options.performance.rewards ?? release.rewards
861
+ }) : void 0;
862
+ return {
863
+ release: {
864
+ values: release.values,
865
+ rewards: release.rewards,
866
+ humanReview: release.humanReview,
867
+ integrations: release.integrations
868
+ },
869
+ member,
870
+ input,
871
+ tracker,
872
+ eligibleAt,
873
+ segments: options.calendar ?? (options.timeZone ? [{ effectiveAt: 0, timeZone: options.timeZone }] : []),
874
+ ...performance ? { performance } : {},
875
+ now,
876
+ seed: options.seed ?? defaultSeed
877
+ };
878
+ }
879
+ function hostContext(run, answer) {
880
+ const { release, member, input, tracker, eligibleAt, performance, now } = run;
881
+ const evidence = typeof input.evidence === "string" ? input.evidence : void 0;
882
+ const calendar = evaluationCalendar({ segments: run.segments }, eligibleAt);
883
+ const calls = [];
884
+ const problems = [];
885
+ let sequence = 0;
886
+ async function deliver(request, sequence2) {
887
+ for (const provider of capabilityIntegrations(request)) {
888
+ if (!release.integrations.includes(provider)) {
889
+ return failure2(`Declare the ${provider} integration on this quest`);
890
+ }
891
+ }
892
+ const evidenceReads = capabilityEvidence(request);
893
+ if (request.kind === "photo" && !evidence || evidenceReads.some((read) => read !== evidence)) {
894
+ return failure2(
895
+ request.kind === "photo" ? "Photo capability cannot access another attempt\u2019s evidence" : "Image capability cannot access another attempt\u2019s evidence"
896
+ );
897
+ }
898
+ const outcome = await answer(request, sequence2);
899
+ if ("error" in outcome) {
900
+ return outcome;
901
+ }
902
+ try {
903
+ const value = z3.json().parse(parseCapabilityResult(request, outcome.value));
904
+ return { value: JSON.parse(JSON.stringify(value)) };
905
+ } catch (error) {
906
+ throw new TestSetupError(
907
+ `The answer to ${request.kind} call ${sequence2} is not a result Relay delivers: ${error instanceof z3.ZodError ? z3.prettifyError(error) : String(error)}`
908
+ );
909
+ }
910
+ }
911
+ const call = async (raw) => {
912
+ const current = sequence++;
913
+ if (current >= maxCapabilityCalls) {
914
+ const error = new TestSetupError(
915
+ `An evaluation run makes at most ${maxCapabilityCalls} capability calls; the quest made more`
916
+ );
917
+ problems.push(error);
918
+ throw error;
919
+ }
920
+ const parsed = parseCapabilityRequest(raw);
921
+ if (!parsed.success) {
922
+ throw CapabilityError.from({
923
+ code: "author-error",
924
+ message: z3.prettifyError(parsed.error),
925
+ retryable: false
926
+ });
927
+ }
928
+ let outcome;
929
+ try {
930
+ outcome = await deliver(parsed.data, current);
931
+ } catch (error) {
932
+ problems.push(error instanceof Error ? error : new Error(String(error)));
933
+ throw error;
934
+ }
935
+ calls.push({ sequence: current, request: parsed.data, outcome });
936
+ calls.sort((a, b) => a.sequence - b.sequence);
937
+ if ("error" in outcome) {
938
+ throw CapabilityError.from(outcome.error);
939
+ }
940
+ return outcome.value;
941
+ };
942
+ const readerInput = { eligibleAt, member: { id: member } };
943
+ const readers = {
944
+ member: readerFactories.member(call, readerInput),
945
+ trackers: readerFactories.trackers(call, readerInput),
946
+ images: readerFactories.images(call),
947
+ review: readerFactories.review(call),
948
+ wallet: readerFactories.wallet(call),
949
+ // Relay gives quest code the X and Discord readers only for declared integrations.
950
+ ...release.integrations.includes("x") ? { x: readerFactories.x(call) } : {},
951
+ ...release.integrations.includes("discord") ? { discord: readerFactories.discord(call) } : {}
952
+ };
953
+ const context = {
954
+ settings: release.values,
955
+ rewards: release.rewards,
956
+ humanReview: release.humanReview,
957
+ input,
958
+ tracker,
959
+ eligibleAt,
960
+ calendar,
961
+ now,
962
+ ...performance ? { performance } : {},
963
+ ...readers
964
+ };
965
+ return { context, calls, problems, release, now, seed: run.seed };
966
+ }
967
+ function failure2(message) {
968
+ return { error: { code: "author-error", message, retryable: false } };
969
+ }
970
+ function createTestContext(quest, options = {}) {
971
+ const { problems: _, ...context } = hostContext(
972
+ hostedRun(quest, options),
973
+ factsAnswerer(options.facts ?? {})
974
+ );
975
+ return context;
976
+ }
977
+ var running = Promise.resolve();
978
+ function exclusive(run) {
979
+ const result = running.then(run, run);
980
+ running = result.catch(() => void 0);
981
+ return result;
982
+ }
983
+ function evaluateHosted(quest, host) {
984
+ return exclusive(async () => {
985
+ let restore = () => {
986
+ };
987
+ let timer;
988
+ let decision;
989
+ try {
990
+ restore = installDeterminism(host.now, host.seed);
991
+ decision = await Promise.race([
992
+ quest.evaluate(
993
+ host.context
994
+ ),
995
+ new Promise((_, reject) => {
996
+ timer = setTimeout(
997
+ () => reject(
998
+ new Error(
999
+ `The evaluation did not finish within ${evaluationTimeoutMs / 1e3} seconds, Relay's limit`
1000
+ )
1001
+ ),
1002
+ evaluationTimeoutMs
1003
+ );
1004
+ })
1005
+ ]);
1006
+ } catch (error) {
1007
+ throw host.problems[0] ?? error;
1008
+ } finally {
1009
+ clearTimeout(timer);
1010
+ restore();
1011
+ }
1012
+ if (host.problems[0]) {
1013
+ throw host.problems[0];
1014
+ }
1015
+ if (decision.kind === "accept" || decision.kind === "review") {
1016
+ const issue = rewardSelectionIssue(
1017
+ decision.rewards ?? [],
1018
+ host.release.rewards
1019
+ );
1020
+ if (issue) {
1021
+ throw new Error(issue);
1022
+ }
1023
+ }
1024
+ return { decision, calls: host.calls, release: host.release };
1025
+ });
1026
+ }
1027
+ async function runQuest(quest, options = {}) {
1028
+ return evaluateHosted(
1029
+ quest,
1030
+ hostContext(
1031
+ hostedRun(quest, options),
1032
+ factsAnswerer(options.facts ?? {})
1033
+ )
1034
+ );
1035
+ }
1036
+
1037
+ // src/testing/assertions.ts
1038
+ var DecisionAssertionError = class extends Error {
1039
+ decision;
1040
+ constructor(message, decision) {
1041
+ super(message);
1042
+ this.name = "DecisionAssertionError";
1043
+ this.decision = decision;
1044
+ }
1045
+ };
1046
+ var verbs = {
1047
+ accept: "accepted",
1048
+ reject: "rejected",
1049
+ review: "sent it to review",
1050
+ defer: "deferred",
1051
+ progress: "recorded progress"
1052
+ };
1053
+ function expectKind(run, kind, reason) {
1054
+ const decision = "decision" in run ? run.decision : run;
1055
+ const detail = "reason" in decision && decision.reason ? `: "${decision.reason}"` : "";
1056
+ if (decision.kind !== kind) {
1057
+ throw new DecisionAssertionError(
1058
+ `Expected the quest to ${kind}, but it ${verbs[decision.kind]}${detail}`,
1059
+ decision
1060
+ );
1061
+ }
1062
+ if (reason !== void 0) {
1063
+ const actual = "reason" in decision ? decision.reason : "";
1064
+ const matches = typeof reason === "string" ? actual === reason : reason.test(actual);
1065
+ if (!matches) {
1066
+ throw new DecisionAssertionError(
1067
+ `Expected the ${kind} reason to ${typeof reason === "string" ? "be" : "match"} ${String(
1068
+ typeof reason === "string" ? `"${reason}"` : reason
1069
+ )}, but it was "${actual}"`,
1070
+ decision
1071
+ );
1072
+ }
1073
+ }
1074
+ return decision;
1075
+ }
1076
+ function expectAccept(run) {
1077
+ return expectKind(run, "accept");
1078
+ }
1079
+ function expectReject(run, reason) {
1080
+ return expectKind(run, "reject", reason);
1081
+ }
1082
+ function expectReview(run, reason) {
1083
+ return expectKind(run, "review", reason);
1084
+ }
1085
+ function expectDefer(run, reason) {
1086
+ return expectKind(run, "defer", reason);
1087
+ }
1088
+ function expectProgress(run, reason) {
1089
+ return expectKind(run, "progress", reason);
1090
+ }
1091
+
1092
+ // src/testing/trackers.ts
1093
+ function record(input, source) {
1094
+ const occurredAt = time(input.occurredAt ?? defaultNow);
1095
+ const observedAt = time(input.observedAt ?? occurredAt);
1096
+ return {
1097
+ id: input.id,
1098
+ source,
1099
+ occurredAt,
1100
+ observedAt,
1101
+ firstSeenAt: observedAt,
1102
+ author: input.author === void 0 ? { id: "author_test", verified: null } : input.author,
1103
+ data: input.data,
1104
+ removed: input.removed ?? false
1105
+ };
1106
+ }
1107
+ function trackerObservation(tracker, observation) {
1108
+ const member = observation.member ?? defaultMember;
1109
+ const pinned = record(
1110
+ { ...observation, id: observation.id ?? "record_test" },
1111
+ tracker.source.kind
1112
+ );
1113
+ return trackerTriggerContextSchema.parse({
1114
+ tracker: tracker.key,
1115
+ key: tracker.key,
1116
+ beneficiary: observation.beneficiary ?? "author",
1117
+ record: pinned,
1118
+ ...observation.body === void 0 ? {} : {
1119
+ content: {
1120
+ revision: "revision_test",
1121
+ observation: "observation_test",
1122
+ body: observation.body
1123
+ }
1124
+ },
1125
+ attribution: {
1126
+ member,
1127
+ capturedAt: pinned.observedAt,
1128
+ account: observation.account ?? { provider: "member", id: member }
1129
+ },
1130
+ ...observation.enrolledAt === void 0 ? {} : { enrolledAt: time(observation.enrolledAt) }
1131
+ });
1132
+ }
1133
+ function trackerPage(tracker, records, options = {}) {
1134
+ const { key, title, source, window, refreshSeconds, maxPages } = tracker;
1135
+ return trackerPageSchema.parse({
1136
+ tracker: key,
1137
+ items: records.map(
1138
+ (item, index) => record({ ...item, id: item.id ?? `record_${index + 1}` }, source.kind)
1139
+ ),
1140
+ nextCursor: options.nextCursor ?? null,
1141
+ total: options.total ?? records.length,
1142
+ status: {
1143
+ published: {
1144
+ id: key,
1145
+ version: "version_test",
1146
+ definition: { key, title, source, window, refreshSeconds, maxPages },
1147
+ publishedAt: 0,
1148
+ actor: null
1149
+ },
1150
+ paused: false,
1151
+ state: "idle",
1152
+ lastStartedAt: null,
1153
+ lastCompletedAt: null,
1154
+ nextSyncAt: null,
1155
+ coverage: "complete",
1156
+ error: null,
1157
+ records: options.total ?? records.length
1158
+ }
1159
+ });
1160
+ }
1161
+
1162
+ // src/testing/replay.ts
1163
+ import { z as z4 } from "zod";
1164
+ var ReplayStopped = class extends Error {
1165
+ constructor(divergence) {
1166
+ super(
1167
+ `Capability call ${divergence.sequence} is not in the capture (${divergence.reason}): pass facts to answer it`
1168
+ );
1169
+ this.divergence = divergence;
1170
+ this.name = "ReplayStopped";
1171
+ }
1172
+ divergence;
1173
+ };
1174
+ function sameJson(a, b) {
1175
+ if (Array.isArray(a) || Array.isArray(b)) {
1176
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
1177
+ }
1178
+ if (a && b && typeof a === "object" && typeof b === "object") {
1179
+ const keys = Object.keys(a).filter(
1180
+ (key) => a[key] !== void 0
1181
+ );
1182
+ return keys.length === Object.keys(b).filter(
1183
+ (key) => b[key] !== void 0
1184
+ ).length && keys.every(
1185
+ (key) => sameJson(
1186
+ a[key],
1187
+ b[key]
1188
+ )
1189
+ );
1190
+ }
1191
+ return a === b;
1192
+ }
1193
+ function captureAnswerer(facts, fallback, diverged) {
1194
+ const answerFallback = fallback ? factsAnswerer(fallback) : null;
1195
+ return async (request, sequence) => {
1196
+ const fact = facts.find((candidate) => candidate.sequence === sequence);
1197
+ const reason = !fact ? "new" : !sameJson(fact.request, request) ? "changed" : !fact.captured || !fact.outcome ? "uncaptured" : null;
1198
+ if (fact?.outcome && !reason) {
1199
+ return fact.outcome;
1200
+ }
1201
+ const divergence = {
1202
+ sequence,
1203
+ reason: reason ?? "new",
1204
+ recorded: fact?.request ?? null,
1205
+ replayed: request
1206
+ };
1207
+ diverged(divergence);
1208
+ if (!answerFallback) {
1209
+ throw new ReplayStopped(divergence);
1210
+ }
1211
+ return answerFallback(request);
1212
+ };
1213
+ }
1214
+ async function replayAttempt(quest, capture, options = {}) {
1215
+ const parsed = attemptCaptureSchema.safeParse(capture);
1216
+ if (!parsed.success) {
1217
+ throw new TestSetupError(
1218
+ `Not an attempt capture: ${z4.prettifyError(parsed.error)}`
1219
+ );
1220
+ }
1221
+ const { quest: captured, context, facts, recorded } = parsed.data;
1222
+ if (captured.key !== null && captured.key !== quest.key) {
1223
+ throw new TestSetupError(
1224
+ `This capture is of quest "${captured.key}", not "${quest.key}"`
1225
+ );
1226
+ }
1227
+ let divergence = null;
1228
+ const host = hostContext(
1229
+ {
1230
+ release: {
1231
+ values: context.settings,
1232
+ rewards: context.rewards,
1233
+ humanReview: context.humanReview,
1234
+ integrations: context.integrations
1235
+ },
1236
+ member: context.member,
1237
+ input: context.input,
1238
+ tracker: context.tracker,
1239
+ eligibleAt: context.eligibleAt,
1240
+ segments: context.calendar.segments,
1241
+ ...context.performance ? { performance: context.performance } : {},
1242
+ now: parsed.data.now,
1243
+ seed: parsed.data.seed
1244
+ },
1245
+ captureAnswerer(facts, options.facts, (found) => {
1246
+ divergence ??= found;
1247
+ })
1248
+ );
1249
+ let decision = null;
1250
+ try {
1251
+ decision = (await evaluateHosted(quest, host)).decision;
1252
+ } catch (error) {
1253
+ if (!(error instanceof ReplayStopped)) {
1254
+ throw error;
1255
+ }
1256
+ }
1257
+ return {
1258
+ decision,
1259
+ recorded,
1260
+ changed: decision && recorded ? !sameJson(decision, recorded) : null,
1261
+ divergence,
1262
+ calls: host.calls
1263
+ };
1264
+ }
1265
+ export {
1266
+ CapabilityError,
1267
+ DecisionAssertionError,
1268
+ TestSetupError,
1269
+ createTestContext,
1270
+ defaultMember,
1271
+ defaultNow,
1272
+ defaultSeed,
1273
+ expectAccept,
1274
+ expectDefer,
1275
+ expectProgress,
1276
+ expectReject,
1277
+ expectReview,
1278
+ replayAttempt,
1279
+ runQuest,
1280
+ trackerObservation,
1281
+ trackerPage
1282
+ };