@volter/twin-standard 1.0.157 → 1.0.159
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/import.mjs +2 -0
- package/client-sdks/package.json +4 -0
- package/client-sdks/pnpm-lock.yaml +609 -0
- package/dist/client-sdks/import.mjs +2 -0
- package/dist/client-sdks/package.json +4 -0
- package/dist/client-sdks/pnpm-lock.yaml +609 -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/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/run.js +19 -14
- package/dist/src/clients/tinybird.js +15 -6
- package/dist/src/clients/vercel.d.ts +2 -0
- package/dist/src/clients/vercel.js +73 -0
- package/dist/src/conformance.d.ts +2 -0
- package/dist/src/conformance.js +392 -184
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +3 -0
- package/dist/src/gate.js +4 -0
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/protocol-3.js +8 -3
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/score-pack.js +6 -5
- package/dist/src/tools/vendor-examples.js +91 -80
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +182 -133
- package/dist/src/walk/journey-kit.d.ts +3 -2
- 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 +38 -2
- package/dist/src/walk/shape.d.ts +25 -4
- package/dist/src/walk/shape.js +166 -56
- 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 +4 -3
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/run.ts +17 -15
- package/src/clients/tinybird.ts +8 -6
- package/src/clients/vercel.ts +67 -0
- package/src/conformance.ts +313 -128
- package/src/consumer.ts +83 -21
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +3 -1
- package/src/gate.ts +3 -0
- package/src/lanes.ts +7 -0
- package/src/protocol-3.ts +7 -3
- package/src/spec-documents.ts +92 -3
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/score-pack.ts +7 -6
- package/src/tools/vendor-examples.ts +88 -75
- package/src/walk/behavior-journey.ts +145 -109
- package/src/walk/journey-kit.ts +7 -5
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +25 -5
- package/src/walk/shape.ts +156 -54
- 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,5 +1,8 @@
|
|
|
1
1
|
import { type TwinStream } from '@volter/world-core/runtime';
|
|
2
2
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
|
|
3
|
+
import { type ReplayStep } from './replay.js';
|
|
4
|
+
import type { ManagedDatabaseWalk } from './walk-database.js';
|
|
5
|
+
export { leaked } from './shape.js';
|
|
3
6
|
export declare function usePacks(dir: string, options?: {
|
|
4
7
|
dependenciesFrom?: string;
|
|
5
8
|
}): void;
|
|
@@ -13,26 +16,31 @@ export declare function inMemoryWorld(label: string): {
|
|
|
13
16
|
root: () => string;
|
|
14
17
|
restore: () => void;
|
|
15
18
|
};
|
|
19
|
+
type SentRequest = {
|
|
20
|
+
method: string;
|
|
21
|
+
url: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
type StepAnswer = {
|
|
25
|
+
status: number;
|
|
26
|
+
body: unknown;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
request?: SentRequest;
|
|
29
|
+
wireBody?: unknown;
|
|
30
|
+
sql?: ReplayStep['sql'];
|
|
31
|
+
connection?: ReplayStep['connection'];
|
|
32
|
+
};
|
|
16
33
|
export type Failure = {
|
|
17
34
|
step: string;
|
|
18
35
|
beat: string;
|
|
19
36
|
detail: string;
|
|
20
37
|
};
|
|
21
|
-
/** 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
|
|
22
|
-
* client: the pack's `_`-prefixed bookkeeping (GitHub's `_links` is the vendor's), and, in what no answer's SHAPE
|
|
23
|
-
* reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
|
|
24
|
-
* carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
|
|
25
|
-
* (Stripe's deleted customer's `deleted: true`). */
|
|
26
|
-
export declare function leaked(v: unknown, vendorFields: Set<string>, at?: string, unshaped?: boolean): string[];
|
|
27
38
|
/** What a caller that scores a walk observes around each step: the step, its resolved path and body,
|
|
28
|
-
* and (after) the answer and the failures the step produced.
|
|
39
|
+
* and (after) the answer and the failures the step produced. HTTP answers include the constructed request and
|
|
40
|
+
* decoded wire body separately from the capture view. */
|
|
29
41
|
export type StepHooks = {
|
|
30
42
|
before?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, body: unknown) => void;
|
|
31
|
-
after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer:
|
|
32
|
-
status: number;
|
|
33
|
-
body: unknown;
|
|
34
|
-
headers?: Record<string, string>;
|
|
35
|
-
}, failures: Failure[]) => void;
|
|
43
|
+
after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: StepAnswer, failures: Failure[]) => void;
|
|
36
44
|
};
|
|
37
45
|
/** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
|
|
38
46
|
* the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
|
|
@@ -42,9 +50,9 @@ export type ServedTwin = (root: string, options: {
|
|
|
42
50
|
application?: import('@volter/world-core').PackFetchOptions['application'];
|
|
43
51
|
}) => Promise<{
|
|
44
52
|
scheme?: 'http' | 'https';
|
|
45
|
-
fetch: (request: Request, options?: {
|
|
53
|
+
fetch: ((request: Request, options?: {
|
|
46
54
|
credentials?: RequestCredentials;
|
|
47
|
-
}) => Promise<Response
|
|
55
|
+
}) => Promise<Response>) & ManagedDatabaseWalk;
|
|
48
56
|
stop: () => void | Promise<void>;
|
|
49
57
|
socket?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
50
58
|
stream?: TwinStream;
|
|
@@ -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, 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,21 @@ 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
|
+
const eventFields = new Map();
|
|
259
|
+
for (const vendor of [journey.vendor, ...companions.map((companion) => companion.vendor)]) {
|
|
260
|
+
const directory = directoryFor(vendor);
|
|
261
|
+
if (!fieldsByVendor.has(vendor))
|
|
262
|
+
fieldsByVendor.set(vendor, await vendorFieldsOf(directory));
|
|
263
|
+
for (const unit of unitsIn(directory)) {
|
|
264
|
+
const { manifest } = await import(__rewriteRelativeImportExtension(join(unit, 'src', 'manifest.ts')));
|
|
265
|
+
const service = manifest.service ?? manifest.vendor;
|
|
266
|
+
const fields = eventFields.get(service) ?? new Set();
|
|
267
|
+
for (const field of specFields(vendor))
|
|
268
|
+
fields.add(field);
|
|
269
|
+
eventFields.set(service, fields);
|
|
270
|
+
}
|
|
245
271
|
}
|
|
246
272
|
const ownKeys = routingKeys(journey.vendor);
|
|
247
273
|
/** 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 +277,11 @@ export async function walk(journey, root, collect, opts) {
|
|
|
251
277
|
const scenarioPath = journey.scenario ? join(directoryFor(journey.vendor), 'journeys', journey.scenario) : undefined;
|
|
252
278
|
if (scenarioPath)
|
|
253
279
|
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
|
-
};
|
|
280
|
+
const shape = await shapeOf(journey.vendor);
|
|
273
281
|
const served = opts.served ? await opts.served(root, { ...(scenarioPath ? { scenarioPath } : {}), application }) : undefined;
|
|
274
282
|
const twin = served ? served.fetch : factory(root, { ...(scenarioPath ? { scenarioPath } : {}), application });
|
|
275
|
-
const shape = await shapeOf(journey.vendor);
|
|
276
283
|
const host = journey.host ?? 'twin.local';
|
|
277
284
|
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
285
|
// a capture's JSON value where it is not text (a number, a boolean, an object such as database-js's `session`)
|
|
281
286
|
const typedValues = {};
|
|
282
287
|
const remember = (name, v) => {
|
|
@@ -301,34 +306,27 @@ export async function walk(journey, root, collect, opts) {
|
|
|
301
306
|
if (step.encode && typeof init.body === 'string')
|
|
302
307
|
init.body = step.encode === 'gzip' ? new Uint8Array(gzipSync(Buffer.from(init.body))) : `data=${encodeURIComponent(Buffer.from(init.body).toString('base64'))}`;
|
|
303
308
|
// 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;
|
|
309
|
+
const companion = companionOf(path);
|
|
310
|
+
await (companion?.fetch ?? twin).managedDatabaseUrl?.();
|
|
311
|
+
const request = new Request(/^https?:\/\//.test(path) ? path : `${origin}${path}`, init);
|
|
312
|
+
const answer = await readHttpResponse(async (request) => {
|
|
313
|
+
// a twin that throws answers 500, as a server would: the walk reports the step and goes on to the next
|
|
314
|
+
try {
|
|
315
|
+
return companion ? await companion.fetch(request) : served ? await served.fetch(request, { credentials: step.credentials ?? 'same-origin' }) : await twin(request);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (served)
|
|
319
|
+
throw error;
|
|
320
|
+
return new Response(`twin threw: ${error instanceof Error ? error.message : String(error)}`, { status: 500 });
|
|
321
|
+
}
|
|
322
|
+
}, request);
|
|
323
|
+
const wireBody = decodeResponse(answer.text, answer.headers['content-type']);
|
|
324
|
+
let body = wireBody;
|
|
325
325
|
// a hosted flow answers with a redirect: its target and query are what a step captures (redirect.code)
|
|
326
|
-
const location =
|
|
326
|
+
const location = answer.status >= 300 && answer.status < 400 ? answer.headers.location : undefined;
|
|
327
327
|
if (location)
|
|
328
328
|
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(', ') } : {}) } };
|
|
329
|
+
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
330
|
};
|
|
333
331
|
// a wait moves the World clock; the walk puts it back where it found it, so a replay starts at the same instant
|
|
334
332
|
// a step's read-backs are read as the step's own person (its headers, captures filled)
|
|
@@ -810,8 +808,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
810
808
|
opened.frames.push(text);
|
|
811
809
|
} };
|
|
812
810
|
ws.onclose = (e) => { opened.closed ??= { code: e.code, reason: e.reason }; };
|
|
813
|
-
|
|
814
|
-
if (!up) {
|
|
811
|
+
if (!await openWebSocket(ws)) {
|
|
815
812
|
collect.push({ step: step.name, beat: 'STATUS', detail: `no socket at ${step.open}` });
|
|
816
813
|
return;
|
|
817
814
|
}
|
|
@@ -946,7 +943,8 @@ export async function walk(journey, root, collect, opts) {
|
|
|
946
943
|
opts.hooks?.before?.(step, 'sql', step.sql);
|
|
947
944
|
let answered = { status: 0, body: undefined };
|
|
948
945
|
try {
|
|
949
|
-
const
|
|
946
|
+
const database = twin;
|
|
947
|
+
const url = await database.managedDatabaseUrl?.();
|
|
950
948
|
if (url === undefined) {
|
|
951
949
|
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
950
|
return;
|
|
@@ -964,9 +962,14 @@ export async function walk(journey, root, collect, opts) {
|
|
|
964
962
|
collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres refused it: ${code} ${e.message}${step.refused ? ` (wanted ${step.refused})` : ''}` });
|
|
965
963
|
return;
|
|
966
964
|
}
|
|
967
|
-
const last = results.at(-1) ?? { fields: [], rows: [], command: '' };
|
|
965
|
+
const last = results.at(-1) ?? { fields: [], fieldTypes: [], rows: [], command: '' };
|
|
968
966
|
const body = { command: last.command, rows: last.rows.map((row) => Object.fromEntries(last.fields.map((field, i) => [field, row[i] ?? null]))) };
|
|
969
|
-
|
|
967
|
+
// Duplicate column names have the same last-column ownership as the visible row projection.
|
|
968
|
+
const fieldTypes = Object.fromEntries(last.fields.map((field, i) => [field, last.fieldTypes[i]]));
|
|
969
|
+
const timeFields = Object.fromEntries(Object.entries(fieldTypes).filter(([, type]) => type === 1114 || type === 1184));
|
|
970
|
+
answered = { status: 0, body, ...(database.managedDatabaseClock === 'wall' ? { sql: { clock: database.managedDatabaseClock, timeFields } } : {}) };
|
|
971
|
+
for (const detail of timestampOrderFailures(body, timeFields, step.timeOrder ?? []))
|
|
972
|
+
collect.push({ step: step.name, beat: 'EXPECT', detail });
|
|
970
973
|
if (step.refused !== undefined) {
|
|
971
974
|
collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres completed it (${last.command}), wanted ${step.refused}` });
|
|
972
975
|
return;
|
|
@@ -1015,7 +1018,12 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1015
1018
|
catch {
|
|
1016
1019
|
return;
|
|
1017
1020
|
}
|
|
1018
|
-
|
|
1021
|
+
const fields = eventFields.get(d.service);
|
|
1022
|
+
if (!fields) {
|
|
1023
|
+
collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} names no selected owner for service ${d.service}` });
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
for (const key of leaked(body, fields, '', true))
|
|
1019
1027
|
collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} carries the twin's own field ${key}` });
|
|
1020
1028
|
});
|
|
1021
1029
|
const previousTwins = setInProcessTwins(Object.fromEntries([
|
|
@@ -1024,6 +1032,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1024
1032
|
]));
|
|
1025
1033
|
try {
|
|
1026
1034
|
for (const step of journey.steps) {
|
|
1035
|
+
await settleEventDeliveries(root);
|
|
1027
1036
|
stepNow = step.name;
|
|
1028
1037
|
if (isMongo(step)) {
|
|
1029
1038
|
await mongoStep(step);
|
|
@@ -1059,7 +1068,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1059
1068
|
continue;
|
|
1060
1069
|
}
|
|
1061
1070
|
try {
|
|
1062
|
-
store.write(clockFile, new Date(Date.parse((
|
|
1071
|
+
store.write(clockFile, new Date(Date.parse(worldNow()) + waitMs(step.wait)).toISOString());
|
|
1063
1072
|
}
|
|
1064
1073
|
catch (e) {
|
|
1065
1074
|
collect.push({ step: step.name, beat: 'WAIT', detail: e.message });
|
|
@@ -1103,8 +1112,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1103
1112
|
// a form is signed as it is sent: its multipart type, whose parts OAuth 1.0a's base string never carries
|
|
1104
1113
|
const sent = formed ? undefined : body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body);
|
|
1105
1114
|
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) };
|
|
1115
|
+
headers = { ...(headers ?? {}), authorization: oauth1Authorization(o, step.method, url, sent, contentType, Date.parse(worldNow())) };
|
|
1108
1116
|
}
|
|
1109
1117
|
if (step.jwt)
|
|
1110
1118
|
headers = { ...(headers ?? {}), authorization: jwtAuthorization(step.jwt, captured) };
|
|
@@ -1113,9 +1121,8 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1113
1121
|
const url = /^https?:\/\//.test(path) ? path : `${origin}${path}`;
|
|
1114
1122
|
// the bytes and the type sent: a form's multipart body and boundary, raw bytes, or the body's text
|
|
1115
1123
|
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
1124
|
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,
|
|
1125
|
+
headers = { ...(headers ?? {}), ...sigv4Headers(step.sigv4, captured, step.method, url, base, sent, Date.parse(worldNow())) };
|
|
1119
1126
|
}
|
|
1120
1127
|
}
|
|
1121
1128
|
catch (e) {
|
|
@@ -1128,14 +1135,10 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1128
1135
|
continue;
|
|
1129
1136
|
}
|
|
1130
1137
|
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));
|
|
1138
|
+
const answer = send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path, abort.signal);
|
|
1136
1139
|
// A rejected body read must not become an unhandled promise while socket steps run.
|
|
1137
1140
|
void answer.catch(() => undefined);
|
|
1138
|
-
pending.set(step.defer, { method: step.method, path, body, headers, answer, abort
|
|
1141
|
+
pending.set(step.defer, { method: step.method, path, body, headers, answer, abort });
|
|
1139
1142
|
continue;
|
|
1140
1143
|
}
|
|
1141
1144
|
const deferred = step.await ? pending.get(step.await) : undefined;
|
|
@@ -1173,43 +1176,70 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1173
1176
|
if (deferred) {
|
|
1174
1177
|
pending.delete(step.await);
|
|
1175
1178
|
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
1179
|
if ('error' in got) {
|
|
1178
|
-
|
|
1179
|
-
collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
|
|
1180
|
+
collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
|
|
1180
1181
|
continue;
|
|
1181
1182
|
}
|
|
1182
|
-
// over a listener, which ends a broken stream as a short body, the ordinary checks hold the part that arrived
|
|
1183
1183
|
res = got.a;
|
|
1184
1184
|
}
|
|
1185
|
-
else
|
|
1186
|
-
|
|
1187
|
-
|
|
1185
|
+
else {
|
|
1186
|
+
try {
|
|
1187
|
+
res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
collect.push({ step: step.name, beat: 'CONNECTION', detail: String(error) });
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
answered = { status: res.status, body: res.body, headers: res.headers, request: res.request, wireBody: res.wireBody };
|
|
1195
|
+
if (step.breaks !== undefined) {
|
|
1196
|
+
if (!res.bodyError || res.bodyError.cancelled)
|
|
1197
|
+
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' });
|
|
1198
|
+
else if (!served && !res.bodyError.message.includes(step.breaks))
|
|
1199
|
+
collect.push({ step: step.name, beat: 'PENDING', detail: res.bodyError.message });
|
|
1200
|
+
}
|
|
1201
|
+
else if (res.bodyError)
|
|
1202
|
+
collect.push({ step: step.name, beat: 'BODY', detail: res.bodyError.message });
|
|
1188
1203
|
if (opts.verbose)
|
|
1189
1204
|
console.log(` ${String(res.status).padEnd(4)} ${step.method.padEnd(6)} ${path.slice(0, 70).padEnd(70)} ${step.name}`);
|
|
1205
|
+
// A companion's answer belongs to its own schema, including when the status is unexpected.
|
|
1206
|
+
const stepShape = serving === journey.vendor ? shape : await shapeOf(serving);
|
|
1207
|
+
const companion = companionOf(res.request.url);
|
|
1208
|
+
const databaseDoor = companion ? companion.databaseDoor : factory.managedDatabaseDoor;
|
|
1209
|
+
if (databaseDoor && res.status >= 200 && res.status < 300) {
|
|
1210
|
+
const selectedPath = new URL(stepShape ? stepShape.pathOf(res.request.method, res.request.url, res.request.headers) : res.request.url).pathname;
|
|
1211
|
+
const declared = worldDoorMatcher([{ method: 'POST', path: databaseDoor }])(res.request.method, selectedPath);
|
|
1212
|
+
if (declared) {
|
|
1213
|
+
const connection = (companion?.fetch ?? twin).managedDatabaseConnection;
|
|
1214
|
+
if (!connection || !stepShape)
|
|
1215
|
+
collect.push({ step: step.name, beat: 'ADAPTER', detail: 'The selected native credential transport omitted its connection verification capability' });
|
|
1216
|
+
else {
|
|
1217
|
+
try {
|
|
1218
|
+
const verified = await connection(res.request.method, selectedPath, res.status, res.wireBody);
|
|
1219
|
+
if (!verified)
|
|
1220
|
+
throw new Error('The selected native credential transport supplied no authentication provenance');
|
|
1221
|
+
answered.connection = verified;
|
|
1222
|
+
}
|
|
1223
|
+
catch (error) {
|
|
1224
|
+
collect.push({ step: step.name, beat: 'NATIVE-CONNECTION', detail: error.message });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const problems = responseProblems(stepShape, specFields(serving), {
|
|
1230
|
+
method: res.request.method, path: res.request.url, requestHeaders: res.request.headers,
|
|
1231
|
+
graphqlBody: graphqlBody ? body : undefined, status: res.status, mediaType: res.headers['content-type'], body: res.wireBody, text: res.text,
|
|
1232
|
+
});
|
|
1233
|
+
for (const detail of problems.shape)
|
|
1234
|
+
collect.push({ step: step.name, beat: 'SHAPE', detail });
|
|
1235
|
+
for (const key of problems.leaks)
|
|
1236
|
+
collect.push({ step: step.name, beat: 'LEAK', detail: `answers the twin's own field ${key}` });
|
|
1190
1237
|
const wanted = step.status;
|
|
1191
1238
|
const ok = wanted === undefined ? res.status >= 200 && res.status < 300 : res.status === wanted;
|
|
1192
1239
|
if (!ok) {
|
|
1193
1240
|
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
1241
|
continue;
|
|
1195
1242
|
}
|
|
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
1243
|
// a request that forbids tools (tool_choice none) is answered without a tool call, in any vendor's form
|
|
1214
1244
|
if (res.status >= 200 && res.status < 300 && step.body && typeof step.body === 'object' && forbidsTools(step.body.tool_choice))
|
|
1215
1245
|
for (const call of toolCallsIn(res.body))
|
|
@@ -1295,30 +1325,49 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1295
1325
|
return captured;
|
|
1296
1326
|
}
|
|
1297
1327
|
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
1328
|
try {
|
|
1312
|
-
|
|
1313
|
-
|
|
1329
|
+
for (const request of pending.values())
|
|
1330
|
+
request.abort.abort();
|
|
1331
|
+
for (const ws of wire.values())
|
|
1332
|
+
ws.close();
|
|
1333
|
+
const upgrade = twin.upgrade;
|
|
1334
|
+
for (const socket of sockets.values())
|
|
1335
|
+
if (!socket.closed)
|
|
1336
|
+
upgrade?.close(socket.peer);
|
|
1314
1337
|
}
|
|
1315
1338
|
finally {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1339
|
+
try {
|
|
1340
|
+
await Promise.allSettled([...pending.values()].map((request) => request.answer));
|
|
1341
|
+
await settleEventDeliveries(root);
|
|
1342
|
+
}
|
|
1343
|
+
finally {
|
|
1344
|
+
setDeliveryObserver(previousObserver);
|
|
1345
|
+
setInProcessTwins(previousTwins);
|
|
1346
|
+
try {
|
|
1347
|
+
if (sqlSession)
|
|
1348
|
+
await sqlSession.then((session) => session.end(), () => { });
|
|
1349
|
+
}
|
|
1350
|
+
finally {
|
|
1351
|
+
try {
|
|
1352
|
+
await served?.stop();
|
|
1353
|
+
}
|
|
1354
|
+
finally {
|
|
1355
|
+
try {
|
|
1356
|
+
// The selected twin's database ends with its walk.
|
|
1357
|
+
const retired = await Promise.allSettled([twin, ...companions.map(companion => companion.fetch)]
|
|
1358
|
+
.map(fetch => fetch.stopDatabase?.()));
|
|
1359
|
+
const failures = retired.filter(result => result.status === 'rejected').map(result => result.reason);
|
|
1360
|
+
if (failures.length)
|
|
1361
|
+
throw new AggregateError(failures, 'Walk native database retirement failed');
|
|
1362
|
+
}
|
|
1363
|
+
finally {
|
|
1364
|
+
if (clockFile && startedAt !== undefined)
|
|
1365
|
+
store.write(clockFile, startedAt);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1319
1370
|
}
|
|
1320
|
-
if (clockFile && startedAt !== undefined)
|
|
1321
|
-
store.write(clockFile, startedAt);
|
|
1322
1371
|
}
|
|
1323
1372
|
}
|
|
1324
1373
|
/** 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
|
|
@@ -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
|
+
};
|