@molecule/api-ai-tools 1.9.18 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -19
- package/dist/command-position.d.ts +3 -1
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +95 -6
- package/dist/output-filter.d.ts +13 -6
- package/dist/output-filter.d.ts.map +1 -1
- package/dist/output-filter.js +35 -16
- package/dist/suite-overlap.d.ts +30 -1
- package/dist/suite-overlap.d.ts.map +1 -1
- package/dist/suite-overlap.js +171 -34
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +214 -24
- package/dist/trace-artifacts.d.ts +71 -0
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +398 -12
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts +32 -12
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +101 -33
- package/package.json +1 -1
package/dist/trace-artifacts.js
CHANGED
|
@@ -197,6 +197,11 @@ function messageOf(value) {
|
|
|
197
197
|
return null
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// A fill/type/selectOption records what the test typed; the note compares it with the body the
|
|
201
|
+
// page submitted (m173: the Settings form sent bio:"" although the test had just typed a value).
|
|
202
|
+
// Only those actions keep it, so a click's selector does not ride every row.
|
|
203
|
+
const FILLING_ACTION = /\\.(?:fill|type|pressSequentially|selectOption)$/
|
|
204
|
+
|
|
200
205
|
// The text a failing assertion waited for, named by its own error: a \`has-text(…)\` CSS
|
|
201
206
|
// pseudo-class, a \`getByText(…)\` locator, or a \`toContainText(…)\` expectation. A strict-mode
|
|
202
207
|
// violation is not this shape — two elements DID match the text — and neither is a wait on a
|
|
@@ -279,6 +284,40 @@ function gotText(buf, entries, r) {
|
|
|
279
284
|
return (websocketText(raw) || raw).slice(0, MAX_WS)
|
|
280
285
|
}
|
|
281
286
|
|
|
287
|
+
// The boolean-valued fields a response body carries, however deeply its entity is wrapped
|
|
288
|
+
// ({"profile":{…,"following":true}}) — what a read of the same field can contradict.
|
|
289
|
+
// A body that does not parse (the trace read bounds what it keeps) names no field.
|
|
290
|
+
function booleanFieldsOf(text) {
|
|
291
|
+
let parsed
|
|
292
|
+
try { parsed = JSON.parse(text) } catch (e) { return null }
|
|
293
|
+
const out = {}
|
|
294
|
+
function walk(value, depth) {
|
|
295
|
+
if (depth > 4 || value === null || typeof value !== 'object') return
|
|
296
|
+
if (Array.isArray(value)) {
|
|
297
|
+
for (let i = 0; i < value.length; i++) walk(value[i], depth + 1)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
for (const key of Object.keys(value)) {
|
|
301
|
+
const item = value[key]
|
|
302
|
+
if (typeof item === 'boolean') {
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(out, key)) out[key] = item
|
|
304
|
+
} else walk(item, depth + 1)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
walk(parsed, 0)
|
|
308
|
+
return Object.keys(out).length > 0 ? out : null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The resource a write acts on: its last path segment is the action (follow, favorite), so
|
|
312
|
+
// /api/profiles/alice/follow is the resource a GET /api/profiles/alice reads.
|
|
313
|
+
function resourceBaseOf(path) {
|
|
314
|
+
const clean = String(path).split(/[?#]/)[0]
|
|
315
|
+
const parts = clean.split('/').filter(function (segment) { return segment !== '' })
|
|
316
|
+
if (parts.length === 0) return clean
|
|
317
|
+
parts.pop()
|
|
318
|
+
return '/' + parts.join('/')
|
|
319
|
+
}
|
|
320
|
+
|
|
282
321
|
let origin = ''
|
|
283
322
|
function pathOf(url) {
|
|
284
323
|
try {
|
|
@@ -446,14 +485,19 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
446
485
|
if (!entry || entry.type !== 'before') continue
|
|
447
486
|
if (typeof entry.class !== 'string' || typeof entry.method !== 'string') continue
|
|
448
487
|
if (entry.class === 'Test' || typeof entry.startTime !== 'number') continue
|
|
488
|
+
const api = entry.class + '.' + entry.method
|
|
489
|
+
const params = entry.params && typeof entry.params === 'object' ? entry.params : {}
|
|
490
|
+
const filling = FILLING_ACTION.test(api)
|
|
449
491
|
const action = {
|
|
450
|
-
api:
|
|
492
|
+
api: api,
|
|
451
493
|
start: entry.startTime,
|
|
452
494
|
end: typeof ended[entry.callId] === 'number' ? ended[entry.callId] : null,
|
|
453
495
|
context: ctx.id,
|
|
454
496
|
pageId: typeof entry.callId === 'string' ? pageByCall[entry.callId] : undefined,
|
|
455
497
|
frameId: undefined,
|
|
456
498
|
stepId: typeof entry.stepId === 'string' ? entry.stepId : undefined,
|
|
499
|
+
...(filling && typeof params.selector === 'string' ? { selector: params.selector } : {}),
|
|
500
|
+
...(filling && typeof params.value === 'string' ? { value: params.value } : {}),
|
|
457
501
|
}
|
|
458
502
|
if (action.pageId !== undefined) {
|
|
459
503
|
for (const frame of ctx.frames) {
|
|
@@ -819,6 +863,66 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
819
863
|
if (got) lead.got = got
|
|
820
864
|
}
|
|
821
865
|
}
|
|
866
|
+
// A write that answered a boolean field and a LATER GET of the same resource that answered the
|
|
867
|
+
// opposite — the read route never saw the viewer (m181's follow: the write answered
|
|
868
|
+
// {"following":true}, the next profile GET answered false). Both bodies are the trace's own
|
|
869
|
+
// facts; the note leads with them and sends the run to the route's middleware, not the page.
|
|
870
|
+
function readContradictionOf() {
|
|
871
|
+
const writes = []
|
|
872
|
+
for (let i = 0; i < requests.length; i++) {
|
|
873
|
+
const r = requests[i]
|
|
874
|
+
if (!/^(?:POST|PUT|PATCH|DELETE)$/.test(r.method)) continue
|
|
875
|
+
if (r.status < 200 || r.status >= 300) continue
|
|
876
|
+
if (r.type !== 'fetch' && r.type !== 'xhr') continue
|
|
877
|
+
writes.push({ index: i, request: r })
|
|
878
|
+
}
|
|
879
|
+
for (let w = 0; w < writes.length; w++) {
|
|
880
|
+
const write = writes[w].request
|
|
881
|
+
const writeGot = gotText(buf, entries, write)
|
|
882
|
+
const writeFields = writeGot ? booleanFieldsOf(writeGot) : null
|
|
883
|
+
if (!writeFields) continue
|
|
884
|
+
const base = resourceBaseOf(write.path)
|
|
885
|
+
let read = null
|
|
886
|
+
let readFields = null
|
|
887
|
+
for (let j = writes[w].index + 1; j < requests.length; j++) {
|
|
888
|
+
const candidate = requests[j]
|
|
889
|
+
if (candidate.method !== 'GET') continue
|
|
890
|
+
if (candidate.status < 200 || candidate.status >= 300) continue
|
|
891
|
+
if (candidate.type !== 'fetch' && candidate.type !== 'xhr') continue
|
|
892
|
+
// The same page only: another context's read is not this page's answer.
|
|
893
|
+
if (candidate.pageId !== undefined && write.pageId !== undefined &&
|
|
894
|
+
candidate.pageId !== write.pageId) continue
|
|
895
|
+
if (candidate.context !== undefined && write.context !== undefined &&
|
|
896
|
+
candidate.context !== write.context) continue
|
|
897
|
+
// LATER, on the trace's own clock when it recorded one.
|
|
898
|
+
if (typeof candidate.startedAt === 'number' && typeof write.startedAt === 'number' &&
|
|
899
|
+
candidate.startedAt <= write.startedAt) continue
|
|
900
|
+
if (String(candidate.path).split(/[?#]/)[0] !== base) continue
|
|
901
|
+
const got = gotText(buf, entries, candidate)
|
|
902
|
+
const fields = got ? booleanFieldsOf(got) : null
|
|
903
|
+
if (!fields) continue
|
|
904
|
+
read = candidate
|
|
905
|
+
readFields = fields
|
|
906
|
+
break
|
|
907
|
+
}
|
|
908
|
+
if (read === null || readFields === null) continue
|
|
909
|
+
for (const key of Object.keys(writeFields)) {
|
|
910
|
+
if (!Object.prototype.hasOwnProperty.call(readFields, key)) continue
|
|
911
|
+
if (readFields[key] === writeFields[key]) continue
|
|
912
|
+
write.got = writeGot
|
|
913
|
+
read.got = gotText(buf, entries, read)
|
|
914
|
+
return {
|
|
915
|
+
field: key,
|
|
916
|
+
writeValue: writeFields[key],
|
|
917
|
+
readValue: readFields[key],
|
|
918
|
+
write: wire(write),
|
|
919
|
+
read: wire(read),
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return null
|
|
924
|
+
}
|
|
925
|
+
const readContradiction = readContradictionOf()
|
|
822
926
|
const failing = []
|
|
823
927
|
const api = []
|
|
824
928
|
const pending = []
|
|
@@ -924,6 +1028,7 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
924
1028
|
// plus the answered ones (xr-molecule-flash-m112's note listed only -1s and 200s).
|
|
925
1029
|
requests: chosen.slice(0, MAX_REQUESTS),
|
|
926
1030
|
...(missingText === null ? {} : { missingText: missingText }),
|
|
1031
|
+
...(readContradiction === null ? {} : { readContradiction: readContradiction }),
|
|
927
1032
|
console: keptConsole.slice(0, MAX_CONSOLE).map(function (text) { return text.slice(0, 300) }),
|
|
928
1033
|
actions: actions.slice(-MAX_ACTIONS),
|
|
929
1034
|
...(unattributed ? { unattributed: true } : {}),
|
|
@@ -978,6 +1083,29 @@ function parseMissingText(value) {
|
|
|
978
1083
|
return null;
|
|
979
1084
|
return { text: raw.text, method: raw.method, path: raw.path };
|
|
980
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* One `readContradiction` out of a `TRACE` line's json, or null when the value is not one.
|
|
1088
|
+
*
|
|
1089
|
+
* Both requests must parse and the two values must be booleans: a partial fact never becomes a
|
|
1090
|
+
* claim about the route.
|
|
1091
|
+
*
|
|
1092
|
+
* @param value - The line's `readContradiction`.
|
|
1093
|
+
* @returns The write, the later read and the field they disagree on, or null.
|
|
1094
|
+
*/
|
|
1095
|
+
function parseReadContradiction(value) {
|
|
1096
|
+
if (typeof value !== 'object' || value === null)
|
|
1097
|
+
return null;
|
|
1098
|
+
const raw = value;
|
|
1099
|
+
if (typeof raw.field !== 'string' || raw.field === '')
|
|
1100
|
+
return null;
|
|
1101
|
+
if (typeof raw.writeValue !== 'boolean' || typeof raw.readValue !== 'boolean')
|
|
1102
|
+
return null;
|
|
1103
|
+
const write = parseTraceRequest(raw.write);
|
|
1104
|
+
const read = parseTraceRequest(raw.read);
|
|
1105
|
+
if (write === null || read === null)
|
|
1106
|
+
return null;
|
|
1107
|
+
return { field: raw.field, writeValue: raw.writeValue, readValue: raw.readValue, write, read };
|
|
1108
|
+
}
|
|
981
1109
|
/**
|
|
982
1110
|
* One `lastAction` out of a `TRACE` line's json, or null when the value is not one.
|
|
983
1111
|
*
|
|
@@ -1024,6 +1152,7 @@ export function parseTraceArtifacts(stdout) {
|
|
|
1024
1152
|
const lead = parseTraceRequest(raw.lead);
|
|
1025
1153
|
const lastAction = parseLastAction(raw.lastAction);
|
|
1026
1154
|
const missingText = parseMissingText(raw.missingText);
|
|
1155
|
+
const readContradiction = parseReadContradiction(raw.readContradiction);
|
|
1027
1156
|
const otherContexts = Array.isArray(raw.otherContexts)
|
|
1028
1157
|
? raw.otherContexts
|
|
1029
1158
|
.filter((c) => typeof c === 'object' && c !== null)
|
|
@@ -1054,10 +1183,13 @@ export function parseTraceArtifacts(stdout) {
|
|
|
1054
1183
|
...(typeof a.context === 'string' ? { context: a.context } : {}),
|
|
1055
1184
|
...(typeof a.pageId === 'string' ? { pageId: a.pageId } : {}),
|
|
1056
1185
|
...(typeof a.frameId === 'string' ? { frameId: a.frameId } : {}),
|
|
1186
|
+
...(typeof a.selector === 'string' && a.selector ? { selector: a.selector } : {}),
|
|
1187
|
+
...(typeof a.value === 'string' ? { value: a.value } : {}),
|
|
1057
1188
|
})),
|
|
1058
1189
|
...(otherContexts.length === 0 ? {} : { otherContexts }),
|
|
1059
1190
|
...(raw.unattributed === true ? { unattributed: true } : {}),
|
|
1060
1191
|
...(missingText === null ? {} : { missingText }),
|
|
1192
|
+
...(readContradiction === null ? {} : { readContradiction }),
|
|
1061
1193
|
});
|
|
1062
1194
|
}
|
|
1063
1195
|
catch (_error) {
|
|
@@ -1589,6 +1721,33 @@ function credentialExchangeBody(sent) {
|
|
|
1589
1721
|
return false;
|
|
1590
1722
|
return (CREDENTIAL_IDENTITY.test(text) || /"(?:credentials|grant_type|refresh_token)"\s*:/i.test(text));
|
|
1591
1723
|
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Whether a request PATH asks the app to mint a credential — a login, a registration, a token
|
|
1726
|
+
* endpoint — as opposed to a protected resource that presents one.
|
|
1727
|
+
*
|
|
1728
|
+
* This is the path half of {@link isCredentialExchange}, exported so a caller holding only a
|
|
1729
|
+
* `METHOD path` reads the same classification: a Hurl failing entry says
|
|
1730
|
+
* `POST /api/users/login` and nothing about the body, and the whole-suite note must not explain
|
|
1731
|
+
* a wrong-password login that answered 200 as a cookie-jar effect (xr-molecule-flash-m176).
|
|
1732
|
+
*
|
|
1733
|
+
* @param path - The request path (`/api/users/login`); a query or fragment is ignored.
|
|
1734
|
+
* @param authEndpoints - The endpoints the app's auth config names (`/users/login`).
|
|
1735
|
+
* @returns True when the path names a credential exchange.
|
|
1736
|
+
*/
|
|
1737
|
+
export function isCredentialExchangePath(path, authEndpoints = []) {
|
|
1738
|
+
const clean = (path.split(/[?#]/)[0] ?? '').replace(/\/+$/, '');
|
|
1739
|
+
if (clean === '')
|
|
1740
|
+
return false;
|
|
1741
|
+
// The app's own config names it: matched as a path suffix, because an auth config writes the
|
|
1742
|
+
// endpoint relative to its API base (`/users/login`) while the trace records the mounted path
|
|
1743
|
+
// (`/api/users/login`).
|
|
1744
|
+
if (authEndpoints.some((endpoint) => {
|
|
1745
|
+
const named = `/${String(endpoint).replace(/^\/+|\/+$/g, '')}`;
|
|
1746
|
+
return named.length > 1 && (clean === named || clean.endsWith(named));
|
|
1747
|
+
}))
|
|
1748
|
+
return true;
|
|
1749
|
+
return CREDENTIAL_EXCHANGE_PATH.test(clean);
|
|
1750
|
+
}
|
|
1592
1751
|
/**
|
|
1593
1752
|
* Whether a refused request is a CREDENTIAL EXCHANGE: the app was asked to mint a token (a login,
|
|
1594
1753
|
* a registration, a token endpoint), so it never needed one attached.
|
|
@@ -1613,15 +1772,7 @@ function isCredentialExchange(request, authEndpoints) {
|
|
|
1613
1772
|
const path = sameOriginPath(request);
|
|
1614
1773
|
if (path === null)
|
|
1615
1774
|
return false;
|
|
1616
|
-
|
|
1617
|
-
// endpoint relative to its API base (`/users/login`) while the trace records the mounted path
|
|
1618
|
-
// (`/api/users/login`).
|
|
1619
|
-
if (authEndpoints.some((endpoint) => {
|
|
1620
|
-
const named = `/${String(endpoint).replace(/^\/+|\/+$/g, '')}`;
|
|
1621
|
-
return named.length > 1 && (path === named || path.endsWith(named));
|
|
1622
|
-
}))
|
|
1623
|
-
return true;
|
|
1624
|
-
if (CREDENTIAL_EXCHANGE_PATH.test(path))
|
|
1775
|
+
if (isCredentialExchangePath(path, authEndpoints))
|
|
1625
1776
|
return true;
|
|
1626
1777
|
return credentialExchangeBody(request.sent);
|
|
1627
1778
|
}
|
|
@@ -2003,6 +2154,196 @@ function missingTextSentence(missing) {
|
|
|
2003
2154
|
`not render it — the page drops it before rendering (a slice/filter/limit in the component ` +
|
|
2004
2155
|
`that renders that list); find where that response is mapped.`);
|
|
2005
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* The `name` attribute a locator's selector reads (`textarea[name="bio"]` → `bio`), or null when
|
|
2159
|
+
* the selector names no field.
|
|
2160
|
+
*
|
|
2161
|
+
* Matching the form field to the request body's key is the whole point: a body that carries
|
|
2162
|
+
* `"bio": ""` says nothing unless the trace also says the test typed a bio.
|
|
2163
|
+
*
|
|
2164
|
+
* @param selector - The selector a `fill`/`type`/`selectOption` action carried.
|
|
2165
|
+
* @returns The field name, or null.
|
|
2166
|
+
*/
|
|
2167
|
+
function fieldNameOf(selector) {
|
|
2168
|
+
const quoted = /\[\s*name\s*=\s*(["'])((?:(?!\1).)*)\1\s*\]/.exec(selector);
|
|
2169
|
+
if (quoted !== null)
|
|
2170
|
+
return quoted[2] === '' ? null : quoted[2];
|
|
2171
|
+
const bare = /\[\s*name\s*=\s*([^\s\]]+)\s*\]/.exec(selector);
|
|
2172
|
+
return bare === null ? null : (bare[1] ?? null);
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Whether a JSON request body carries `key` with an EMPTY value (`""` or null), looking inside the
|
|
2176
|
+
* wrapper object an API nests its entity under (`user`, `data`, `props`).
|
|
2177
|
+
*
|
|
2178
|
+
* A body that does not parse, or carries the key with a real value, is not evidence: the note only
|
|
2179
|
+
* claims the page sent a field empty when it did.
|
|
2180
|
+
*
|
|
2181
|
+
* @param sent - The body the page sent, as the trace recorded it.
|
|
2182
|
+
* @param key - The field name from the filled input's `name` attribute.
|
|
2183
|
+
* @returns True when the body carries that key empty.
|
|
2184
|
+
*/
|
|
2185
|
+
function bodyCarriesEmptyField(sent, key) {
|
|
2186
|
+
let parsed;
|
|
2187
|
+
try {
|
|
2188
|
+
parsed = JSON.parse(sent);
|
|
2189
|
+
}
|
|
2190
|
+
catch (_error) {
|
|
2191
|
+
// A truncated body (the trace read bounds what it keeps) names no field confidently.
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
const read = (value, level) => {
|
|
2195
|
+
if (level > 4 || value === null || typeof value !== 'object')
|
|
2196
|
+
return null;
|
|
2197
|
+
if (Array.isArray(value)) {
|
|
2198
|
+
for (const item of value) {
|
|
2199
|
+
const found = read(item, level + 1);
|
|
2200
|
+
if (found !== null)
|
|
2201
|
+
return found;
|
|
2202
|
+
}
|
|
2203
|
+
return null;
|
|
2204
|
+
}
|
|
2205
|
+
const record = value;
|
|
2206
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) {
|
|
2207
|
+
return record[key] === '' || record[key] === null;
|
|
2208
|
+
}
|
|
2209
|
+
for (const nested of Object.values(record)) {
|
|
2210
|
+
const found = read(nested, level + 1);
|
|
2211
|
+
if (found !== null)
|
|
2212
|
+
return found;
|
|
2213
|
+
}
|
|
2214
|
+
return null;
|
|
2215
|
+
};
|
|
2216
|
+
return read(parsed, 0) === true;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* The field a test typed into that the same page submitted empty, when the trace recorded both.
|
|
2220
|
+
*
|
|
2221
|
+
* This is the Settings race of xr-molecule-flash-m173: `page.fill('textarea[name="bio"]', newBio)`
|
|
2222
|
+
* then `button[type=submit]`, and the `PUT /api/user` carried `"bio": ""`. The form's seeding
|
|
2223
|
+
* effect re-ran between the fill and the submit — on the deployed URL the background session
|
|
2224
|
+
* verification lands later than it does locally, and it handed the form a new user object. The
|
|
2225
|
+
* reading only pairs facts the trace holds: the filled value (its `selector`/`value`), the field
|
|
2226
|
+
* name the selector names, and the next call to the app's API on that page whose body carries
|
|
2227
|
+
* that key empty.
|
|
2228
|
+
*
|
|
2229
|
+
* @param item - One traced failure.
|
|
2230
|
+
* @returns The evidence, or null when no filled field was submitted empty.
|
|
2231
|
+
*/
|
|
2232
|
+
function formResetOf(item) {
|
|
2233
|
+
const requests = [...(item.lastAction?.requests ?? []), ...item.requests];
|
|
2234
|
+
// A trace read before `actions` existed (or a hand-built one) carries none: no claim.
|
|
2235
|
+
for (const action of item.actions ?? []) {
|
|
2236
|
+
if (action.selector === undefined || action.value === undefined || action.value === '')
|
|
2237
|
+
continue;
|
|
2238
|
+
const field = fieldNameOf(action.selector);
|
|
2239
|
+
if (field === null)
|
|
2240
|
+
continue;
|
|
2241
|
+
let next = null;
|
|
2242
|
+
for (const request of requests) {
|
|
2243
|
+
if (!/^(?:POST|PUT|PATCH)$/.test(request.method) || request.sent === undefined)
|
|
2244
|
+
continue;
|
|
2245
|
+
// The submit follows the typing; a request that started before it cannot have sent it.
|
|
2246
|
+
if (typeof request.startedAt === 'number' && request.startedAt < action.start)
|
|
2247
|
+
continue;
|
|
2248
|
+
// Same page only: another context's set-up call is not this form's submit.
|
|
2249
|
+
if (action.pageId !== undefined &&
|
|
2250
|
+
request.pageId !== undefined &&
|
|
2251
|
+
action.pageId !== request.pageId)
|
|
2252
|
+
continue;
|
|
2253
|
+
if (!bodyCarriesEmptyField(request.sent, field))
|
|
2254
|
+
continue;
|
|
2255
|
+
const earlier = next === null ||
|
|
2256
|
+
(typeof request.startedAt === 'number' &&
|
|
2257
|
+
(typeof next.startedAt !== 'number' || request.startedAt < next.startedAt));
|
|
2258
|
+
if (earlier)
|
|
2259
|
+
next = request;
|
|
2260
|
+
}
|
|
2261
|
+
if (next !== null)
|
|
2262
|
+
return { value: action.value, selector: action.selector, field, request: next };
|
|
2263
|
+
}
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* The sentence for a submitted form field the test had just typed into, sent empty.
|
|
2268
|
+
*
|
|
2269
|
+
* It states the two recorded facts and the one inference they support — the form re-seeded after
|
|
2270
|
+
* the typing — then the fix at the source: copy the loaded data into the form once, on its first
|
|
2271
|
+
* load, instead of on every change of the data it was seeded from.
|
|
2272
|
+
*
|
|
2273
|
+
* @param reset - The filled field and the submit that carried it empty.
|
|
2274
|
+
* @returns The sentence the note leads with.
|
|
2275
|
+
*/
|
|
2276
|
+
function formResetSentence(reset) {
|
|
2277
|
+
return (`The test typed "${reset.value}" into ${reset.selector}, but ` +
|
|
2278
|
+
`${requestLabel(reset.request)} sent "${reset.field}": "" — the form's state was reset after ` +
|
|
2279
|
+
`typing (an effect re-seeding the form from data that changed identity ran again after the ` +
|
|
2280
|
+
`test typed). Seed the form once, not every time that data changes.`);
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* The sentence for a write whose answer a later GET of the same resource contradicted.
|
|
2284
|
+
*
|
|
2285
|
+
* It states the two recorded facts and the one inference they support — the read route never saw
|
|
2286
|
+
* the viewer — then the place to look: the route's middleware, not the page. It names both
|
|
2287
|
+
* possibilities the trace cannot separate (a missing auth/optional-auth middleware on that route,
|
|
2288
|
+
* or a different user), so the run checks the route instead of rewriting the component.
|
|
2289
|
+
*
|
|
2290
|
+
* @param contradiction - The write's answer and the later read's opposite one.
|
|
2291
|
+
* @returns The sentence the note leads with.
|
|
2292
|
+
*/
|
|
2293
|
+
function readContradictionSentence(contradiction) {
|
|
2294
|
+
return (`The server answered \`${contradiction.field}\`: ${contradiction.writeValue} to ` +
|
|
2295
|
+
`${requestLabel(contradiction.write)} and \`${contradiction.field}\`: ` +
|
|
2296
|
+
`${contradiction.readValue} to ${requestLabel(contradiction.read)} — the read route does not ` +
|
|
2297
|
+
`see the viewer (a missing auth/optional-auth middleware on that route, or a different user); ` +
|
|
2298
|
+
`check the route's middleware, not the page.`);
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Chromium's webfont decode failure: the URL it fetched, and OTS's reason.
|
|
2302
|
+
*
|
|
2303
|
+
* A page that serves its SPA fallback for a missing `.woff` produces exactly this line — the
|
|
2304
|
+
* browser downloads `index.html` and OTS rejects the first four bytes.
|
|
2305
|
+
*/
|
|
2306
|
+
const FONT_DECODE_FAILURE = /Failed to decode downloaded font:\s*(\S+)\s*\|\s*OTS parsing error:\s*(.+?)\s*$/i;
|
|
2307
|
+
/**
|
|
2308
|
+
* Whether an OTS `invalid sfntVersion: <n>` number is four ASCII bytes that start an HTML
|
|
2309
|
+
* document (`<!do` — `0x3C21646F` is the doctype's first four bytes).
|
|
2310
|
+
*
|
|
2311
|
+
* @param reason - The OTS reason text after `OTS parsing error:`.
|
|
2312
|
+
* @returns True when the codec read an HTML signature instead of a font signature.
|
|
2313
|
+
*/
|
|
2314
|
+
function sfntVersionIsHtml(reason) {
|
|
2315
|
+
const raw = /invalid sfntVersion:\s*(\d+)/i.exec(reason)?.[1];
|
|
2316
|
+
if (raw === undefined)
|
|
2317
|
+
return false;
|
|
2318
|
+
const value = Number(raw);
|
|
2319
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff)
|
|
2320
|
+
return false;
|
|
2321
|
+
const magic = String.fromCharCode((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff);
|
|
2322
|
+
return magic.startsWith('<');
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* One line of a failure row's `console:` list, with the webfont-decode signature explained.
|
|
2326
|
+
*
|
|
2327
|
+
* `Failed to decode downloaded font: <url> | OTS parsing error: invalid sfntVersion: 1008821359`
|
|
2328
|
+
* is not a font to debug: the URL answered HTML (the app's `index.html`, the SPA fallback), so
|
|
2329
|
+
* the file the `@font-face` names is not there. xr-molecule-flash-m186's page fetched
|
|
2330
|
+
* `/fonts/ionicons.woff`, got the index document, and the note printed the raw decode error
|
|
2331
|
+
* while the model went looking elsewhere.
|
|
2332
|
+
*
|
|
2333
|
+
* @param text - The console line, as the trace recorded it.
|
|
2334
|
+
* @returns The line to print, naming the URL that served HTML when it is that failure.
|
|
2335
|
+
*/
|
|
2336
|
+
function consoleLine(text) {
|
|
2337
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
2338
|
+
const failure = FONT_DECODE_FAILURE.exec(normalized);
|
|
2339
|
+
if (!failure)
|
|
2340
|
+
return normalized;
|
|
2341
|
+
const [, url, reason] = failure;
|
|
2342
|
+
if (!sfntVersionIsHtml(reason))
|
|
2343
|
+
return normalized;
|
|
2344
|
+
return (`${url} answered the app's index.html, not a font — the file is missing from ` +
|
|
2345
|
+
`\`app/public/\` (or the asset import); copy/import it`);
|
|
2346
|
+
}
|
|
2006
2347
|
/**
|
|
2007
2348
|
* The note a failed suite run's result carries: what each failing test's page actually sent, and
|
|
2008
2349
|
* what came back.
|
|
@@ -2042,6 +2383,20 @@ function missingTextSentence(missing) {
|
|
|
2042
2383
|
* context and the assertion's page cannot be identified, the note lists the requests and makes NO
|
|
2043
2384
|
* causal claim — no "the test's last action", no "the page stayed at …" — instead of merging them.
|
|
2044
2385
|
*
|
|
2386
|
+
* A field the test filled and the page submitted EMPTY ({@link formResetOf}) leads the note: the
|
|
2387
|
+
* trace holds both facts — the value typed into `textarea[name="bio"]` and the `PUT /api/user`
|
|
2388
|
+
* whose body carried `"bio": ""` — so the form's state was reset between the typing and the
|
|
2389
|
+
* submit. The note names the effect that re-seeds a form from data whose identity changed and the
|
|
2390
|
+
* fix at the source (seed the form once). xr-molecule-flash-m173 lost a deploy-and-walk cycle to
|
|
2391
|
+
* this on the deployed URL only, where the background session verification lands later than it
|
|
2392
|
+
* does locally.
|
|
2393
|
+
*
|
|
2394
|
+
* A write whose response said a boolean field is true and a LATER GET of the same resource that
|
|
2395
|
+
* said it is false leads the note first ({@link readContradictionSentence}): the read route never
|
|
2396
|
+
* saw the viewer, so the fix is on that route's middleware, not in the page. xr-molecule-flash-m181
|
|
2397
|
+
* spent ~8 minutes rewriting the profile component over exactly that pair — the write answered
|
|
2398
|
+
* `{"following":true}` and the next profile GET answered `false`.
|
|
2399
|
+
*
|
|
2045
2400
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
2046
2401
|
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
2047
2402
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
@@ -2304,7 +2659,7 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
2304
2659
|
if (item.console.length > 0)
|
|
2305
2660
|
parts.push(`console: ${item.console
|
|
2306
2661
|
.slice(0, 2)
|
|
2307
|
-
.map((text) => text
|
|
2662
|
+
.map((text) => consoleLine(text).slice(0, 160))
|
|
2308
2663
|
.join(' | ')}`);
|
|
2309
2664
|
// A request from another browser context the test opened is not the failing action's effect:
|
|
2310
2665
|
// it is set-up work on a different page, and it is named as such rather than merged into the
|
|
@@ -2402,7 +2757,38 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
2402
2757
|
const missingTextLead = missingTexts.length === 0
|
|
2403
2758
|
? ''
|
|
2404
2759
|
: `${missingTexts.slice(0, 2).map(missingTextSentence).join(' ')}\n\n`;
|
|
2405
|
-
|
|
2760
|
+
// A write whose answer a later GET of the same resource contradicted: the read route does not
|
|
2761
|
+
// see the viewer (m181's `following` — the write answered true, the next profile GET answered
|
|
2762
|
+
// false). It reads as a frontend race and is not one, so it leads the note before any row and
|
|
2763
|
+
// before the form-reset reading.
|
|
2764
|
+
const readContradictions = [
|
|
2765
|
+
...new Map(traced
|
|
2766
|
+
.filter((item) => item.readContradiction !== undefined)
|
|
2767
|
+
.map((item) => {
|
|
2768
|
+
const contradiction = item.readContradiction;
|
|
2769
|
+
return [
|
|
2770
|
+
`${contradiction.field}\u0000${contradiction.write.method} ${contradiction.write.path}`,
|
|
2771
|
+
contradiction,
|
|
2772
|
+
];
|
|
2773
|
+
})).values(),
|
|
2774
|
+
];
|
|
2775
|
+
const readContradictionLead = readContradictions.length === 0
|
|
2776
|
+
? ''
|
|
2777
|
+
: `${readContradictions.slice(0, 2).map(readContradictionSentence).join(' ')}\n\n`;
|
|
2778
|
+
// A field the test typed and the page submitted empty: the form re-seeded itself after the
|
|
2779
|
+
// typing (m173's Settings race — the deployed URL lands the background session verification
|
|
2780
|
+
// later than a local run, and its new user object re-ran the form's seeding effect between the
|
|
2781
|
+
// fill and the submit). It is a direct cause of the failing request, so it leads the note.
|
|
2782
|
+
const formResets = [
|
|
2783
|
+
...new Map(traced
|
|
2784
|
+
.map(formResetOf)
|
|
2785
|
+
.filter((reset) => reset !== null)
|
|
2786
|
+
.map((reset) => [`${reset.request.method} ${reset.request.path}\u0000${reset.field}`, reset])).values(),
|
|
2787
|
+
];
|
|
2788
|
+
const formResetLead = formResets.length === 0 ? '' : `${formResets.slice(0, 2).map(formResetSentence).join(' ')}\n\n`;
|
|
2789
|
+
return (readContradictionLead +
|
|
2790
|
+
formResetLead +
|
|
2791
|
+
missingTextLead +
|
|
2406
2792
|
transportLead +
|
|
2407
2793
|
setupOriginLead +
|
|
2408
2794
|
stalledLead +
|
package/dist/types.d.ts
CHANGED
|
@@ -164,6 +164,25 @@ export interface ToolBuildConfig {
|
|
|
164
164
|
* @returns The quoted titles the request carves out.
|
|
165
165
|
*/
|
|
166
166
|
excludedTestTitles?: () => readonly string[];
|
|
167
|
+
/**
|
|
168
|
+
* The `NAME=value` settings the person's request REQUIRES of a suite run (molecule-dev's
|
|
169
|
+
* `requiredRunSettings` — its mode, say), asked when the cause note composes a narrowed re-run
|
|
170
|
+
* of a finished run's failures. A re-run composed from the executor's own command inherits what
|
|
171
|
+
* that command set — and nothing it left out, so a run that measured the suite in `spa` handed
|
|
172
|
+
* back a re-run that would measure `spa` again (xr-molecule-flash-m182 +12:06: the request names
|
|
173
|
+
* `TEST_MODE=fullstack`, the run and its re-run both omitted it).
|
|
174
|
+
*
|
|
175
|
+
* A getter, not an array: the request is read per turn while the tool set is built once, and
|
|
176
|
+
* what the suite's own files read is the consumer's reading. Unset answers "nothing required".
|
|
177
|
+
*
|
|
178
|
+
* @param command - The command the finished run was started with.
|
|
179
|
+
* @param cwd - The resolved working directory it ran in.
|
|
180
|
+
* @returns The settings to put on a re-run of that command.
|
|
181
|
+
*/
|
|
182
|
+
requiredSuiteEnv?: (command: string, cwd: string) => Promise<readonly {
|
|
183
|
+
name: string;
|
|
184
|
+
value: string;
|
|
185
|
+
}[] | null | undefined>;
|
|
167
186
|
/**
|
|
168
187
|
* The supplied suite's own budget, when the Playwright config a command would load sets
|
|
169
188
|
* different settings, asked BEFORE the run executes. The consumer knows where the suite and its
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEzB,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEvC,qEAAqE;IACrE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvD,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvC,mCAAmC;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC,CAAA;IAEnF;;;OAGG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GACA,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,iFAAiF;IACjF,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,2FAA2F;IAC3F,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAE1E;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAE3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAE7B;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;IAE5C;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAE1F,uGAAuG;IACvG,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAE9C,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAE3C,wFAAwF;IACxF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAA;IACxC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IAEjB,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IAEnB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,EAAE,CAAA;IAEf,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAA;IAE/B,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEzB,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEvC,qEAAqE;IACrE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvD,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvC,mCAAmC;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAC,CAAA;IAEnF;;;OAGG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GACA,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAA;IACf;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,iFAAiF;IACjF,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB,2FAA2F;IAC3F,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAE1E;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAE3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAE7B;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAA;IAEhC;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;IAE5C;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,KACR,OAAO,CAAC,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAE3E;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAE1F,uGAAuG;IACvG,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAE9C,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAE3C,wFAAwF;IACxF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAA;IACxC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IAEjB,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IAEnB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,EAAE,CAAA;IAEf,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAA;IAE/B,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B"}
|
package/dist/utilities.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { type RunFailureKind } from './failure-kind.js';
|
|
8
|
-
import { type DiagnosisRunContext } from './suite-overlap.js';
|
|
8
|
+
import { type DiagnosisRunContext, type RunEnvSetting } from './suite-overlap.js';
|
|
9
9
|
/**
|
|
10
10
|
* Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
|
|
11
11
|
* single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
|
|
@@ -171,15 +171,19 @@ export declare const PLAYWRIGHT_FAILURES_INLINE_ENV = "PLAYWRIGHT_LIST_PRINT_FAI
|
|
|
171
171
|
* is exactly when the executor reads the log, and without it a running log carries `✘` rows and
|
|
172
172
|
* nothing else (xr-molecule-flash-m99 spent ~90 s on greps of one, then fixed what it guessed).
|
|
173
173
|
*
|
|
174
|
-
* Only the
|
|
175
|
-
* something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
176
|
-
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
|
|
174
|
+
* Only the step that runs the suite is touched, and only its reporter, and only when that step
|
|
175
|
+
* runs something: `npx playwright test --list` takes two seconds and writes nothing per test, so
|
|
176
|
+
* neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}). A trailing
|
|
177
|
+
* status step (`; echo EXIT $?`, `&& true`) is not the run and is skipped when the run step is
|
|
178
|
+
* found ({@link isStatusOnlyStep}); a run whose output is redirected to a file is still readable
|
|
179
|
+
* through that file, which the launch records as the task's own output.
|
|
177
180
|
*
|
|
178
181
|
* A command that starts the suite MORE THAN ONCE is not launched at all: every run it carries is
|
|
179
182
|
* a whole walk of the same suite against the same app, and only the last step is rewritten, so the
|
|
180
183
|
* earlier walks run unmeasured for nothing. X1 xr-molecule-flash-m157 (+14:02) chained two whole
|
|
181
184
|
* walks of one 138-test suite with `;` — 4.5 minutes each — and the second measured nothing the
|
|
182
|
-
* first had not. The platform
|
|
185
|
+
* first had not. The platform refuses a second CONCURRENT run against one app and database by the
|
|
186
|
+
* ONE rule ({@link suiteRunInTheWay}) — for every pair, whatever uid a run mints — and this is the
|
|
183
187
|
* same hazard inside one command ({@link suiteRunStatements}). `error` then carries the refusal,
|
|
184
188
|
* naming each suite step and the ONE command to send instead (the last suite step, rewritten as
|
|
185
189
|
* this function rewrites a run), and nothing is launched.
|
|
@@ -944,10 +948,21 @@ export interface UniversalCause {
|
|
|
944
948
|
}
|
|
945
949
|
/**
|
|
946
950
|
* How many tests a run must have walked — with nothing passing since its first failure — before
|
|
947
|
-
* "the page renders nothing" can be its one cause.
|
|
948
|
-
*
|
|
949
|
-
|
|
950
|
-
|
|
951
|
+
* "the page renders nothing" can be its one cause.
|
|
952
|
+
*
|
|
953
|
+
* THREE, and the number is the cost of the alternative. The old floor of 15 was chosen when the
|
|
954
|
+
* reading was only offered at a completed run; once it STOPS the walk, every test waited for is
|
|
955
|
+
* a test whose result is thrown away, and a blank page costs its per-test budget each time
|
|
956
|
+
* (m177: 18 tests, ~4.2 minutes, on a page whose thrown error was visible in the first
|
|
957
|
+
* `pageerror`). Three is the smallest count that is not one page's own bug: the cause additionally
|
|
958
|
+
* needs three DISTINCT spec files (`UNIVERSAL_CAUSE_FILES`, or the files a narrowed run selected),
|
|
959
|
+
* each spec waiting for its OWN first element, and no test passing after the first failure. A page
|
|
960
|
+
* that renders misses elements, but three unrelated specs in three files failing at three
|
|
961
|
+
* different first locators with nothing passing in between is the page, not three bugs. At the
|
|
962
|
+
* suite's own 5 s action timeout that verdict lands ~20 s into the walk, so the early stop costs
|
|
963
|
+
* about one test's worth of time instead of fifteen.
|
|
964
|
+
*/
|
|
965
|
+
export declare const EMPTY_PAGE_CAUSE_TESTS = 3;
|
|
951
966
|
/**
|
|
952
967
|
* How many tests a connection-class cause must cover before the run is stopped even when they
|
|
953
968
|
* are all in one file — see {@link universalCause}.
|
|
@@ -1154,8 +1169,8 @@ export declare function failuresByCause(log: string, excluded?: readonly string[
|
|
|
1154
1169
|
* @param log - The finished run's output.
|
|
1155
1170
|
* @param excluded - The test titles the request carves out, if the caller knows them.
|
|
1156
1171
|
* @param run - How the run was started, when the caller knows it: the narrowed re-run is built
|
|
1157
|
-
* from `command`/`cwd`/`config`, and `log` (the path the output was written to) is what
|
|
1158
|
-
* "left out" sentence greps when more causes exist than the result has room for.
|
|
1172
|
+
* from `command`/`cwd`/`config`/`env`, and `log` (the path the output was written to) is what
|
|
1173
|
+
* the "left out" sentence greps when more causes exist than the result has room for.
|
|
1159
1174
|
* @returns The note, or null.
|
|
1160
1175
|
*/
|
|
1161
1176
|
export declare function failureCausesNote(log: string, excluded?: readonly string[], run?: {
|
|
@@ -1163,6 +1178,7 @@ export declare function failureCausesNote(log: string, excluded?: readonly strin
|
|
|
1163
1178
|
cwd?: string;
|
|
1164
1179
|
log?: string;
|
|
1165
1180
|
config?: string | null;
|
|
1181
|
+
env?: readonly RunEnvSetting[];
|
|
1166
1182
|
}, pageErrors?: readonly CausePageErrors[]): string | null;
|
|
1167
1183
|
/**
|
|
1168
1184
|
* The command that re-runs the tests at `locations` the way the run IN FLIGHT ran them: the same
|
|
@@ -1177,7 +1193,9 @@ export declare function failureCausesNote(log: string, excluded?: readonly strin
|
|
|
1177
1193
|
* refused while the run walks ({@link suiteRunInTheWay}).
|
|
1178
1194
|
*
|
|
1179
1195
|
* @param run - The run in flight: the command it was started with, as it was run, the directory
|
|
1180
|
-
* that command ran in,
|
|
1196
|
+
* that command ran in, the config it loaded when the caller knows it, and the settings the
|
|
1197
|
+
* request required and the run left out (`env`) — the re-run carries them, so it never
|
|
1198
|
+
* reproduces the configuration the run was measured under by mistake (m182).
|
|
1181
1199
|
* @param locations - The failing `file:line`s to re-run, as the run's log printed them.
|
|
1182
1200
|
* @param excluded - The test titles the request carves out: the re-run inverts them out
|
|
1183
1201
|
* (`--grep-invert`) so it cannot report them again.
|
|
@@ -1187,6 +1205,7 @@ export declare function rerunOfRunningSuite(run: {
|
|
|
1187
1205
|
command: string;
|
|
1188
1206
|
cwd?: string;
|
|
1189
1207
|
config?: string | null;
|
|
1208
|
+
env?: readonly RunEnvSetting[];
|
|
1190
1209
|
}, locations: readonly string[], excluded?: readonly string[]): string | null;
|
|
1191
1210
|
/**
|
|
1192
1211
|
* The sentence that names a cause repeating among a running suite's failures, mid-run.
|
|
@@ -1233,6 +1252,7 @@ export declare function repeatingFailureNote(found: RepeatingFailure, o: {
|
|
|
1233
1252
|
run?: {
|
|
1234
1253
|
command: string;
|
|
1235
1254
|
cwd?: string;
|
|
1255
|
+
env?: readonly RunEnvSetting[];
|
|
1236
1256
|
};
|
|
1237
1257
|
editsInvalidateRun?: boolean;
|
|
1238
1258
|
}): string;
|