@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.
- 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/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/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 +3 -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/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
|
@@ -16,7 +16,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { listActions, observeTransitions, stateOf } from '@volter/world-core/runtime';
|
|
18
18
|
import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
|
|
19
|
-
import { lanesOf, shapeChecker } from "../walk/shape.js";
|
|
19
|
+
import { lanesOf, lineShape, shapeChecker } from "../walk/shape.js";
|
|
20
20
|
import { sharingLanesIn } from "../lanes.js";
|
|
21
21
|
/** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
|
|
22
22
|
export async function coverageWalks(PACKS, vendor) {
|
|
@@ -36,9 +36,12 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
36
36
|
const { manifest } = (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, 'src', 'manifest.ts'))));
|
|
37
37
|
// the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
|
|
38
38
|
// ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
|
|
39
|
-
const
|
|
39
|
+
const units = [{ dir: join(PACKS, vendor), manifest, lane: undefined }, ...await Promise.all(sharingLanes(vendor).map(async (l) => {
|
|
40
|
+
const dir = join(PACKS, vendor, l);
|
|
41
|
+
return { dir, lane: l, manifest: (await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')))).manifest };
|
|
42
|
+
}))];
|
|
40
43
|
// a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
|
|
41
|
-
const allResources =
|
|
44
|
+
const allResources = units.flatMap(({ manifest: mf }) => Object.entries(mf.resources ?? {}));
|
|
42
45
|
const machines = new Map();
|
|
43
46
|
for (const [resource, decl] of allResources)
|
|
44
47
|
for (const [field, machine] of Object.entries(decl.state ?? {})) {
|
|
@@ -50,23 +53,28 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
50
53
|
machines.set(name, machine);
|
|
51
54
|
}
|
|
52
55
|
const where = new Map();
|
|
53
|
-
|
|
56
|
+
const httpTransitions = new Set();
|
|
57
|
+
for (const { dir, manifest: mf } of units) {
|
|
58
|
+
const http = !lineShape(dir);
|
|
54
59
|
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
55
60
|
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
56
61
|
m.transitions.forEach((t, i) => {
|
|
62
|
+
if (http)
|
|
63
|
+
httpTransitions.add(t);
|
|
57
64
|
const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
|
|
58
65
|
const places = where.get(t) ?? [];
|
|
59
66
|
if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service))
|
|
60
67
|
where.set(t, [...places, place]);
|
|
61
68
|
});
|
|
62
69
|
}
|
|
70
|
+
}
|
|
63
71
|
const moves = new Map();
|
|
64
72
|
const subjectKey = (service, type, id) => JSON.stringify([service, type, id]);
|
|
65
73
|
observeTransitions((_decl, t, outcome, from, entry) => {
|
|
66
74
|
const places = where.get(t) ?? [];
|
|
67
75
|
if (outcome === 'refused') {
|
|
68
|
-
//
|
|
69
|
-
if (entry)
|
|
76
|
+
// HTTP refusals are credited from their received status and body; a line protocol owns its session's replies.
|
|
77
|
+
if (entry || httpTransitions.has(t))
|
|
70
78
|
return;
|
|
71
79
|
const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
|
|
72
80
|
if (groups.size === 1)
|
|
@@ -100,11 +108,14 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
100
108
|
const shape = await shapeChecker(join(PACKS, vendor));
|
|
101
109
|
const answered = [];
|
|
102
110
|
try {
|
|
103
|
-
await walk(life, root, failures, { beats: w.beats, hooks: { after: (
|
|
111
|
+
await walk(life, root, failures, { beats: w.beats, hooks: { after: (_step, _path, answer) => {
|
|
104
112
|
// a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
113
|
+
const request = answer.request;
|
|
114
|
+
if (!request)
|
|
115
|
+
return;
|
|
116
|
+
const hit = shape?.operationOf(request.method, request.url, request.headers);
|
|
117
|
+
if (hit)
|
|
118
|
+
answered.push({ op: hit.op.id, lane: hit.lane, status: answer.status, body: answer.wireBody });
|
|
108
119
|
} } });
|
|
109
120
|
// a declared refusal is reached when a step on its operation was answered with it: its code, or its message
|
|
110
121
|
// up to the first placeholder
|
|
@@ -112,26 +123,27 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
112
123
|
const holds = (value, code) => (value !== null && typeof value === 'object'
|
|
113
124
|
? Object.values(value).some((v) => holds(v, code))
|
|
114
125
|
: (typeof value === 'string' || typeof value === 'number') && String(value) === code);
|
|
115
|
-
const answers = (a, r) => r.code !== undefined ? holds(a.body, String(r.code)) : a.
|
|
116
|
-
for (const
|
|
117
|
-
for (const [
|
|
118
|
-
m.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const answers = (a, r) => a.status === r.status && (r.code !== undefined ? holds(a.body, String(r.code)) : JSON.stringify(a.body)?.includes(r.message.split('{')[0].trim()) === true);
|
|
127
|
+
for (const { manifest: mf, lane } of units)
|
|
128
|
+
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
129
|
+
for (const [field, m] of Object.entries(decl.state ?? {})) {
|
|
130
|
+
m.transitions.forEach((t, i) => {
|
|
131
|
+
if (!t.operation)
|
|
132
|
+
return;
|
|
133
|
+
const on = answered.filter((a) => a.op === t.operation && a.lane === lane);
|
|
134
|
+
if (t.refusal && on.some((a) => answers(a, t.refusal)))
|
|
135
|
+
seen.add(`${resource}.${field}#${i}|refused|*`);
|
|
136
|
+
for (const [from, r] of Object.entries(t.refusals ?? {}))
|
|
137
|
+
if (on.some((a) => answers(a, r)))
|
|
138
|
+
seen.add(`${resource}.${field}#${i}|refused|${from}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
128
141
|
}
|
|
129
|
-
}
|
|
130
142
|
// every change the life made to a declared state field, from the World's log in order: a move is the declared
|
|
131
143
|
// transition that allows it (from its state to the next, by its operation when both name one); a change no
|
|
132
144
|
// transition allows is an undeclared move
|
|
133
145
|
const fieldsOf = new Map();
|
|
134
|
-
for (const mf of
|
|
146
|
+
for (const { manifest: mf } of units)
|
|
135
147
|
for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
|
|
136
148
|
const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
|
|
137
149
|
for (const [field, m] of Object.entries(decl.state ?? {}))
|
|
@@ -139,7 +151,7 @@ export async function coverageWalks(PACKS, vendor) {
|
|
|
139
151
|
}
|
|
140
152
|
const current = new Map();
|
|
141
153
|
// the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
|
|
142
|
-
const services = [...new Set(
|
|
154
|
+
const services = [...new Set(units.map(({ manifest: mf }) => mf.service).filter((x) => typeof x === 'string'))];
|
|
143
155
|
if (!services.length)
|
|
144
156
|
services.push(manifest.vendor);
|
|
145
157
|
for (const a of services.flatMap((service) => listActions(service, root))) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// life per pack is walked through the pack's own fetch on the in-memory World, and the report says:
|
|
4
4
|
// - the life: whether a judge's plausible verdict names its current content (a verdict its author wrote is not one);
|
|
5
5
|
// - correctness: every check on every step (status, expectations, captures, SHAPE, the beats) and the life
|
|
6
|
-
// answering
|
|
6
|
+
// answering the same on a second fresh World under its declared engine outputs;
|
|
7
7
|
// - coverage: the hand-written code the life and the vendor's published examples run and the declared state logic
|
|
8
8
|
// they reach (life-coverage.ts), with what they never reach;
|
|
9
9
|
// - the vendor's published examples: replayed and judged (vendor-examples.ts);
|
|
@@ -26,6 +26,7 @@ import { dirname, join, sep } from 'node:path';
|
|
|
26
26
|
import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "../walk/behavior-journey.js";
|
|
27
27
|
import { expandFills, waitMs } from "../walk/journey-kit.js";
|
|
28
28
|
import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
|
|
29
|
+
import { compareReplays } from "../walk/replay.js";
|
|
29
30
|
import { existenceProbe } from "../walk/journey-kit.js";
|
|
30
31
|
import { kernelCheckout, packsRoot, toolPath } from "./packs-root.js";
|
|
31
32
|
const PACKS = packsRoot();
|
|
@@ -85,7 +86,7 @@ await walk(life, world.root(), failures, { beats: true });
|
|
|
85
86
|
const answers = async () => {
|
|
86
87
|
const seen = [];
|
|
87
88
|
// the World's own draws (its seed, its keys) are the same in both: two Worlds differ only by what is beyond them (R9)
|
|
88
|
-
await withPinnedWorldDraws(() => walk(life, world.root(),
|
|
89
|
+
await withPinnedWorldDraws(() => walk(life, world.root(), failures, { beats: false, hooks: { after: (step, path, answer) => seen.push({ name: step.name ?? '', path, status: answer.status, body: answer.body, ...(answer.sql ? { sql: answer.sql } : {}), ...(answer.connection ? { connection: answer.connection } : {}) }) } }));
|
|
89
90
|
return seen;
|
|
90
91
|
};
|
|
91
92
|
const [first, second] = [await answers(), await answers()];
|
|
@@ -184,9 +185,9 @@ const flags = [];
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail
|
|
188
|
+
const replayComparison = compareReplays(first, second);
|
|
189
|
+
for (const detail of replayComparison.diagnostics)
|
|
190
|
+
failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail });
|
|
190
191
|
// ── breadth ──────────────────────────────────────────────────────────────────────────────────
|
|
191
192
|
const factory = await vendorFetch(PACKS, vendor);
|
|
192
193
|
const fetch = factory?.(world.root());
|
|
@@ -56,7 +56,7 @@ import { basename, join } from 'node:path';
|
|
|
56
56
|
import { gunzipSync } from 'node:zlib';
|
|
57
57
|
import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
|
|
58
58
|
import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
|
|
59
|
-
import { at as selectorAt, captureAt, expandFills, isResp, isGit,
|
|
59
|
+
import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait } from "../walk/journey-kit.js";
|
|
60
60
|
import { lanesOf, shapeChecker } from "../walk/shape.js";
|
|
61
61
|
import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug } from "../spec.js";
|
|
62
62
|
import { packsRoot } from "./packs-root.js";
|
|
@@ -70,6 +70,7 @@ let specSchemas = {};
|
|
|
70
70
|
const schemasByOperation = {};
|
|
71
71
|
/** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
|
|
72
72
|
const operationSchemas = {};
|
|
73
|
+
const operationKey = (operation, lane) => lane ? `${lane}:${operation}` : operation;
|
|
73
74
|
/** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
|
|
74
75
|
* Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
|
|
75
76
|
function answerKeys(output, published, sent, placeholder, rawBindings = false) {
|
|
@@ -106,7 +107,7 @@ function served(pkg, doc) {
|
|
|
106
107
|
applySpecPatch(patched, p);
|
|
107
108
|
return patched;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
const restXmlService = (model) => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
|
|
110
111
|
/** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
|
|
111
112
|
function requestMapKeysHold(published, translated, sent, placeholder) {
|
|
112
113
|
if (!placeholder || !published || typeof published !== 'object')
|
|
@@ -124,11 +125,11 @@ function requestMapKeysHold(published, translated, sent, placeholder) {
|
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
const PRELUDE = { Boolean: 'boolean', PrimitiveBoolean: 'boolean', Integer: 'integer', PrimitiveInteger: 'integer', Long: 'long', PrimitiveLong: 'long', Float: 'float', Double: 'double' };
|
|
127
|
-
const scalarType = (target) =>
|
|
128
|
-
const scalarOf = (target, v) => {
|
|
128
|
+
const scalarType = (model, target) => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
|
|
129
|
+
const scalarOf = (model, target, v) => {
|
|
129
130
|
if (typeof v !== 'string')
|
|
130
131
|
return v;
|
|
131
|
-
const t = scalarType(target);
|
|
132
|
+
const t = scalarType(model, target);
|
|
132
133
|
if (t === 'boolean')
|
|
133
134
|
return v === 'true';
|
|
134
135
|
if (['integer', 'long', 'short', 'byte'].includes(t))
|
|
@@ -138,10 +139,10 @@ const scalarOf = (target, v) => {
|
|
|
138
139
|
const arrayOf = (v) => (v === undefined || v === '' ? [] : Array.isArray(v) ? v : [v]);
|
|
139
140
|
/** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
|
|
140
141
|
* repeated element, a wrapped list its container's member elements, scalars as their shape's type. */
|
|
141
|
-
function typedXml(target, v) {
|
|
142
|
-
const shape =
|
|
142
|
+
function typedXml(model, target, v) {
|
|
143
|
+
const shape = model.shapes[target];
|
|
143
144
|
if (!shape || shape.type !== 'structure')
|
|
144
|
-
return scalarOf(target, v);
|
|
145
|
+
return scalarOf(model, target, v);
|
|
145
146
|
if (!v || typeof v !== 'object')
|
|
146
147
|
return v === '' ? {} : v;
|
|
147
148
|
const obj = v;
|
|
@@ -150,38 +151,38 @@ function typedXml(target, v) {
|
|
|
150
151
|
if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t)))
|
|
151
152
|
continue;
|
|
152
153
|
const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
|
|
153
|
-
const ts =
|
|
154
|
+
const ts = model.shapes[m.target];
|
|
154
155
|
const raw = obj[el];
|
|
155
156
|
if (ts?.type === 'list') {
|
|
156
157
|
if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) {
|
|
157
158
|
if (raw !== undefined)
|
|
158
|
-
out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i));
|
|
159
|
+
out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i));
|
|
159
160
|
continue;
|
|
160
161
|
}
|
|
161
162
|
if (raw === undefined)
|
|
162
163
|
continue;
|
|
163
164
|
const memberEl = String(ts.member.traits?.['smithy.api#xmlName'] ?? 'member');
|
|
164
|
-
out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(ts.member.target, i));
|
|
165
|
+
out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(model, ts.member.target, i));
|
|
165
166
|
}
|
|
166
167
|
else if (raw !== undefined)
|
|
167
|
-
out[name] = typedXml(m.target, raw);
|
|
168
|
+
out[name] = typedXml(model, m.target, raw);
|
|
168
169
|
}
|
|
169
170
|
return out;
|
|
170
171
|
}
|
|
171
172
|
/** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
|
|
172
173
|
* member's), and the members its headers carry. */
|
|
173
|
-
function restXmlAnswer(opId, body, headers = {}) {
|
|
174
|
-
const out =
|
|
174
|
+
function restXmlAnswer({ id: opId, model }, body, headers = {}) {
|
|
175
|
+
const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
|
|
175
176
|
if (!out)
|
|
176
177
|
return body;
|
|
177
178
|
const members = Object.entries((out.members ?? {}));
|
|
178
179
|
const payload = members.find(([, m]) => m.traits?.['smithy.api#httpPayload'] !== undefined);
|
|
179
|
-
const answer = (payload ? { [payload[0]]: typedXml(payload[1].target, body) } : typedXml(
|
|
180
|
+
const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body));
|
|
180
181
|
const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
181
182
|
for (const [name, m] of members) {
|
|
182
183
|
const h = m.traits?.['smithy.api#httpHeader'];
|
|
183
184
|
if (h !== undefined && lower[String(h).toLowerCase()] !== undefined)
|
|
184
|
-
answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
|
|
185
|
+
answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
|
|
185
186
|
// a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
|
|
186
187
|
const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
|
|
187
188
|
if (prefix !== undefined)
|
|
@@ -191,8 +192,8 @@ function restXmlAnswer(opId, body, headers = {}) {
|
|
|
191
192
|
}
|
|
192
193
|
/** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
|
|
193
194
|
* label, a query key, a header; a payload or body member is the step's body and is not compared). */
|
|
194
|
-
function restXmlInputProblems(opId, input, path, headers, placeholder, rawPath) {
|
|
195
|
-
const shape =
|
|
195
|
+
function restXmlInputProblems({ id: opId, model }, input, path, headers, placeholder, rawPath) {
|
|
196
|
+
const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
|
|
196
197
|
const url = new URL(path, 'http://twin.local');
|
|
197
198
|
// the step as written, its captures unfilled: a value it takes from a capture is the World's own id (an S3 upload's
|
|
198
199
|
// UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
|
|
@@ -236,11 +237,13 @@ function lenientJson(text) {
|
|
|
236
237
|
* response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
|
|
237
238
|
* fixtures (one sample per resource, beside its spec), and OpenAI's `x-oaiMeta.examples` (its `response` is JSON text; one that is not JSON, a stream or an abridged answer, is
|
|
238
239
|
* kept as text and compared with the published framing and payload). */
|
|
239
|
-
function published(pkg) {
|
|
240
|
+
function published(pkg, lane) {
|
|
240
241
|
// Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
|
|
241
242
|
// lane, the operation being the lane's own (its owner is `<lane>:<id>` in the vendor's owners)
|
|
242
|
-
const
|
|
243
|
-
|
|
243
|
+
const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
|
|
244
|
+
const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) : undefined;
|
|
245
|
+
const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
|
|
246
|
+
return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
|
|
244
247
|
}
|
|
245
248
|
/** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
|
|
246
249
|
* it means), vendored in `spec/doc-examples.json` as `[{title, source, operation, request?, input?, output?, means, status?}]`:
|
|
@@ -251,7 +254,7 @@ function published(pkg) {
|
|
|
251
254
|
* reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
|
|
252
255
|
* shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
|
|
253
256
|
* string where the schema says number, integer or boolean is read as that type, in the answer and the request body. */
|
|
254
|
-
function fromDocs(pkg) {
|
|
257
|
+
function fromDocs(pkg, lane, model) {
|
|
255
258
|
const file = join(pkg, 'spec', 'doc-examples.json');
|
|
256
259
|
if (!existsSync(file))
|
|
257
260
|
return [];
|
|
@@ -294,12 +297,11 @@ function fromDocs(pkg) {
|
|
|
294
297
|
for (const d of docs)
|
|
295
298
|
if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim()))
|
|
296
299
|
throw new Error(`${file}: "${d.title}" names placeholders without the page's words for them (placeholders.quote)`);
|
|
297
|
-
const smithy = ['smithy.json.gz', 'smithy.json'].some((f) => existsSync(join(pkg, 'spec', f)));
|
|
298
300
|
// a page's example is a fragment (an expression, not the request that carries it): the body holds it
|
|
299
301
|
return docs.map((d) => {
|
|
300
302
|
if (!d.rendered)
|
|
301
303
|
return fromPage(d, d.output, d.input);
|
|
302
|
-
const schemas = operationSchemas[d.operation];
|
|
304
|
+
const schemas = operationSchemas[operationKey(d.operation, lane)];
|
|
303
305
|
const schema = schemas?.responses[String(d.status ?? 200)];
|
|
304
306
|
if (!schema)
|
|
305
307
|
throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
|
|
@@ -311,7 +313,14 @@ function fromDocs(pkg) {
|
|
|
311
313
|
function fromPage(d, output, input) {
|
|
312
314
|
const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
313
315
|
const placeholder = named ? new RegExp(`${placeholderIds.source}|^(?:${named})$`, placeholderIds.flags) : placeholderIds;
|
|
314
|
-
|
|
316
|
+
let restXml;
|
|
317
|
+
if (model && restXmlService(model)) {
|
|
318
|
+
const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
|
|
319
|
+
if (!id)
|
|
320
|
+
throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
|
|
321
|
+
restXml = { id, model };
|
|
322
|
+
}
|
|
323
|
+
return { names: d.placeholders?.names, ...(d.wire ? { wire: d.wire } : {}), id: `${d.operation}#doc ${d.title}`, operation: d.operation, output, ...(d.status ? { status: d.status } : {}), ...(input !== undefined ? { input, partial: true, placeholder } : {}), ...(model ? (restXml ? { restXml } : { target: d.operation }) : {}), ...(d.request ? { request: d.request } : {}), ...(authorizationAbsent.has(d) ? { authorizationAbsent: true } : {}) };
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
/** A rendered object the page does not fill: any object stands where it is. */
|
|
@@ -400,12 +409,25 @@ function decodedExample(media, value) {
|
|
|
400
409
|
return value;
|
|
401
410
|
}
|
|
402
411
|
}
|
|
403
|
-
function fromSpec(pkg) {
|
|
412
|
+
function fromSpec(pkg, lane, model) {
|
|
404
413
|
const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
405
414
|
if (rfc)
|
|
406
415
|
return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
|
|
416
|
+
const out = [];
|
|
417
|
+
if (model) {
|
|
418
|
+
const restXml = restXmlService(model);
|
|
419
|
+
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
420
|
+
const examples = shape.traits?.['smithy.api#examples'];
|
|
421
|
+
const operation = id.split('#')[1];
|
|
422
|
+
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
423
|
+
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
424
|
+
for (const e of examples ?? [])
|
|
425
|
+
out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
407
429
|
// a spec vendored as published or gzipped, as derive-pack reads it
|
|
408
|
-
const spec = ['
|
|
430
|
+
const spec = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'discovery.json.gz', 'discovery.json'].map((f) => join(pkg, 'spec', f)).find((p) => existsSync(p));
|
|
409
431
|
// a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
|
|
410
432
|
if (!spec && existsSync(join(pkg, 'spec', 'proto')))
|
|
411
433
|
return [];
|
|
@@ -420,22 +442,6 @@ function fromSpec(pkg) {
|
|
|
420
442
|
if (!spec && !documents)
|
|
421
443
|
throw new Error(`${pkg}: no published-example reader for this spec yet`);
|
|
422
444
|
const text = spec ? (spec.endsWith('.gz') ? gunzipSync(readFileSync(spec)) : readFileSync(spec)).toString('utf8') : '';
|
|
423
|
-
const out = [];
|
|
424
|
-
if (spec && /smithy\.json(\.gz)?$/.test(spec)) {
|
|
425
|
-
const model = parseExactJson(text);
|
|
426
|
-
const restXml = Object.values(model.shapes).some((sh) => sh.type === 'service' && sh.traits?.['aws.protocols#restXml'] !== undefined);
|
|
427
|
-
if (restXml)
|
|
428
|
-
xmlModel = model;
|
|
429
|
-
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
430
|
-
const examples = shape.traits?.['smithy.api#examples'];
|
|
431
|
-
const operation = id.split('#')[1];
|
|
432
|
-
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
433
|
-
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
434
|
-
for (const e of examples ?? [])
|
|
435
|
-
out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: id } : { target: operation }) });
|
|
436
|
-
}
|
|
437
|
-
return out;
|
|
438
|
-
}
|
|
439
445
|
// a Google API Discovery document publishes no examples; it is read as the OpenAPI document derive-pack reads it as,
|
|
440
446
|
// its patches applied to the Discovery document first (spec-ir-discovery.ts)
|
|
441
447
|
const discovery = spec !== undefined && /discovery\.json(\.gz)?$/.test(spec);
|
|
@@ -454,8 +460,9 @@ function fromSpec(pkg) {
|
|
|
454
460
|
continue;
|
|
455
461
|
// Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
|
|
456
462
|
const bodyParam = (op.parameters ?? []).find((x) => x.in === 'body');
|
|
457
|
-
|
|
458
|
-
|
|
463
|
+
const key = operationKey(idOf(op, method, path), lane);
|
|
464
|
+
schemasByOperation[key] = specSchemas;
|
|
465
|
+
operationSchemas[key] = {
|
|
459
466
|
request: json(op.requestBody?.content) ?? bodyParam?.schema,
|
|
460
467
|
responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? r.schema; return sch ? [[code, sch]] : []; })),
|
|
461
468
|
};
|
|
@@ -978,17 +985,17 @@ function outputSchemaMismatches(schema, value, prefix, depth = 0) {
|
|
|
978
985
|
* recorded with the per-example classification. Request echoes and generated output are absent. */
|
|
979
986
|
const storedCollectionFields = {
|
|
980
987
|
'ai-gateway': { listModels: ['data'] },
|
|
981
|
-
aws: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
982
|
-
cloudflare: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
988
|
+
aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
989
|
+
cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
983
990
|
discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
|
|
984
991
|
github: { 'repos/get-branch': ['commit.parents'], 'repos/get-commit': ['parents', 'files'], 'repos/get-content': ['', 'entries'], 'repos/compare-commits': ['commits', 'files'], 'git/get-tree': ['tree'], 'git/list-matching-refs': [''], 'repos/get-repo-rulesets': [''] },
|
|
985
|
-
hackernews: { getItem: ['kids'], getUser: ['submitted'], getUpdates: ['items', 'profiles'], getTopStories: [''], getNewStories: [''], getBestStories: [''], getAskStories: [''], getShowStories: [''], getJobStories: [''] },
|
|
992
|
+
hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
|
|
986
993
|
linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
|
|
987
994
|
'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
|
|
988
995
|
openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
|
|
989
996
|
redis: { SCAN: ['[].1'] },
|
|
990
997
|
resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
|
|
991
|
-
sentry: { listOrganizations: ['', '[].features'] },
|
|
998
|
+
sentry: { 'api:listOrganizations': ['', '[].features'] },
|
|
992
999
|
slack: { conversations_rename: ['channel.previous_names'], conversations_invite: ['channel.previous_names'], conversations_members: ['members'], conversations_list: ['channels', 'channels.[].previous_names'], users_conversations: ['channels', 'channels.[].previous_names'], conversations_info: ['channel.previous_names'], conversations_replies: ['messages'], conversations_history: ['messages'], usergroups_list: ['usergroups'] },
|
|
993
1000
|
tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
|
|
994
1001
|
};
|
|
@@ -1104,7 +1111,7 @@ if (!vendor || vendor.startsWith('-')) {
|
|
|
1104
1111
|
const PKG = join(PACKS, vendor);
|
|
1105
1112
|
const file = join(PKG, 'journeys', 'vendor-examples.json');
|
|
1106
1113
|
await loadPlaceholders(PKG);
|
|
1107
|
-
const examples = published(PKG);
|
|
1114
|
+
const examples = published(PKG, undefined);
|
|
1108
1115
|
const byId = new Map(examples.map((e) => [e.id, e]));
|
|
1109
1116
|
if (byId.size !== examples.length)
|
|
1110
1117
|
throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
|
|
@@ -1121,11 +1128,13 @@ const shape = await shapeChecker(PKG);
|
|
|
1121
1128
|
for (const e of examples) {
|
|
1122
1129
|
if (e.patchRemoved || !e.request || !shape)
|
|
1123
1130
|
continue;
|
|
1124
|
-
const
|
|
1131
|
+
const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
|
|
1132
|
+
const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
1125
1133
|
if (e.operation === 'gap' && routed === undefined)
|
|
1126
1134
|
continue;
|
|
1127
|
-
|
|
1128
|
-
|
|
1135
|
+
const expected = operationKey(e.operation, e.lane);
|
|
1136
|
+
if (routed !== expected)
|
|
1137
|
+
throw new Error(`spec/doc-examples.json: "${e.id}" sends ${e.request.method} ${e.request.url}, which the spec routes to ${routed ?? 'no operation'}, not ${expected}`);
|
|
1129
1138
|
}
|
|
1130
1139
|
const sweeping = process.argv.includes('--sweep');
|
|
1131
1140
|
const sweepHits = [];
|
|
@@ -1258,18 +1267,20 @@ await walk(journey, world.root(), failures, {
|
|
|
1258
1267
|
after: (step, _path, answer, stepFailures) => {
|
|
1259
1268
|
if (isGit(step) || isWait(step) || isSocket(step))
|
|
1260
1269
|
return;
|
|
1261
|
-
|
|
1270
|
+
const request = answer.request;
|
|
1271
|
+
const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
|
|
1272
|
+
const operation = hit?.op;
|
|
1273
|
+
if (request) {
|
|
1262
1274
|
const st = step;
|
|
1263
|
-
const method =
|
|
1275
|
+
const method = request.method;
|
|
1264
1276
|
// a read by what the operation does (an RPC wire POSTs its reads), else by its verb
|
|
1265
|
-
const
|
|
1266
|
-
const opClass = op?.class;
|
|
1277
|
+
const opClass = operation?.class;
|
|
1267
1278
|
const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
|
|
1268
1279
|
if (isRead)
|
|
1269
|
-
reads.push({ path: bare(
|
|
1280
|
+
reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
|
|
1270
1281
|
else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
|
|
1271
1282
|
const body = answer.body;
|
|
1272
|
-
const segs = bare(
|
|
1283
|
+
const segs = bare(request.url).split('/');
|
|
1273
1284
|
// an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
|
|
1274
1285
|
// a write whose subject is neither minted nor in its path (an RPC call naming it in its body) is left to the judge
|
|
1275
1286
|
const minted = typeof body.id === 'string' ? body.id : '';
|
|
@@ -1277,8 +1288,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1277
1288
|
if (minted || idInPath) {
|
|
1278
1289
|
const base = minted && !segs.includes(minted) ? `${segs.join('/')}/${minted}` : /^[a-z_-]+$/.test(segs.at(-1) ?? '') && segs.length > 2 ? segs.slice(0, -1).join('/') : segs.join('/');
|
|
1279
1290
|
// only an effect the vendor gives a door to read: a GET of the subject or of its collection (a moderation has neither)
|
|
1280
|
-
const
|
|
1281
|
-
const gets = (at) => shape?.operationOf('GET', at, hdrs)?.op.method.toUpperCase() === 'GET';
|
|
1291
|
+
const gets = (at) => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
|
|
1282
1292
|
const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
|
|
1283
1293
|
if (door)
|
|
1284
1294
|
thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
|
|
@@ -1308,20 +1318,20 @@ await walk(journey, world.root(), failures, {
|
|
|
1308
1318
|
unknown.push(s.example);
|
|
1309
1319
|
return;
|
|
1310
1320
|
}
|
|
1311
|
-
|
|
1321
|
+
const key = operationKey(ex.operation, ex.lane);
|
|
1322
|
+
specSchemas = schemasByOperation[key] ?? {};
|
|
1312
1323
|
replayed.add(s.example);
|
|
1313
|
-
if (ex.operation === 'gap' && ex.request) {
|
|
1314
|
-
|
|
1315
|
-
if (s.method !== ex.request.method || sent.href !== ex.request.url)
|
|
1324
|
+
if (ex.operation === 'gap' && ex.request && request) {
|
|
1325
|
+
if (request.method !== ex.request.method || request.url !== ex.request.url)
|
|
1316
1326
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
|
|
1317
1327
|
}
|
|
1318
|
-
if (ex.authorizationAbsent &&
|
|
1328
|
+
if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization'))
|
|
1319
1329
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
|
|
1320
1330
|
// the step calls the example's operation (the spec's, as the walk's SHAPE check reads the request)
|
|
1321
|
-
if (
|
|
1322
|
-
const called =
|
|
1323
|
-
if (called && called !==
|
|
1324
|
-
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${
|
|
1331
|
+
if (request && shape) {
|
|
1332
|
+
const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
1333
|
+
if (called && called !== key)
|
|
1334
|
+
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
|
|
1325
1335
|
}
|
|
1326
1336
|
// the example is sent as published: its input as the body, to its operation
|
|
1327
1337
|
// a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
|
|
@@ -1329,26 +1339,28 @@ await walk(journey, world.root(), failures, {
|
|
|
1329
1339
|
for (const [path, why] of Object.entries(s.unsent ?? {})) {
|
|
1330
1340
|
if (!why)
|
|
1331
1341
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
|
|
1332
|
-
if (requiredAt(operationSchemas[
|
|
1342
|
+
if (requiredAt(operationSchemas[key]?.request, path))
|
|
1333
1343
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
|
|
1334
1344
|
}
|
|
1335
1345
|
const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
|
|
1336
1346
|
const inputType = Object.entries(ex.request?.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1]
|
|
1337
|
-
??
|
|
1347
|
+
?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
|
|
1338
1348
|
const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
|
|
1339
1349
|
// the body the step sends, every check below reading it alike: no body (a GET) is the empty input a page publishes for it
|
|
1340
1350
|
const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
|
|
1341
1351
|
// A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
|
|
1342
1352
|
const input = answerKeys(publishedInput, publishedInput, body, ex.placeholder, true);
|
|
1343
|
-
if (ex.restXml)
|
|
1344
|
-
|
|
1345
|
-
|
|
1353
|
+
if (ex.restXml) {
|
|
1354
|
+
if (request)
|
|
1355
|
+
for (const p of restXmlInputProblems(ex.restXml, (input ?? {}), shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, s.path))
|
|
1356
|
+
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
|
|
1357
|
+
}
|
|
1346
1358
|
else if (input !== undefined && (!requestMapKeysHold(publishedInput, input, body, ex.placeholder) || (ex.partial ? !holds(body, input, ex.placeholder, undefined, ex.names, false, inputType) : !same(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, false, inputType))))
|
|
1347
1359
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: typeof input === 'string' ? comparedText(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, inputType)?.problem ?? "the body is not the example's published input" : "the body is not the example's published input" });
|
|
1348
1360
|
// a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
|
|
1349
1361
|
if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines))
|
|
1350
1362
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `sends ${JSON.stringify(s.send ?? null)}, not the scenario's ${JSON.stringify(ex.lines)}` });
|
|
1351
|
-
const target =
|
|
1363
|
+
const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
|
|
1352
1364
|
if (ex.target && !target?.endsWith(`.${ex.target}`))
|
|
1353
1365
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
|
|
1354
1366
|
if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status)
|
|
@@ -1391,9 +1403,8 @@ await walk(journey, world.root(), failures, {
|
|
|
1391
1403
|
const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
|
|
1392
1404
|
const responseType = answer.headers?.['content-type'];
|
|
1393
1405
|
const literal = isResp(s) || (published !== null && typeof published !== 'object');
|
|
1394
|
-
const valueSchema = ex.schema ?? operationSchemas[
|
|
1395
|
-
comparisonCollections = [...(storedCollectionFields[vendor]?.[
|
|
1396
|
-
const operation = shape?.operationOf((s.method ?? 'GET').toUpperCase(), _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op;
|
|
1406
|
+
const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
|
|
1407
|
+
comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
|
|
1397
1408
|
// A derived resource list names its holder; never treat every array under a read as a list.
|
|
1398
1409
|
if (operation?.class === 'list' && operation.answers?.list) {
|
|
1399
1410
|
const holder = operation.answers.key ? published?.[operation.answers.key] : published;
|
|
@@ -1439,7 +1450,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1439
1450
|
},
|
|
1440
1451
|
});
|
|
1441
1452
|
const owners = (await vendorFetch(PACKS, vendor))?.(world.root())?.owners?.() ?? (await vendorStream(PACKS, vendor))?.(world.root())?.owners?.() ?? {};
|
|
1442
|
-
const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[e.
|
|
1453
|
+
const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
|
|
1443
1454
|
const patchRemoved = examples.filter((e) => e.patchRemoved);
|
|
1444
1455
|
const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
|
|
1445
1456
|
const recorded = Object.keys(journey.notReplayed ?? {});
|