@volter/twin-standard 1.0.158 → 1.0.160
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 +3 -2
- package/client-sdks/package.json +1 -0
- package/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/client-sdks/package.json +1 -0
- package/dist/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/src/assessment.d.ts +13 -3
- package/dist/src/assessment.js +10 -6
- package/dist/src/browser-assessment.js +16 -13
- package/dist/src/check-sources.js +2 -2
- package/dist/src/clients/harness.d.ts +25 -4
- package/dist/src/clients/harness.js +184 -22
- package/dist/src/clients/jira.js +11 -2
- package/dist/src/clients/resend.first-use.json +2 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/conformance.d.ts +3 -4
- package/dist/src/conformance.js +396 -203
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/create-pack.d.ts +1 -1
- package/dist/src/create-pack.js +13 -12
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +6 -3
- package/dist/src/gate.js +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/pack-facts.d.ts +3 -0
- package/dist/src/pack-facts.js +36 -1
- package/dist/src/protocol-3.js +13 -8
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/spec-ir-client.js +1 -1
- package/dist/src/spec-ir-commands.js +1 -1
- package/dist/src/spec-ir-lines.js +1 -1
- package/dist/src/spec-ir-proto.d.ts +1 -1
- package/dist/src/spec-patches.js +2 -2
- package/dist/src/tools/judge-brief.js +1 -2
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/life-coverage.js +2 -2
- package/dist/src/tools/packs-root.js +3 -3
- package/dist/src/tools/score-pack.js +7 -7
- package/dist/src/tools/vendor-examples.js +95 -85
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +199 -133
- package/dist/src/walk/journey-kit.d.ts +5 -4
- package/dist/src/walk/journey-kit.js +6 -2
- package/dist/src/walk/replay.d.ts +23 -0
- package/dist/src/walk/replay.js +118 -0
- package/dist/src/walk/serve-pack.d.ts +2 -2
- package/dist/src/walk/serve-pack.js +44 -2
- package/dist/src/walk/shape.d.ts +26 -5
- package/dist/src/walk/shape.js +167 -57
- package/dist/src/walk/vendor-fetch.d.ts +7 -2
- package/dist/src/walk/vendor-fetch.js +13 -2
- package/dist/src/walk/walk-database.d.ts +9 -1
- package/dist/src/walk/walk-database.js +35 -13
- package/dist/src/walk/xml-view.d.ts +3 -2
- package/dist/src/walk/xml-view.js +5 -4
- package/package.json +6 -4
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/check-sources.ts +2 -2
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/resend.first-use.json +2 -2
- package/src/clients/run.ts +17 -15
- package/src/conformance.ts +317 -147
- package/src/consumer.ts +83 -21
- package/src/create-pack.ts +13 -12
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +6 -4
- package/src/gate.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lanes.ts +7 -0
- package/src/pack-facts.ts +25 -2
- package/src/protocol-3.ts +12 -8
- package/src/spec-documents.ts +92 -3
- package/src/spec-ir-client.ts +1 -1
- package/src/spec-ir-commands.ts +1 -1
- package/src/spec-ir-lines.ts +1 -1
- package/src/spec-ir-proto.ts +1 -1
- package/src/spec-patches.ts +2 -2
- package/src/tools/judge-brief.ts +1 -2
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/life-coverage.ts +2 -2
- package/src/tools/packs-root.ts +3 -3
- package/src/tools/score-pack.ts +8 -8
- package/src/tools/vendor-examples.ts +92 -80
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +158 -109
- package/src/walk/journey-kit.ts +9 -7
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +30 -5
- package/src/walk/shape.ts +157 -55
- package/src/walk/vendor-fetch.ts +16 -4
- package/src/walk/walk-database.ts +41 -15
- package/src/walk/xml-view.ts +5 -4
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
2
10
|
import { attributionName } from "../verdict.js";
|
|
3
11
|
// THE WALK — a pack's life (journeys/customer-life.json) walked step by step against the pack's own fetch, on an
|
|
@@ -11,16 +19,18 @@ import { attributionName } from "../verdict.js";
|
|
|
11
19
|
import { mongodb, mysql, redis } from '@volter/world-core';
|
|
12
20
|
import { createRequire } from 'node:module';
|
|
13
21
|
import { gzipSync } from 'node:zlib';
|
|
14
|
-
import { parse, visit } from 'graphql';
|
|
15
22
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
16
23
|
import { join } from 'node:path';
|
|
17
|
-
import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins } from '@volter/world-core/runtime';
|
|
24
|
+
import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessMailServers, setInProcessTwins, settleEventDeliveries, worldDoorMatcher, worldNow } from '@volter/world-core/runtime';
|
|
18
25
|
import { createHostTable } from '@volter/world-core/host-routing';
|
|
19
26
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
20
27
|
import { isMongo, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from "./journey-kit.js";
|
|
21
|
-
import {
|
|
28
|
+
import { decodeResponse, leaked, lineShape, responseProblems, shapeChecker, vendorFieldsOf } from "./shape.js";
|
|
22
29
|
import { walkPackDirectory } from "./pack-directory.js";
|
|
23
|
-
import {
|
|
30
|
+
import { unitsIn } from "../lanes.js";
|
|
31
|
+
import { openWebSocket, readHttpResponse } from "../clients/harness.js";
|
|
32
|
+
import { timestampOrderFailures } from "./replay.js";
|
|
33
|
+
export { leaked } from "./shape.js";
|
|
24
34
|
/** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
|
|
25
35
|
let packsRoot;
|
|
26
36
|
/** Installed companions resolve from the selected pack, including its workspace-local dependencies. */
|
|
@@ -146,18 +156,6 @@ const claims = (keys, url) => {
|
|
|
146
156
|
const u = new URL(url);
|
|
147
157
|
return keys.some((key) => walkHosts[key](u.hostname, u.pathname));
|
|
148
158
|
};
|
|
149
|
-
/** What a stored row keeps for the twin that no schema of the vendor's spec names, by path, in anything the World shows a
|
|
150
|
-
* client: the pack's `_`-prefixed bookkeeping (GitHub's `_links` is the vendor's), and, in what no answer's SHAPE
|
|
151
|
-
* reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
|
|
152
|
-
* carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
|
|
153
|
-
* (Stripe's deleted customer's `deleted: true`). */
|
|
154
|
-
export function leaked(v, vendorFields, at = '', unshaped = false) {
|
|
155
|
-
if (Array.isArray(v))
|
|
156
|
-
return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
|
|
157
|
-
if (!v || typeof v !== 'object')
|
|
158
|
-
return [];
|
|
159
|
-
return Object.entries(v).flatMap(([k, x]) => ((k.startsWith('_') || (unshaped && k === 'updatedAt')) && !vendorFields.has(k) ? [`${at}${at ? '.' : ''}${k}`] : leaked(x, vendorFields, `${at}${at ? '.' : ''}${k}`, unshaped))).slice(0, 5);
|
|
160
|
-
}
|
|
161
159
|
/** Whether a tool_choice forbids every tool call (OpenAI's 'none', Anthropic's { type: 'none' }). */
|
|
162
160
|
const forbidsTools = (choice) => choice === 'none' || (!!choice && typeof choice === 'object' && choice.type === 'none');
|
|
163
161
|
/** The tool calls an answer or frames carry, in any vendor's form: a chat message's `tool_calls`, a Responses or
|
|
@@ -180,16 +178,9 @@ function toolCallsIn(v, out = []) {
|
|
|
180
178
|
toolCallsIn(x, out);
|
|
181
179
|
return out;
|
|
182
180
|
}
|
|
183
|
-
/**
|
|
181
|
+
/** The selected vendor's shared schema and native-error inventory, loaded before walking. */
|
|
184
182
|
const fieldsByVendor = new Map();
|
|
185
183
|
function specFields(vendor) {
|
|
186
|
-
if (!fieldsByVendor.has(vendor)) {
|
|
187
|
-
// the pack's surface and each of its lanes' (a walk reaches a lane through the pack's router)
|
|
188
|
-
const directory = directoryFor(vendor);
|
|
189
|
-
const files = [directory, ...lanesOf(directory)].map((dir) => join(dir, 'src', 'generated', 'surface.gen.json')).filter((f) => existsSync(f));
|
|
190
|
-
const surfaces = files.map((f) => parseExactJson(readFileSync(f, 'utf8')));
|
|
191
|
-
fieldsByVendor.set(vendor, new Set(surfaces.flatMap((surface) => (surface.resources ?? []).flatMap((r) => r.fields.map((f) => f.name)))));
|
|
192
|
-
}
|
|
193
184
|
return fieldsByVendor.get(vendor);
|
|
194
185
|
}
|
|
195
186
|
/** The complete replies a connection has written since last read, leaving an unfinished one. */
|
|
@@ -231,6 +222,27 @@ export async function walk(journey, root, collect, opts) {
|
|
|
231
222
|
collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `no create<Name>Fetch adapter resolved for ${journey.vendor}` });
|
|
232
223
|
return {};
|
|
233
224
|
}
|
|
225
|
+
// what an earlier walk captured, when this one continues it (the branch round-trip sends a life's write again)
|
|
226
|
+
const captured = { ...(opts.captured ?? {}) };
|
|
227
|
+
// the application the vendors call is the life's stand-in, shared by this walk's packs: its answers, in order,
|
|
228
|
+
// by method and URL prefix, each request header it declares checked as sent; a question none takes is unreachable, as
|
|
229
|
+
// in a sealed World
|
|
230
|
+
const answers = [...(journey.application ?? [])];
|
|
231
|
+
// the step being walked, for what the stand-in and the delivery observer report
|
|
232
|
+
let stepNow = '(setup)';
|
|
233
|
+
const application = async (url, init) => {
|
|
234
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
235
|
+
const i = answers.findIndex((a) => (a.method ?? 'POST').toUpperCase() === method && url.startsWith(a.url));
|
|
236
|
+
if (i < 0)
|
|
237
|
+
return 'unreachable';
|
|
238
|
+
const [a] = answers.splice(i, 1);
|
|
239
|
+
const sent = new Headers(init.headers);
|
|
240
|
+
for (const [name, value] of Object.entries(a.requestHeaders ?? {})) {
|
|
241
|
+
if (sent.get(name) !== fill(value, captured))
|
|
242
|
+
collect.push({ step: stepNow, beat: 'APPLICATION', detail: `the vendor request to ${url} did not carry the declared ${name} header` });
|
|
243
|
+
}
|
|
244
|
+
return { status: a.status, ...(a.headers ? { headers: a.headers } : {}), ...(a.bodyError === undefined ? {} : { bodyError: a.bodyError }), body: typeof a.body === 'string' ? a.body : a.body === undefined ? '' : JSON.stringify(a.body) };
|
|
245
|
+
};
|
|
234
246
|
// the life's companions (`companions`): the packs its vendor calls, each over this walk's root, on the same World and
|
|
235
247
|
// clock; reached in process by the vendor's ctx.vendorFetch and by a step to a URL their host rules claim
|
|
236
248
|
const companions = [];
|
|
@@ -241,7 +253,36 @@ export async function walk(journey, root, collect, opts) {
|
|
|
241
253
|
collect.push({ step: '(setup)', beat: 'ADAPTER', detail: vendor === journey.vendor ? `the life's own vendor ${vendor} is no companion of it` : !companion ? `no create<Name>Fetch adapter for the companion ${vendor}` : `the companion ${vendor} declares no host rules (pack facts) a call could reach it by` });
|
|
242
254
|
return {};
|
|
243
255
|
}
|
|
244
|
-
companions.push({ vendor, keys, fetch: companion(root) });
|
|
256
|
+
companions.push({ vendor, keys, fetch: companion(root, { application }), databaseDoor: companion.managedDatabaseDoor });
|
|
257
|
+
}
|
|
258
|
+
// a companion that receives mail (an SMTP side port: gmail's, gmail.com's MX) is the mail server ctx.mail reaches for
|
|
259
|
+
// the domains its host rules claim, on its byte-stream door in process over this walk's World, as a World reaches its
|
|
260
|
+
// listener: the life declares the provider of every address it mails
|
|
261
|
+
const mailServers = [];
|
|
262
|
+
for (const c of companions) {
|
|
263
|
+
const sides = walkFacts[c.vendor.split('/')[0]]?.sidePorts ?? [];
|
|
264
|
+
if (!sides.some((side) => side.scheme === 'smtp'))
|
|
265
|
+
continue;
|
|
266
|
+
const stream = await vendorStream(packsDir(), c.vendor, dependenciesDir());
|
|
267
|
+
if (!stream) {
|
|
268
|
+
collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `the companion ${c.vendor} receives mail on an smtp side port and exports no create<Name>TwinStream` });
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
mailServers.push({ vendor: c.vendor, receives: (domain) => c.keys.some((key) => walkHosts[key](domain, '/')), stream: stream(root, { application }) });
|
|
272
|
+
}
|
|
273
|
+
const eventFields = new Map();
|
|
274
|
+
for (const vendor of [journey.vendor, ...companions.map((companion) => companion.vendor)]) {
|
|
275
|
+
const directory = directoryFor(vendor);
|
|
276
|
+
if (!fieldsByVendor.has(vendor))
|
|
277
|
+
fieldsByVendor.set(vendor, await vendorFieldsOf(directory));
|
|
278
|
+
for (const unit of unitsIn(directory)) {
|
|
279
|
+
const { manifest } = await import(__rewriteRelativeImportExtension(join(unit, 'src', 'manifest.ts')));
|
|
280
|
+
const service = manifest.service ?? manifest.vendor;
|
|
281
|
+
const fields = eventFields.get(service) ?? new Set();
|
|
282
|
+
for (const field of specFields(vendor))
|
|
283
|
+
fields.add(field);
|
|
284
|
+
eventFields.set(service, fields);
|
|
285
|
+
}
|
|
245
286
|
}
|
|
246
287
|
const ownKeys = routingKeys(journey.vendor);
|
|
247
288
|
/** The companion that serves a step's URL: one whose host rules claim it where the life's own vendor's do not. */
|
|
@@ -251,32 +292,11 @@ export async function walk(journey, root, collect, opts) {
|
|
|
251
292
|
const scenarioPath = journey.scenario ? join(directoryFor(journey.vendor), 'journeys', journey.scenario) : undefined;
|
|
252
293
|
if (scenarioPath)
|
|
253
294
|
getActiveWorldStore().write(scenarioPath, readFileSync(scenarioPath, 'utf8'));
|
|
254
|
-
|
|
255
|
-
// by method and URL prefix, each request header it declares checked as sent; a question none takes is unreachable, as
|
|
256
|
-
// in a sealed World
|
|
257
|
-
const answers = [...(journey.application ?? [])];
|
|
258
|
-
// the step being walked, for what the stand-in and the delivery observer report
|
|
259
|
-
let stepNow = '(setup)';
|
|
260
|
-
const application = async (url, init) => {
|
|
261
|
-
const method = (init.method ?? 'GET').toUpperCase();
|
|
262
|
-
const i = answers.findIndex((a) => (a.method ?? 'POST').toUpperCase() === method && url.startsWith(a.url));
|
|
263
|
-
if (i < 0)
|
|
264
|
-
return 'unreachable';
|
|
265
|
-
const [a] = answers.splice(i, 1);
|
|
266
|
-
const sent = new Headers(init.headers);
|
|
267
|
-
for (const [name, value] of Object.entries(a.requestHeaders ?? {})) {
|
|
268
|
-
if (sent.get(name) !== fill(value, captured))
|
|
269
|
-
collect.push({ step: stepNow, beat: 'APPLICATION', detail: `the vendor request to ${url} did not carry the declared ${name} header` });
|
|
270
|
-
}
|
|
271
|
-
return { status: a.status, ...(a.headers ? { headers: a.headers } : {}), ...(a.bodyError === undefined ? {} : { bodyError: a.bodyError }), body: typeof a.body === 'string' ? a.body : a.body === undefined ? '' : JSON.stringify(a.body) };
|
|
272
|
-
};
|
|
295
|
+
const shape = await shapeOf(journey.vendor);
|
|
273
296
|
const served = opts.served ? await opts.served(root, { ...(scenarioPath ? { scenarioPath } : {}), application }) : undefined;
|
|
274
297
|
const twin = served ? served.fetch : factory(root, { ...(scenarioPath ? { scenarioPath } : {}), application });
|
|
275
|
-
const shape = await shapeOf(journey.vendor);
|
|
276
298
|
const host = journey.host ?? 'twin.local';
|
|
277
299
|
const origin = `${served?.scheme ?? 'http'}://${host}`;
|
|
278
|
-
// what an earlier walk captured, when this one continues it (the branch round-trip sends a life's write again)
|
|
279
|
-
const captured = { ...(opts.captured ?? {}) };
|
|
280
300
|
// a capture's JSON value where it is not text (a number, a boolean, an object such as database-js's `session`)
|
|
281
301
|
const typedValues = {};
|
|
282
302
|
const remember = (name, v) => {
|
|
@@ -301,34 +321,27 @@ export async function walk(journey, root, collect, opts) {
|
|
|
301
321
|
if (step.encode && typeof init.body === 'string')
|
|
302
322
|
init.body = step.encode === 'gzip' ? new Uint8Array(gzipSync(Buffer.from(init.body))) : `data=${encodeURIComponent(Buffer.from(init.body).toString('base64'))}`;
|
|
303
323
|
// a step may follow an absolute url an answer gave it (a hosted page's), as the application does
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
body = text;
|
|
321
|
-
}
|
|
322
|
-
// an XML answer (S3's) is its root element's members (xml-view.ts), as a JSON answer is its object
|
|
323
|
-
if (typeof body === 'string' && /xml/.test(res.headers.get('content-type') ?? ''))
|
|
324
|
-
body = xmlMembers(text) ?? text;
|
|
324
|
+
const companion = companionOf(path);
|
|
325
|
+
await (companion?.fetch ?? twin).managedDatabaseUrl?.();
|
|
326
|
+
const request = new Request(/^https?:\/\//.test(path) ? path : `${origin}${path}`, init);
|
|
327
|
+
const answer = await readHttpResponse(async (request) => {
|
|
328
|
+
// a twin that throws answers 500, as a server would: the walk reports the step and goes on to the next
|
|
329
|
+
try {
|
|
330
|
+
return companion ? await companion.fetch(request) : served ? await served.fetch(request, { credentials: step.credentials ?? 'same-origin' }) : await twin(request);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (served)
|
|
334
|
+
throw error;
|
|
335
|
+
return new Response(`twin threw: ${error instanceof Error ? error.message : String(error)}`, { status: 500 });
|
|
336
|
+
}
|
|
337
|
+
}, request);
|
|
338
|
+
const wireBody = decodeResponse(answer.text, answer.headers['content-type']);
|
|
339
|
+
let body = wireBody;
|
|
325
340
|
// a hosted flow answers with a redirect: its target and query are what a step captures (redirect.code)
|
|
326
|
-
const location =
|
|
341
|
+
const location = answer.status >= 300 && answer.status < 400 ? answer.headers.location : undefined;
|
|
327
342
|
if (location)
|
|
328
343
|
body = { location, redirect: Object.fromEntries(new URL(location, origin).searchParams) };
|
|
329
|
-
|
|
330
|
-
const cookies = res.headers.getSetCookie();
|
|
331
|
-
return { status: res.status, body, text, headers: { ...Object.fromEntries(res.headers), ...(cookies.length ? { 'set-cookie': cookies.join(', ') } : {}) } };
|
|
344
|
+
return { status: answer.status, body, wireBody, request: { method: request.method, url: request.url, headers: Object.fromEntries(request.headers) }, text: answer.text, headers: answer.headers, ...(answer.bodyError ? { bodyError: answer.bodyError } : {}) };
|
|
332
345
|
};
|
|
333
346
|
// a wait moves the World clock; the walk puts it back where it found it, so a replay starts at the same instant
|
|
334
347
|
// a step's read-backs are read as the step's own person (its headers, captures filled)
|
|
@@ -810,8 +823,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
810
823
|
opened.frames.push(text);
|
|
811
824
|
} };
|
|
812
825
|
ws.onclose = (e) => { opened.closed ??= { code: e.code, reason: e.reason }; };
|
|
813
|
-
|
|
814
|
-
if (!up) {
|
|
826
|
+
if (!await openWebSocket(ws)) {
|
|
815
827
|
collect.push({ step: step.name, beat: 'STATUS', detail: `no socket at ${step.open}` });
|
|
816
828
|
return;
|
|
817
829
|
}
|
|
@@ -946,7 +958,8 @@ export async function walk(journey, root, collect, opts) {
|
|
|
946
958
|
opts.hooks?.before?.(step, 'sql', step.sql);
|
|
947
959
|
let answered = { status: 0, body: undefined };
|
|
948
960
|
try {
|
|
949
|
-
const
|
|
961
|
+
const database = twin;
|
|
962
|
+
const url = await database.managedDatabaseUrl?.();
|
|
950
963
|
if (url === undefined) {
|
|
951
964
|
collect.push({ step: step.name, beat: 'ADAPTER', detail: `${journey.vendor} binds no managed database in this walk, so a SQL step has none to run on` });
|
|
952
965
|
return;
|
|
@@ -964,9 +977,14 @@ export async function walk(journey, root, collect, opts) {
|
|
|
964
977
|
collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres refused it: ${code} ${e.message}${step.refused ? ` (wanted ${step.refused})` : ''}` });
|
|
965
978
|
return;
|
|
966
979
|
}
|
|
967
|
-
const last = results.at(-1) ?? { fields: [], rows: [], command: '' };
|
|
980
|
+
const last = results.at(-1) ?? { fields: [], fieldTypes: [], rows: [], command: '' };
|
|
968
981
|
const body = { command: last.command, rows: last.rows.map((row) => Object.fromEntries(last.fields.map((field, i) => [field, row[i] ?? null]))) };
|
|
969
|
-
|
|
982
|
+
// Duplicate column names have the same last-column ownership as the visible row projection.
|
|
983
|
+
const fieldTypes = Object.fromEntries(last.fields.map((field, i) => [field, last.fieldTypes[i]]));
|
|
984
|
+
const timeFields = Object.fromEntries(Object.entries(fieldTypes).filter(([, type]) => type === 1114 || type === 1184));
|
|
985
|
+
answered = { status: 0, body, ...(database.managedDatabaseClock === 'wall' ? { sql: { clock: database.managedDatabaseClock, timeFields } } : {}) };
|
|
986
|
+
for (const detail of timestampOrderFailures(body, timeFields, step.timeOrder ?? []))
|
|
987
|
+
collect.push({ step: step.name, beat: 'EXPECT', detail });
|
|
970
988
|
if (step.refused !== undefined) {
|
|
971
989
|
collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres completed it (${last.command}), wanted ${step.refused}` });
|
|
972
990
|
return;
|
|
@@ -1015,15 +1033,22 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1015
1033
|
catch {
|
|
1016
1034
|
return;
|
|
1017
1035
|
}
|
|
1018
|
-
|
|
1036
|
+
const fields = eventFields.get(d.service);
|
|
1037
|
+
if (!fields) {
|
|
1038
|
+
collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} names no selected owner for service ${d.service}` });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
for (const key of leaked(body, fields, '', true))
|
|
1019
1042
|
collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} carries the twin's own field ${key}` });
|
|
1020
1043
|
});
|
|
1021
1044
|
const previousTwins = setInProcessTwins(Object.fromEntries([
|
|
1022
1045
|
...companions.flatMap((c) => c.keys.map((key) => [key, c.fetch])),
|
|
1023
1046
|
...ownKeys.map((key) => [key, twin]),
|
|
1024
1047
|
]));
|
|
1048
|
+
const previousMailServers = setInProcessMailServers(mailServers);
|
|
1025
1049
|
try {
|
|
1026
1050
|
for (const step of journey.steps) {
|
|
1051
|
+
await settleEventDeliveries(root);
|
|
1027
1052
|
stepNow = step.name;
|
|
1028
1053
|
if (isMongo(step)) {
|
|
1029
1054
|
await mongoStep(step);
|
|
@@ -1059,7 +1084,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1059
1084
|
continue;
|
|
1060
1085
|
}
|
|
1061
1086
|
try {
|
|
1062
|
-
store.write(clockFile, new Date(Date.parse((
|
|
1087
|
+
store.write(clockFile, new Date(Date.parse(worldNow()) + waitMs(step.wait)).toISOString());
|
|
1063
1088
|
}
|
|
1064
1089
|
catch (e) {
|
|
1065
1090
|
collect.push({ step: step.name, beat: 'WAIT', detail: e.message });
|
|
@@ -1103,8 +1128,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1103
1128
|
// a form is signed as it is sent: its multipart type, whose parts OAuth 1.0a's base string never carries
|
|
1104
1129
|
const sent = formed ? undefined : body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body);
|
|
1105
1130
|
const contentType = Object.entries(headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1] ?? (formed ? formed.contentType : step.bodyBase64 === undefined ? defaultType(body) : undefined);
|
|
1106
|
-
|
|
1107
|
-
headers = { ...(headers ?? {}), authorization: oauth1Authorization(o, step.method, url, sent, contentType, Number.isNaN(instant) ? 0 : instant) };
|
|
1131
|
+
headers = { ...(headers ?? {}), authorization: oauth1Authorization(o, step.method, url, sent, contentType, Date.parse(worldNow())) };
|
|
1108
1132
|
}
|
|
1109
1133
|
if (step.jwt)
|
|
1110
1134
|
headers = { ...(headers ?? {}), authorization: jwtAuthorization(step.jwt, captured) };
|
|
@@ -1113,9 +1137,8 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1113
1137
|
const url = /^https?:\/\//.test(path) ? path : `${origin}${path}`;
|
|
1114
1138
|
// the bytes and the type sent: a form's multipart body and boundary, raw bytes, or the body's text
|
|
1115
1139
|
const sent = formed ? formed.bytes : step.bodyBase64 !== undefined ? Uint8Array.from(Buffer.from(step.bodyBase64, 'base64')) : body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body);
|
|
1116
|
-
const instant = clockFile && store.exists(clockFile) ? Date.parse((store.read(clockFile) ?? '').trim()) : Number.NaN;
|
|
1117
1140
|
const base = { ...(formed ? { 'content-type': formed.contentType } : step.bodyBase64 === undefined ? { 'content-type': defaultType(body) } : {}), ...(journey.headers ?? {}), ...(headers ?? {}) };
|
|
1118
|
-
headers = { ...(headers ?? {}), ...sigv4Headers(step.sigv4, captured, step.method, url, base, sent,
|
|
1141
|
+
headers = { ...(headers ?? {}), ...sigv4Headers(step.sigv4, captured, step.method, url, base, sent, Date.parse(worldNow())) };
|
|
1119
1142
|
}
|
|
1120
1143
|
}
|
|
1121
1144
|
catch (e) {
|
|
@@ -1128,14 +1151,10 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1128
1151
|
continue;
|
|
1129
1152
|
}
|
|
1130
1153
|
const abort = new AbortController();
|
|
1131
|
-
const
|
|
1132
|
-
const answer = Promise.race([
|
|
1133
|
-
send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path, abort.signal),
|
|
1134
|
-
new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(abort.signal.reason), { once: true })),
|
|
1135
|
-
]).finally(() => clearTimeout(timer));
|
|
1154
|
+
const answer = send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path, abort.signal);
|
|
1136
1155
|
// A rejected body read must not become an unhandled promise while socket steps run.
|
|
1137
1156
|
void answer.catch(() => undefined);
|
|
1138
|
-
pending.set(step.defer, { method: step.method, path, body, headers, answer, abort
|
|
1157
|
+
pending.set(step.defer, { method: step.method, path, body, headers, answer, abort });
|
|
1139
1158
|
continue;
|
|
1140
1159
|
}
|
|
1141
1160
|
const deferred = step.await ? pending.get(step.await) : undefined;
|
|
@@ -1173,43 +1192,70 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1173
1192
|
if (deferred) {
|
|
1174
1193
|
pending.delete(step.await);
|
|
1175
1194
|
const got = await deferred.answer.then((a) => ({ a }), (error) => ({ error }));
|
|
1176
|
-
// a body the vendor broke off, as the collect expects (`breaks`): its words are the answer
|
|
1177
1195
|
if ('error' in got) {
|
|
1178
|
-
|
|
1179
|
-
collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
|
|
1196
|
+
collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
|
|
1180
1197
|
continue;
|
|
1181
1198
|
}
|
|
1182
|
-
// over a listener, which ends a broken stream as a short body, the ordinary checks hold the part that arrived
|
|
1183
1199
|
res = got.a;
|
|
1184
1200
|
}
|
|
1185
|
-
else
|
|
1186
|
-
|
|
1187
|
-
|
|
1201
|
+
else {
|
|
1202
|
+
try {
|
|
1203
|
+
res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
collect.push({ step: step.name, beat: 'CONNECTION', detail: String(error) });
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
answered = { status: res.status, body: res.body, headers: res.headers, request: res.request, wireBody: res.wireBody };
|
|
1211
|
+
if (step.breaks !== undefined) {
|
|
1212
|
+
if (!res.bodyError || res.bodyError.cancelled)
|
|
1213
|
+
collect.push({ step: step.name, beat: 'PENDING', detail: 'expected a response body interrupted by the vendor; clean EOF or caller cancellation is not an interruption' });
|
|
1214
|
+
else if (!served && !res.bodyError.message.includes(step.breaks))
|
|
1215
|
+
collect.push({ step: step.name, beat: 'PENDING', detail: res.bodyError.message });
|
|
1216
|
+
}
|
|
1217
|
+
else if (res.bodyError)
|
|
1218
|
+
collect.push({ step: step.name, beat: 'BODY', detail: res.bodyError.message });
|
|
1188
1219
|
if (opts.verbose)
|
|
1189
1220
|
console.log(` ${String(res.status).padEnd(4)} ${step.method.padEnd(6)} ${path.slice(0, 70).padEnd(70)} ${step.name}`);
|
|
1221
|
+
// A companion's answer belongs to its own schema, including when the status is unexpected.
|
|
1222
|
+
const stepShape = serving === journey.vendor ? shape : await shapeOf(serving);
|
|
1223
|
+
const companion = companionOf(res.request.url);
|
|
1224
|
+
const databaseDoor = companion ? companion.databaseDoor : factory.managedDatabaseDoor;
|
|
1225
|
+
if (databaseDoor && res.status >= 200 && res.status < 300) {
|
|
1226
|
+
const selectedPath = new URL(stepShape ? stepShape.pathOf(res.request.method, res.request.url, res.request.headers) : res.request.url).pathname;
|
|
1227
|
+
const declared = worldDoorMatcher([{ method: 'POST', path: databaseDoor }])(res.request.method, selectedPath);
|
|
1228
|
+
if (declared) {
|
|
1229
|
+
const connection = (companion?.fetch ?? twin).managedDatabaseConnection;
|
|
1230
|
+
if (!connection || !stepShape)
|
|
1231
|
+
collect.push({ step: step.name, beat: 'ADAPTER', detail: 'The selected native credential transport omitted its connection verification capability' });
|
|
1232
|
+
else {
|
|
1233
|
+
try {
|
|
1234
|
+
const verified = await connection(res.request.method, selectedPath, res.status, res.wireBody);
|
|
1235
|
+
if (!verified)
|
|
1236
|
+
throw new Error('The selected native credential transport supplied no authentication provenance');
|
|
1237
|
+
answered.connection = verified;
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
collect.push({ step: step.name, beat: 'NATIVE-CONNECTION', detail: error.message });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const problems = responseProblems(stepShape, specFields(serving), {
|
|
1246
|
+
method: res.request.method, path: res.request.url, requestHeaders: res.request.headers,
|
|
1247
|
+
graphqlBody: graphqlBody ? body : undefined, status: res.status, mediaType: res.headers['content-type'], body: res.wireBody, text: res.text,
|
|
1248
|
+
});
|
|
1249
|
+
for (const detail of problems.shape)
|
|
1250
|
+
collect.push({ step: step.name, beat: 'SHAPE', detail });
|
|
1251
|
+
for (const key of problems.leaks)
|
|
1252
|
+
collect.push({ step: step.name, beat: 'LEAK', detail: `answers the twin's own field ${key}` });
|
|
1190
1253
|
const wanted = step.status;
|
|
1191
1254
|
const ok = wanted === undefined ? res.status >= 200 && res.status < 300 : res.status === wanted;
|
|
1192
1255
|
if (!ok) {
|
|
1193
1256
|
collect.push({ step: step.name, beat: 'STATUS', detail: `${step.method} ${path} answered ${res.status}${wanted ? ` (wanted ${wanted})` : ' (wanted 2xx)'}: ${res.text.slice(0, 160)}` });
|
|
1194
1257
|
continue;
|
|
1195
1258
|
}
|
|
1196
|
-
// a step a companion served is checked against the companion's spec
|
|
1197
|
-
const stepShape = serving === journey.vendor ? shape : await shapeOf(serving);
|
|
1198
|
-
const answering = stepShape && res.status >= 200 && res.status < 300 && !stepShape.pinsOlder({ ...(journey.headers ?? {}), ...(headers ?? {}) }) ? stepShape.operationOf(step.method, path, { ...(journey.headers ?? {}), ...(headers ?? {}) }) : undefined;
|
|
1199
|
-
if (answering)
|
|
1200
|
-
for (const detail of stepShape.failures(answering.op, res.body, res.headers['content-type'], new Headers({ ...(journey.headers ?? {}), ...(headers ?? {}) }).get('accept') ?? undefined, res.status, { method: step.method, responseText: res.text }))
|
|
1201
|
-
collect.push({ step: step.name, beat: 'SHAPE', detail });
|
|
1202
|
-
// what a pack keeps for itself is `_`-prefixed and never answered, under any name (Protocol 3, State)
|
|
1203
|
-
const responseFields = new Set(specFields(serving));
|
|
1204
|
-
if (body && typeof body === 'object' && typeof body.query === 'string' && graphqlBody) {
|
|
1205
|
-
try {
|
|
1206
|
-
visit(parse(body.query), { Field: (field) => { responseFields.add(field.alias?.value ?? field.name.value); } });
|
|
1207
|
-
}
|
|
1208
|
-
catch { /* malformed documents have no selected response keys */ }
|
|
1209
|
-
}
|
|
1210
|
-
if (res.status >= 200 && res.status < 300)
|
|
1211
|
-
for (const key of leaked(res.body, responseFields))
|
|
1212
|
-
collect.push({ step: step.name, beat: 'LEAK', detail: `answers the twin's own field ${key}` });
|
|
1213
1259
|
// a request that forbids tools (tool_choice none) is answered without a tool call, in any vendor's form
|
|
1214
1260
|
if (res.status >= 200 && res.status < 300 && step.body && typeof step.body === 'object' && forbidsTools(step.body.tool_choice))
|
|
1215
1261
|
for (const call of toolCallsIn(res.body))
|
|
@@ -1295,30 +1341,50 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1295
1341
|
return captured;
|
|
1296
1342
|
}
|
|
1297
1343
|
finally {
|
|
1298
|
-
for (const request of pending.values()) {
|
|
1299
|
-
clearTimeout(request.timer);
|
|
1300
|
-
request.abort.abort();
|
|
1301
|
-
}
|
|
1302
|
-
for (const ws of wire.values())
|
|
1303
|
-
ws.close();
|
|
1304
|
-
const upgrade = twin.upgrade;
|
|
1305
|
-
for (const socket of sockets.values())
|
|
1306
|
-
if (!socket.closed)
|
|
1307
|
-
upgrade?.close(socket.peer);
|
|
1308
|
-
await Promise.allSettled([...pending.values()].map((request) => request.answer));
|
|
1309
|
-
setDeliveryObserver(previousObserver);
|
|
1310
|
-
setInProcessTwins(previousTwins);
|
|
1311
1344
|
try {
|
|
1312
|
-
|
|
1313
|
-
|
|
1345
|
+
for (const request of pending.values())
|
|
1346
|
+
request.abort.abort();
|
|
1347
|
+
for (const ws of wire.values())
|
|
1348
|
+
ws.close();
|
|
1349
|
+
const upgrade = twin.upgrade;
|
|
1350
|
+
for (const socket of sockets.values())
|
|
1351
|
+
if (!socket.closed)
|
|
1352
|
+
upgrade?.close(socket.peer);
|
|
1314
1353
|
}
|
|
1315
1354
|
finally {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1355
|
+
try {
|
|
1356
|
+
await Promise.allSettled([...pending.values()].map((request) => request.answer));
|
|
1357
|
+
await settleEventDeliveries(root);
|
|
1358
|
+
}
|
|
1359
|
+
finally {
|
|
1360
|
+
setDeliveryObserver(previousObserver);
|
|
1361
|
+
setInProcessTwins(previousTwins);
|
|
1362
|
+
setInProcessMailServers(previousMailServers);
|
|
1363
|
+
try {
|
|
1364
|
+
if (sqlSession)
|
|
1365
|
+
await sqlSession.then((session) => session.end(), () => { });
|
|
1366
|
+
}
|
|
1367
|
+
finally {
|
|
1368
|
+
try {
|
|
1369
|
+
await served?.stop();
|
|
1370
|
+
}
|
|
1371
|
+
finally {
|
|
1372
|
+
try {
|
|
1373
|
+
// The selected twin's database ends with its walk.
|
|
1374
|
+
const retired = await Promise.allSettled([twin, ...companions.map(companion => companion.fetch)]
|
|
1375
|
+
.map(fetch => fetch.stopDatabase?.()));
|
|
1376
|
+
const failures = retired.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
1377
|
+
if (failures.length)
|
|
1378
|
+
throw new AggregateError(failures, 'Walk native database retirement failed');
|
|
1379
|
+
}
|
|
1380
|
+
finally {
|
|
1381
|
+
if (clockFile && startedAt !== undefined)
|
|
1382
|
+
store.write(clockFile, startedAt);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1319
1387
|
}
|
|
1320
|
-
if (clockFile && startedAt !== undefined)
|
|
1321
|
-
store.write(clockFile, startedAt);
|
|
1322
1388
|
}
|
|
1323
1389
|
}
|
|
1324
1390
|
/** Run `fn` with every draw the kernel makes from the platform's random source (the World's seed and keys: a caller in
|
|
@@ -10,7 +10,7 @@ export type JourneyStep = {
|
|
|
10
10
|
defer?: string;
|
|
11
11
|
/** Collect a previously deferred request once, with all ordinary response checks. */
|
|
12
12
|
await?: string;
|
|
13
|
-
/**
|
|
13
|
+
/** Require an interrupted body; in-process, its error also contains these words. Status and received bytes retain all checks. */
|
|
14
14
|
breaks?: string;
|
|
15
15
|
body?: unknown;
|
|
16
16
|
/** A body of raw bytes (a gzip'd package, an image, an audio file), base64 in the journey and sent decoded: a
|
|
@@ -127,7 +127,7 @@ export type JourneyStep = {
|
|
|
127
127
|
* whose values the wire encodes (a psdb result's rows, base64 values sliced by per-column lengths) is asserted by
|
|
128
128
|
* what it holds. Captures read the answer as it came. */
|
|
129
129
|
view?: string;
|
|
130
|
-
/** This step sends a vendor's published example, `<operation>#<title>` (
|
|
130
|
+
/** This step sends a vendor's published example, `<operation>#<title>` (tools/vendor-examples.ts): its answer
|
|
131
131
|
* must hold every key path of the example's answer with the same JSON type. */
|
|
132
132
|
example?: string;
|
|
133
133
|
/** Caller-chosen values of the example's named placeholders, compared literally; captures may fill them.
|
|
@@ -138,7 +138,7 @@ export type JourneyStep = {
|
|
|
138
138
|
exampleDiffers?: Record<string, string>;
|
|
139
139
|
/** A published body field this step leaves out (a dot path; `a[].b` in each item) → why: a rendered value naming an
|
|
140
140
|
* object the World does not hold (a Git repository behind an integration). Only a field the request schema does not
|
|
141
|
-
* require may be left out (
|
|
141
|
+
* require may be left out (tools/vendor-examples.ts). */
|
|
142
142
|
unsent?: Record<string, string>;
|
|
143
143
|
/** answer paths whose value equals something the setup sent without the setup deciding it (a default the vendor applies
|
|
144
144
|
* that coincides), each with the reason: `vendor-examples.ts --sweep` does not ask them asserted, and the judge reads the reason */
|
|
@@ -376,12 +376,13 @@ export declare const isWait: (step: Step) => step is WaitStep;
|
|
|
376
376
|
* as written. Its answer is the last statement's `{ command, rows }`, each row its
|
|
377
377
|
* columns by name in Postgres's text format, which `expect` reads; a refusal fails the step with
|
|
378
378
|
* Postgres's SQLSTATE, unless the step names that SQLSTATE as its `refused` (the application's own catch of it), when
|
|
379
|
-
* its answer is `{ code, message }`. */
|
|
379
|
+
* its answer is `{ code, message }`. `timeOrder` names native timestamp columns whose rows must be nondecreasing. */
|
|
380
380
|
export type SqlStep = {
|
|
381
381
|
name: string;
|
|
382
382
|
sql: string;
|
|
383
383
|
values?: Array<string | null>;
|
|
384
384
|
refused?: string;
|
|
385
|
+
timeOrder?: string[];
|
|
385
386
|
expect?: JourneyStep['expect'];
|
|
386
387
|
capture?: JourneyStep['capture'];
|
|
387
388
|
};
|
|
@@ -321,14 +321,18 @@ export function fill(template, captured, graphqlDocument = false) {
|
|
|
321
321
|
// `{encoded:name}` is a capture percent-encoded (encodeURIComponent), as a URL's path segment or query value or a form
|
|
322
322
|
// field carries it: a URN a vendor's page prints encoded in its paths (LinkedIn's /rest/posts/urn%3Ali%3Ashare%3A…), a
|
|
323
323
|
// sign-in's return path holding `&` that a browser posts back
|
|
324
|
-
const encoded = basic.replace(/\{encoded:([A-Za-z0-9_.]+)\}/g, (_, k) => {
|
|
324
|
+
const encoded = basic.replace(/\{(encoded|base64|md5):([A-Za-z0-9_.]+)\}/g, (_, transform, k) => {
|
|
325
325
|
if (captured[k] === undefined)
|
|
326
326
|
throw new Error(`journey: step refers to {${k}}, which no earlier step captured`);
|
|
327
|
+
if (transform === 'base64')
|
|
328
|
+
return Buffer.from(captured[k], 'utf8').toString('base64');
|
|
329
|
+
if (transform === 'md5')
|
|
330
|
+
return digest('md5', captured[k], 'base64');
|
|
327
331
|
return encodeURIComponent(captured[k]);
|
|
328
332
|
});
|
|
329
333
|
const selections = new Set();
|
|
330
334
|
// Only request query documents opt into GraphQL grammar (their selection sets, found in the text the captures fill,
|
|
331
|
-
// which
|
|
335
|
+
// which the credential and byte transforms have already filled); variables remain ordinary captured JSON data.
|
|
332
336
|
if (graphqlDocument && !Object.hasOwn(captured, /^\{([A-Za-z_][A-Za-z0-9_.]*)\}$/.exec(encoded)?.[1] ?? '')) {
|
|
333
337
|
try {
|
|
334
338
|
visit(parse(encoded), { SelectionSet(node) { if (node.loc)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A received step, with verified native engine provenance outside its unchanged response body. */
|
|
2
|
+
export type ReplayStep = {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
status: number;
|
|
6
|
+
body: unknown;
|
|
7
|
+
sql?: {
|
|
8
|
+
clock: 'wall';
|
|
9
|
+
timeFields: Record<string, 1114 | 1184>;
|
|
10
|
+
};
|
|
11
|
+
connection?: {
|
|
12
|
+
kind: 'postgres';
|
|
13
|
+
field: string;
|
|
14
|
+
authenticated: true;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** A journey's declared native timestamp columns are present, valid and nondecreasing down the returned rows. */
|
|
18
|
+
export declare function timestampOrderFailures(body: unknown, timeFields: Record<string, 1114 | 1184>, columns: string[]): string[];
|
|
19
|
+
/** Compare received traces by their selected engine declarations, retaining every undeclared byte. */
|
|
20
|
+
export declare function compareReplays(first: ReadonlyArray<ReplayStep>, second: ReadonlyArray<ReplayStep>): {
|
|
21
|
+
equal: boolean;
|
|
22
|
+
diagnostics: string[];
|
|
23
|
+
};
|