@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.
@@ -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: entry.class + '.' + entry.method,
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
- // The app's own config names it: matched as a path suffix, because an auth config writes the
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.replace(/\s+/g, ' ').trim().slice(0, 160))
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
- return (missingTextLead +
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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -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 command's last step is touched, and only its reporter, and only when that step runs
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 already refuses a second CONCURRENT run against one app; this is the
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. A handful of failures is a handful of bugs;
948
- * a page that never painted fails every spec that reaches it, and m141's walk had 36 by +20:30.
949
- */
950
- export declare const EMPTY_PAGE_CAUSE_TESTS = 15;
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 the
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, and the config it loaded when the caller knows it.
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;