@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/README.md +206 -171
- package/dist/assert.d.ts +1 -1
- package/dist/assert.js +4 -4
- package/dist/{builder-c7tXey03.d.ts → builder-CiAO8dEG.d.ts} +29 -29
- package/dist/{chunk-3IL457A7.js → chunk-76BU4M6E.js} +26 -3
- package/dist/chunk-TO3T2D2Y.js +84 -0
- package/dist/compiler.d.ts +2 -2
- package/dist/engine-L0JWPRkj.d.ts +47 -0
- package/dist/express.d.ts +5 -4
- package/dist/express.js +4 -7
- package/dist/fastify.d.ts +5 -4
- package/dist/fastify.js +4 -7
- package/dist/index.d.ts +5 -28
- package/dist/index.js +529 -270
- package/dist/lockfile.d.ts +2 -2
- package/dist/nextjs.d.ts +5 -4
- package/dist/nextjs.js +6 -9
- package/dist/{types-oYS_Yv4G.d.ts → types-Degkxs1f.d.ts} +10 -1
- package/package.json +1 -1
- package/dist/chunk-CD3M7H5A.js +0 -332
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-
|
|
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/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
|
|
56
|
+
|
|
57
|
+
// src/builder.ts
|
|
58
|
+
function precondition(name) {
|
|
59
|
+
return buildPreconditionStart(name);
|
|
119
60
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
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
|
|
131
|
-
|
|
64
|
+
function test(id, options) {
|
|
65
|
+
validateTestId(id);
|
|
66
|
+
return buildTestName(id, options?.uiOnly);
|
|
132
67
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
257
|
-
|
|
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
|
-
|
|
239
|
+
const description = getDescription();
|
|
266
240
|
return {
|
|
267
|
-
description(text) {
|
|
268
|
-
description = text;
|
|
269
|
-
return this;
|
|
270
|
-
},
|
|
271
241
|
expectedOutcome(text) {
|
|
272
|
-
return
|
|
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
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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((
|
|
337
|
-
const nodes = getOrderedNodes(
|
|
319
|
+
result.tests.forEach((test2) => {
|
|
320
|
+
const nodes = getOrderedNodes(test2);
|
|
338
321
|
const report = (diagnostic) => {
|
|
339
|
-
diagnostics.push({ ...diagnostic, test:
|
|
322
|
+
diagnostics.push({ ...diagnostic, test: test2.slug });
|
|
340
323
|
};
|
|
341
324
|
RULES.forEach((rule) => {
|
|
342
|
-
rule(nodes,
|
|
325
|
+
rule(nodes, test2, report);
|
|
343
326
|
});
|
|
344
327
|
});
|
|
345
328
|
return { diagnostics };
|
|
346
329
|
}
|
|
347
|
-
function getOrderedNodes(
|
|
330
|
+
function getOrderedNodes(test2) {
|
|
348
331
|
const result = [];
|
|
349
|
-
let currentId =
|
|
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 =
|
|
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,
|
|
374
|
-
const hasVariables = Object.keys(
|
|
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,
|
|
392
|
-
const variableKeys = Object.keys(
|
|
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,
|
|
465
|
-
if (!
|
|
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,
|
|
513
|
-
if (!
|
|
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,
|
|
525
|
-
if (!
|
|
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,
|
|
650
|
-
if (!
|
|
632
|
+
function expectedOutcomeKeywordCoverage(nodes, test2, report) {
|
|
633
|
+
if (!test2.implemented || nodes.length === 0) {
|
|
651
634
|
return;
|
|
652
635
|
}
|
|
653
|
-
const outcomeTokens = new Set(tokenize(
|
|
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,
|
|
688
|
-
if (!
|
|
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,
|
|
714
|
-
const variableKeys = Object.keys(
|
|
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
|
};
|