@voxgig/sdkgen 4.15.0 → 4.17.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 +8 -0
- package/bin/voxgig-sdkgen +2 -2
- package/dist/action/action.js +2 -2
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.js +9 -8
- package/dist/action/check.js.map +1 -1
- package/dist/action/dispatch.js +4 -4
- package/dist/action/dispatch.js.map +1 -1
- package/dist/action/doctor.js +16 -15
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.d.ts +4 -0
- package/dist/action/{docs.js → edition.js} +57 -54
- package/dist/action/edition.js.map +1 -0
- package/dist/action/kind.d.ts +1 -2
- package/dist/action/kind.js +22 -38
- package/dist/action/kind.js.map +1 -1
- package/dist/action/package.js +17 -16
- package/dist/action/package.js.map +1 -1
- package/dist/action/resolve.d.ts +3 -1
- package/dist/action/resolve.js +29 -11
- package/dist/action/resolve.js.map +1 -1
- package/dist/admin/status.d.ts +43 -0
- package/dist/admin/status.js +161 -0
- package/dist/admin/status.js.map +1 -0
- package/dist/cmp/Test.js +11 -0
- package/dist/cmp/Test.js.map +1 -1
- package/dist/helpers/kindCollection.d.ts +1 -0
- package/dist/helpers/kindCollection.js +14 -0
- package/dist/helpers/kindCollection.js.map +1 -0
- package/dist/helpers/modelcheck.d.ts +1 -1
- package/dist/helpers/modelcheck.js +13 -3
- package/dist/helpers/modelcheck.js.map +1 -1
- package/dist/helpers/shipped.js +2 -3
- package/dist/helpers/shipped.js.map +1 -1
- package/dist/helpers/testPolicy.d.ts +1 -1
- package/dist/helpers/testPolicy.js +6 -37
- package/dist/helpers/testPolicy.js.map +1 -1
- package/dist/sdkgen.d.ts +7 -1
- package/dist/sdkgen.js +146 -89
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.js +9 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +5 -52
- package/package.json +1 -1
- package/project/.sdk/src/cmp/go/Config_go.ts +4 -4
- package/project/.sdk/src/cmp/go/utility_go.ts +10 -2
- package/project/.sdk/src/cmp/js/Package_js.ts +5 -1
- package/project/.sdk/src/cmp/js/TestDirect_js.ts +11 -7
- package/project/.sdk/src/cmp/js/TestEntity_js.ts +16 -2
- package/project/.sdk/src/cmp/js/TestLive_js.ts +32 -0
- package/project/.sdk/src/cmp/js/Test_js.ts +2 -0
- package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +1 -1
- package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +4 -2
- package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +30 -9
- package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +8 -3
- package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +5 -4
- package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +2 -2
- package/project/.sdk/src/cmp/ts/Package_ts.ts +5 -1
- package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +5 -0
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -11
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +13 -16
- package/project/.sdk/src/cmp/ts/TestLive_ts.ts +32 -0
- package/project/.sdk/src/cmp/ts/Test_ts.ts +2 -0
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +1 -0
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +2 -0
- package/project/.sdk/tm/csharp/test/feature/secrets/SecretsFeatureTest.cs +9 -2
- package/project/.sdk/tm/js/test/live-contract.js +183 -0
- package/project/.sdk/tm/js/test/live-entity.js +169 -0
- package/project/.sdk/tm/js/test/live-runner.js +129 -0
- package/project/.sdk/tm/js/test/live-scenarios.js +142 -0
- package/project/.sdk/tm/js/test/utility.js +84 -0
- package/project/.sdk/tm/ts/test/live-contract.ts +104 -0
- package/project/.sdk/tm/ts/test/live-entity.ts +142 -0
- package/project/.sdk/tm/ts/test/live-runner.ts +166 -0
- package/project/.sdk/tm/ts/test/live-scenarios.ts +112 -0
- package/project/.sdk/tm/ts/test/utility.ts +1 -2
- package/project/sdkgen-package.json +1 -1
- package/src/action/action.ts +2 -2
- package/src/action/check.ts +3 -2
- package/src/action/dispatch.ts +4 -4
- package/src/action/doctor.ts +10 -9
- package/src/action/{docs.ts → edition.ts} +56 -53
- package/src/action/kind.ts +12 -33
- package/src/action/package.ts +4 -3
- package/src/action/resolve.ts +26 -2
- package/src/admin/status.ts +124 -0
- package/src/cmp/Test.ts +13 -1
- package/src/helpers/kindCollection.ts +11 -0
- package/src/helpers/modelcheck.ts +10 -3
- package/src/helpers/shipped.ts +2 -3
- package/src/helpers/testPolicy.ts +7 -45
- package/src/sdkgen.ts +189 -93
- package/src/utility.ts +8 -2
- package/dist/action/docs.d.ts +0 -4
- package/dist/action/docs.js.map +0 -1
- package/dist/cmp/Docs.d.ts +0 -4
- package/dist/cmp/Docs.js +0 -98
- package/dist/cmp/Docs.js.map +0 -1
- package/dist/cmp/ExternalDocs.d.ts +0 -2
- package/dist/cmp/ExternalDocs.js +0 -43
- package/dist/cmp/ExternalDocs.js.map +0 -1
- package/src/cmp/Docs.ts +0 -125
- package/src/cmp/ExternalDocs.ts +0 -54
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveRecipe = resolveRecipe;
|
|
7
|
+
exports.recipeNeeds = recipeNeeds;
|
|
8
|
+
exports.runLiveScenarios = runLiveScenarios;
|
|
9
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
10
|
+
const live_runner_1 = require("./live-runner");
|
|
11
|
+
const live_contract_1 = require("./live-contract");
|
|
12
|
+
const utility_1 = require("./utility");
|
|
13
|
+
const at = (value, path) => (path || '').split('.').filter(Boolean).reduce((v, k) => v?.[k], value);
|
|
14
|
+
function resolveRecipe(recipe, values) {
|
|
15
|
+
if (Array.isArray(recipe))
|
|
16
|
+
return recipe.map(v => resolveRecipe(v, values));
|
|
17
|
+
if (recipe && typeof recipe === 'object') {
|
|
18
|
+
if (recipe.from) {
|
|
19
|
+
if (!values.has(recipe.from))
|
|
20
|
+
throw new live_runner_1.LiveBlocked('Missing recipe output: ' + recipe.from);
|
|
21
|
+
let value = values.get(recipe.from);
|
|
22
|
+
if (recipe.where) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
throw new live_runner_1.LiveBlocked('Discovery output is not a list');
|
|
25
|
+
value = value.find((item) => Object.entries(recipe.where).every(([k, v]) => at(item, k) === v) &&
|
|
26
|
+
(!recipe.related || (values.get(recipe.related.from) || []).some((other) => at(item, recipe.related.local) === at(other, recipe.related.foreign) &&
|
|
27
|
+
Object.entries(recipe.related.where || {}).every(([k, v]) => at(other, k) === v))));
|
|
28
|
+
}
|
|
29
|
+
value = at(value, recipe.path);
|
|
30
|
+
if (value === undefined || value === null)
|
|
31
|
+
throw new live_runner_1.LiveBlocked('Discovery found no compatible value');
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries(Object.entries(recipe).map(([k, v]) => [k, resolveRecipe(v, values)]));
|
|
35
|
+
}
|
|
36
|
+
return recipe;
|
|
37
|
+
}
|
|
38
|
+
function recipeNeeds(value) {
|
|
39
|
+
if (!value || typeof value !== 'object')
|
|
40
|
+
return [];
|
|
41
|
+
return [...new Set([...(typeof value.from === 'string' ? [value.from] : []), ...Object.values(value).flatMap(recipeNeeds)])];
|
|
42
|
+
}
|
|
43
|
+
async function runLiveScenarios(SDK, plan, envPrefix, liveDefaults = {}) {
|
|
44
|
+
const transport = (0, live_runner_1.createLiveTransport)();
|
|
45
|
+
const steps = plan.map(point => {
|
|
46
|
+
const hint = point.facts.live || {};
|
|
47
|
+
const control = (0, utility_1.isControlSkipped)('entityOp', point.entity + '.' + point.op, 'live');
|
|
48
|
+
return { role: hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account'), retention: hint.retention, id: hint.id || point.id, needs: [...new Set([...(hint.needs || []), ...recipeNeeds(hint.input), ...recipeNeeds(hint.credential), ...recipeNeeds(hint.assert)])],
|
|
49
|
+
cleanup: !!hint.cleanup,
|
|
50
|
+
excluded: control.skip ? control.reason || 'Excluded by test control' : hint.excluded,
|
|
51
|
+
run: async (ctx) => {
|
|
52
|
+
transport.enter(ctx);
|
|
53
|
+
if (point.contractVersion && point.contractVersion !== 1)
|
|
54
|
+
throw new live_runner_1.LiveBlocked('Unsupported operation contract version');
|
|
55
|
+
if (point.op === 'remove' || hint.cleanup) {
|
|
56
|
+
const owned = recipeNeeds(hint.input).some(id => plan.some(source => (source.facts.live?.id || source.id) === id && source.entity === point.entity && source.op === 'create'));
|
|
57
|
+
if (!owned)
|
|
58
|
+
throw new live_runner_1.LiveBlocked('Cleanup input is not bound to a resource created by this run');
|
|
59
|
+
}
|
|
60
|
+
if (!point.reachable)
|
|
61
|
+
throw new live_runner_1.LiveBlocked('Point selector is indistinguishable; add a guide action');
|
|
62
|
+
const request = (0, live_contract_1.requestContract)(point.facts);
|
|
63
|
+
const explicit = hint.input === undefined ? request.example : resolveRecipe(hint.input, ctx.values);
|
|
64
|
+
let input = request.schema ? (0, live_contract_1.synthesizeInput)(request.schema, explicit) : explicit ?? {};
|
|
65
|
+
for (const kind of ['params', 'query', 'header', 'cookie'])
|
|
66
|
+
for (const arg of point.args?.[kind] || []) {
|
|
67
|
+
if (arg.reqd && input[arg.name] === undefined) {
|
|
68
|
+
if (arg.example === undefined)
|
|
69
|
+
throw new live_runner_1.LiveBlocked('Missing required argument: ' + arg.name);
|
|
70
|
+
input[arg.name] = arg.example;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const role = hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account');
|
|
74
|
+
let apikey = role === 'public' ? null : role === 'issued' ? resolveRecipe(hint.credential, ctx.values) : process.env[envPrefix + '_APIKEY'];
|
|
75
|
+
if (role === 'issued' && (typeof apikey !== 'string' || !apikey))
|
|
76
|
+
throw new live_runner_1.LiveBlocked('Issued credential unavailable');
|
|
77
|
+
let wire;
|
|
78
|
+
let wireSchema;
|
|
79
|
+
const options = (0, utility_1.liveClientOptions)();
|
|
80
|
+
const client = new SDK({ ...options, ...liveDefaults, apikey,
|
|
81
|
+
feature: { ...options.feature, test: { active: false }, retry: { active: false },
|
|
82
|
+
secrets: { ...options.feature?.secrets, active: role === 'account' && (!apikey || options.feature?.secrets?.active === true) } },
|
|
83
|
+
system: { ...options.system, fetch: async (url, init) => {
|
|
84
|
+
const headers = new Headers(init?.headers);
|
|
85
|
+
if (role === 'public')
|
|
86
|
+
(0, strict_1.default)(!headers.has('authorization'), 'Public request carries authentication');
|
|
87
|
+
else if (!headers.get('authorization'))
|
|
88
|
+
throw new live_runner_1.LiveBlocked('Credential unavailable for ' + role + ' request');
|
|
89
|
+
const response = await transport.fetch(url, { ...init, redirect: 'error' });
|
|
90
|
+
const expectedPath = point.path.replace(/\{([^}]+)\}/g, (_, name) => encodeURIComponent(input[point.rename?.param?.[name] || name] ?? input[name]));
|
|
91
|
+
if (point.kind === 'graphql') {
|
|
92
|
+
const payload = JSON.parse(init.body);
|
|
93
|
+
strict_1.default.equal(payload.query, point.graphql.doc, 'Entity selected wrong GraphQL operation');
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
strict_1.default.equal(new URL(String(url)).pathname, expectedPath, 'Entity selected wrong route');
|
|
97
|
+
strict_1.default.equal(init?.method || 'GET', point.method);
|
|
98
|
+
(0, strict_1.default)(response.status >= 200 && response.status < 300, 'Unsuccessful HTTP response');
|
|
99
|
+
if (point.kind === 'graphql')
|
|
100
|
+
wire = await response.clone().json();
|
|
101
|
+
const def = point.facts.responses?.[response.status] || point.facts.responses?.[String(response.status)[0] + 'XX'] || point.facts.responses?.default;
|
|
102
|
+
if (point.facts.responses && !def)
|
|
103
|
+
throw new Error('Undeclared response status');
|
|
104
|
+
if (def) {
|
|
105
|
+
const schema = def.content?.['application/json']?.schema ?? def.schema;
|
|
106
|
+
if (schema && response.status !== 204) {
|
|
107
|
+
wireSchema = schema;
|
|
108
|
+
wire = await response.clone().json();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return response;
|
|
112
|
+
} } });
|
|
113
|
+
if (point.action)
|
|
114
|
+
input = { ...input, $action: point.action };
|
|
115
|
+
(0, strict_1.default)(JSON.stringify(input).length <= 1024 * 1024, 'Request exceeds test payload limit');
|
|
116
|
+
const result = await client[point.accessor]()[point.op](input);
|
|
117
|
+
const data = Array.isArray(result) ? result.map(item => { strict_1.default.equal(typeof item?.data, 'function'); return item.data(); }) : (strict_1.default.equal(typeof result?.data, 'function'), result.data());
|
|
118
|
+
ctx.publish(data);
|
|
119
|
+
if (wireSchema)
|
|
120
|
+
(0, live_contract_1.validateContract)(wireSchema, wire, 'response');
|
|
121
|
+
if (Array.isArray(wire?.errors) && wire.errors.length)
|
|
122
|
+
throw new Error('GraphQL operation reported errors');
|
|
123
|
+
if (wire?.success === false)
|
|
124
|
+
throw new Error('API reported unsuccessful operation');
|
|
125
|
+
const checks = resolveRecipe(hint.assert || {}, ctx.values);
|
|
126
|
+
for (const [path, expected] of Object.entries(checks.equal || {}))
|
|
127
|
+
strict_1.default.deepEqual(at(data, path), expected);
|
|
128
|
+
for (const path of checks.nonempty || [])
|
|
129
|
+
(0, strict_1.default)(at(data, path)?.length > 0, 'Empty required output');
|
|
130
|
+
if (checks.vectors) {
|
|
131
|
+
const vectors = at(data, checks.vectors.path);
|
|
132
|
+
(0, strict_1.default)(Array.isArray(vectors) && vectors.length === checks.vectors.count);
|
|
133
|
+
for (const vector of vectors)
|
|
134
|
+
(0, strict_1.default)(Array.isArray(vector) && vector.length === checks.vectors.dimension && vector.every((n) => typeof n === 'number' && Number.isFinite(n)), 'Invalid vector shape');
|
|
135
|
+
}
|
|
136
|
+
} };
|
|
137
|
+
});
|
|
138
|
+
const report = await (0, live_runner_1.runLiveSteps)(steps, { delayMs: (0, utility_1.liveDelayMs)(), report: result => console.log('LIVE STEP ' + JSON.stringify(result)) });
|
|
139
|
+
console.log('LIVE SUMMARY ' + JSON.stringify(report));
|
|
140
|
+
(0, live_runner_1.assertLiveReport)(report);
|
|
141
|
+
return report;
|
|
142
|
+
}
|
|
@@ -148,7 +148,91 @@ function liveDelay(liveEnvVar) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
function isControlSkipped(kind, name, mode) {
|
|
152
|
+
const ctrl = loadTestControl();
|
|
153
|
+
const list = ctrl?.test?.skip?.[mode]?.[kind] ?? [];
|
|
154
|
+
for (const e of list) {
|
|
155
|
+
if (kind === 'direct' && e?.test === name) {
|
|
156
|
+
return { skip: true, reason: e.reason };
|
|
157
|
+
}
|
|
158
|
+
if (kind === 'entityOp') {
|
|
159
|
+
const key = (e?.entity ?? '') + '.' + (e?.op ?? '');
|
|
160
|
+
if (key === name)
|
|
161
|
+
return { skip: true, reason: e.reason };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { skip: false };
|
|
165
|
+
}
|
|
166
|
+
function maybeSkipControl(t, kind, name, live) {
|
|
167
|
+
const decision = isControlSkipped(kind, name, live ? 'live' : 'unit');
|
|
168
|
+
if (decision.skip) {
|
|
169
|
+
t.skip(decision.reason || 'skipped via sdk-test-control.json');
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function skipIfMissingIds(t, setup, requiredKeys) {
|
|
175
|
+
if (!setup.live)
|
|
176
|
+
return false;
|
|
177
|
+
const missing = requiredKeys.filter(k => null == setup.idmap?.[k]);
|
|
178
|
+
if (missing.length > 0) {
|
|
179
|
+
throw new Error(`Live test blocked: needs ${missing.join(', ')} via *_ENTID env var`);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function loadEnvLocal(file) {
|
|
184
|
+
let text;
|
|
185
|
+
try {
|
|
186
|
+
text = Fs.readFileSync(file, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if ('ENOENT' === err.code) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
195
|
+
const line = raw.trim();
|
|
196
|
+
// Blank and comment lines. A '#' INSIDE a value is not a comment.
|
|
197
|
+
if ('' === line || line.startsWith('#')) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const eq = line.indexOf('=');
|
|
201
|
+
if (0 >= eq) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const key = line.slice(0, eq).trim().replace(/^export\s+/, '');
|
|
205
|
+
let val = line.slice(eq + 1).trim();
|
|
206
|
+
const quote = ("'" === val[0] || '"' === val[0]) ? val[0] : '';
|
|
207
|
+
if ('' !== quote) {
|
|
208
|
+
// Quoted: the value runs to the CLOSING quote, and a '#' inside it is
|
|
209
|
+
// part of the value. Anything after the closing quote is a comment.
|
|
210
|
+
const close = val.indexOf(quote, 1);
|
|
211
|
+
val = 0 < close ? val.slice(1, close) : val.slice(1);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// Unquoted: the first '#' starts an inline comment, with or without
|
|
215
|
+
// preceding whitespace — `A=a#b` is `a` to dotenv, not `a#b`.
|
|
216
|
+
// Dropping this made `KEY=secret # note` resolve to the whole string
|
|
217
|
+
// including the note, and a generated live test would then send that
|
|
218
|
+
// as the credential. Verified against dotenv's own parse().
|
|
219
|
+
const hash = val.indexOf('#');
|
|
220
|
+
if (0 <= hash) {
|
|
221
|
+
val = val.slice(0, hash);
|
|
222
|
+
}
|
|
223
|
+
val = val.trim();
|
|
224
|
+
}
|
|
225
|
+
if (undefined === process.env[key]) {
|
|
226
|
+
process.env[key] = val;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
151
231
|
module.exports = {
|
|
232
|
+
isControlSkipped,
|
|
233
|
+
maybeSkipControl,
|
|
234
|
+
skipIfMissingIds,
|
|
235
|
+
loadEnvLocal,
|
|
152
236
|
makeStepData,
|
|
153
237
|
makeMatch,
|
|
154
238
|
makeReqdata,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LiveBlocked } from './live-runner'
|
|
2
|
+
|
|
3
|
+
// Deliberately conservative: unsupported constraints block preparation instead
|
|
4
|
+
// of labelling an invented value as live-valid. Errors never include values.
|
|
5
|
+
export function validateContract(schema: any, value: any, direction = 'request', depth = 0): void {
|
|
6
|
+
const bad = () => { throw new Error('Operation contract mismatch') }
|
|
7
|
+
if (depth > 40 || schema === false) return bad()
|
|
8
|
+
if (schema === true || schema == null) return
|
|
9
|
+
for (const key of ['if', 'then', 'else', 'dependentRequired', 'dependentSchemas', 'patternProperties', 'contains', 'unevaluatedProperties', 'unevaluatedItems']) {
|
|
10
|
+
if (schema[key] !== undefined) throw new LiveBlocked('Unsupported contract constraint: ' + key)
|
|
11
|
+
}
|
|
12
|
+
if (schema.$ref) throw new LiveBlocked('Unresolved contract reference')
|
|
13
|
+
if (value === null && (schema.nullable || schema.type === 'null' || schema.type?.includes?.('null'))) return
|
|
14
|
+
for (const sub of schema.allOf || []) validateContract(sub, value, direction, depth + 1)
|
|
15
|
+
for (const key of ['oneOf', 'anyOf']) if (schema[key]) {
|
|
16
|
+
let matches = 0
|
|
17
|
+
for (const sub of schema[key]) { try { validateContract(sub, value, direction, depth + 1); matches++ } catch {} }
|
|
18
|
+
if (key === 'oneOf' ? matches !== 1 : matches === 0) bad()
|
|
19
|
+
}
|
|
20
|
+
if (schema.not) { let matched = true; try { validateContract(schema.not, value, direction, depth + 1) } catch { matched = false }; if (matched) bad() }
|
|
21
|
+
if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const)) bad()
|
|
22
|
+
if (schema.enum && !schema.enum.some((v: any) => JSON.stringify(v) === JSON.stringify(value))) bad()
|
|
23
|
+
const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value
|
|
24
|
+
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []
|
|
25
|
+
if (types.length && !types.some((t: string) => t === kind || t === 'integer' && Number.isInteger(value))) bad()
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
if (!Number.isFinite(value)) bad()
|
|
28
|
+
if (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum) bad()
|
|
29
|
+
if (typeof schema.exclusiveMinimum === 'number' && value <= schema.exclusiveMinimum || schema.exclusiveMinimum === true && value <= schema.minimum) bad()
|
|
30
|
+
if (typeof schema.exclusiveMaximum === 'number' && value >= schema.exclusiveMaximum || schema.exclusiveMaximum === true && value >= schema.maximum) bad()
|
|
31
|
+
if (schema.multipleOf && Math.abs(value / schema.multipleOf - Math.round(value / schema.multipleOf)) > 1e-8) bad()
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
if (value.length < (schema.minLength || 0) || value.length > (schema.maxLength ?? Infinity)) bad()
|
|
35
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value)) bad()
|
|
36
|
+
if (schema.format === 'date-time' && !Number.isFinite(Date.parse(value))) bad()
|
|
37
|
+
if (schema.format === 'date' && !/^\d{4}-\d{2}-\d{2}$/.test(value)) bad()
|
|
38
|
+
if (schema.format === 'uuid' && !/^[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value)) bad()
|
|
39
|
+
if (schema.format === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) bad()
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
if (value.length < (schema.minItems || 0) || value.length > (schema.maxItems ?? Infinity)) bad()
|
|
43
|
+
if (schema.uniqueItems && new Set(value.map(v => JSON.stringify(v))).size !== value.length) bad()
|
|
44
|
+
value.forEach((v, i) => validateContract(schema.prefixItems?.[i] ?? schema.items, v, direction, depth + 1))
|
|
45
|
+
} else if (value !== null && typeof value === 'object') {
|
|
46
|
+
for (const key of schema.required || []) {
|
|
47
|
+
const prop = schema.properties?.[key]
|
|
48
|
+
if (direction === 'request' && prop?.readOnly || direction === 'response' && prop?.writeOnly) continue
|
|
49
|
+
if (value[key] === undefined) bad()
|
|
50
|
+
}
|
|
51
|
+
for (const [key, v] of Object.entries(value)) {
|
|
52
|
+
const prop = schema.properties?.[key]
|
|
53
|
+
if (direction === 'request' && prop?.readOnly || direction === 'response' && prop?.writeOnly) bad()
|
|
54
|
+
if (prop === undefined && schema.additionalProperties === false) bad()
|
|
55
|
+
validateContract(prop ?? schema.additionalProperties, v, direction, depth + 1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function synthesizeInput(schema: any, explicit?: any, depth = 0): any {
|
|
61
|
+
if (depth > 30) throw new LiveBlocked('Request schema nesting exceeds limit')
|
|
62
|
+
if (schema?.$ref) throw new LiveBlocked('Unresolved request reference')
|
|
63
|
+
if (explicit !== undefined) { validateContract(schema, explicit); return explicit }
|
|
64
|
+
if (!schema || schema === true) throw new LiveBlocked('No request contract or input recipe')
|
|
65
|
+
for (const key of ['example', 'default', 'const']) if (schema[key] !== undefined) {
|
|
66
|
+
try { validateContract(schema, schema[key]); return schema[key] } catch {}
|
|
67
|
+
}
|
|
68
|
+
for (const value of schema.examples || schema.enum || []) {
|
|
69
|
+
try { validateContract(schema, value); return value } catch {}
|
|
70
|
+
}
|
|
71
|
+
for (const choice of schema.oneOf || schema.anyOf || []) {
|
|
72
|
+
try { const v = synthesizeInput(choice, undefined, depth + 1); validateContract(schema, v); return v } catch {}
|
|
73
|
+
}
|
|
74
|
+
let value: any
|
|
75
|
+
if (schema.allOf) value = Object.assign({}, ...schema.allOf.map((s: any) => synthesizeInput(s, undefined, depth + 1)))
|
|
76
|
+
else if (schema.type === 'object' || schema.properties) {
|
|
77
|
+
value = {}
|
|
78
|
+
for (const key of schema.required || []) if (!schema.properties?.[key]?.readOnly) {
|
|
79
|
+
value[key] = synthesizeInput(schema.properties?.[key], undefined, depth + 1)
|
|
80
|
+
}
|
|
81
|
+
} else if (schema.type === 'array') {
|
|
82
|
+
const count = Math.max(1, schema.minItems || 0)
|
|
83
|
+
if (count > 16) throw new LiveBlocked('Required input array exceeds test payload limit')
|
|
84
|
+
value = Array.from({ length: count }, () => synthesizeInput(schema.items, undefined, depth + 1))
|
|
85
|
+
} else if (schema.type === 'integer' || schema.type === 'number') {
|
|
86
|
+
value = Math.max(1, schema.minimum ?? 1)
|
|
87
|
+
if (typeof schema.exclusiveMinimum === 'number') value = Math.max(value, schema.exclusiveMinimum + 1)
|
|
88
|
+
if (schema.multipleOf) value = Math.ceil(value / schema.multipleOf) * schema.multipleOf
|
|
89
|
+
} else if (schema.type === 'boolean') value = true
|
|
90
|
+
else throw new LiveBlocked('Required input needs a guide recipe or validated example')
|
|
91
|
+
validateContract(schema, value)
|
|
92
|
+
return value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function requestContract(facts: any): { schema?: any, example?: any, required?: boolean } {
|
|
96
|
+
const body = facts?.requestBody
|
|
97
|
+
if (body) {
|
|
98
|
+
const media = body.content?.['application/json'] ?? body.content?.['application/*+json']
|
|
99
|
+
if (!media) throw new LiveBlocked('Unsupported request media type')
|
|
100
|
+
return { schema: media.schema, example: media.example ?? Object.values(media.examples || {}).map((v: any) => v.value)[0], required: body.required }
|
|
101
|
+
}
|
|
102
|
+
const arg = facts?.parameters?.find((p: any) => p.in === 'body')
|
|
103
|
+
return arg ? { schema: arg.schema, example: arg.example, required: arg.required } : {}
|
|
104
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { requestContract, synthesizeInput } from './live-contract'
|
|
3
|
+
import { runLiveSteps, assertLiveReport, LiveBlocked } from './live-runner'
|
|
4
|
+
import { isControlSkipped, liveDelayMs } from './utility'
|
|
5
|
+
|
|
6
|
+
// The offline flow keeps its deterministic fixture assertions. Live flows
|
|
7
|
+
// resolve real prerequisites per operation and collect failures until done.
|
|
8
|
+
async function runLiveEntity(setup: any, entity: any, flow: any, accessor: string) {
|
|
9
|
+
const { client, transport } = setup
|
|
10
|
+
const steps: any[] = flow.step || []
|
|
11
|
+
const created = new Map<string, any>()
|
|
12
|
+
const listed: any[] = []
|
|
13
|
+
const marks = new Map<string, { name: string, value: any }>()
|
|
14
|
+
const idField = entity.id?.field || 'id'
|
|
15
|
+
const copy = (value: any) => JSON.parse(JSON.stringify(value ?? {}))
|
|
16
|
+
const hasCreate = steps.some(step => step.op === 'create')
|
|
17
|
+
const report = await runLiveSteps(steps.map((step, index) => {
|
|
18
|
+
const ref = step.input?.ref || entity.name + '_ref01'
|
|
19
|
+
const op = step.op
|
|
20
|
+
const excluded = isControlSkipped('entityOp', entity.name + '.' + op, 'live')
|
|
21
|
+
return {
|
|
22
|
+
id: entity.name + '.' + op + '.' + index,
|
|
23
|
+
cleanup: op === 'remove' || (step.valid || []).some((v: any) => v.apply === 'ItemNotExists'),
|
|
24
|
+
excluded: excluded.skip ? excluded.reason || 'Excluded by test control' : undefined,
|
|
25
|
+
run: async (context: any) => {
|
|
26
|
+
transport.enter(context)
|
|
27
|
+
const points = entity.op?.[op]?.points || []
|
|
28
|
+
if (!points.length) throw new LiveBlocked('No modelled operation point')
|
|
29
|
+
const record = created.get(ref)
|
|
30
|
+
// A failed create must not turn a later update/remove into a write
|
|
31
|
+
// against an arbitrary record found by a list operation.
|
|
32
|
+
if ((op === 'update' || op === 'remove') && hasCreate && !record) {
|
|
33
|
+
throw new LiveBlocked('Create did not provide a usable resource')
|
|
34
|
+
}
|
|
35
|
+
if (op === 'remove' && !record) throw new LiveBlocked('No resource created by this run')
|
|
36
|
+
if (op === 'remove' && !entity.id) throw new LiveBlocked('No modelled resource identity for cleanup')
|
|
37
|
+
if (record && ['update', 'remove'].includes(op) && entity.id &&
|
|
38
|
+
null == (record[idField] ?? record.id)) {
|
|
39
|
+
throw new LiveBlocked('Created resource has no usable identity')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let input: any = op === 'create'
|
|
43
|
+
? copy(setup.data.new?.[entity.name]?.[ref]) : {}
|
|
44
|
+
for (const [name, binding] of Object.entries({ ...step.match, ...step.data })) {
|
|
45
|
+
const value = setup.idmap[binding as string] ?? setup.idmap[name]
|
|
46
|
+
if (undefined !== value) input[name] = value
|
|
47
|
+
}
|
|
48
|
+
const loaded = record || (op === 'load' ? listed[0] : undefined)
|
|
49
|
+
if (loaded && ['load', 'update', 'remove'].includes(op)) {
|
|
50
|
+
const id = loaded[idField] ?? loaded.id
|
|
51
|
+
if (undefined !== id) input.id = id
|
|
52
|
+
}
|
|
53
|
+
// A nested route needing a parent id must not hide an available
|
|
54
|
+
// parameter-free route for the same operation. Use the first viable
|
|
55
|
+
// candidate; the SDK still performs its normal route selection.
|
|
56
|
+
let resolved: any
|
|
57
|
+
let selected: any
|
|
58
|
+
const missing = new Set<string>()
|
|
59
|
+
for (const point of points) {
|
|
60
|
+
if (point.select?.$action !== input.$action) continue
|
|
61
|
+
const candidate = { ...input }
|
|
62
|
+
let viable = true
|
|
63
|
+
const params = point.args?.params || []
|
|
64
|
+
const query = (point.args?.query || []).filter((arg: any) => arg.reqd)
|
|
65
|
+
for (const arg of [...params, ...query]) {
|
|
66
|
+
if (undefined !== candidate[arg.name] && null !== candidate[arg.name]) continue
|
|
67
|
+
const key = arg.name === 'id' ? entity.name + '01' : arg.name.replace(/_id$/, '') + '01'
|
|
68
|
+
const value = setup.idmap[key] ?? setup.idmap[arg.name] ?? loaded?.[arg.name] ?? arg.example
|
|
69
|
+
if (undefined !== value && null !== value) candidate[arg.name] = value
|
|
70
|
+
else if (arg.reqd !== false) { viable = false; missing.add(arg.name) }
|
|
71
|
+
}
|
|
72
|
+
if (viable) { resolved = candidate; selected = point; break }
|
|
73
|
+
}
|
|
74
|
+
if (!resolved) throw new LiveBlocked('No usable route; missing arguments: ' + [...missing].join(', '))
|
|
75
|
+
input = resolved
|
|
76
|
+
if (op === 'create' && selected.contract) {
|
|
77
|
+
const facts = JSON.parse(selected.contract.json)
|
|
78
|
+
const request = requestContract(facts)
|
|
79
|
+
if (request.schema) input = { ...synthesizeInput(request.schema, facts.live?.input ?? request.example) }
|
|
80
|
+
else if (facts.protocol === 'http') input = {}
|
|
81
|
+
for (const arg of selected.args?.params || []) if (resolved[arg.name] !== undefined) input[arg.name] = resolved[arg.name]
|
|
82
|
+
}
|
|
83
|
+
let intendedMark: { name: string, value: any } | undefined
|
|
84
|
+
if (op === 'update') {
|
|
85
|
+
for (const spec of step.spec || []) {
|
|
86
|
+
if (spec.apply === 'TextFieldMark' && step.input?.textfield) {
|
|
87
|
+
const mark = { name: step.input.textfield, value: spec.def.mark + '_' + setup.now }
|
|
88
|
+
input[mark.name] = mark.value
|
|
89
|
+
intendedMark = mark
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A fresh entity prevents state left by a failed operation from
|
|
95
|
+
// changing the request for the next independent operation.
|
|
96
|
+
const result = await client[accessor]()[op](input)
|
|
97
|
+
if (intendedMark) marks.set(ref, intendedMark)
|
|
98
|
+
if (op === 'list') {
|
|
99
|
+
assert(Array.isArray(result), 'Expected a list of entity instances')
|
|
100
|
+
const data = result.map((item: any) => {
|
|
101
|
+
assert.equal(typeof item?.data, 'function')
|
|
102
|
+
return item.data()
|
|
103
|
+
})
|
|
104
|
+
listed.splice(0, listed.length, ...data)
|
|
105
|
+
context.publish(data)
|
|
106
|
+
for (const validation of step.valid || []) {
|
|
107
|
+
const previous = created.get(validation.def?.ref)
|
|
108
|
+
const id = previous?.[idField] ?? previous?.id
|
|
109
|
+
if (undefined === id) continue
|
|
110
|
+
const found = data.some((item: any) => (item?.[idField] ?? item?.id) === id)
|
|
111
|
+
if (validation.apply === 'ItemExists') assert(found, 'Created resource missing from list')
|
|
112
|
+
if (validation.apply === 'ItemNotExists') assert(!found, 'Removed resource still in list')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
assert.equal(typeof result?.data, 'function', 'Expected an entity instance')
|
|
117
|
+
const data = result.data()
|
|
118
|
+
assert(null != data, 'Expected operation data')
|
|
119
|
+
// Publish before assertion checks: cleanup still needs the id
|
|
120
|
+
// when the server created a resource with incorrect field values.
|
|
121
|
+
if (op === 'create') created.set(ref, data)
|
|
122
|
+
context.publish(data)
|
|
123
|
+
if (op === 'create' && entity.id) assert(null != (data[idField] ?? data.id), 'Missing created identity')
|
|
124
|
+
if (['load', 'update'].includes(op) && input.id != null && entity.id) {
|
|
125
|
+
assert.equal(data[idField] ?? data.id, input.id, 'Response identity mismatch')
|
|
126
|
+
}
|
|
127
|
+
const mark = marks.get(ref)
|
|
128
|
+
if (mark && ['update', 'load'].includes(op)) {
|
|
129
|
+
assert.equal(data[mark.name], mark.value, 'Updated field mismatch')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
}), {
|
|
135
|
+
delayMs: liveDelayMs(),
|
|
136
|
+
report: result => console.log('LIVE STEP ' + JSON.stringify(result)),
|
|
137
|
+
})
|
|
138
|
+
console.log('LIVE SUMMARY ' + JSON.stringify({ entity: entity.name, ...report }))
|
|
139
|
+
assertLiveReport(report)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { runLiveEntity }
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Execute every independent live step before deciding the suite's result.
|
|
2
|
+
// Values never appear in reports: they can contain credentials or API data.
|
|
3
|
+
|
|
4
|
+
type LiveState = 'passed' | 'failed' | 'blocked' | 'excluded'
|
|
5
|
+
|
|
6
|
+
type LiveResult = {
|
|
7
|
+
id: string
|
|
8
|
+
state: LiveState
|
|
9
|
+
attempted: boolean
|
|
10
|
+
role?: string
|
|
11
|
+
retention?: string
|
|
12
|
+
reason?: string
|
|
13
|
+
requests?: { method: string, path: string, status?: number }[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type LiveContext = {
|
|
17
|
+
values: Map<string, any>
|
|
18
|
+
publish(value: any): void
|
|
19
|
+
attempted(): void
|
|
20
|
+
requests: { method: string, path: string, status?: number }[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type LiveStep = {
|
|
24
|
+
id: string
|
|
25
|
+
needs?: string[]
|
|
26
|
+
cleanup?: boolean
|
|
27
|
+
role?: string
|
|
28
|
+
retention?: string
|
|
29
|
+
excluded?: string
|
|
30
|
+
run(ctx: LiveContext): Promise<void>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class LiveBlocked extends Error {}
|
|
34
|
+
|
|
35
|
+
type LiveReport = {
|
|
36
|
+
planned: number
|
|
37
|
+
attempted: number
|
|
38
|
+
passed: number
|
|
39
|
+
failed: number
|
|
40
|
+
blocked: number
|
|
41
|
+
excluded: number
|
|
42
|
+
results: LiveResult[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runLiveSteps(
|
|
46
|
+
steps: LiveStep[],
|
|
47
|
+
options: { delayMs?: number, report?: (result: LiveResult) => void } = {},
|
|
48
|
+
): Promise<LiveReport> {
|
|
49
|
+
const values = new Map<string, any>()
|
|
50
|
+
const results: LiveResult[] = []
|
|
51
|
+
const ids = new Set<string>()
|
|
52
|
+
for (const step of steps) {
|
|
53
|
+
if (!step.id || ids.has(step.id)) throw new Error('Duplicate or empty live step id')
|
|
54
|
+
ids.add(step.id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const record = (result: LiveResult) => {
|
|
58
|
+
const step = steps.find(step => step.id === result.id)
|
|
59
|
+
if (step?.role) result.role = step.role
|
|
60
|
+
if (step?.retention && result.attempted) result.retention = step.retention
|
|
61
|
+
results.push(result)
|
|
62
|
+
options.report?.(result)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cleanup runs after ordinary work, even if an assertion failed after a
|
|
66
|
+
// resource was created. A step publishes usable data before assertions.
|
|
67
|
+
for (const cleanup of [false, true]) {
|
|
68
|
+
const pending = steps.filter(step => !!step.cleanup === cleanup)
|
|
69
|
+
while (pending.length) {
|
|
70
|
+
let progressed = false
|
|
71
|
+
for (let i = 0; i < pending.length;) {
|
|
72
|
+
const step = pending[i]
|
|
73
|
+
const missing = (step.needs || []).filter(id => !values.has(id) ||
|
|
74
|
+
(!cleanup && results.some(result => result.id === id && result.state !== 'passed')))
|
|
75
|
+
if (!step.excluded && missing.some(id => pending.some(other => other.id === id))) {
|
|
76
|
+
i++
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
pending.splice(i, 1)
|
|
80
|
+
progressed = true
|
|
81
|
+
if (step.excluded) {
|
|
82
|
+
record({ id: step.id, state: 'excluded', attempted: false, reason: step.excluded })
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (missing.length) {
|
|
86
|
+
record({ id: step.id, state: 'blocked', attempted: false,
|
|
87
|
+
reason: 'Missing output: ' + missing.join(', ') })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
let attempted = false
|
|
91
|
+
const requests: LiveContext['requests'] = []
|
|
92
|
+
try {
|
|
93
|
+
await step.run({
|
|
94
|
+
values,
|
|
95
|
+
publish: value => { if (undefined !== value) values.set(step.id, value) },
|
|
96
|
+
attempted: () => { attempted = true },
|
|
97
|
+
requests,
|
|
98
|
+
})
|
|
99
|
+
if (!attempted) throw new LiveBlocked('No request attempted')
|
|
100
|
+
record({ id: step.id, state: 'passed', attempted, requests })
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Ordinary SDK errors can contain request headers, bodies, or an
|
|
104
|
+
// issued token. Report the step's failure without serializing it.
|
|
105
|
+
record({ id: step.id, state: error instanceof LiveBlocked ? 'blocked' : 'failed',
|
|
106
|
+
attempted, requests,
|
|
107
|
+
reason: error instanceof LiveBlocked ? error.message : 'Request or assertion failed' })
|
|
108
|
+
}
|
|
109
|
+
if (attempted && (options.delayMs || 0) > 0) {
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, options.delayMs))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!progressed) {
|
|
114
|
+
for (const step of pending.splice(0)) {
|
|
115
|
+
record({ id: step.id, state: 'blocked', attempted: false,
|
|
116
|
+
reason: 'Cyclic or unavailable dependency' })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
planned: steps.length,
|
|
124
|
+
attempted: results.filter(result => result.attempted).length,
|
|
125
|
+
passed: results.filter(result => result.state === 'passed').length,
|
|
126
|
+
failed: results.filter(result => result.state === 'failed').length,
|
|
127
|
+
blocked: results.filter(result => result.state === 'blocked').length,
|
|
128
|
+
excluded: results.filter(result => result.state === 'excluded').length,
|
|
129
|
+
results,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function assertLiveReport(report: LiveReport): void {
|
|
134
|
+
if (report.failed || report.blocked || !report.attempted) {
|
|
135
|
+
throw new Error('Live coverage incomplete: ' + JSON.stringify(report))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createLiveTransport(timeoutMs = 30000) {
|
|
140
|
+
let context: LiveContext | undefined
|
|
141
|
+
const requests: LiveContext['requests'] = []
|
|
142
|
+
const nativeFetch = globalThis.fetch
|
|
143
|
+
return {
|
|
144
|
+
requests,
|
|
145
|
+
enter(next: LiveContext) { context = next },
|
|
146
|
+
fetch: async (input: any, init?: any) => {
|
|
147
|
+
const url = new URL(String(input))
|
|
148
|
+
const request: LiveContext['requests'][number] = {
|
|
149
|
+
method: init?.method || 'GET', path: url.origin + url.pathname,
|
|
150
|
+
}
|
|
151
|
+
requests.push(request)
|
|
152
|
+
context?.requests.push(request)
|
|
153
|
+
context?.attempted()
|
|
154
|
+
if (!context) console.log('LIVE REQUEST ' + JSON.stringify(request))
|
|
155
|
+
const timeout = AbortSignal.timeout(timeoutMs)
|
|
156
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout
|
|
157
|
+
const response = await nativeFetch(input, { ...init, signal })
|
|
158
|
+
request.status = response.status
|
|
159
|
+
if (!context) console.log('LIVE RESPONSE ' + JSON.stringify(request))
|
|
160
|
+
return response
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { runLiveSteps, assertLiveReport, LiveBlocked, createLiveTransport }
|
|
166
|
+
export type { LiveStep, LiveContext, LiveReport, LiveResult }
|