From 6c84fd2f765ac3bed35deca73a250057c1f1c975 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:13:16 +0000 Subject: [PATCH] fix(e2e): journey-runner rung 2 runs for mustNotFire-only spans (#150) Rung 2 required a non-empty spans.ordered, so a step proving only that a microflow did NOT fire was skipped with no output. Gate is now ordered OR mustNotFire non-empty; assertSequence only runs when ordered is non-empty. Checks: node --check ok; gate-condition probe over 5 span shapes; no live Jaeger run available, so no end-to-end execution. Selftest covers otel assertions only and does not exercise the runner loop. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + project-tests/e2e/journey-runner.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3d67a9..ebedc9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(e2e): **journey-runner rung 2 now runs for a negative-only span claim.** `spans: { ordered: [], mustNotFire: [...] }` was skipped silently because the rung required a non-empty `ordered`; it now runs when either list is non-empty (sequence assert only when `ordered` is non-empty). Zero captured spans still reports INVALID (a vacuous negative is not proof). Verified by `node --check` and a gate-condition probe, no live Jaeger run (#150) — MendixMau - perf(routing): **CLAUDE.local.md routing carries triggers only.** The `baseline` view (the copy loaded into every session of every project) now cuts each row's "when" cell at its first " — " and replaces the three-paragraph preamble with two lines pointing at `ROUTING.md` for the full text; `readme-baseline` (README, runbook) is unchanged. Fresh scaffold: CLAUDE.local.md 17.5 KB → 11.7 KB (−33%). Field run 2026-10-07 (existing app, one main session per run, 2 runs per side, an entity + 2 microflows + 2 pages): 10/10 steps applied each side, new mx errors 5 vs 5 (the same CE2421 page-binding slip in both), cost $1.39 vs $1.38; the slim lanes opened the MDL pre-flight skill as often or more. Also drops two stray merge-artifact lines from this section. — MendixMau - perf(ui-loop): **fewer mxbuilds, no fixed sleeps, legible screenshots.** Field run 2026-10-07 (a requirements-driven build): `exec.sh` was 22% of tool time at two mxbuilds per exec, journeys and page waits slept a fixed time, and screenshots were 11.5% of re-read tokens. `exec.sh` now skips the pre-flight baseline mxbuild when the model is byte-identical to its last *clean* pass: the verification stamp gains an `errors:` field (`model-stamp.sh check --clean` accepts only `errors: 0`, so a delta-gate pass over pre-existing errors never counts; `SKIP_BASELINE=0` is unchanged). New `tests/e2e/settle.js` waits for the page's own state (no visible Mendix progress/loading indicator, DOM size stable) instead of a fixed time; `journey-runner.js` uses it after each action (an explicit `settleMs` still wins) and `page-audit.js` per page. Probed in a real browser: on a page that loads in 1.8 s the old 1200 ms sleep and networkidle+600 both captured it half-loaded, settle waited for it; on a fast page settle took 775 ms vs 1200. `SETTLE_MODE=fixed` restores the old waits for an A/B run. `page-audit.js` also saves a viewport shot `page-audit-.top.png` beside the full-page one, and `ui-loop.md` says to look at viewport shots: a tall full-page shot reaches the model at ~1024 px high, text unreadable. `shrink-image-read.sh` now works off macOS (ImageMagick or Pillow, python when jq is absent; it was a silent no-op without `sips`). — MendixMau - perf(ui-loop): **a stale app reloads instead of restarting, journeys stop sleeping, worst pages are looked at first.** Field run 2026-10-07, existing app. `test-stack-up.sh` records which model the running app was built from (`.claude/loop/served-model`); when the model has changed since, it hot-reloads with `mxcli docker reload` (adding `--skip-check` when the exec.sh gate already passed this model) and restarts only if that fails: 93 s instead of 179 s. `ui-loop.md` and the script header carry the measured table and point at `mxcli run --local --watch` as the fast loop (~18 s per page change). `journey-runner.js` waits on page state after a combobox action instead of fixed pauses, and a combobox step now opens the widget it names: it used to open the page's first combobox, so on a form with three every step but the first failed. `page-audit.js` clicks the first copy of a nav item a user could actually click, not just the first match: with the sidebar collapsed, every in-nav page faulted while the same item sat in the top menu bar. It also writes `page-audit-look-order.txt`, all pages worst first (`module-review.md` §4a: order, not selection, every page is still looked at). `design-audit.js` drops its fixed 1.2 s wait. Same 7-step journey on the same app: 56 s with `SETTLE_MODE=fixed`, 24 s with settle, 11/11 pass both ways, 3 runs each. — MendixMau diff --git a/project-tests/e2e/journey-runner.js b/project-tests/e2e/journey-runner.js index d0140248..b4bd118b 100644 --- a/project-tests/e2e/journey-runner.js +++ b/project-tests/e2e/journey-runner.js @@ -740,9 +740,12 @@ async function runJourney(page, j) { } // ── RUNG 2: ordered spans ──────────────────────────────────────────────── - if (step.spans && step.spans.ordered && step.spans.ordered.length) { + // Runs for a positive claim (ordered) OR a negative-only claim (mustNotFire with + // ordered empty/absent) — a step whose whole proof is "this did NOT run" (#150). + const claimed = (step.spans && step.spans.ordered) || []; + const mustNot = (step.spans && step.spans.mustNotFire) || []; + if (claimed.length || mustNot.length) { // Wait for the claimed microflows, not just any span — see otel.js capture() `until`. - const claimed = step.spans.ordered; const spans = await O.capture(t0, { min: 1, until: sp => { const got = O.microflowNames(sp); return claimed.every(n => got.includes(n)); } }); if (!spans.length) { @@ -750,9 +753,10 @@ async function runJourney(page, j) { 'zero spans captured — Jaeger down or OTel off. Trace rung did NOT run.', req); } else { O.guard(spans, otelReporter(req), `${step.name}: capture non-empty`); - O.assertSequence(spans, step.spans.ordered, otelReporter(req), `${step.name}: ordered`); + if (claimed.length) + O.assertSequence(spans, claimed, otelReporter(req), `${step.name}: ordered`); O.assertNoErrors(spans, otelReporter(req), `${step.name}: no ERROR at any level`); - for (const n of step.spans.mustNotFire || []) { + for (const n of mustNot) { O.assertNotFired(spans, new RegExp(n), otelReporter(req), `${step.name}: ${n} did not run`); } }