@volter/twin-standard 1.0.158 → 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.
Files changed (69) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/package.json +1 -0
  3. package/client-sdks/pnpm-lock.yaml +210 -0
  4. package/dist/client-sdks/package.json +1 -0
  5. package/dist/client-sdks/pnpm-lock.yaml +210 -0
  6. package/dist/src/assessment.d.ts +13 -3
  7. package/dist/src/assessment.js +10 -6
  8. package/dist/src/browser-assessment.js +16 -13
  9. package/dist/src/clients/harness.d.ts +25 -4
  10. package/dist/src/clients/harness.js +184 -22
  11. package/dist/src/clients/jira.js +11 -2
  12. package/dist/src/clients/run.js +19 -14
  13. package/dist/src/conformance.d.ts +2 -0
  14. package/dist/src/conformance.js +392 -184
  15. package/dist/src/consumer.d.ts +22 -1
  16. package/dist/src/consumer.js +91 -26
  17. package/dist/src/credential-issuer-import.d.ts +3 -0
  18. package/dist/src/credential-issuer-import.js +42 -0
  19. package/dist/src/derive.js +3 -0
  20. package/dist/src/gate.js +4 -0
  21. package/dist/src/lanes.d.ts +2 -0
  22. package/dist/src/lanes.js +7 -0
  23. package/dist/src/protocol-3.js +8 -3
  24. package/dist/src/spec-documents.d.ts +4 -0
  25. package/dist/src/spec-documents.js +110 -3
  26. package/dist/src/tools/life-coverage-walk.js +38 -26
  27. package/dist/src/tools/score-pack.js +6 -5
  28. package/dist/src/tools/vendor-examples.js +91 -80
  29. package/dist/src/walk/behavior-journey.d.ts +22 -14
  30. package/dist/src/walk/behavior-journey.js +182 -133
  31. package/dist/src/walk/journey-kit.d.ts +3 -2
  32. package/dist/src/walk/journey-kit.js +6 -2
  33. package/dist/src/walk/replay.d.ts +23 -0
  34. package/dist/src/walk/replay.js +118 -0
  35. package/dist/src/walk/serve-pack.d.ts +2 -2
  36. package/dist/src/walk/serve-pack.js +38 -2
  37. package/dist/src/walk/shape.d.ts +25 -4
  38. package/dist/src/walk/shape.js +166 -56
  39. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  40. package/dist/src/walk/vendor-fetch.js +13 -2
  41. package/dist/src/walk/walk-database.d.ts +9 -1
  42. package/dist/src/walk/walk-database.js +35 -13
  43. package/dist/src/walk/xml-view.d.ts +3 -2
  44. package/dist/src/walk/xml-view.js +5 -4
  45. package/package.json +3 -3
  46. package/src/assessment.ts +12 -8
  47. package/src/browser-assessment.ts +12 -5
  48. package/src/clients/harness.ts +142 -19
  49. package/src/clients/jira.ts +4 -2
  50. package/src/clients/run.ts +17 -15
  51. package/src/conformance.ts +313 -128
  52. package/src/consumer.ts +83 -21
  53. package/src/credential-issuer-import.ts +38 -0
  54. package/src/derive.ts +3 -1
  55. package/src/gate.ts +3 -0
  56. package/src/lanes.ts +7 -0
  57. package/src/protocol-3.ts +7 -3
  58. package/src/spec-documents.ts +92 -3
  59. package/src/tools/life-coverage-walk.ts +32 -22
  60. package/src/tools/score-pack.ts +7 -6
  61. package/src/tools/vendor-examples.ts +88 -75
  62. package/src/walk/behavior-journey.ts +145 -109
  63. package/src/walk/journey-kit.ts +7 -5
  64. package/src/walk/replay.ts +103 -0
  65. package/src/walk/serve-pack.ts +25 -5
  66. package/src/walk/shape.ts +156 -54
  67. package/src/walk/vendor-fetch.ts +16 -4
  68. package/src/walk/walk-database.ts +41 -15
  69. 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 { lanesOf, lineShape, shapeChecker } from "./shape.js";
28
+ import { decodeResponse, leaked, lineShape, responseProblems, shapeChecker, vendorFieldsOf } from "./shape.js";
22
29
  import { walkPackDirectory } from "./pack-directory.js";
23
- import { xmlMembers } from "./xml-view.js";
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
- /** Every field name the vendor's spec gives any resource (the derived surface), per vendor. */
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
- // the application the vendor asks (ctx.ask) is the life's stand-in, this walk's fetch's alone: its answers, in order,
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
- // a twin that throws answers 500, as a server would: the walk reports the step and goes on to the next
305
- let res;
306
- try {
307
- const request = new Request(/^https?:\/\//.test(path) ? path : `${origin}${path}`, init);
308
- const companion = companionOf(path);
309
- res = companion ? await companion.fetch(request) : served ? await served.fetch(request, { credentials: step.credentials ?? 'same-origin' }) : await twin(request);
310
- }
311
- catch (error) {
312
- res = new Response(`twin threw: ${error instanceof Error ? error.message : String(error)}`, { status: 500 });
313
- }
314
- const text = await res.text();
315
- let body;
316
- try {
317
- body = parseExactJson(text);
318
- }
319
- catch {
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 = res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
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
- // every cookie an answer sets, joined as one header: a sign-in sets several, and a map keeps only the last
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
- const up = await new Promise((done) => { ws.onopen = () => done(true); ws.onerror = () => done(false); setTimeout(() => done(false), 5000); });
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 url = await twin.managedDatabaseUrl?.();
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
- answered = { status: 0, body };
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
- for (const key of leaked(body, specFields(journey.vendor), '', true))
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((store.read(clockFile) ?? '').trim()) + waitMs(step.wait)).toISOString());
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
- const instant = clockFile && store.exists(clockFile) ? Date.parse((store.read(clockFile) ?? '').trim()) : Number.NaN;
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, Number.isNaN(instant) ? 0 : instant) };
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 timer = setTimeout(() => abort.abort(new Error('deferred request exceeded 10 s')), 10_000);
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, timer });
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
- if (step.breaks === undefined || !String(got.error).includes(step.breaks))
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
- res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
1187
- answered = { status: res.status, body: res.body, headers: res.headers };
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
- if (sqlSession)
1313
- await sqlSession.then((session) => session.end(), () => { });
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
- await served?.stop();
1317
- // a World's managed database ends with its walk (vendor-fetch.ts), so the next World declares it at the same port
1318
- await twin.stopDatabase?.();
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
- /** A collect whose answer's body breaks off with these words (a stream the vendor ends in an error), not arriving whole. */
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 `{basic:…}` and `{encoded:…}` have already filled); variables remain ordinary captured JSON data.
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
+ };