@molecule/api-ai-tools 1.9.16 → 1.9.19

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/dist/utilities.js CHANGED
@@ -350,6 +350,25 @@ const PRINTING_REPORTERS = new Set(['list', 'line', 'dot', 'basic', 'spec']);
350
350
  * edited `app/src` while the run was still walking — which voids the run.
351
351
  */
352
352
  export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE';
353
+ /**
354
+ * Whether a command step only reports or discards the status of the steps before it: `echo EXIT
355
+ * $?`, `echo $?`, `printf …`, `true`, `exit 0`.
356
+ *
357
+ * The shape models and the platform's own re-run notes send routinely ends on one of these —
358
+ * `npx playwright test … > /tmp/pw.log 2>&1; echo EXIT $?` — and reading the split's last part as
359
+ * the run withheld the reporter, the inline-failure switch and `--retries=0` from every one of
360
+ * them: the log then carried `✘` rows and nothing else while the wait note promised each failure's
361
+ * error under them (xr-molecule-flash-m173 +17:07, +39:14). Whitespace and a group's closing
362
+ * punctuation are not part of the step; {@link streamingBackgroundCommand} strips those before
363
+ * asking, and strips them here too for the split it is given.
364
+ *
365
+ * @param step - One statement of the command, as the `&&`/`||`/`;` split left it.
366
+ * @returns True when the step runs nothing but a status report.
367
+ */
368
+ function isStatusOnlyStep(step) {
369
+ const text = step.replace(/[\s)}]+$/, '').trim();
370
+ return /^(?:echo|printf)\b/.test(text) || /^true$/.test(text) || /^exit(?:\s+\S+)?$/.test(text);
371
+ }
353
372
  /**
354
373
  * Rewrite a command about to run in the background so its log grows while it
355
374
  * runs. Two shapes left the log empty until exit, and an empty log is what
@@ -384,24 +403,32 @@ export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_IN
384
403
  * is exactly when the executor reads the log, and without it a running log carries `✘` rows and
385
404
  * nothing else (xr-molecule-flash-m99 spent ~90 s on greps of one, then fixed what it guessed).
386
405
  *
387
- * Only the command's last step is touched, and only its reporter, and only when that step runs
388
- * something: `npx playwright test --list` takes two seconds and writes nothing per test, so
389
- * neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}).
406
+ * Only the step that runs the suite is touched, and only its reporter, and only when that step
407
+ * runs something: `npx playwright test --list` takes two seconds and writes nothing per test, so
408
+ * neither reporter nor `--retries=0` is added to it ({@link isPlaywrightRunCommand}). A trailing
409
+ * status step (`; echo EXIT $?`, `&& true`) is not the run and is skipped when the run step is
410
+ * found ({@link isStatusOnlyStep}); a run whose output is redirected to a file is still readable
411
+ * through that file, which the launch records as the task's own output.
390
412
  *
391
413
  * A command that starts the suite MORE THAN ONCE is not launched at all: every run it carries is
392
414
  * a whole walk of the same suite against the same app, and only the last step is rewritten, so the
393
415
  * earlier walks run unmeasured for nothing. X1 xr-molecule-flash-m157 (+14:02) chained two whole
394
416
  * walks of one 138-test suite with `;` — 4.5 minutes each — and the second measured nothing the
395
- * first had not. The platform already refuses a second CONCURRENT run against one app; this is the
417
+ * first had not. The platform refuses a second CONCURRENT run against one app and database by the
418
+ * ONE rule ({@link suiteRunInTheWay}) — for every pair, whatever uid a run mints — and this is the
396
419
  * same hazard inside one command ({@link suiteRunStatements}). `error` then carries the refusal,
397
420
  * naming each suite step and the ONE command to send instead (the last suite step, rewritten as
398
421
  * this function rewrites a run), and nothing is launched.
399
422
  *
400
423
  * @param command - The command as the executor sent it.
424
+ * @param context - What the caller knows beyond the command: the tests the person's request
425
+ * excludes by name ({@link DiagnosisRunContext}). A `--grep-invert` of exactly those is the
426
+ * request's own scope, not a diagnosis, so it keeps the suite's retries on a deployed proof
427
+ * (xr-molecule-flash-m170 +33:16).
401
428
  * @returns The command to run and one sentence per change made (empty when none), or `error` when
402
429
  * the command starts the suite more than once.
403
430
  */
404
- export function streamingBackgroundCommand(command) {
431
+ export function streamingBackgroundCommand(command, context) {
405
432
  // A command that runs no test is not a run to keep a growing log for: nothing about it is
406
433
  // rewritten (flash-m79's `--list` was read as a whole-suite run end to end, and got a ~4 KB
407
434
  // refusal to send itself again with `--max-failures=15`).
@@ -442,10 +469,15 @@ export function streamingBackgroundCommand(command) {
442
469
  }
443
470
  // The last step that RUNS something. A trailing group closer is punctuation, not a step: a
444
471
  // brace group writes it after a `;` (`{ …; }`), so reading the split's last part literally
445
- // made `}` the step and withheld every rewrite from a run inside a group.
472
+ // made `}` the step and withheld every rewrite from a run inside a group. A trailing status
473
+ // step is not the run either: `… > /tmp/pw.log 2>&1; echo EXIT $?` ends on the echo, and
474
+ // treating THAT as the run withheld the reporter, the inline-failure switch and `--retries=0`
475
+ // from the exact shape models and the platform's re-run notes send (xr-molecule-flash-m173
476
+ // +17:07, +39:14).
446
477
  const parts = out.split(/&&|\|\||;/);
447
478
  let stepIndex = parts.length - 1;
448
- while (stepIndex > 0 && /^[\s)}]*$/.test(parts[stepIndex] ?? ''))
479
+ while (stepIndex > 0 &&
480
+ (/^[\s)}]*$/.test(parts[stepIndex] ?? '') || isStatusOnlyStep(parts[stepIndex] ?? '')))
449
481
  stepIndex -= 1;
450
482
  const rawLastStep = parts[stepIndex] ?? '';
451
483
  // The step's own text, with the punctuation that closes the group it sits in taken off — the
@@ -455,34 +487,28 @@ export function streamingBackgroundCommand(command) {
455
487
  // A command that starts the suite more than once is not launched: each statement is a whole walk
456
488
  // of the same suite against the same app, only the last of them is rewritten, and the earlier
457
489
  // ones measure nothing the first did. X1 xr-molecule-flash-m157 (+14:02) chained two whole walks
458
- // of one 138-test suite with `;` (4.5 minutes each); the platform already refuses a second
459
- // CONCURRENT run against one app, and this is the same hazard inside one command.
460
- const suiteSteps = suiteRunStatements(out);
461
- if (suiteSteps.length > 1) {
462
- // The ONE run to send instead: the last step that runs the suite, rewritten exactly as this
463
- // function rewrites a run (its own count is one — guarded, so this cannot recurse).
464
- const lastSuitePart = [...parts]
465
- .reverse()
466
- .map((p) => p.trim())
467
- .find((p) => isSuiteRunCommand(p));
468
- const single = lastSuitePart && suiteRunStatements(lastSuitePart).length === 1
469
- ? streamingBackgroundCommand(lastSuitePart).command
470
- : (lastSuitePart ?? '');
471
- return {
472
- command: out,
473
- changes: [],
474
- error: `This command runs the suite ${suiteSteps.length === 2 ? 'twice' : `${suiteSteps.length} times`} ` +
475
- `(${suiteSteps.map((s, i) => `step ${i + 1}: \`${s}\``).join(', ')}). One walk is the ` +
476
- `measurement; run exactly one: \`${single}\``,
477
- };
478
- }
490
+ // of one 138-test suite with `;` (4.5 minutes each); the platform refuses a second CONCURRENT
491
+ // run against one app and database by the ONE rule ({@link suiteRunInTheWay}) — for every pair,
492
+ // whatever uid a run mints — and this is the same hazard inside one command. The reading is
493
+ // {@link repeatedSuiteRunError}'s, so a foreground `exec_command` refuses the same commands.
494
+ const repeated = repeatedSuiteRunError(out);
495
+ if (repeated)
496
+ return { command: out, changes: [], error: repeated };
479
497
  // The last step runs Playwright — whether or not anything is piped from it. Keying this on
480
498
  // "nothing is piped out of the last step" withheld the reporter, the inline-failure switch and
481
499
  // `--retries=0` from exactly the runs whose output was filtered (xr-molecule-flash-m101
482
500
  // +25:54); what the runner writes into its log does not depend on what reads the pipe. A
483
- // REDIRECT still sends the output somewhere this tool cannot read it (`> out.txt`); `2>&1` only
484
- // joins stderr to it.
485
- const finalPlaywrightRun = isPlaywrightRunCommand(lastStep) && !/>/.test(lastStep.replace(/\d?>&\d/g, ''));
501
+ // REDIRECT used to withhold them too (`> out.txt`), but a redirect to a FILE is readable: the
502
+ // launch records that file as the task's own output (`outputFileByTask`, resolved from this
503
+ // same reader) and every wait reads it back. `> /tmp/pw-full.log 2>&1; echo EXIT $?` is the
504
+ // shape models and the platform's re-run notes send, so a run whose own redirect target is the
505
+ // one the launch will read is a readable run (xr-molecule-flash-m173 +17:07, +39:14). A target
506
+ // that is not a file (`/dev/null`, `2>&1`) still leaves the output unreadable.
507
+ const runRedirect = outputRedirectTarget(lastStep);
508
+ const finalPlaywrightRun = isPlaywrightRunCommand(lastStep) &&
509
+ (runRedirect === null
510
+ ? !/>/.test(lastStep.replace(/\d?>&\d/g, ''))
511
+ : runRedirect === outputRedirectTarget(out));
486
512
  /**
487
513
  * Add a word to the suite runner's own arguments; false when the runner could not be placed.
488
514
  *
@@ -494,14 +520,17 @@ export function streamingBackgroundCommand(command) {
494
520
  * platform's runner shape (xr-molecule-flash-m129 +34:29); appending after a pipe hands the
495
521
  * word to the filter (m102's dangling `--trace` flag, in the same shape).
496
522
  *
497
- * A suite the parse could not place — one inside a nested `sh -c '<command>'` string, whose
498
- * offsets are positions in THAT string, or a runner that is not the command's last step — is
499
- * left exactly as written and the caller reports no rewrite. There is nowhere in this command
500
- * the word belongs, and guessing at the text is what produced both failures above.
523
+ * The suite's own segment is found wherever it sits ({@link lastSuiteRun}), because a trailing
524
+ * status step is not another command's output: in `… > /tmp/pw.log 2>&1; echo EXIT $?` the run
525
+ * is what the launcher measures, and the echo only repeats its status
526
+ * ({@link isStatusOnlyStep}). A suite the parse could not place — one inside a nested
527
+ * `sh -c '<command>'` string, whose offsets are positions in THAT string — is left exactly as
528
+ * written and the caller reports no rewrite. There is nowhere in this command the word
529
+ * belongs, and guessing at the text is what produced both failures above.
501
530
  */
502
531
  const addToRun = (flag) => {
503
- const pipeline = lastSuitePipeline(out);
504
- const runner = pipeline ? pipeline.located[pipeline.at] : undefined;
532
+ const run = lastSuiteRun(out);
533
+ const runner = run ? run.located[run.at] : undefined;
505
534
  if (!runner)
506
535
  return false;
507
536
  const at = statementArgumentEnd(out, runner.start, runner.end);
@@ -522,7 +551,7 @@ export function streamingBackgroundCommand(command) {
522
551
  // {@link isDiagnosisRun}'s answer and nobody else's: the platform's notes ask the same
523
552
  // function, so a run cannot be a diagnosis to one of them and the whole suite to the other.
524
553
  // Which targets are local is {@link suiteCommandTarget}'s answer.
525
- const diagnosis = isDiagnosisRun(lastStep);
554
+ const diagnosis = isDiagnosisRun(lastStep, context);
526
555
  const localTarget = suiteCommandTarget(lastStep) === 'local';
527
556
  // The last step's reporters: one printer, and it is `list`. Every branch below only ever moves
528
557
  // the command TO that shape — a reporter set is never left with two printers in it.
@@ -590,8 +619,14 @@ export function streamingBackgroundCommand(command) {
590
619
  'and you need not wait for it to end to start on them.');
591
620
  }
592
621
  if (finalPlaywrightRun && (diagnosis || localTarget) && !/--retries\b/.test(lastStep)) {
622
+ // The reason named is the one that fired: a narrowed diagnosis against a DEPLOYED URL is not a
623
+ // local run, and calling it one (xr-molecule-flash-m170's note did) reads as the platform having
624
+ // misread the target.
625
+ const why = diagnosis
626
+ ? 'a diagnosis or re-run walks each failure once; the suite’s retries apply on the deployed proof run'
627
+ : 'a local run walks each failure once; the suite’s retries apply on the deployed proof run';
593
628
  if (addToRun('--retries=0'))
594
- changes.push('Added `--retries=0`: a local run walks each failure once; the suite’s retries apply on the deployed proof run.');
629
+ changes.push(`Added \`--retries=0\`: ${why}.`);
595
630
  }
596
631
  return { command: out, changes };
597
632
  }
@@ -613,6 +648,15 @@ const SUITE_RUN_COMMANDS = [
613
648
  /^vitest\b/,
614
649
  // `hurl --test …` or `hurl … x.hurl`.
615
650
  /^hurl\s+[^|\n]*(?:--test|\.hurl)/,
651
+ // The suite's own runner SCRIPT, whoever wrote it: the supplied trees ship
652
+ // `run-api-tests-hurl.sh` and `run-hurl-tests.sh`, and the command-position parser puts the
653
+ // script's own NAME where the program goes (`bash fixtures/…/run-api-tests-hurl.sh` →
654
+ // `run-api-tests-hurl.sh`). xr-molecule-flash-m170 (+06:02) chained two invocations of one such
655
+ // script in the FOREGROUND, which no launch site counted: the first walk's tally was the only
656
+ // evidence the note had, and it reported a passing run as 13 failures. A script named for the
657
+ // suite is a whole walk of it; `head -30 run-api-tests-hurl.sh` is not in command position and
658
+ // stays a read ({@link commandSegments}).
659
+ /^run-(?:api-)?(?:hurl-)?tests?[\w-]*\.sh\b/,
616
660
  ];
617
661
  /**
618
662
  * Every statement of a command that starts a whole test suite, in command order.
@@ -649,6 +693,122 @@ export function suiteRunStatements(command) {
649
693
  export function isSuiteRunCommand(command) {
650
694
  return suiteRunStatements(command).length > 0;
651
695
  }
696
+ /**
697
+ * The refusal for a command that starts the suite MORE THAN ONCE — the ONE reading every launch
698
+ * site asks, foreground and background alike.
699
+ *
700
+ * Each statement is a whole walk of the same suite against the same app, and only the last of them
701
+ * is rewritten, so the earlier ones measure nothing the first did. X1 xr-molecule-flash-m157
702
+ * (+14:02) chained two whole walks of one 138-test suite with `;` (4.5 minutes each) and the
703
+ * background launcher refused it; X1 xr-molecule-flash-m170 (+06:02) chained two invocations of the
704
+ * suite's own runner script in the foreground, where nothing counted them, and the note reported
705
+ * the passing second walk as the first walk's 13 failures.
706
+ *
707
+ * The one run handed back is the LAST suite statement, rewritten exactly as
708
+ * {@link streamingBackgroundCommand} rewrites a run (a filter or `| tail` after the runner is
709
+ * dropped, and a Playwright run gets the reporter that prints failures). Its own count is one —
710
+ * guarded, so this cannot recurse.
711
+ *
712
+ * @param command - The shell command as the executor sent it.
713
+ * @returns The refusal sentence, or null when the command starts the suite zero or one time.
714
+ */
715
+ export function repeatedSuiteRunError(command) {
716
+ if (commandRunsNoTests(command))
717
+ return null;
718
+ const suiteSteps = suiteRunStatements(command);
719
+ if (suiteSteps.length <= 1)
720
+ return null;
721
+ const lastSuitePart = command
722
+ .split(/&&|\|\||;/)
723
+ .reverse()
724
+ .map((p) => p.trim())
725
+ .find((p) => isSuiteRunCommand(p));
726
+ const single = lastSuitePart && suiteRunStatements(lastSuitePart).length === 1
727
+ ? streamingBackgroundCommand(lastSuitePart).command
728
+ : (lastSuitePart ?? '');
729
+ return (`This command runs the suite ${suiteSteps.length === 2 ? 'twice' : `${suiteSteps.length} times`} ` +
730
+ `(${suiteSteps.map((s, i) => `step ${i + 1}: \`${s}\``).join(', ')}). One walk is the ` +
731
+ `measurement; run exactly one: \`${single}\``);
732
+ }
733
+ /** A flag with its value, as {@link suiteRunBudgetCommand} writes it: `--timeout=15000`. */
734
+ const BUDGET_FLAG = /^(--[A-Za-z][\w-]*)=(\S+)$/;
735
+ /**
736
+ * The statement the suite runner occupies, and where its own arguments end.
737
+ *
738
+ * The LAST suite statement, wherever it sits: unlike a reporter or `--retries=0` (which only make
739
+ * sense on the run a launcher measures), the suite's budget belongs on the run itself even when
740
+ * another command follows it (`… ; npm run build`).
741
+ */
742
+ function suiteRunnerArguments(command) {
743
+ const pipeline = lastSuiteRun(command);
744
+ const runner = pipeline ? pipeline.located[pipeline.at] : undefined;
745
+ if (!runner || runner.start === null || runner.end === null)
746
+ return null;
747
+ const start = runner.start;
748
+ const end = runner.end;
749
+ const at = statementArgumentEnd(command, start, end);
750
+ if (at === null)
751
+ return null;
752
+ return { start, end, at };
753
+ }
754
+ /**
755
+ * A command with the supplied suite's own settings written back onto its runner as Playwright
756
+ * flags, and the flags that could not be placed.
757
+ *
758
+ * The grader walks the suite under its OWN budget, so a run whose runner config sets a different
759
+ * timeout or worker count measures a run the grader never sees (xr-molecule-flash-m169: a 60 s
760
+ * wrapper config produced a green local proof the official 15 s grader then failed). Playwright
761
+ * lets a command-line flag win over every config file, so the suite's values are put on the
762
+ * runner's own arguments — written where {@link statementArgumentEnd} places them, never after a
763
+ * pipe, redirect or group closer. Only the command's LAST suite step is touched: that is the run
764
+ * every launch site measures.
765
+ *
766
+ * A flag the command already carries is REWRITTEN in place (last occurrence wins, as it does for
767
+ * Playwright); a flag it does not carry is inserted at the end of the runner's arguments. A
768
+ * command whose runner cannot be placed is returned exactly as written, with every flag reported
769
+ * as unplaced, so the caller states the fact instead of claiming a pin it did not make.
770
+ *
771
+ * @param command - The shell command about to run.
772
+ * @param flags - The flags to put on it, each as `--name=value`.
773
+ * @returns The command to run, the flags placed on it, and the ones it could not carry.
774
+ */
775
+ export function suiteRunBudgetCommand(command, flags) {
776
+ const placed = [];
777
+ const unplaced = [];
778
+ let out = command;
779
+ for (const flag of flags) {
780
+ const parts = BUDGET_FLAG.exec(flag);
781
+ if (parts === null) {
782
+ unplaced.push(flag);
783
+ continue;
784
+ }
785
+ const name = parts[1] ?? '';
786
+ const value = parts[2] ?? '';
787
+ const run = suiteRunnerArguments(out);
788
+ if (run === null) {
789
+ unplaced.push(flag);
790
+ continue;
791
+ }
792
+ // `--name=value` and `--name value` alike; the last occurrence is the one Playwright uses.
793
+ // The value is digits (milliseconds, worker counts), so a percentage or an expression is
794
+ // left alone rather than half-rewritten.
795
+ const written = new RegExp(`(?:^|[\\s;&|(])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:=|\\s+)(['"]?)(\\d[\\d_]*)(?![\\w%.])`, 'g');
796
+ const text = out.slice(run.start, run.at);
797
+ let last = null;
798
+ for (let m = written.exec(text); m !== null; m = written.exec(text))
799
+ last = m;
800
+ if (last !== null) {
801
+ const digits = last[2] ?? '';
802
+ const at = run.start + last.index + last[0].length - digits.length;
803
+ out = `${out.slice(0, at)}${value}${out.slice(at + digits.length)}`;
804
+ placed.push(`${name}=${value}`);
805
+ continue;
806
+ }
807
+ out = `${out.slice(0, run.at)} ${flag}${out.slice(run.at)}`;
808
+ placed.push(flag);
809
+ }
810
+ return { command: out, placed, unplaced };
811
+ }
652
812
  /**
653
813
  * The command with the `timeout <N>` wrapper lifted off the statement that runs the suite, and the
654
814
  * bound that was lifted.
@@ -2224,10 +2384,21 @@ function failureBlockTests(text) {
2224
2384
  export const UNIVERSAL_CAUSE_FILES = 3;
2225
2385
  /**
2226
2386
  * How many tests a run must have walked — with nothing passing since its first failure — before
2227
- * "the page renders nothing" can be its one cause. A handful of failures is a handful of bugs;
2228
- * a page that never painted fails every spec that reaches it, and m141's walk had 36 by +20:30.
2229
- */
2230
- export const EMPTY_PAGE_CAUSE_TESTS = 15;
2387
+ * "the page renders nothing" can be its one cause.
2388
+ *
2389
+ * THREE, and the number is the cost of the alternative. The old floor of 15 was chosen when the
2390
+ * reading was only offered at a completed run; once it STOPS the walk, every test waited for is
2391
+ * a test whose result is thrown away, and a blank page costs its per-test budget each time
2392
+ * (m177: 18 tests, ~4.2 minutes, on a page whose thrown error was visible in the first
2393
+ * `pageerror`). Three is the smallest count that is not one page's own bug: the cause additionally
2394
+ * needs three DISTINCT spec files (`UNIVERSAL_CAUSE_FILES`, or the files a narrowed run selected),
2395
+ * each spec waiting for its OWN first element, and no test passing after the first failure. A page
2396
+ * that renders misses elements, but three unrelated specs in three files failing at three
2397
+ * different first locators with nothing passing in between is the page, not three bugs. At the
2398
+ * suite's own 5 s action timeout that verdict lands ~20 s into the walk, so the early stop costs
2399
+ * about one test's worth of time instead of fifteen.
2400
+ */
2401
+ export const EMPTY_PAGE_CAUSE_TESTS = 3;
2231
2402
  /**
2232
2403
  * A failure body whose whole complaint is a wait for an element the page never rendered:
2233
2404
  * Playwright's `waiting for locator(…)` call log, its `element(s) not found` line, or the
@@ -2251,22 +2422,32 @@ function pageErrorInLog(text) {
2251
2422
  return m?.[1]?.trim() || null;
2252
2423
  }
2253
2424
  /**
2254
- * The "nothing has passed and nothing renders" cause: every failure block the log has written is
2255
- * a locator wait, the run has walked at least {@link EMPTY_PAGE_CAUSE_TESTS} tests across the
2256
- * file threshold, and no test has passed since the first failure.
2425
+ * The "nothing has passed and nothing renders" cause: the run's FIRST consecutive failures — from
2426
+ * its first failed test, with no pass since — are all locator waits, at least
2427
+ * {@link EMPTY_PAGE_CAUSE_TESTS} of them across {@link UNIVERSAL_CAUSE_FILES} different spec
2428
+ * files, and each waits for its own spec's first element.
2429
+ *
2430
+ * The file span stays at {@link UNIVERSAL_CAUSE_FILES} whatever the run selected: "the page
2431
+ * renders nothing" is a claim about the APP, and one file's failures can be that file's own setup
2432
+ * (a broken login helper fails every test of one spec at its first locator). A run narrowed to one
2433
+ * or two files still has the signature and connection causes for a whole-run stop.
2434
+ *
2435
+ * Stops the walk as soon as that verdict is readable, not after a fixed floor of failures have
2436
+ * been burned: the page error arrives with the first test, so every further test only repeats it.
2257
2437
  *
2258
2438
  * This is ONE cause even though the signatures differ — each spec asks for its own first element
2259
2439
  * (`input[name="username"]`, `a.navbar-brand`, `h1`), so {@link repeatingFailure} cannot group
2260
2440
  * them and {@link universalCause}'s signature test cannot see the run is over. xr-molecule-flash-
2261
2441
  * m141 (+20:30) walked a built app that rendered nothing: 36 tests had failed, every one at its
2262
- * first locator, and the note told the run to keep walking for another ~9 minutes.
2442
+ * first locator, and the note told the run to keep walking for another ~9 minutes. xr-molecule-
2443
+ * flash-m177 (+08:38 → +12:52) repeated it: 18 tests, one whole walk, on a page whose
2444
+ * `PAGEERROR No 'auth-client' provider bonded` was in the log from the first one.
2263
2445
  *
2264
2446
  * @param text - The log (ANSI already stripped).
2265
- * @param threshold - The spec-file threshold this run is judged against ({@link universalCause}).
2266
2447
  * @param found - The repeating cause {@link repeatingFailure} found, when it found one.
2267
2448
  * @returns The cause, or null when the log is not this shape.
2268
2449
  */
2269
- function emptyPageCause(text, threshold, found) {
2450
+ function emptyPageCause(text, found) {
2270
2451
  const marks = resultMarks(text);
2271
2452
  const firstFailure = marks.indexOf('✘');
2272
2453
  if (firstFailure === -1 || marks.slice(firstFailure).includes('✓'))
@@ -2284,10 +2465,15 @@ function emptyPageCause(text, threshold, found) {
2284
2465
  // (which names the repeated error) says more than "the page renders nothing" would.
2285
2466
  if (found !== null && found.message !== null && found.count >= tests.length)
2286
2467
  return null;
2468
+ // The cause is the FIRST consecutive failures: `tests` starts at the first ✘ and no ✓ follows
2469
+ // it, so a page that has failed three tests in a row across three files has already said
2470
+ // everything the rest of the walk can say (m177 paid 4.2 minutes learning that).
2287
2471
  if (tests.length < EMPTY_PAGE_CAUSE_TESTS)
2288
2472
  return null;
2289
2473
  const files = [...new Set(tests.map((t) => specFile(t.where)).filter(Boolean))];
2290
- if (files.length < threshold)
2474
+ // The blank-page claim is cross-file by construction (see the doc): a selected-file count
2475
+ // below the span does not shrink it, it removes the claim.
2476
+ if (files.length < UNIVERSAL_CAUSE_FILES)
2291
2477
  return null;
2292
2478
  const pageError = pageErrorInLog(text);
2293
2479
  return {
@@ -2424,7 +2610,7 @@ export function universalCause(log, found, command) {
2424
2610
  const threshold = selected === null ? UNIVERSAL_CAUSE_FILES : Math.min(UNIVERSAL_CAUSE_FILES, selected);
2425
2611
  // Nothing has passed and nothing renders: EVERY spec so far is waiting for its own first
2426
2612
  // element, so the signatures differ while the cause does not (xr-molecule-flash-m141).
2427
- const emptyPage = emptyPageCause(text, threshold, found);
2613
+ const emptyPage = emptyPageCause(text, found);
2428
2614
  if (emptyPage)
2429
2615
  return emptyPage;
2430
2616
  if (!found)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.9.16",
3
+ "version": "1.9.19",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",