@domino-sdk/relay 1.11.1 → 1.15.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.
- package/README.md +1 -0
- package/build.mjs +15 -0
- package/dist/accounts-5ciAkCMP.d.ts +147 -0
- package/dist/{request-error-D_dUPLwo.d.ts → action-DBzILFx9.d.ts} +31 -1
- package/dist/attempt-captures-DHwIVoRP.d.ts +285 -0
- package/dist/authoring.d.ts +12 -10
- package/dist/authoring.js +6 -4
- package/dist/{automatic-outcomes-kPIDv6X_.d.ts → automatic-outcomes-BcEn_L46.d.ts} +1 -1
- package/dist/browser.d.ts +9 -9
- package/dist/browser.js +7 -6
- package/dist/capability-error-oTd3eYFH.d.ts +151 -0
- package/dist/chunk-2UAVZCMH.js +314 -0
- package/dist/{chunk-AGFUDCNM.js → chunk-3PPEXDWR.js} +1 -1
- package/dist/chunk-CHD6HHWG.js +485 -0
- package/dist/{chunk-T657WO56.js → chunk-LLMJ2SUT.js} +6 -2
- package/dist/chunk-NIYCAW2I.js +79 -0
- package/dist/{chunk-XK5E4HAS.js → chunk-PJXBW754.js} +80 -28
- package/dist/{chunk-PUDL4JBY.js → chunk-PUY35TH3.js} +2 -2
- package/dist/chunk-UJACD6K2.js +987 -0
- package/dist/chunk-W335CWYJ.js +53 -0
- package/dist/{chunk-X7ECZ3AK.js → chunk-XMBM2XD5.js} +1 -1
- package/dist/{chunk-2K2QBPAD.js → chunk-ZLYBUM7K.js} +6 -4
- package/dist/{client-fUSERMAC.d.ts → client-DFSCmf3H.d.ts} +27 -8
- package/dist/{activity-CNiPDPK6.d.ts → deployments-Ci6GyANg.d.ts} +15 -15
- package/dist/{groups-BK8ts2yK.d.ts → groups-Bi7lc54m.d.ts} +1 -1
- package/dist/{images-aw6W7vFZ.d.ts → images-Cb8QHGjn.d.ts} +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +8 -6
- package/dist/{capability-error-ty3SqD_b.d.ts → integrations-Cr9EhGI_.d.ts} +1 -149
- package/dist/internal.d.ts +11 -4
- package/dist/internal.js +8 -3
- package/dist/{offers-C2FANzrG.d.ts → offers-Catj06u7.d.ts} +2 -2
- package/dist/{pages-Chb6OZSB.d.ts → pages-CpYt56Km.d.ts} +1 -1
- package/dist/{participant-activity-DkL3nY5a.d.ts → participant-activity-DYdUUhw2.d.ts} +1 -0
- package/dist/portal-proxy.js +1 -1
- package/dist/react/index.d.ts +247 -179
- package/dist/react/index.js +954 -383
- package/dist/{schema-DEM8y8Fm.d.ts → schema-BaVl2VBS.d.ts} +1 -1
- package/dist/schema.d.ts +15 -13
- package/dist/schema.js +16 -14
- package/dist/server/index.d.ts +12 -70916
- package/dist/server/index.js +594 -841
- package/dist/{sign-in-options-iWOp9puZ.d.ts → sign-in-options-CU1ND6-y.d.ts} +2 -2
- package/dist/submission-input-Bi4HipqI.d.ts +146 -0
- package/dist/testing.d.ts +641 -0
- package/dist/testing.js +1282 -0
- package/dist/transport-0ttRwUat.d.ts +73361 -0
- package/package.json +6 -2
- package/dist/action-9Xl0StBg.d.ts +0 -33
- package/dist/chunk-OCXGG75K.js +0 -171
- package/dist/chunk-ZFABFKIY.js +0 -1295
- package/dist/referral-reward-kinds-M3f0PdiR.d.ts +0 -171
- package/dist/submission-input-BbBJTVlT.d.ts +0 -290
package/dist/testing.js
ADDED
|
@@ -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
|
+
};
|