@ripplo/testing 0.1.1 → 0.3.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,147 @@
1
+ import {
2
+ DEFAULT_IGNORE_PATHS,
3
+ DEFAULT_WATCH_PATHS,
4
+ createTestValue,
5
+ makeObserverHandle,
6
+ readObserverBudget,
7
+ readObserverDescription,
8
+ readObserverName,
9
+ readPreconditionDepMapping,
10
+ readPreconditionDependsOn,
11
+ readPreconditionDescription,
12
+ readPreconditionName,
13
+ readTestValue,
14
+ userDslConfigSchema
15
+ } from "./chunk-76BU4M6E.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 parsed = userDslConfigSchema.parse(rawConfig);
70
+ const webhookSecret = parsed.webhookSecret ?? process.env["RIPPLO_WEBHOOK_SECRET"] ?? "";
71
+ if (webhookSecret.length === 0) {
72
+ throw new Error(
73
+ "RIPPLO_WEBHOOK_SECRET is required. Set it in .ripplo/.env or pass webhookSecret to createRipplo()."
74
+ );
75
+ }
76
+ const config = { ...parsed, webhookSecret };
77
+ const { observers, preconditions, tests } = registries;
78
+ validateUniqueNames(preconditions, observers, tests);
79
+ const preconditionDefs = Object.values(preconditions).map((p) => stubPreconditionDef(p));
80
+ const observerDefs = Object.values(observers).map((o) => stubObserverDef(o));
81
+ const testDefs = [...tests];
82
+ return {
83
+ config,
84
+ observers,
85
+ preconditions,
86
+ tests: testDefs,
87
+ getConfig: () => config,
88
+ getObservers: () => observerDefs,
89
+ getPreconditions: () => preconditionDefs,
90
+ getTests: () => testDefs,
91
+ getUnimplemented: () => ({
92
+ observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
93
+ preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
94
+ tests: testDefs.filter((t) => !t.implemented).map((t) => t.id)
95
+ })
96
+ };
135
97
  }
136
- function pushStub(params) {
137
- params.preconditions.push({
138
- dependsOn: params.depMapping.map(([_, depName]) => depName),
139
- depMapping: params.depMapping,
140
- description: params.description,
98
+ function stubPreconditionDef(p) {
99
+ const name = readPreconditionName(p);
100
+ return {
101
+ dependsOn: readPreconditionDependsOn(p),
102
+ depMapping: readPreconditionDepMapping(p),
103
+ description: readPreconditionDescription(p),
141
104
  implemented: false,
142
- name: params.name,
105
+ name,
143
106
  returns: [],
144
107
  teardown: void 0,
145
108
  setup: () => Promise.resolve({})
146
- });
147
- return makePrecondition(params.name);
109
+ };
148
110
  }
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
- }
111
+ function stubObserverDef(o) {
112
+ const name = readObserverName(o);
113
+ return {
114
+ budget: readObserverBudget(o),
115
+ description: readObserverDescription(o),
116
+ implemented: false,
117
+ name,
118
+ run: () => Promise.resolve(createFailOutcome(`observer "${name}" not implemented`))
166
119
  };
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);
120
+ }
121
+ function validateUniqueNames(preconditions, observers, tests) {
122
+ const pNames = /* @__PURE__ */ new Set();
123
+ Object.values(preconditions).forEach((p) => {
124
+ const name = readPreconditionName(p);
125
+ if (pNames.has(name)) {
126
+ throw new Error(`Duplicate precondition name: "${name}"`);
188
127
  }
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
- }
128
+ pNames.add(name);
129
+ });
130
+ const oNames = /* @__PURE__ */ new Set();
131
+ Object.values(observers).forEach((o) => {
132
+ const name = readObserverName(o);
133
+ if (oNames.has(name)) {
134
+ throw new Error(`Duplicate observer name: "${name}"`);
135
+ }
136
+ oNames.add(name);
137
+ });
138
+ const tIds = /* @__PURE__ */ new Set();
139
+ tests.forEach((t) => {
140
+ if (tIds.has(t.id)) {
141
+ throw new Error(`Duplicate test id: "${t.id}"`);
142
+ }
143
+ tIds.add(t.id);
144
+ });
216
145
  }
217
146
  var TEST_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
218
147
  function validateTestId(id) {
@@ -222,135 +151,189 @@ function validateTestId(id) {
222
151
  );
223
152
  }
224
153
  }
225
- function buildTestName(id, tests, uiOnly) {
226
- return {
227
- name(displayName) {
228
- return buildTestRequires({ id, name: displayName, tests, uiOnly });
154
+ function makePrecondition(params) {
155
+ return params;
156
+ }
157
+ function buildDepMapping(deps) {
158
+ return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
159
+ }
160
+ function buildPreconditionStart(name) {
161
+ let description = "";
162
+ const self = {
163
+ contract: () => makePrecondition({
164
+ dependsOn: [],
165
+ depMapping: [],
166
+ description,
167
+ name
168
+ }),
169
+ description(text) {
170
+ description = text;
171
+ return self;
172
+ },
173
+ requires(deps) {
174
+ return buildPreconditionWithDeps({ deps, name, getDescription: () => description });
229
175
  }
230
176
  };
177
+ return self;
231
178
  }
232
- function buildTestRequires({
233
- id,
234
- name,
235
- tests,
236
- uiOnly
179
+ function buildPreconditionWithDeps({
180
+ deps,
181
+ getDescription,
182
+ name
237
183
  }) {
184
+ const description = getDescription();
185
+ const depMapping = buildDepMapping(deps);
186
+ const dependsOn = depMapping.map(([, depName]) => depName);
187
+ return {
188
+ contract: () => makePrecondition({
189
+ dependsOn,
190
+ depMapping,
191
+ description,
192
+ name
193
+ })
194
+ };
195
+ }
196
+ function buildTestName(id, uiOnly) {
197
+ return {
198
+ name: (displayName) => buildTestRequires({ id, name: displayName, uiOnly })
199
+ };
200
+ }
201
+ function castOutcome(value) {
202
+ return value;
203
+ }
204
+ function buildTestRequires({ id, name, uiOnly }) {
238
205
  let description = "";
239
206
  const self = {
240
- requires: castBuilder(requiresImpl),
241
207
  description(text) {
242
208
  description = text;
243
209
  return self;
210
+ },
211
+ requires(reqs) {
212
+ const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
213
+ const requiresKeys = {};
214
+ Object.entries(reqs).forEach(([key, p]) => {
215
+ requiresKeys[key] = readPreconditionName(p);
216
+ });
217
+ return castOutcome(
218
+ buildTestOutcome({
219
+ id,
220
+ name,
221
+ reqNames,
222
+ requiresKeys,
223
+ uiOnly,
224
+ getDescription: () => description
225
+ })
226
+ );
244
227
  }
245
228
  };
246
229
  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
230
  }
256
- function buildOutcome({
257
- description: initialDesc,
231
+ function buildTestOutcome({
232
+ getDescription,
258
233
  id,
259
234
  name,
260
235
  reqNames,
261
236
  requiresKeys,
262
- tests,
263
237
  uiOnly
264
238
  }) {
265
- let description = initialDesc;
239
+ const description = getDescription();
266
240
  return {
267
- description(text) {
268
- description = text;
269
- return this;
270
- },
271
241
  expectedOutcome(text) {
272
- return buildStartsAt({
242
+ return buildTestStartsAt({
273
243
  description,
274
244
  expectedOutcome: text,
275
245
  id,
276
246
  name,
277
247
  reqNames,
278
248
  requiresKeys,
279
- tests,
280
249
  uiOnly
281
250
  });
282
251
  }
283
252
  };
284
253
  }
285
- function buildStartsAt({
254
+ function buildTestStartsAt({
286
255
  description,
287
256
  expectedOutcome,
288
257
  id,
289
258
  name,
290
259
  reqNames,
291
260
  requiresKeys,
292
- tests,
293
261
  uiOnly
294
262
  }) {
295
263
  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({
264
+ notImplemented: () => ({
265
+ description,
266
+ expectedOutcome,
267
+ id,
268
+ implemented: false,
269
+ name,
270
+ requires: [...reqNames],
271
+ requiresKeys,
272
+ startsAtFn: void 0,
273
+ stepsFn: void 0,
274
+ uiOnly
275
+ }),
276
+ startsAt(fn) {
277
+ return buildTestSteps({
318
278
  description,
319
279
  expectedOutcome,
320
280
  id,
321
- implemented: true,
322
281
  name,
323
- requires: [...reqNames],
282
+ reqNames,
324
283
  requiresKeys,
325
284
  startsAtFn: fn,
326
- stepsFn,
327
285
  uiOnly
328
286
  });
329
287
  }
330
- }
288
+ };
289
+ }
290
+ function buildTestSteps({
291
+ description,
292
+ expectedOutcome,
293
+ id,
294
+ name,
295
+ reqNames,
296
+ requiresKeys,
297
+ startsAtFn,
298
+ uiOnly
299
+ }) {
300
+ return {
301
+ steps: (stepsFn) => ({
302
+ description,
303
+ expectedOutcome,
304
+ id,
305
+ implemented: true,
306
+ name,
307
+ requires: [...reqNames],
308
+ requiresKeys,
309
+ startsAtFn,
310
+ stepsFn,
311
+ uiOnly
312
+ })
313
+ };
331
314
  }
332
315
 
333
316
  // src/lint.ts
334
317
  function lint(result) {
335
318
  const diagnostics = [];
336
- result.tests.forEach((test) => {
337
- const nodes = getOrderedNodes(test);
319
+ result.tests.forEach((test2) => {
320
+ const nodes = getOrderedNodes(test2);
338
321
  const report = (diagnostic) => {
339
- diagnostics.push({ ...diagnostic, test: test.slug });
322
+ diagnostics.push({ ...diagnostic, test: test2.slug });
340
323
  };
341
324
  RULES.forEach((rule) => {
342
- rule(nodes, test, report);
325
+ rule(nodes, test2, report);
343
326
  });
344
327
  });
345
328
  return { diagnostics };
346
329
  }
347
- function getOrderedNodes(test) {
330
+ function getOrderedNodes(test2) {
348
331
  const result = [];
349
- let currentId = test.spec.entryNode;
332
+ let currentId = test2.spec.entryNode;
350
333
  const visited = /* @__PURE__ */ new Set();
351
334
  while (currentId != null && !visited.has(currentId)) {
352
335
  visited.add(currentId);
353
- const found = test.spec.nodes[currentId];
336
+ const found = test2.spec.nodes[currentId];
354
337
  if (found == null) {
355
338
  break;
356
339
  }
@@ -370,8 +353,8 @@ function exactTextMatch(nodes, _test, report) {
370
353
  }
371
354
  });
372
355
  }
373
- function noHardcodedData(nodes, test, report) {
374
- const hasVariables = Object.keys(test.spec.variables ?? {}).length > 0;
356
+ function noHardcodedData(nodes, test2, report) {
357
+ const hasVariables = Object.keys(test2.spec.variables ?? {}).length > 0;
375
358
  if (!hasVariables) {
376
359
  return;
377
360
  }
@@ -388,8 +371,8 @@ function noHardcodedData(nodes, test, report) {
388
371
  }
389
372
  });
390
373
  }
391
- function preferPreconditionData(nodes, test, report) {
392
- const variableKeys = Object.keys(test.spec.variables ?? {});
374
+ function preferPreconditionData(nodes, test2, report) {
375
+ const variableKeys = Object.keys(test2.spec.variables ?? {});
393
376
  if (variableKeys.length === 0) {
394
377
  return;
395
378
  }
@@ -461,8 +444,8 @@ function assertMatchesOutcome(nodes, _test, report) {
461
444
  });
462
445
  }
463
446
  }
464
- function noEmptySteps(nodes, test, report) {
465
- if (!test.implemented) {
447
+ function noEmptySteps(nodes, test2, report) {
448
+ if (!test2.implemented) {
466
449
  return;
467
450
  }
468
451
  if (nodes.length === 0) {
@@ -509,8 +492,8 @@ var EFFECT_ASSERTION_TYPES = /* @__PURE__ */ new Set([
509
492
  function isEffectAssertion(node) {
510
493
  return EFFECT_ASSERTION_TYPES.has(node.type);
511
494
  }
512
- function noAssertions(nodes, test, report) {
513
- if (!test.implemented || nodes.length === 0) {
495
+ function noAssertions(nodes, test2, report) {
496
+ if (!test2.implemented || nodes.length === 0) {
514
497
  return;
515
498
  }
516
499
  if (!nodes.some((n) => isAssertionNode(n))) {
@@ -521,8 +504,8 @@ function noAssertions(nodes, test, report) {
521
504
  });
522
505
  }
523
506
  }
524
- function lowAssertionRatio(nodes, test, report) {
525
- if (!test.implemented || nodes.length <= 3) {
507
+ function lowAssertionRatio(nodes, test2, report) {
508
+ if (!test2.implemented || nodes.length <= 3) {
526
509
  return;
527
510
  }
528
511
  const assertions = nodes.filter((n) => isAssertionNode(n)).length;
@@ -646,11 +629,11 @@ function nodeKeywords(node) {
646
629
  const locName = loc.by === "role" ? loc.name ?? "" : loc.value;
647
630
  return [...fromLabel, ...tokenize(locName)];
648
631
  }
649
- function expectedOutcomeKeywordCoverage(nodes, test, report) {
650
- if (!test.implemented || nodes.length === 0) {
632
+ function expectedOutcomeKeywordCoverage(nodes, test2, report) {
633
+ if (!test2.implemented || nodes.length === 0) {
651
634
  return;
652
635
  }
653
- const outcomeTokens = new Set(tokenize(test.expectedOutcome));
636
+ const outcomeTokens = new Set(tokenize(test2.expectedOutcome));
654
637
  if (outcomeTokens.size === 0) {
655
638
  return;
656
639
  }
@@ -684,8 +667,8 @@ function isLikelyBackendMutation(node) {
684
667
  const name = loc.by === "role" ? loc.name ?? "" : loc.value;
685
668
  return BACKEND_MUTATION_KEYWORDS.test(name);
686
669
  }
687
- function mutationWithoutObserverCoverage(nodes, test, report) {
688
- if (!test.implemented || test.spec.uiOnly === true) {
670
+ function mutationWithoutObserverCoverage(nodes, test2, report) {
671
+ if (!test2.implemented || test2.spec.uiOnly === true) {
689
672
  return;
690
673
  }
691
674
  nodes.forEach((node, index) => {
@@ -710,8 +693,8 @@ function mutationWithoutObserverCoverage(nodes, test, report) {
710
693
  });
711
694
  });
712
695
  }
713
- function observerParamsReferenceVariables(nodes, test, report) {
714
- const variableKeys = Object.keys(test.spec.variables ?? {});
696
+ function observerParamsReferenceVariables(nodes, test2, report) {
697
+ const variableKeys = Object.keys(test2.spec.variables ?? {});
715
698
  if (variableKeys.length === 0) {
716
699
  return;
717
700
  }
@@ -752,6 +735,278 @@ var RULES = [
752
735
  mutationWithoutObserverCoverage,
753
736
  observerParamsReferenceVariables
754
737
  ];
738
+
739
+ // src/engine.ts
740
+ function notImplemented(reason) {
741
+ return { reason: reason ?? "not implemented" };
742
+ }
743
+ function isNotImplemented(value) {
744
+ return typeof value === "object" && value !== null && "reason" in value && Object.keys(value).length === 1;
745
+ }
746
+ function createEngine(ripplo, impls) {
747
+ const preconditionDefs = wirePreconditions(ripplo.preconditions, impls.preconditions);
748
+ const observerDefs = wireObservers(ripplo.observers, impls.observers);
749
+ const preconditionsByName = new Map(preconditionDefs.map((d) => [d.name, d]));
750
+ const observersByName = new Map(observerDefs.map((d) => [d.name, d]));
751
+ return {
752
+ executeObserver: (name, params) => executeObserver(observersByName, name, params),
753
+ executePreconditions: (names, options) => executePreconditions({ defsByName: preconditionsByName, names, options }),
754
+ getConfig: () => ripplo.getConfig(),
755
+ getObservers: () => observerDefs,
756
+ getPreconditions: () => preconditionDefs,
757
+ getUnimplemented: () => ({
758
+ observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
759
+ preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
760
+ tests: ripplo.tests.filter((t) => !t.implemented).map((t) => t.id)
761
+ }),
762
+ teardown: (names, data) => teardown(preconditionsByName, names, data)
763
+ };
764
+ }
765
+ function wirePreconditions(registry, impls) {
766
+ return Object.entries(registry).map(([key, handle]) => {
767
+ const impl = Reflect.get(impls, key);
768
+ if (isNotImplemented(impl)) {
769
+ return makeNotImplementedPreconditionDef(handle, impl);
770
+ }
771
+ return makeWiredPreconditionDef(handle, impl);
772
+ });
773
+ }
774
+ function wireObservers(registry, impls) {
775
+ return Object.entries(registry).map(([key, handle]) => {
776
+ const impl = Reflect.get(impls, key);
777
+ if (isNotImplemented(impl)) {
778
+ return makeNotImplementedObserverDef(handle, impl);
779
+ }
780
+ return makeWiredObserverDef(handle, impl);
781
+ });
782
+ }
783
+ function makeNotImplementedPreconditionDef(handle, sentinel) {
784
+ const name = readPreconditionName(handle);
785
+ return {
786
+ dependsOn: readPreconditionDepMappingKeys(handle),
787
+ depMapping: readPreconditionDepMapping(handle),
788
+ description: readDescriptionOf(handle),
789
+ implemented: false,
790
+ name,
791
+ returns: [],
792
+ teardown: void 0,
793
+ setup: () => Promise.reject(new Error(`Precondition "${name}" is ${sentinel.reason}`))
794
+ };
795
+ }
796
+ function makeWiredPreconditionDef(handle, impl) {
797
+ const name = readPreconditionName(handle);
798
+ const mapping = readPreconditionDepMapping(handle);
799
+ const typedImpl = impl;
800
+ return {
801
+ dependsOn: mapping.map(([, depName]) => depName),
802
+ depMapping: mapping,
803
+ description: readDescriptionOf(handle),
804
+ implemented: true,
805
+ name,
806
+ returns: [],
807
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- typedImpl.teardown has narrower TData, wider context at engine boundary
808
+ teardown: typedImpl.teardown,
809
+ setup: async (ctx, allDeps) => {
810
+ const resolved = {};
811
+ mapping.forEach(([key, depName]) => {
812
+ const data = allDeps[depName];
813
+ if (data != null) {
814
+ resolved[key] = data;
815
+ }
816
+ });
817
+ return typedImpl.setup(ctx, resolved);
818
+ }
819
+ };
820
+ }
821
+ function makeNotImplementedObserverDef(handle, sentinel) {
822
+ const name = readObserverName(handle);
823
+ return {
824
+ budget: readBudgetOf(handle),
825
+ description: readDescriptionOfObserver(handle),
826
+ implemented: false,
827
+ name,
828
+ run: () => Promise.resolve(createFailOutcome(`observer "${name}" is ${sentinel.reason}`))
829
+ };
830
+ }
831
+ function makeWiredObserverDef(handle, impl) {
832
+ const name = readObserverName(handle);
833
+ const typedImpl = impl;
834
+ return {
835
+ budget: readBudgetOf(handle),
836
+ description: readDescriptionOfObserver(handle),
837
+ implemented: true,
838
+ name,
839
+ run: async (ctx, params) => {
840
+ return typedImpl(ctx, params);
841
+ }
842
+ };
843
+ }
844
+ function readPreconditionDepMappingKeys(handle) {
845
+ return readPreconditionDepMapping(handle).map(([, depName]) => depName);
846
+ }
847
+ function readDescriptionOf(handle) {
848
+ return handle.description;
849
+ }
850
+ function readDescriptionOfObserver(handle) {
851
+ return handle.description;
852
+ }
853
+ function readBudgetOf(handle) {
854
+ return handle.budget;
855
+ }
856
+ async function executePreconditions({
857
+ defsByName,
858
+ names,
859
+ options
860
+ }) {
861
+ const runId = crypto.randomUUID().slice(0, 12);
862
+ const cookies = [];
863
+ const defaultDomain = deriveDefaultDomain(options?.appUrl);
864
+ const state = {
865
+ cookies,
866
+ ctx: createSetupContext({ cookies, defaultDomain, runId }),
867
+ data: {},
868
+ defsByName,
869
+ executed: [],
870
+ runId
871
+ };
872
+ return runBatchSequence(state, names);
873
+ }
874
+ async function runBatchSequence(state, names) {
875
+ let index = 0;
876
+ while (index < names.length) {
877
+ const name = names[index];
878
+ if (name == null) {
879
+ break;
880
+ }
881
+ const error = validatePrecondition(state.defsByName, name);
882
+ if (error != null) {
883
+ return fail(state, error);
884
+ }
885
+ const stepError = await executeOnePrecondition(state, name);
886
+ if (stepError != null) {
887
+ return fail(state, stepError);
888
+ }
889
+ index += 1;
890
+ }
891
+ return {
892
+ cookies: state.cookies,
893
+ data: state.data,
894
+ error: void 0,
895
+ executed: state.executed,
896
+ runId: state.runId,
897
+ success: true
898
+ };
899
+ }
900
+ function validatePrecondition(defsByName, name) {
901
+ const def = defsByName.get(name);
902
+ if (def == null) {
903
+ return `Unknown precondition: "${name}"`;
904
+ }
905
+ if (!def.implemented) {
906
+ return `Precondition "${name}" is not implemented`;
907
+ }
908
+ return void 0;
909
+ }
910
+ async function executeOnePrecondition(state, name) {
911
+ const def = state.defsByName.get(name);
912
+ if (def == null) {
913
+ return `Unknown precondition: "${name}"`;
914
+ }
915
+ try {
916
+ const result = await def.setup(state.ctx, state.data);
917
+ const resolved = {};
918
+ Object.entries(result).forEach(([key, value]) => {
919
+ resolved[key] = readTestValue(value);
920
+ });
921
+ state.data[name] = resolved;
922
+ state.executed.push(name);
923
+ return void 0;
924
+ } catch (error) {
925
+ return error instanceof Error ? error.message : String(error);
926
+ }
927
+ }
928
+ function fail(state, error) {
929
+ return {
930
+ cookies: state.cookies,
931
+ data: state.data,
932
+ error,
933
+ executed: state.executed,
934
+ runId: state.runId,
935
+ success: false
936
+ };
937
+ }
938
+ async function executeObserver(observersByName, name, params) {
939
+ const def = observersByName.get(name);
940
+ if (def == null) {
941
+ return { error: `Unknown observer: "${name}"`, outcome: void 0, success: false };
942
+ }
943
+ if (!def.implemented) {
944
+ return { error: `Observer "${name}" is not implemented`, outcome: void 0, success: false };
945
+ }
946
+ const ctx = createObserverContext(crypto.randomUUID().slice(0, 12));
947
+ try {
948
+ const outcome = await def.run(ctx, params);
949
+ return { error: void 0, outcome, success: true };
950
+ } catch (error) {
951
+ const message = error instanceof Error ? error.message : String(error);
952
+ return { error: void 0, outcome: createFailOutcome(message), success: true };
953
+ }
954
+ }
955
+ function createObserverContext(runId) {
956
+ return {
957
+ runId,
958
+ fail: (reason) => createFailOutcome(reason),
959
+ pass: () => createPassOutcome(),
960
+ retry: (reason) => createRetryOutcome(reason)
961
+ };
962
+ }
963
+ async function teardown(defsByName, names, data) {
964
+ const reversed = [...names].toReversed();
965
+ let index = 0;
966
+ while (index < reversed.length) {
967
+ const name = reversed[index];
968
+ if (name != null) {
969
+ await teardownOne(defsByName, name, data);
970
+ }
971
+ index += 1;
972
+ }
973
+ }
974
+ async function teardownOne(defsByName, name, data) {
975
+ const def = defsByName.get(name);
976
+ if (def?.teardown == null) {
977
+ return;
978
+ }
979
+ try {
980
+ await def.teardown({ data: data[name] ?? {} });
981
+ } catch {
982
+ }
983
+ }
984
+ function createSetupContext({
985
+ cookies,
986
+ defaultDomain,
987
+ runId
988
+ }) {
989
+ return {
990
+ runId,
991
+ fixed: (value) => createTestValue(value),
992
+ setCookie: (name, value, options) => {
993
+ const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
994
+ cookies.push({ name, options: resolvedOptions, value });
995
+ },
996
+ uniqueEmail: () => createTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
997
+ uniqueId: (prefix) => createTestValue(`ripplo-test-${prefix}-${runId}`)
998
+ };
999
+ }
1000
+ function deriveDefaultDomain(baseUrl) {
1001
+ if (baseUrl == null) {
1002
+ return void 0;
1003
+ }
1004
+ try {
1005
+ return new URL(baseUrl).hostname;
1006
+ } catch {
1007
+ return void 0;
1008
+ }
1009
+ }
755
1010
  export {
756
1011
  DEFAULT_IGNORE_PATHS,
757
1012
  DEFAULT_WATCH_PATHS,
@@ -760,6 +1015,10 @@ export {
760
1015
  createEngine,
761
1016
  createRipplo,
762
1017
  lint,
1018
+ notImplemented,
1019
+ observer,
1020
+ precondition,
763
1021
  serializeCookie,
1022
+ test,
764
1023
  verifyWebhookSignature
765
1024
  };