From 27faa58afbba38e77a9939bdb44453d17383c9f7 Mon Sep 17 00:00:00 2001 From: MendixMau Date: Wed, 14 Oct 2026 13:23:40 +0800 Subject: [PATCH 1/2] learn(detection-gaps, testing-shape, ui-loop): snippet-call class, 403 false sign-out, widget-pinned probes, restart signs everyone out Field run 2026-10-14 (mxcli v0.26.0, Mendix 11.15.0). Refs #336 #337 #338 #339. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + skills/learned-detection-gaps.md | 1 + skills/testing-shape.md | 2 ++ skills/ui-loop.md | 6 ++++++ 4 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59bca1b3..4a794c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- learn(learned-detection-gaps, testing-shape, ui-loop): **a class on a snippet call never reaches the DOM; a 403 bounce and a widget-pinned session probe both read as sign-outs; every restart signs every user out.** Detection-gaps row: `Class` on a `SNIPPETCALL` is green through check, exec, mxbuild, lint and DESCRIBE, and absent in the browser ([#336](https://github.com/MendixMau/mxcli-project-toolkit/issues/336)). Testing-shape §4 gains two false-red rows: `login.html?messageCode=403` with the session kept ([#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)), and instruments pinned to one grid name or a typed menu list (1/7 and 7/34 false FAILs after a redesign, [#339](https://github.com/MendixMau/mxcli-project-toolkit/issues/339)). ui-loop: say so before a restart while a human uses the app ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)) — three-app field run, 2026-10-14 - process(docs): **1.0 release notes, a versioning rule, and a README for adopters.** New `docs/releases/v1.0.md`: the first named milestone, covering 2026-09-30 to 2026-10-14 by theme (independent review before merge, baseline words 79,997 → 76,259, exec.sh lock and restart verdicts, lint batches 1–4 with SEC014 and the stock-revert guard, journey/e2e fixes, the Stage 3 interaction model, delivery rounds), with upgrade steps, known gaps and credits. `docs/RELEASE-PROCESS.md` §8 is the single source for versioning: a milestone (1.0, 1.1, 2.0) is a train release the maintainer also names, with curated notes, a GitHub Release and a CHANGELOG pointer; major = a consuming project breaks without action (needs a migration section and a sync path), minor = no action needed; deprecations are announced in one milestone and removed no earlier than the next major; the `vYYYY.MM.DD` train and `bin/cut-release.sh` are unchanged; plus how a consumer checks, upgrades and pins a version. The header above now defers to it. README rewritten for newcomers: what it is, how it works, ways to use it, quick start, Make it yours (company brain, CLAUDE.local.md, lint opt-outs, waivers and --adopt, interview mode, exec approval and permission profiles, agent stubs, other harnesses), testing, a call for input, releases. The earlier material sits under Reference with the generated routing blocks untouched. Corrected: the Windows `pgrep` hazard (replaced by the per-project exec lock in #304), the hand-edit steps in "How to add a new skill" (tables are generated), and the stale Stage 6 and `examples/` notes. — MendixMau - fix(interview-mode): **`attended` and `unattended` are now read as aliases for `steering` and `auto` — a project that opted into unattended as documented was silently run as steering.** CLAUDE.md, the runbook and intake Q9 taught `Interview mode: unattended`, but `bin/interview-mode.sh` knew only `steering | assist | auto` and fell back to steering. The aliases are accepted in every source (env, `.interview-mode`, `PROJECT.md`, `--set`) under the same parse rules, with one stderr note. The `PROJECT.md` value is read after stripping bold/backticks: a bare mode or alias (`**auto**`, `` `auto` ``, `auto.`) resolves, a note may follow `steering`, but `assist`/`auto` with any trailing text falls back to steering with a "must stand alone on the line" message — so an annotated or hedged line never resolves quieter than before (two hedge word lists were tried in review and both leaked); env, session file and `--set` stay exact. Output stays canonical so `interview-gate.sh` is unchanged, and any other value still falls back to steering. Docs now name the canonical values (vocabulary in `interview-protocol.md`); intake Q9's body offers the three modes with steering the default (heading kept: it is sync's locator), and `sync-project.sh` reads an earlier unanswered Q9 body as unanswered rather than "edited". `test-interview-mode.sh` +57 checks (19 → 76) — MendixMau (found in the 1.0 README review) - fix(dispatching, sync-project, obligation-check): **review follow-ups for the 2026-10-14 integration batch.** `dispatching.md` names app-restart exits 4 and 5 beside 3 and 1. `test-test-scratch.sh` drops `ls | grep -q` under pipefail for here-strings; sync step 4d no longer aborts on empty doctor output; `_ob_has_denominator` strips NUL bytes so bash stops warning. — integration 2026-10-14 reviews diff --git a/skills/learned-detection-gaps.md b/skills/learned-detection-gaps.md index 18706b8c..806731d9 100644 --- a/skills/learned-detection-gaps.md +++ b/skills/learned-detection-gaps.md @@ -56,6 +56,7 @@ verification rungs form a ladder, and a green result only certifies what that ru | A page layout migrated with `ALTER PAGES … WHERE LAYOUT = X`, in a project whose page scripts **hardcode** `Layout:` inside `create or replace page` | rungs 1–6 — every rung, including live runtime: the app loads, every page renders, every journey passes. There is no error anywhere | a screenshot **of the nav shell** on every page, or `grep -l 'Layout: ' mdlsource/` after the migration | `create or replace page` rewrites the page **wholesale**, layout included — so re-running any older page script silently reverts that page to the old shell, and the app ships **two navigation shells at once**. Found when 4 of 7 screens had no navigation at all, just a bare hamburger, after a UI fix loop re-ran five page scripts. A layout migration is not done until the `Layout:` literal is fixed **in the source scripts**; the `ALTER` is a one-shot, the scripts are the repeat offender | field build (Maurits Visser), 2026-09 | | A pluggable **combo box filter with `lazyLoading: true`** whose options data source also sets the **preset value** | rungs 1–4, lint, and every scripted check — the model is valid and the list *is* filtered | rung 6, by eye, **before touching the filter**: the list is already narrowed by the preset while the filter looks empty (no value, no clear button) until its menu is first opened | Lazy loading defers the options source, so the preset it sets is not rendered until first open. Turn lazy loading off for short lists (≤10 options). Before the build, check every pluggable property that changes *when* data loads (lazy loading, load-on-open), on any widget that should show a value on page load | field run (jopterhorst), 2026-10, [#299](https://github.com/MendixMau/mxcli-project-toolkit/issues/299) | | A widget **inserted into an existing container** whose theme pins `height` on **positional row classes** (`widget-row-N` and kin) | rungs 1–4, lint, and every scripted check — the MDL insert is correct and the widget renders | rung 6, by eye: the next row overlaps the inserted widget and covers part of it (here the filter's clear button) | The theme sized rows by position, not content, so a taller row 1 runs under row 2. Before an insert into an existing container, grep the theme SCSS for fixed `height` on that container's row/position classes; if found, add a `:has()` rule adjusting both rows | field run (jopterhorst), 2026-10, [#299](https://github.com/MendixMau/mxcli-project-toolkit/issues/299) | +| A **`Class` (or `Style`) on a `SNIPPETCALL`** — `alter page … set (Class: 'x') on ` | rungs 1–4, lint, **and DESCRIBE**, which shows the class on the call | rung 6, a DOM probe: `document.querySelector('.x')` is null on every page that carries it | A snippet call renders **no element of its own**; the snippet's widgets are inlined into the parent, so the class has nothing to sit on. Put the class on the container around the call, or on the snippet's own top-level container. Found when a stepper marker on 6 snippet calls changed nothing on screen with every gate green | field run, mxcli v0.26.0, 2026-10-14, [#336](https://github.com/MendixMau/mxcli-project-toolkit/issues/336) | | Units **mxcli < v0.26** wrote | rungs 1–6 | Studio Pro merge/diff: *"… do not have the same properties"* | after mxcli upgrades, before merging: `bin/doctor.sh --property-sets `; `platform-link.md` | #295 | ## Operating rules that fall out of this table diff --git a/skills/testing-shape.md b/skills/testing-shape.md index 925a7f48..aecd3e70 100644 --- a/skills/testing-shape.md +++ b/skills/testing-shape.md @@ -210,6 +210,8 @@ so the next person argues with the incident rather than with the rule. | **UI** | A login helper **silently falls back to an admin account** (a guard against lockout) — a role-scoped test then proves nothing, and neither the run nor the report looks any different (2026-08-05) | Any spec whose result depends on *who* is logged in must **assert the actual identity**, not merely that login succeeded | | **UI** | A helper **no-ops without erroring** when its expected page is absent, so the spec runs to completion against the wrong page (2026-08-05, a mobile scanning spec) | Helpers must **throw** when a precondition is missing, never return quietly. Dump live page state rather than trusting the assertion's own label. | | **UI** | A full `page.goto(baseUrl)` **silently logs the session out** once security is above `Off`. Suites that log in once and then "reset navigation" with a goto bounce to `/login.html` and every downstream assertion breaks for the wrong reason (2026-08-15, after raising `Off` → `Prototype`) | Fix at the helper, not the call site: the shared navigate-home helper detects the login form (`#usernameInput`) and **re-authenticates every time**. Never trust a cookie across `page.goto`. | +| **UI** | **False red: a correct access block reads as a sign-out.** A signed-in user opening a page their role may not open is redirected to `login.html?messageCode=403`, and the stock login page shows **no message**. The session is kept (same `XASSESSIONID`; `/` loads the user's home again). A crawler or wrong-role spec that treats "landed on login.html" as "session lost" reports a security failure over a correct block, and a human calls it "the app signs me out" (2026-10-14, Mendix 11.15, [#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)) | Classify `messageCode=403` as **blocked, session kept**, separately from a real sign-out, then prove the session: navigate to `/` and assert not-on-login plus menu present. Before calling any sign-out a product defect, check the runtime log for a restart at that time: every `mxcli run` restart ends every session ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)). | +| **UI** | **False red: an instrument pinned to one widget name or a typed menu list fails after a redesign.** A session-alive probe that waited for one named home grid reported **6/7 "SESSION DID NOT SURVIVE"** after a home redesign moved that grid to a second tab; with the selector widened, 7/7 PASS, and the blocks had been correct throughout. A nav-crawl comparing menus to a hardcoded EXPECTED list went **7/34**: all 27 FAILs were expectation rows (20 MISSING, 6 EXTRA), none an error or a sign-out (2026-10-14, [#339](https://github.com/MendixMau/mxcli-project-toolkit/issues/339)) | A session-alive check asserts **not on `login.html` and the menu bar present**, never one page's widget. Menu expectations are **derived from the model** at test start (`list navigation menu` plus the role grants), never typed. The tell: every FAIL has the same reason, and it names the instrument's own expectation. | | **UI** | `:has-text(...)` is a **Playwright-only** pseudo-class. Inside `page.evaluate` it reaches `document.querySelector`, which throws `SyntaxError`; generic `.catch()` swallows it, so a `has_widget`-style helper evaluates **permanently false** and a visible row reads as absent (2026-08-15) | In `evaluate` context use a plain `.textContent.includes(...)` scan. Reserve `:has-text` / `:visible` for real Playwright locator strings. | | **UI** | `display: contents` elements always report `offsetParent === null` — so an `offsetParent !== null` visibility filter returns **empty** for a Data Grid 2 the user is actively looking at (browser layout quirk, not a Mendix bug) | Check the ancestor tabpanel's own `getComputedStyle(...).display`, never the row's `offsetParent`. | | **UI** | **A chat/transcript panel whose message pane is clipped to zero height reads as a full, correct answer to every DOM-level check.** The marketplace `ConversationalUI` snippet lays its container out as a flex column and expects the *host* to supply a height; a host that supplies none leaves the fixed-height composer taking every pixel and the flexing message pane resolving to `0px`. Because that pane's own `overflow-y` is `auto`, the entire conversation is simply scrolled out of a zero-height window: `textContent` returns it in full, the element exists, `count()` is non-zero, and a human sees an empty box. On 2026-08-31 an e2e walk read **776 characters of a correct, grounded answer** off a panel the user had just reported as showing nothing, and graded the step PASS. | Answer checks must assert **legibility, not presence**: after the answer lands, measure the pane's `getBoundingClientRect().height` against its content's, and fail when the pane is near-zero around a taller conversation. Take an element-scoped screenshot of the panel itself — a page-level shot at this size cannot distinguish an empty panel from a full one. A host that embeds a flex-column chat snippet owes it a definite height. | diff --git a/skills/ui-loop.md b/skills/ui-loop.md index 12fc9822..6c1e8c93 100644 --- a/skills/ui-loop.md +++ b/skills/ui-loop.md @@ -239,6 +239,12 @@ started it>'` once; it is remembered after that. `--dry-run` says what it would app uses `bin/test-stack-up.sh --restart` instead. This is the one restart an implementer may run without the lead (`dispatching.md`). +**A restart signs every user out** (sessions live in the runtime's memory), and so does the +trial licence's own 2–4 h stop. If a human may be using the app, say so in chat **before** +restarting. When a human reports "it keeps signing me out", compare the runtime log's +`successfully started` lines with the times first: one field app showed 7 starts in ~90 min of +theme work, which was the whole report (#337). + ## When it finds something — which side is actually wrong The symptom is easy to see. **Where the fix goes is not, and the instinct is usually wrong.** From 190a2d7b839335ce20cfec3e0181a81bf4250671 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 13:59:46 +0000 Subject: [PATCH 2/2] fix(ui-loop, testing-shape): scope the restart claim to its evidence; reconcile the 403 probe with the goto row Review of #340 (MERGE-AFTER-FIX): - ui-loop: drop "sessions live in the runtime's memory", which contradicts measured-claims.md (sessions persist in System.Session and survive a container restart). Scope to mxcli run --local and state the evidence as a timing match (7 starts vs the reports), not an inspected session table. - testing-shape 403 row: name the probe actually used (a full page.goto('/')), keep the re-authenticating helper rule of the 2026-08-15 row, and amend that row with an unconfirmed cross-reference: its bounce may have been this 403 misread. - CHANGELOG headline matches. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- skills/testing-shape.md | 4 ++-- skills/ui-loop.md | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e89146..d2deb422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- learn(learned-detection-gaps, testing-shape, ui-loop): **a class on a snippet call never reaches the DOM; a 403 bounce and a widget-pinned session probe both read as sign-outs; every restart signs every user out.** Detection-gaps row: `Class` on a `SNIPPETCALL` is green through check, exec, mxbuild, lint and DESCRIBE, and absent in the browser ([#336](https://github.com/MendixMau/mxcli-project-toolkit/issues/336)). Testing-shape §4 gains two false-red rows: `login.html?messageCode=403` with the session kept ([#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)), and instruments pinned to one grid name or a typed menu list (1/7 and 7/34 false FAILs after a redesign, [#339](https://github.com/MendixMau/mxcli-project-toolkit/issues/339)). ui-loop: say so before a restart while a human uses the app ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)) — three-app field run, 2026-10-14 +- learn(learned-detection-gaps, testing-shape, ui-loop): **a class on a snippet call never reaches the DOM; a 403 bounce and a widget-pinned session probe both read as sign-outs; app restarts matched a "signs me out" report by timing.** Detection-gaps row: `Class` on a `SNIPPETCALL` is green through check, exec, mxbuild, lint and DESCRIBE, and absent in the browser ([#336](https://github.com/MendixMau/mxcli-project-toolkit/issues/336)). Testing-shape §4 gains two false-red rows: `login.html?messageCode=403` with the session kept ([#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)), and instruments pinned to one grid name or a typed menu list (1/7 and 7/34 false FAILs after a redesign, [#339](https://github.com/MendixMau/mxcli-project-toolkit/issues/339)). ui-loop: say so before a restart while a human uses the app ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)) — three-app field run, 2026-10-14 - process(build): **Stage 5 drafts the ready set in parallel and applies serially.** The runbook dispatch row said "sequential within a module", which serialised drafting even for independent scripts; it now points to `iterative-build-loop.md` → Gate cost, the single statement of the rule. The unit is the build-plan row: every `BUILD` row whose "Depends on" rows are all applied, minus overlapping writers (two rows that replace, alter or drop+create the same document never share a ready set, and no row is drafted while it overlaps a queued unapplied one; `brd-to-build-plan.md` has Produces name every document a row writes), runs at once, its scripts in order, one `mdl-agent` per script. Applies stay one at a time through `bin/exec.sh`, and the ready set is recomputed after each. A queued draft made before a sibling apply landed is re-checked with `mxcli diff --format struct` (preflight STOP rule 23); any removal line (checked against `describe` per BUG-146) or STOP rule 21 target means re-draft, never patch, because a stale restatement of a shared document is exactly rule 21's silent seed-call loss and passes `check --references`. Report `ready set N, drafted N, applied k`; the `mdl-agent` template says a drafter never applies and never depends on an unapplied draft. Field evidence: none yet. — MendixMau - fix(platform-link): **CLI commits now show their changed documents in Studio Pro History.** Studio Pro's History → Model changes pane reads a `ModelChanges` array from the commit's `refs/notes/mx_metadata` note. Trap 6c's hand-written template set it to `[]`, so every mxcli/agent commit showed "Model changes 0" (field project: 270 of 270 notes empty). New `project-bin/mx-metadata-note.py` (stdlib only) computes the array from the commit's changed `.mxunit` files, in Studio Pro's entry shape: Status, UnitID, UnitType, UnitName and Module (walked up the `.mpr` Unit tree). It has `changes` / `write` / `backfill` / `sync ` subcommands. `install-project-hooks.sh` now also installs post-commit and post-rewrite hooks that write the note and never block, and `--check` covers them, so `sync-project.sh` rolls them out to wired projects. Notes are published with `mx-metadata-note.py sync ` after the branch push. Push config is never touched. With mxcli v0.26+ in the project root the hooks call `mxcli git note --write` instead (mendixlabs/mxcli#1337); the Python script is the fallback for older pins, and on 160 field commits both wrote the same 3,594 entries. Trap 6c rewritten. Measured against Studio Pro's own notes, 51 commits in 5 apps: 3387/3389 entries exact on all five fields. Field run on a 357-commit Mendix 11.15.0 app: 158 notes backfilled and pushed, and Studio Pro History lists the documents per revision. Review fixes: an earlier revision added `HEAD` + the notes ref to `remote..push`. It was dropped because any push refspec replaces `push.default`. A branch tracking a differently named upstream got a new same-named branch on Team Server where `simple` refuses, a missing local notes ref failed the whole push, and nobody knows whether Studio Pro's own push reads that config. The installer now only fetches the remote's notes (non-forced). It removes that exact `HEAD` + notes pair if present, and leaves and reports any other entry. `sync` handles a remote with no notes ref (it used to exit 1) and retries a rejected push once after re-merging. A note that already lists changes (Studio Pro's) is never overwritten by `write`/`backfill` without `--force`, and `sync` keeps a colleague's non-empty note over ours. The mxcli probe greps `git note --help` for `--write`. Hooks and docs call the script through `resolve_py`. Fixture `tests/wave2/test-mx-metadata-note.sh` (40 cases, over real captured template units; single- and two-tree layouts, amend, backfill and `--force`, push exit codes identical to a no-hooks clone, leftover-refspec cleanup, sync to an empty remote and its retry, notes fetch at install, a diverged notes merge; green under `init.defaultBranch` master and main). — field project (Mendix 11.15 app), MendixMau - learn(fixture-seeding): **explore the data structure first, choose rows by use case, scan actions for resets before any run.** Two seeding runs on public template apps: seeding raised journey passes 33 -> 58 and 20 -> 51 of the same sets (FAIL 93 -> 68, 163 -> 132; INVALID unchanged), and a "Reset all demodata" journey wiped accounts so 104 of 127 journeys reported `The username or password you entered is incorrect`, identical on empty and seeded databases. Skill gains three questions a seed must answer, a use-case checklist, a destructive-action scan, a generic traps table with symptoms, measured expectations and a denominator checklist; routing trigger now fires on seed/test/demo data and empty pages. Independent review fixes: object ids follow the Mendix bigint pattern with a business-key guard (no uuids), associations are seeded where the page's path stores them, four new traps (enum names, inheritance chain, calculated attributes, view/external entities), and the port-offset and container-label traps moved to `harness-architecture.md`. — MendixMau diff --git a/skills/testing-shape.md b/skills/testing-shape.md index aecd3e70..ec0f1fb9 100644 --- a/skills/testing-shape.md +++ b/skills/testing-shape.md @@ -209,8 +209,8 @@ so the next person argues with the incident rather than with the rule. | **UI** | **A logout helper that reports success without ending the session** turns a licence cap into a phantom product failure. Closing the browser or clearing cookies does not end a Mendix session; nor, measured 2026-09-12 on Mendix 11.14, does `POST /xas/ {action:'logout'}` — it answers **HTTP 200 and the session survives**. A helper returning `status === 200` therefore reported a clean logout it had never once performed. Sessions accumulated across runs until the trial cap refused the next login, and the runtime says so (`Maximum number of sessions exceeded! (You are currently using a trial license)` in the runtime log) while the browser shows only a generic "Sign in failed" — so the suite blamed the credentials, then the feature. The same suite leaked a second session per run at every **persona switch**, because switching identity called the login helper again and orphaned the session already on the server. | Call `mx.logout()` — the client's own call — and **return the verified outcome, not a status code**: a login form back on screen. Log out before every persona switch, not only at the end of the run. Prove it with the one check that cannot be faked: `SELECT COUNT(*) FROM System.Session` before and after a login/logout cycle, and again after a whole suite run. Zero leaked sessions is the bar; a suite you cannot run twice without restarting the runtime is leaking. | | **UI** | A login helper **silently falls back to an admin account** (a guard against lockout) — a role-scoped test then proves nothing, and neither the run nor the report looks any different (2026-08-05) | Any spec whose result depends on *who* is logged in must **assert the actual identity**, not merely that login succeeded | | **UI** | A helper **no-ops without erroring** when its expected page is absent, so the spec runs to completion against the wrong page (2026-08-05, a mobile scanning spec) | Helpers must **throw** when a precondition is missing, never return quietly. Dump live page state rather than trusting the assertion's own label. | -| **UI** | A full `page.goto(baseUrl)` **silently logs the session out** once security is above `Off`. Suites that log in once and then "reset navigation" with a goto bounce to `/login.html` and every downstream assertion breaks for the wrong reason (2026-08-15, after raising `Off` → `Prototype`) | Fix at the helper, not the call site: the shared navigate-home helper detects the login form (`#usernameInput`) and **re-authenticates every time**. Never trust a cookie across `page.goto`. | -| **UI** | **False red: a correct access block reads as a sign-out.** A signed-in user opening a page their role may not open is redirected to `login.html?messageCode=403`, and the stock login page shows **no message**. The session is kept (same `XASSESSIONID`; `/` loads the user's home again). A crawler or wrong-role spec that treats "landed on login.html" as "session lost" reports a security failure over a correct block, and a human calls it "the app signs me out" (2026-10-14, Mendix 11.15, [#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)) | Classify `messageCode=403` as **blocked, session kept**, separately from a real sign-out, then prove the session: navigate to `/` and assert not-on-login plus menu present. Before calling any sign-out a product defect, check the runtime log for a restart at that time: every `mxcli run` restart ends every session ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)). | +| **UI** | A full `page.goto(baseUrl)` **silently logs the session out** once security is above `Off`. Suites that log in once and then "reset navigation" with a goto bounce to `/login.html` and every downstream assertion breaks for the wrong reason (2026-08-15, after raising `Off` → `Prototype`) | Fix at the helper, not the call site: the shared navigate-home helper detects the login form (`#usernameInput`) and **re-authenticates every time**. Never trust a cookie across `page.goto`. Unconfirmed: this bounce may have been the 403 one in the next row, misread (the record does not say whether the URL carried `messageCode=403`). | +| **UI** | **False red: a correct access block reads as a sign-out.** A signed-in user opening a page their role may not open is redirected to `login.html?messageCode=403`, and the stock login page shows **no message**. The session is kept (same `XASSESSIONID`; a full `page.goto('/')` then loads the user's home with no login). A crawler or wrong-role spec that treats "landed on login.html" as "session lost" reports a security failure over a correct block, and a human calls it "the app signs me out" (2026-10-14, Mendix 11.15, [#338](https://github.com/MendixMau/mxcli-project-toolkit/issues/338)) | Classify `messageCode=403` as **blocked, session kept**, separately from a real sign-out, then prove the session with that probe: `page.goto('/')` and assert not-on-login plus menu present. A login form there is a real sign-out; the row above's re-authenticating helper still applies. Before calling any sign-out a product defect, check the runtime log for a restart at that time ([#337](https://github.com/MendixMau/mxcli-project-toolkit/issues/337)). | | **UI** | **False red: an instrument pinned to one widget name or a typed menu list fails after a redesign.** A session-alive probe that waited for one named home grid reported **6/7 "SESSION DID NOT SURVIVE"** after a home redesign moved that grid to a second tab; with the selector widened, 7/7 PASS, and the blocks had been correct throughout. A nav-crawl comparing menus to a hardcoded EXPECTED list went **7/34**: all 27 FAILs were expectation rows (20 MISSING, 6 EXTRA), none an error or a sign-out (2026-10-14, [#339](https://github.com/MendixMau/mxcli-project-toolkit/issues/339)) | A session-alive check asserts **not on `login.html` and the menu bar present**, never one page's widget. Menu expectations are **derived from the model** at test start (`list navigation menu` plus the role grants), never typed. The tell: every FAIL has the same reason, and it names the instrument's own expectation. | | **UI** | `:has-text(...)` is a **Playwright-only** pseudo-class. Inside `page.evaluate` it reaches `document.querySelector`, which throws `SyntaxError`; generic `.catch()` swallows it, so a `has_widget`-style helper evaluates **permanently false** and a visible row reads as absent (2026-08-15) | In `evaluate` context use a plain `.textContent.includes(...)` scan. Reserve `:has-text` / `:visible` for real Playwright locator strings. | | **UI** | `display: contents` elements always report `offsetParent === null` — so an `offsetParent !== null` visibility filter returns **empty** for a Data Grid 2 the user is actively looking at (browser layout quirk, not a Mendix bug) | Check the ancestor tabpanel's own `getComputedStyle(...).display`, never the row's `offsetParent`. | diff --git a/skills/ui-loop.md b/skills/ui-loop.md index 6c1e8c93..8076ab92 100644 --- a/skills/ui-loop.md +++ b/skills/ui-loop.md @@ -239,11 +239,13 @@ started it>'` once; it is remembered after that. `--dry-run` says what it would app uses `bin/test-stack-up.sh --restart` instead. This is the one restart an implementer may run without the lead (`dispatching.md`). -**A restart signs every user out** (sessions live in the runtime's memory), and so does the -trial licence's own 2–4 h stop. If a human may be using the app, say so in chat **before** -restarting. When a human reports "it keeps signing me out", compare the runtime log's -`successfully started` lines with the times first: one field app showed 7 starts in ~90 min of -theme work, which was the whole report (#337). +**Under `mxcli run --local`, restarts matched every "it signs me out" report** in one field app, +and the trial licence's own 2–4 h stop is a restart too. If a human may be using the app, say so +in chat **before** restarting. When a human reports "it keeps signing me out", compare the runtime +log's `successfully started` lines with the times first: 7 starts in ~90 min of theme work lined up +with the reports (#337). That is a **timing match**; no session table was inspected, and sessions +do persist in `System.Session` across a container restart (`measured-claims.md`), so the +mechanism is open. ## When it finds something — which side is actually wrong