@ripplo/testing 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,218 +1,140 @@
1
+ import {
2
+ DEFAULT_IGNORE_PATHS,
3
+ DEFAULT_WATCH_PATHS,
4
+ createTestValue,
5
+ dslConfigSchema,
6
+ makeObserverHandle,
7
+ readObserverBudget,
8
+ readObserverDescription,
9
+ readObserverName,
10
+ readPreconditionDepMapping,
11
+ readPreconditionDependsOn,
12
+ readPreconditionDescription,
13
+ readPreconditionName,
14
+ readTestValue
15
+ } from "./chunk-P4ZI7G5M.js";
1
16
  import {
2
17
  compile
3
18
  } from "./chunk-KNF4K4JH.js";
4
19
  import "./chunk-MGATMMCZ.js";
5
20
  import {
6
- buildObserver,
7
21
  buildSetCookieHeader,
8
- createEngine,
9
22
  serializeCookie,
10
23
  verifyWebhookSignature
11
- } from "./chunk-CD3M7H5A.js";
12
- import {
13
- DEFAULT_IGNORE_PATHS,
14
- DEFAULT_WATCH_PATHS,
15
- dslConfigSchema,
16
- readObserverName,
17
- readPreconditionName
18
- } from "./chunk-3IL457A7.js";
24
+ } from "./chunk-TO3T2D2Y.js";
19
25
 
20
- // src/builder.ts
21
- function createRipplo(rawConfig) {
22
- const config = dslConfigSchema.parse(rawConfig);
23
- const preconditions = [];
24
- const observers = [];
25
- const tests = [];
26
- const preconditionNames = /* @__PURE__ */ new Set();
27
- const observerNames = /* @__PURE__ */ new Set();
28
- const testNames = /* @__PURE__ */ new Set();
29
- function implementPrecondition(handle, impl) {
30
- const preconditionName = readPreconditionName(handle);
31
- const idx = preconditions.findIndex((p) => p.name === preconditionName);
32
- if (idx === -1) {
33
- throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
34
- }
35
- const existing = preconditions[idx];
36
- if (existing == null) {
37
- throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
38
- }
39
- const mapping = existing.depMapping;
40
- preconditions[idx] = {
41
- ...existing,
42
- implemented: true,
43
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TData narrows Record<string, string>, safe at engine boundary
44
- teardown: impl.teardown,
45
- setup: async (ctx, allDeps) => {
46
- const resolved = {};
47
- mapping.forEach(([key, depName]) => {
48
- const data = allDeps[depName];
49
- if (data != null) {
50
- resolved[key] = data;
51
- }
52
- });
53
- return impl.setup(ctx, resolved);
54
- }
55
- };
56
- }
57
- function implementObserver(handle, impl) {
58
- const name = readObserverName(handle);
59
- const idx = observers.findIndex((o) => o.name === name);
60
- if (idx === -1) {
61
- throw new Error(`Cannot implement unknown observer: "${name}"`);
62
- }
63
- const existing = observers[idx];
64
- if (existing == null) {
65
- throw new Error(`Cannot implement unknown observer: "${name}"`);
66
- }
67
- observers[idx] = {
68
- ...existing,
69
- implemented: true,
70
- run: async (ctx, params) => {
71
- return impl(ctx, params);
72
- }
73
- };
74
- }
75
- return {
76
- implementObserver,
77
- implementPrecondition,
78
- getConfig: () => config,
79
- getObservers: () => observers,
80
- getPreconditions: () => preconditions,
81
- getTests: () => tests,
82
- getUnimplemented() {
83
- return {
84
- observers: observers.filter((o) => !o.implemented).map((o) => o.name),
85
- preconditions: preconditions.filter((p) => !p.implemented).map((p) => p.name),
86
- tests: tests.filter((t) => !t.implemented).map((t) => t.name)
87
- };
88
- },
89
- observer(name) {
90
- if (observerNames.has(name)) {
91
- observers.splice(0, observers.length, ...observers.filter((o) => o.name !== name));
92
- }
93
- observerNames.add(name);
94
- return buildObserver({ name, observers });
26
+ // src/observer.ts
27
+ function createPassOutcome() {
28
+ return { kind: "pass" };
29
+ }
30
+ function createRetryOutcome(reason) {
31
+ return { kind: "retry", reason };
32
+ }
33
+ function createFailOutcome(reason) {
34
+ return { kind: "fail", reason };
35
+ }
36
+ function buildObserver(name) {
37
+ let description = "";
38
+ let budget = "fast";
39
+ const self = {
40
+ budget(tier) {
41
+ budget = tier;
42
+ return self;
95
43
  },
96
- precondition(name) {
97
- if (preconditionNames.has(name)) {
98
- preconditions.splice(
99
- 0,
100
- preconditions.length,
101
- ...preconditions.filter((p) => p.name !== name)
102
- );
103
- }
104
- preconditionNames.add(name);
105
- return buildPrecondition(name, preconditions);
44
+ description(text) {
45
+ description = text;
46
+ return self;
106
47
  },
107
- test(id, options) {
108
- validateTestId(id);
109
- if (testNames.has(id)) {
110
- tests.splice(0, tests.length, ...tests.filter((t) => t.id !== id));
111
- }
112
- testNames.add(id);
113
- return buildTestName(id, tests, options?.uiOnly);
48
+ input() {
49
+ return {
50
+ contract: () => makeObserverHandle({ budget, description, name })
51
+ };
114
52
  }
115
53
  };
54
+ return self;
116
55
  }
117
- function buildDepMapping(deps) {
118
- return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
56
+
57
+ // src/builder.ts
58
+ function precondition(name) {
59
+ return buildPreconditionStart(name);
119
60
  }
120
- function remapDeps(mapping, allDeps) {
121
- const resolved = {};
122
- mapping.forEach(([key, depName]) => {
123
- const data = allDeps[depName];
124
- if (data != null) {
125
- resolved[key] = data;
126
- }
127
- });
128
- return resolved;
61
+ function observer(name) {
62
+ return buildObserver(name);
129
63
  }
130
- function makePrecondition(name) {
131
- return { name };
64
+ function test(id, options) {
65
+ validateTestId(id);
66
+ return buildTestName(id, options?.uiOnly);
132
67
  }
133
- function castBuilder(value) {
134
- return value;
68
+ function createRipplo(rawConfig, registries) {
69
+ const config = dslConfigSchema.parse(rawConfig);
70
+ const { observers, preconditions, tests } = registries;
71
+ validateUniqueNames(preconditions, observers, tests);
72
+ const preconditionDefs = Object.values(preconditions).map((p) => stubPreconditionDef(p));
73
+ const observerDefs = Object.values(observers).map((o) => stubObserverDef(o));
74
+ const testDefs = [...tests];
75
+ return {
76
+ config,
77
+ observers,
78
+ preconditions,
79
+ tests: testDefs,
80
+ getConfig: () => config,
81
+ getObservers: () => observerDefs,
82
+ getPreconditions: () => preconditionDefs,
83
+ getTests: () => testDefs,
84
+ getUnimplemented: () => ({
85
+ observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
86
+ preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
87
+ tests: testDefs.filter((t) => !t.implemented).map((t) => t.id)
88
+ })
89
+ };
135
90
  }
136
- function pushStub(params) {
137
- params.preconditions.push({
138
- dependsOn: params.depMapping.map(([_, depName]) => depName),
139
- depMapping: params.depMapping,
140
- description: params.description,
91
+ function stubPreconditionDef(p) {
92
+ const name = readPreconditionName(p);
93
+ return {
94
+ dependsOn: readPreconditionDependsOn(p),
95
+ depMapping: readPreconditionDepMapping(p),
96
+ description: readPreconditionDescription(p),
141
97
  implemented: false,
142
- name: params.name,
98
+ name,
143
99
  returns: [],
144
100
  teardown: void 0,
145
101
  setup: () => Promise.resolve({})
146
- });
147
- return makePrecondition(params.name);
102
+ };
148
103
  }
149
- function buildPrecondition(name, preconditions) {
150
- let description = "";
151
- let currentDeps = {};
152
- const self = {
153
- contract: castBuilder(
154
- () => pushStub({ depMapping: buildDepMapping(currentDeps), description, name, preconditions })
155
- ),
156
- requires: castBuilder(requiresImpl),
157
- setup: castBuilder(setupNoDepsImpl),
158
- description(text) {
159
- description = text;
160
- return self;
161
- },
162
- notImplemented() {
163
- pushStub({ depMapping: [], description, name, preconditions });
164
- return makePrecondition(name);
165
- }
104
+ function stubObserverDef(o) {
105
+ const name = readObserverName(o);
106
+ return {
107
+ budget: readObserverBudget(o),
108
+ description: readObserverDescription(o),
109
+ implemented: false,
110
+ name,
111
+ run: () => Promise.resolve(createFailOutcome(`observer "${name}" not implemented`))
166
112
  };
167
- return self;
168
- function requiresImpl(deps) {
169
- currentDeps = deps;
170
- const mapping = buildDepMapping(deps);
171
- const withDeps = {
172
- contract: castBuilder(
173
- () => pushStub({ depMapping: mapping, description, name, preconditions })
174
- ),
175
- setup: castBuilder(setupWithDepsImpl),
176
- description(text) {
177
- description = text;
178
- return withDeps;
179
- },
180
- notImplemented() {
181
- pushStub({ depMapping: mapping, description, name, preconditions });
182
- return makePrecondition(name);
183
- }
184
- };
185
- return castBuilder(withDeps);
186
- function setupWithDepsImpl(fn) {
187
- return registerPrecondition(deps, fn);
113
+ }
114
+ function validateUniqueNames(preconditions, observers, tests) {
115
+ const pNames = /* @__PURE__ */ new Set();
116
+ Object.values(preconditions).forEach((p) => {
117
+ const name = readPreconditionName(p);
118
+ if (pNames.has(name)) {
119
+ throw new Error(`Duplicate precondition name: "${name}"`);
188
120
  }
189
- }
190
- function setupNoDepsImpl(fn) {
191
- return registerPrecondition({}, async (ctx) => fn(ctx));
192
- }
193
- function registerPrecondition(deps, fn) {
194
- const mapping = buildDepMapping(deps);
195
- const def = {
196
- dependsOn: mapping.map(([_, depName]) => depName),
197
- depMapping: mapping,
198
- description,
199
- implemented: true,
200
- name,
201
- returns: [],
202
- teardown: void 0,
203
- setup: async (ctx, allDeps) => {
204
- const resolved = remapDeps(mapping, allDeps);
205
- return fn(ctx, resolved);
206
- }
207
- };
208
- preconditions.push(def);
209
- return {
210
- teardown(tdFn) {
211
- def.teardown = tdFn;
212
- return makePrecondition(name);
213
- }
214
- };
215
- }
121
+ pNames.add(name);
122
+ });
123
+ const oNames = /* @__PURE__ */ new Set();
124
+ Object.values(observers).forEach((o) => {
125
+ const name = readObserverName(o);
126
+ if (oNames.has(name)) {
127
+ throw new Error(`Duplicate observer name: "${name}"`);
128
+ }
129
+ oNames.add(name);
130
+ });
131
+ const tIds = /* @__PURE__ */ new Set();
132
+ tests.forEach((t) => {
133
+ if (tIds.has(t.id)) {
134
+ throw new Error(`Duplicate test id: "${t.id}"`);
135
+ }
136
+ tIds.add(t.id);
137
+ });
216
138
  }
217
139
  var TEST_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
218
140
  function validateTestId(id) {
@@ -222,135 +144,189 @@ function validateTestId(id) {
222
144
  );
223
145
  }
224
146
  }
225
- function buildTestName(id, tests, uiOnly) {
226
- return {
227
- name(displayName) {
228
- return buildTestRequires({ id, name: displayName, tests, uiOnly });
147
+ function makePrecondition(params) {
148
+ return params;
149
+ }
150
+ function buildDepMapping(deps) {
151
+ return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
152
+ }
153
+ function buildPreconditionStart(name) {
154
+ let description = "";
155
+ const self = {
156
+ contract: () => makePrecondition({
157
+ dependsOn: [],
158
+ depMapping: [],
159
+ description,
160
+ name
161
+ }),
162
+ description(text) {
163
+ description = text;
164
+ return self;
165
+ },
166
+ requires(deps) {
167
+ return buildPreconditionWithDeps({ deps, name, getDescription: () => description });
229
168
  }
230
169
  };
170
+ return self;
231
171
  }
232
- function buildTestRequires({
233
- id,
234
- name,
235
- tests,
236
- uiOnly
172
+ function buildPreconditionWithDeps({
173
+ deps,
174
+ getDescription,
175
+ name
237
176
  }) {
177
+ const description = getDescription();
178
+ const depMapping = buildDepMapping(deps);
179
+ const dependsOn = depMapping.map(([, depName]) => depName);
180
+ return {
181
+ contract: () => makePrecondition({
182
+ dependsOn,
183
+ depMapping,
184
+ description,
185
+ name
186
+ })
187
+ };
188
+ }
189
+ function buildTestName(id, uiOnly) {
190
+ return {
191
+ name: (displayName) => buildTestRequires({ id, name: displayName, uiOnly })
192
+ };
193
+ }
194
+ function castOutcome(value) {
195
+ return value;
196
+ }
197
+ function buildTestRequires({ id, name, uiOnly }) {
238
198
  let description = "";
239
199
  const self = {
240
- requires: castBuilder(requiresImpl),
241
200
  description(text) {
242
201
  description = text;
243
202
  return self;
203
+ },
204
+ requires(reqs) {
205
+ const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
206
+ const requiresKeys = {};
207
+ Object.entries(reqs).forEach(([key, p]) => {
208
+ requiresKeys[key] = readPreconditionName(p);
209
+ });
210
+ return castOutcome(
211
+ buildTestOutcome({
212
+ id,
213
+ name,
214
+ reqNames,
215
+ requiresKeys,
216
+ uiOnly,
217
+ getDescription: () => description
218
+ })
219
+ );
244
220
  }
245
221
  };
246
222
  return self;
247
- function requiresImpl(reqs) {
248
- const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
249
- const requiresKeys = {};
250
- Object.entries(reqs).forEach(([key, precondition]) => {
251
- requiresKeys[key] = readPreconditionName(precondition);
252
- });
253
- return buildOutcome({ description, id, name, reqNames, requiresKeys, tests, uiOnly });
254
- }
255
223
  }
256
- function buildOutcome({
257
- description: initialDesc,
224
+ function buildTestOutcome({
225
+ getDescription,
258
226
  id,
259
227
  name,
260
228
  reqNames,
261
229
  requiresKeys,
262
- tests,
263
230
  uiOnly
264
231
  }) {
265
- let description = initialDesc;
232
+ const description = getDescription();
266
233
  return {
267
- description(text) {
268
- description = text;
269
- return this;
270
- },
271
234
  expectedOutcome(text) {
272
- return buildStartsAt({
235
+ return buildTestStartsAt({
273
236
  description,
274
237
  expectedOutcome: text,
275
238
  id,
276
239
  name,
277
240
  reqNames,
278
241
  requiresKeys,
279
- tests,
280
242
  uiOnly
281
243
  });
282
244
  }
283
245
  };
284
246
  }
285
- function buildStartsAt({
247
+ function buildTestStartsAt({
286
248
  description,
287
249
  expectedOutcome,
288
250
  id,
289
251
  name,
290
252
  reqNames,
291
253
  requiresKeys,
292
- tests,
293
254
  uiOnly
294
255
  }) {
295
256
  return {
296
- startsAt: castBuilder(startsAtImpl),
297
- notImplemented() {
298
- tests.push({
299
- description,
300
- expectedOutcome,
301
- id,
302
- implemented: false,
303
- name,
304
- requires: [...reqNames],
305
- requiresKeys,
306
- startsAtFn: void 0,
307
- stepsFn: void 0,
308
- uiOnly
309
- });
310
- }
311
- };
312
- function startsAtImpl(fn) {
313
- return {
314
- steps: castBuilder(stepsImpl)
315
- };
316
- function stepsImpl(stepsFn) {
317
- tests.push({
257
+ notImplemented: () => ({
258
+ description,
259
+ expectedOutcome,
260
+ id,
261
+ implemented: false,
262
+ name,
263
+ requires: [...reqNames],
264
+ requiresKeys,
265
+ startsAtFn: void 0,
266
+ stepsFn: void 0,
267
+ uiOnly
268
+ }),
269
+ startsAt(fn) {
270
+ return buildTestSteps({
318
271
  description,
319
272
  expectedOutcome,
320
273
  id,
321
- implemented: true,
322
274
  name,
323
- requires: [...reqNames],
275
+ reqNames,
324
276
  requiresKeys,
325
277
  startsAtFn: fn,
326
- stepsFn,
327
278
  uiOnly
328
279
  });
329
280
  }
330
- }
281
+ };
282
+ }
283
+ function buildTestSteps({
284
+ description,
285
+ expectedOutcome,
286
+ id,
287
+ name,
288
+ reqNames,
289
+ requiresKeys,
290
+ startsAtFn,
291
+ uiOnly
292
+ }) {
293
+ return {
294
+ steps: (stepsFn) => ({
295
+ description,
296
+ expectedOutcome,
297
+ id,
298
+ implemented: true,
299
+ name,
300
+ requires: [...reqNames],
301
+ requiresKeys,
302
+ startsAtFn,
303
+ stepsFn,
304
+ uiOnly
305
+ })
306
+ };
331
307
  }
332
308
 
333
309
  // src/lint.ts
334
310
  function lint(result) {
335
311
  const diagnostics = [];
336
- result.tests.forEach((test) => {
337
- const nodes = getOrderedNodes(test);
312
+ result.tests.forEach((test2) => {
313
+ const nodes = getOrderedNodes(test2);
338
314
  const report = (diagnostic) => {
339
- diagnostics.push({ ...diagnostic, test: test.slug });
315
+ diagnostics.push({ ...diagnostic, test: test2.slug });
340
316
  };
341
317
  RULES.forEach((rule) => {
342
- rule(nodes, test, report);
318
+ rule(nodes, test2, report);
343
319
  });
344
320
  });
345
321
  return { diagnostics };
346
322
  }
347
- function getOrderedNodes(test) {
323
+ function getOrderedNodes(test2) {
348
324
  const result = [];
349
- let currentId = test.spec.entryNode;
325
+ let currentId = test2.spec.entryNode;
350
326
  const visited = /* @__PURE__ */ new Set();
351
327
  while (currentId != null && !visited.has(currentId)) {
352
328
  visited.add(currentId);
353
- const found = test.spec.nodes[currentId];
329
+ const found = test2.spec.nodes[currentId];
354
330
  if (found == null) {
355
331
  break;
356
332
  }
@@ -370,8 +346,8 @@ function exactTextMatch(nodes, _test, report) {
370
346
  }
371
347
  });
372
348
  }
373
- function noHardcodedData(nodes, test, report) {
374
- const hasVariables = Object.keys(test.spec.variables ?? {}).length > 0;
349
+ function noHardcodedData(nodes, test2, report) {
350
+ const hasVariables = Object.keys(test2.spec.variables ?? {}).length > 0;
375
351
  if (!hasVariables) {
376
352
  return;
377
353
  }
@@ -388,8 +364,8 @@ function noHardcodedData(nodes, test, report) {
388
364
  }
389
365
  });
390
366
  }
391
- function preferPreconditionData(nodes, test, report) {
392
- const variableKeys = Object.keys(test.spec.variables ?? {});
367
+ function preferPreconditionData(nodes, test2, report) {
368
+ const variableKeys = Object.keys(test2.spec.variables ?? {});
393
369
  if (variableKeys.length === 0) {
394
370
  return;
395
371
  }
@@ -461,8 +437,8 @@ function assertMatchesOutcome(nodes, _test, report) {
461
437
  });
462
438
  }
463
439
  }
464
- function noEmptySteps(nodes, test, report) {
465
- if (!test.implemented) {
440
+ function noEmptySteps(nodes, test2, report) {
441
+ if (!test2.implemented) {
466
442
  return;
467
443
  }
468
444
  if (nodes.length === 0) {
@@ -509,8 +485,8 @@ var EFFECT_ASSERTION_TYPES = /* @__PURE__ */ new Set([
509
485
  function isEffectAssertion(node) {
510
486
  return EFFECT_ASSERTION_TYPES.has(node.type);
511
487
  }
512
- function noAssertions(nodes, test, report) {
513
- if (!test.implemented || nodes.length === 0) {
488
+ function noAssertions(nodes, test2, report) {
489
+ if (!test2.implemented || nodes.length === 0) {
514
490
  return;
515
491
  }
516
492
  if (!nodes.some((n) => isAssertionNode(n))) {
@@ -521,8 +497,8 @@ function noAssertions(nodes, test, report) {
521
497
  });
522
498
  }
523
499
  }
524
- function lowAssertionRatio(nodes, test, report) {
525
- if (!test.implemented || nodes.length <= 3) {
500
+ function lowAssertionRatio(nodes, test2, report) {
501
+ if (!test2.implemented || nodes.length <= 3) {
526
502
  return;
527
503
  }
528
504
  const assertions = nodes.filter((n) => isAssertionNode(n)).length;
@@ -646,11 +622,11 @@ function nodeKeywords(node) {
646
622
  const locName = loc.by === "role" ? loc.name ?? "" : loc.value;
647
623
  return [...fromLabel, ...tokenize(locName)];
648
624
  }
649
- function expectedOutcomeKeywordCoverage(nodes, test, report) {
650
- if (!test.implemented || nodes.length === 0) {
625
+ function expectedOutcomeKeywordCoverage(nodes, test2, report) {
626
+ if (!test2.implemented || nodes.length === 0) {
651
627
  return;
652
628
  }
653
- const outcomeTokens = new Set(tokenize(test.expectedOutcome));
629
+ const outcomeTokens = new Set(tokenize(test2.expectedOutcome));
654
630
  if (outcomeTokens.size === 0) {
655
631
  return;
656
632
  }
@@ -684,8 +660,8 @@ function isLikelyBackendMutation(node) {
684
660
  const name = loc.by === "role" ? loc.name ?? "" : loc.value;
685
661
  return BACKEND_MUTATION_KEYWORDS.test(name);
686
662
  }
687
- function mutationWithoutObserverCoverage(nodes, test, report) {
688
- if (!test.implemented || test.spec.uiOnly === true) {
663
+ function mutationWithoutObserverCoverage(nodes, test2, report) {
664
+ if (!test2.implemented || test2.spec.uiOnly === true) {
689
665
  return;
690
666
  }
691
667
  nodes.forEach((node, index) => {
@@ -710,8 +686,8 @@ function mutationWithoutObserverCoverage(nodes, test, report) {
710
686
  });
711
687
  });
712
688
  }
713
- function observerParamsReferenceVariables(nodes, test, report) {
714
- const variableKeys = Object.keys(test.spec.variables ?? {});
689
+ function observerParamsReferenceVariables(nodes, test2, report) {
690
+ const variableKeys = Object.keys(test2.spec.variables ?? {});
715
691
  if (variableKeys.length === 0) {
716
692
  return;
717
693
  }
@@ -752,6 +728,278 @@ var RULES = [
752
728
  mutationWithoutObserverCoverage,
753
729
  observerParamsReferenceVariables
754
730
  ];
731
+
732
+ // src/engine.ts
733
+ function notImplemented(reason) {
734
+ return { reason: reason ?? "not implemented" };
735
+ }
736
+ function isNotImplemented(value) {
737
+ return typeof value === "object" && value !== null && "reason" in value && Object.keys(value).length === 1;
738
+ }
739
+ function createEngine(ripplo, impls) {
740
+ const preconditionDefs = wirePreconditions(ripplo.preconditions, impls.preconditions);
741
+ const observerDefs = wireObservers(ripplo.observers, impls.observers);
742
+ const preconditionsByName = new Map(preconditionDefs.map((d) => [d.name, d]));
743
+ const observersByName = new Map(observerDefs.map((d) => [d.name, d]));
744
+ return {
745
+ executeObserver: (name, params) => executeObserver(observersByName, name, params),
746
+ executePreconditions: (names, options) => executePreconditions({ defsByName: preconditionsByName, names, options }),
747
+ getConfig: () => ripplo.getConfig(),
748
+ getObservers: () => observerDefs,
749
+ getPreconditions: () => preconditionDefs,
750
+ getUnimplemented: () => ({
751
+ observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
752
+ preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
753
+ tests: ripplo.tests.filter((t) => !t.implemented).map((t) => t.id)
754
+ }),
755
+ teardown: (names, data) => teardown(preconditionsByName, names, data)
756
+ };
757
+ }
758
+ function wirePreconditions(registry, impls) {
759
+ return Object.entries(registry).map(([key, handle]) => {
760
+ const impl = Reflect.get(impls, key);
761
+ if (isNotImplemented(impl)) {
762
+ return makeNotImplementedPreconditionDef(handle, impl);
763
+ }
764
+ return makeWiredPreconditionDef(handle, impl);
765
+ });
766
+ }
767
+ function wireObservers(registry, impls) {
768
+ return Object.entries(registry).map(([key, handle]) => {
769
+ const impl = Reflect.get(impls, key);
770
+ if (isNotImplemented(impl)) {
771
+ return makeNotImplementedObserverDef(handle, impl);
772
+ }
773
+ return makeWiredObserverDef(handle, impl);
774
+ });
775
+ }
776
+ function makeNotImplementedPreconditionDef(handle, sentinel) {
777
+ const name = readPreconditionName(handle);
778
+ return {
779
+ dependsOn: readPreconditionDepMappingKeys(handle),
780
+ depMapping: readPreconditionDepMapping(handle),
781
+ description: readDescriptionOf(handle),
782
+ implemented: false,
783
+ name,
784
+ returns: [],
785
+ teardown: void 0,
786
+ setup: () => Promise.reject(new Error(`Precondition "${name}" is ${sentinel.reason}`))
787
+ };
788
+ }
789
+ function makeWiredPreconditionDef(handle, impl) {
790
+ const name = readPreconditionName(handle);
791
+ const mapping = readPreconditionDepMapping(handle);
792
+ const typedImpl = impl;
793
+ return {
794
+ dependsOn: mapping.map(([, depName]) => depName),
795
+ depMapping: mapping,
796
+ description: readDescriptionOf(handle),
797
+ implemented: true,
798
+ name,
799
+ returns: [],
800
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- typedImpl.teardown has narrower TData, wider context at engine boundary
801
+ teardown: typedImpl.teardown,
802
+ setup: async (ctx, allDeps) => {
803
+ const resolved = {};
804
+ mapping.forEach(([key, depName]) => {
805
+ const data = allDeps[depName];
806
+ if (data != null) {
807
+ resolved[key] = data;
808
+ }
809
+ });
810
+ return typedImpl.setup(ctx, resolved);
811
+ }
812
+ };
813
+ }
814
+ function makeNotImplementedObserverDef(handle, sentinel) {
815
+ const name = readObserverName(handle);
816
+ return {
817
+ budget: readBudgetOf(handle),
818
+ description: readDescriptionOfObserver(handle),
819
+ implemented: false,
820
+ name,
821
+ run: () => Promise.resolve(createFailOutcome(`observer "${name}" is ${sentinel.reason}`))
822
+ };
823
+ }
824
+ function makeWiredObserverDef(handle, impl) {
825
+ const name = readObserverName(handle);
826
+ const typedImpl = impl;
827
+ return {
828
+ budget: readBudgetOf(handle),
829
+ description: readDescriptionOfObserver(handle),
830
+ implemented: true,
831
+ name,
832
+ run: async (ctx, params) => {
833
+ return typedImpl(ctx, params);
834
+ }
835
+ };
836
+ }
837
+ function readPreconditionDepMappingKeys(handle) {
838
+ return readPreconditionDepMapping(handle).map(([, depName]) => depName);
839
+ }
840
+ function readDescriptionOf(handle) {
841
+ return handle.description;
842
+ }
843
+ function readDescriptionOfObserver(handle) {
844
+ return handle.description;
845
+ }
846
+ function readBudgetOf(handle) {
847
+ return handle.budget;
848
+ }
849
+ async function executePreconditions({
850
+ defsByName,
851
+ names,
852
+ options
853
+ }) {
854
+ const runId = crypto.randomUUID().slice(0, 12);
855
+ const cookies = [];
856
+ const defaultDomain = deriveDefaultDomain(options?.appUrl);
857
+ const state = {
858
+ cookies,
859
+ ctx: createSetupContext({ cookies, defaultDomain, runId }),
860
+ data: {},
861
+ defsByName,
862
+ executed: [],
863
+ runId
864
+ };
865
+ return runBatchSequence(state, names);
866
+ }
867
+ async function runBatchSequence(state, names) {
868
+ let index = 0;
869
+ while (index < names.length) {
870
+ const name = names[index];
871
+ if (name == null) {
872
+ break;
873
+ }
874
+ const error = validatePrecondition(state.defsByName, name);
875
+ if (error != null) {
876
+ return fail(state, error);
877
+ }
878
+ const stepError = await executeOnePrecondition(state, name);
879
+ if (stepError != null) {
880
+ return fail(state, stepError);
881
+ }
882
+ index += 1;
883
+ }
884
+ return {
885
+ cookies: state.cookies,
886
+ data: state.data,
887
+ error: void 0,
888
+ executed: state.executed,
889
+ runId: state.runId,
890
+ success: true
891
+ };
892
+ }
893
+ function validatePrecondition(defsByName, name) {
894
+ const def = defsByName.get(name);
895
+ if (def == null) {
896
+ return `Unknown precondition: "${name}"`;
897
+ }
898
+ if (!def.implemented) {
899
+ return `Precondition "${name}" is not implemented`;
900
+ }
901
+ return void 0;
902
+ }
903
+ async function executeOnePrecondition(state, name) {
904
+ const def = state.defsByName.get(name);
905
+ if (def == null) {
906
+ return `Unknown precondition: "${name}"`;
907
+ }
908
+ try {
909
+ const result = await def.setup(state.ctx, state.data);
910
+ const resolved = {};
911
+ Object.entries(result).forEach(([key, value]) => {
912
+ resolved[key] = readTestValue(value);
913
+ });
914
+ state.data[name] = resolved;
915
+ state.executed.push(name);
916
+ return void 0;
917
+ } catch (error) {
918
+ return error instanceof Error ? error.message : String(error);
919
+ }
920
+ }
921
+ function fail(state, error) {
922
+ return {
923
+ cookies: state.cookies,
924
+ data: state.data,
925
+ error,
926
+ executed: state.executed,
927
+ runId: state.runId,
928
+ success: false
929
+ };
930
+ }
931
+ async function executeObserver(observersByName, name, params) {
932
+ const def = observersByName.get(name);
933
+ if (def == null) {
934
+ return { error: `Unknown observer: "${name}"`, outcome: void 0, success: false };
935
+ }
936
+ if (!def.implemented) {
937
+ return { error: `Observer "${name}" is not implemented`, outcome: void 0, success: false };
938
+ }
939
+ const ctx = createObserverContext(crypto.randomUUID().slice(0, 12));
940
+ try {
941
+ const outcome = await def.run(ctx, params);
942
+ return { error: void 0, outcome, success: true };
943
+ } catch (error) {
944
+ const message = error instanceof Error ? error.message : String(error);
945
+ return { error: void 0, outcome: createFailOutcome(message), success: true };
946
+ }
947
+ }
948
+ function createObserverContext(runId) {
949
+ return {
950
+ runId,
951
+ fail: (reason) => createFailOutcome(reason),
952
+ pass: () => createPassOutcome(),
953
+ retry: (reason) => createRetryOutcome(reason)
954
+ };
955
+ }
956
+ async function teardown(defsByName, names, data) {
957
+ const reversed = [...names].toReversed();
958
+ let index = 0;
959
+ while (index < reversed.length) {
960
+ const name = reversed[index];
961
+ if (name != null) {
962
+ await teardownOne(defsByName, name, data);
963
+ }
964
+ index += 1;
965
+ }
966
+ }
967
+ async function teardownOne(defsByName, name, data) {
968
+ const def = defsByName.get(name);
969
+ if (def?.teardown == null) {
970
+ return;
971
+ }
972
+ try {
973
+ await def.teardown({ data: data[name] ?? {} });
974
+ } catch {
975
+ }
976
+ }
977
+ function createSetupContext({
978
+ cookies,
979
+ defaultDomain,
980
+ runId
981
+ }) {
982
+ return {
983
+ runId,
984
+ fixed: (value) => createTestValue(value),
985
+ setCookie: (name, value, options) => {
986
+ const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
987
+ cookies.push({ name, options: resolvedOptions, value });
988
+ },
989
+ uniqueEmail: () => createTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
990
+ uniqueId: (prefix) => createTestValue(`ripplo-test-${prefix}-${runId}`)
991
+ };
992
+ }
993
+ function deriveDefaultDomain(baseUrl) {
994
+ if (baseUrl == null) {
995
+ return void 0;
996
+ }
997
+ try {
998
+ return new URL(baseUrl).hostname;
999
+ } catch {
1000
+ return void 0;
1001
+ }
1002
+ }
755
1003
  export {
756
1004
  DEFAULT_IGNORE_PATHS,
757
1005
  DEFAULT_WATCH_PATHS,
@@ -760,6 +1008,10 @@ export {
760
1008
  createEngine,
761
1009
  createRipplo,
762
1010
  lint,
1011
+ notImplemented,
1012
+ observer,
1013
+ precondition,
763
1014
  serializeCookie,
1015
+ test,
764
1016
  verifyWebhookSignature
765
1017
  };