From bdb2001bb24658bb331fb2971e531546b5c84ac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 07:04:07 +0000 Subject: [PATCH 1/6] new(lint): UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned Two Starlark rules delivered through the existing lint-rules install/refresh path (install-manifest MXTK_LINT_RULES, README row each, delivery fixture count 4 -> 6). UX001 flags a button that deletes directly (Forms$DeleteClientAction has no confirmation setting) or calls a microflow/nanoflow without "Ask confirmation" when that flow deletes objects (bounded call-graph walk) or is named as an irreversible step (Approve/Reject/Submit/Archive/...). UX002 flags decisions left on their auto-generated caption and flows of 8+ top-level activities with neither an annotation nor a documentation text. Both need mxcli >= 0.25.0 (widget action_type/has_confirmation, activity caption/auto_generate_caption) and emit a `_rule` blindness finding on anything older, so lint-gate.sh never reads them as a clean pass. Vocabulary was read from the mxcli catalog builder source, not from the bundled API guide. No mxcli binary was reachable in the authoring session, so the first field calibration on a scratch copy of a real model is still owed; the README note says so and names the probe queries. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/lib/install-manifest.sh | 2 +- lint-rules/README.md | 11 + .../ux001_confirm_before_destructive.star | 203 ++++++++++++++++++ lint-rules/ux002_decisions_captioned.star | 125 +++++++++++ tests/test-lint-delivery.sh | 2 +- 6 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 lint-rules/ux001_confirm_before_destructive.star create mode 100644 lint-rules/ux002_decisions_captioned.star diff --git a/CHANGELOG.md b/CHANGELOG.md index df3d67a9..98a41e98 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 +- new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — 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/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index 4bb29c67..66df2248 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -150,7 +150,7 @@ MXTK_PROJECT_BIN_NOINSTALL="report-disposition-check.sh" # gate-check.sh runs i # project that re-runs init loses these fixes and its lint gate goes quietly blind. Re-running # sync-project.sh restores them; lint-rules/STOCK-HASHES.txt is what lets it tell a # reverted-to-stock file (safe to restore) from one a project deliberately tuned (hands off). -MXTK_LINT_RULES="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star" +MXTK_LINT_RULES="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star ux001_confirm_before_destructive.star ux002_decisions_captioned.star" MXTK_LINT_RULES_CONFIGURABLE="conv020_action_user_feedback.star" # Files in lint-rules/ deliberately NOT installed. Same contract as MXTK_PROJECT_BIN_NOINSTALL: diff --git a/lint-rules/README.md b/lint-rules/README.md index c6182c00..8a48eb8c 100644 --- a/lint-rules/README.md +++ b/lint-rules/README.md @@ -10,6 +10,8 @@ and configure the marked constants — **after** running `mxcli init`, never bef | `conv020_action_user_feedback.star` | Page-triggered microflows that commit / import / delete but tell the user nothing | **yes** — `PROJECT_MODULES`, and `ACTION_PREFIX` if your prefixes differ | | `data_change_microflows.star` | ARCH002 — persistent entities written from pages instead of microflows | no | | `entity_business_key.star` | ARCH003 — persistent entities with no UNIQUE NOT NULL business key | no | +| `ux001_confirm_before_destructive.star` | UX001 — a button that deletes, approves, rejects, submits or archives (direct delete action, or a flow that deletes / is named so) without "Ask confirmation" | `DESTRUCTIVE_NAME_PATTERN` if your verbs differ | +| `ux002_decisions_captioned.star` | UX002 — decisions (`ExclusiveSplit`) with an auto-generated caption instead of the question they answer; flows of 8+ activities with neither an annotation nor a documentation text | `ANNOTATE_FROM` | `data_change_microflows` and `entity_business_key` are **repaired copies of rules `mxcli init` already seeds**, not new ones. Both shipped comparing `entity.entity_type` to `"PERSISTENT"` @@ -24,6 +26,15 @@ while it was dead. It now carries the same `SKIP_MODULES` list, and the same pro are excluded at the gate (`bin/lint-gate.sh -e`, from a per-project `.claude/lint-vendor-modules.txt`), not hardcoded in a shared rule. +`ux001` and `ux002` read widget `action_type` / `has_confirmation` and activity +`caption` / `auto_generate_caption`, which exist from **mxcli 0.25.0** (catalog schema with +widget actions and activity captions). On an older binary both rules emit a single `_rule` +finding ("needs mxcli >= 0.25.0") and nothing else — `bin/lint-gate.sh` reads that as blindness, +never as a clean pass. Their vocabulary (`Forms$DeleteClientAction`, `ExclusiveSplit`, +`DeleteObjectAction`) was read from the mxcli source at 0.25.0, not from `write-lint-rules.md`; +the first field calibration is still owed — run both on a scratch copy of a real model and +record the counts here before trusting the numbers. + `conv010` here is likewise the repaired form of the rule mxcli seeds, and supersedes the version this directory shipped on 11 Aug: one violation per microflow instead of one per activity, plus four action types allowlisted. On PROJECT-C that is 399 rows -> 51 findings. diff --git a/lint-rules/ux001_confirm_before_destructive.star b/lint-rules/ux001_confirm_before_destructive.star new file mode 100644 index 00000000..be1795e5 --- /dev/null +++ b/lint-rules/ux001_confirm_before_destructive.star @@ -0,0 +1,203 @@ +# mxtk-lint-rule: ux001_confirm_before_destructive +# Shipped by mxcli-project-toolkit. Needs no per-project configuration; sync-project.sh +# refreshes it while it is unedited and reports (never overwrites) a locally edited copy. +# See lint-rules/README.md. +# --- end mxtk-lint-rule header --- +# UX001: A button that destroys or finalises must ask first +# +# A click that deletes, approves, rejects, submits or archives cannot be undone by the +# person who clicked it. Mendix has a confirmation dialog for exactly this ("Ask +# confirmation" on a microflow/nanoflow button), and the house rule is that every such +# button uses it. Three shapes are flagged, one finding per button: +# +# 1. the button uses the DELETE client action directly +# (Forms$DeleteClientAction). That action has NO confirmation setting at all, so +# the only fix is to route it through a microflow that deletes, and confirm there. +# 2. the button calls a microflow or nanoflow WITHOUT confirmation, and that flow (or a +# flow it calls, bounded depth) contains a Delete objects action. +# 3. the button calls a flow WITHOUT confirmation whose NAME says it is irreversible +# (ACT_Order_Approve, ACT_RejectRequest, SUB_Submit...). Name-based, so it is the +# weakest leg; the pattern below is deliberately CamelCase-strict ("Approved" in +# ACT_ShowApprovedOrders does not match, "Approve" at a word end does). +# +# What is deliberately NOT flagged: Save/Cancel, show page, create, datasource +# microflows (they are not buttons and carry no action_type), and buttons that already +# ask. One finding per button, located at the page or snippet holding it. +# +# REQUIRES mxcli >= 0.25.0: widget action_type / has_confirmation / page_ref were added +# to the Starlark widgets() projection there (mendixlabs/mxcli#1268). On an older +# binary the rule does not go quiet: it emits a `_rule` finding saying it saw nothing. +# +# Vocabulary below was read from the mxcli catalog builder source (builder_pages.go +# widgetPrimaryAction stores the action's stored $Type verbatim; builder_microflows.go +# strips the "Microflows$" prefix from an action's type), NOT from write-lint-rules.md. +# Probe it on your model before trusting it: +# sqlite3 .mxcli/catalog.db "SELECT DISTINCT ActionType FROM widgets ORDER BY 1;" +# sqlite3 .mxcli/catalog.db "SELECT DISTINCT ActionType FROM activities ORDER BY 1;" + +RULE_ID = "UX001" +RULE_NAME = "ConfirmBeforeDestructive" +DESCRIPTION = "Buttons that delete, or call a flow that deletes or finalises, must ask for confirmation" +CATEGORY = "quality" +SEVERITY = "warning" + +REQUIRES = ["full"] + +# Platform modules are never ours to fix. Marketplace modules are excluded at the +# gate instead (bin/lint-gate.sh reads .claude/lint-vendor-modules.txt), not hardcoded here. +SKIP_MODULES = ("System", "Administration") + +# Stored client-action types on a button / on-click action (widgets.ActionType). +DIRECT_DELETE = "Forms$DeleteClientAction" +FLOW_CALLS = ("Forms$MicroflowClientAction", "Forms$CallNanoflowClientAction") + +# Microflow action types that destroy data (activities.ActionType, "Microflows$" stripped). +DESTRUCTIVE_ACTIONS = ("DeleteObjectAction",) + +# Flow names that announce an irreversible step. Anchored to a name segment start and a +# CamelCase/segment end, so "Approved", "Deleted", "Submitted" never match. +DESTRUCTIVE_NAME_PATTERN = "(^|_)(Delete|Remove|Approve|Reject|Submit|Archive|Finalize|Finalise|Publish|Withdraw|Revoke|Discard|Purge)($|_|[A-Z0-9])" + +MAX_CALL_DEPTH = 3 + + +def _action_types(qname, cache): + """Action types directly inside one flow.""" + if qname in cache: + return cache[qname] + found = [] + for act in activities_for(qname): + if act.action_type: + found.append(act.action_type) + cache[qname] = found + return found + + +def _callees(qname, cache): + """Flows called by this one (microflows and nanoflows).""" + if qname in cache: + return cache[qname] + found = [] + for ref in refs_from(qname): + if ref.target_type in ("MICROFLOW", "NANOFLOW") and ref.target_name != qname: + found.append(ref.target_name) + cache[qname] = found + return found + + +def _destroys(qname, act_cache, call_cache): + """True when the flow, or anything it calls within MAX_CALL_DEPTH, deletes objects. + Starlark has no recursion, so the call graph is walked with a bounded frontier.""" + seen = {qname: True} + frontier = [qname] + for _ in range(MAX_CALL_DEPTH): + next_frontier = [] + for q in frontier: + for a in _action_types(q, act_cache): + if a in DESTRUCTIVE_ACTIONS: + return True + for callee in _callees(q, call_cache): + if callee not in seen: + seen[callee] = True + next_frontier.append(callee) + frontier = next_frontier + if not frontier: + break + return False + + +def _doc_type(container_type): + # container_type is UPPERCASE in the API ("PAGE" / "SNIPPET"), like every other *_type. + if container_type == "SNIPPET": + return "Snippet" + return "Page" + + +def check(): + violations = [] + act_cache = {} + call_cache = {} + widgets_in_scope = 0 + widgets_with_action = 0 + api_missing = False + + for w in widgets(): + if w.module_name in SKIP_MODULES: + continue + widgets_in_scope += 1 + + # getattr, not w.action_type: on a pre-0.25.0 binary the attribute does not exist + # and a bare access aborts the rule with "struct has no .action_type attribute", + # which the gate would then baseline as a crash rather than a finding. + at = getattr(w, "action_type", None) + if at == None: + api_missing = True + break + if not at: + continue + widgets_with_action += 1 + + loc = location( + module=w.module_name, + document_type=_doc_type(w.container_type), + document_name=w.container_qualified_name, + ) + + if at == DIRECT_DELETE: + violations.append(violation( + message="Button '{}' on {} deletes directly (Forms$DeleteClientAction), which has no confirmation setting.".format( + w.name, w.container_qualified_name + ), + location=loc, + suggestion="Call a microflow that deletes the object instead, and tick 'Ask confirmation' on the button (ACTIONBUTTON ... (Action: MICROFLOW Mod.ACT_X_Delete(...), Confirmation: 'Delete this ...?')).", + )) + continue + + if at not in FLOW_CALLS: + continue + if getattr(w, "has_confirmation", False): + continue + + target = w.microflow_ref + if not target: + target = getattr(w, "nanoflow_ref", "") + if not target: + continue + + why = "" + if _destroys(target, act_cache, call_cache): + why = "deletes objects (directly or through a called flow)" + elif matches(target.split(".")[-1], DESTRUCTIVE_NAME_PATTERN): + why = "is named as an irreversible step" + if not why: + continue + + violations.append(violation( + message="Button '{}' on {} calls {} without confirmation, and that flow {}.".format( + w.name, w.container_qualified_name, target, why + ), + location=loc, + suggestion="Add a confirmation to the button (Confirmation: '...?' on the ACTIONBUTTON, or 'Ask confirmation' in Studio Pro) saying what will happen and that it cannot be undone.", + )) + + # SELF-CHECKS. A rule that cannot see must say so, never report a clean pass. + if api_missing: + violations.append(violation( + message="UX001 cannot run on this mxcli: widgets() has no action_type/has_confirmation (added in mxcli 0.25.0). This rule inspected NOTHING.", + location=location(module="_rule", document_type="Page", document_name="UX001"), + suggestion="Upgrade mxcli to 0.25.0 or later, or remove this rule deliberately rather than leaving it inert.", + )) + elif widgets_in_scope == 0: + violations.append(violation( + message="UX001 saw no widgets outside System/Administration. widgets() needs the FULL catalog; this rule inspected NOTHING.", + location=location(module="_rule", document_type="Page", document_name="UX001"), + suggestion="Run `REFRESH CATALOG FULL` (mxcli lint should request it itself from 0.24 on) and lint again.", + )) + elif widgets_with_action == 0: + violations.append(violation( + message="UX001 saw {} widgets and not one reports an action type, so no button was inspected. The catalog is probably from an older schema.".format(widgets_in_scope), + location=location(module="_rule", document_type="Page", document_name="UX001"), + suggestion="Run `REFRESH CATALOG FULL` on mxcli >= 0.25.0 so the widgets table carries ActionType, then lint again.", + )) + + return violations diff --git a/lint-rules/ux002_decisions_captioned.star b/lint-rules/ux002_decisions_captioned.star new file mode 100644 index 00000000..e5281bbd --- /dev/null +++ b/lint-rules/ux002_decisions_captioned.star @@ -0,0 +1,125 @@ +# mxtk-lint-rule: ux002_decisions_captioned +# Shipped by mxcli-project-toolkit. Needs no per-project configuration; sync-project.sh +# refreshes it while it is unedited and reports (never overwrites) a locally edited copy. +# See lint-rules/README.md. +# --- end mxtk-lint-rule header --- +# UX002: Decisions carry a question, long flows carry an annotation +# +# The next reader of a microflow is a reviewer, a tester, or the next session's agent, +# and none of them can run it in their head. Two things make a flow readable on the +# canvas and in DESCRIBE MICROFLOW output, and both are cheap at write time and +# expensive to add later: +# +# 1. every decision (ExclusiveSplit) has its own caption, written as the question it +# answers ("Order already approved?"), not the auto-generated expression. In MDL: +# @caption 'Order already approved?' +# IF $Order/Status = 'Approved' THEN ... +# 2. a flow with ANNOTATE_FROM or more top-level activities has at least one +# annotation or a documentation text saying what the flow is for. In MDL: +# @annotation 'Rejects the order, notifies the requester, closes the task.' +# before an activity, or a /** ... */ doc block before CREATE MICROFLOW. +# +# One finding per microflow per leg (a 30-activity flow with 6 bare decisions is two +# findings, not seven), so the ratchet in bin/lint-gate.sh moves one step per flow. +# Rules (RULE microflow_type) are skipped: they are expressions, not narratives. +# +# REQUIRES mxcli >= 0.25.0: activity caption / auto_generate_caption / description +# (mendixlabs/mxcli#1266, #1267). Before that every activity's caption was the +# placeholder "Activity". On an older binary the rule emits a `_rule` finding instead +# of a silent clean pass. +# +# Vocabulary: activity_type "ExclusiveSplit" / "Annotation" are the names +# getMicroflowObjectType() in mxcli's catalog builder returns (builder_microflows.go); +# microflow_type is UPPERCASE ("MICROFLOW" / "NANOFLOW" / "RULE"), verified on 0.24.0 +# (skills/lint-that-actually-runs.md). activities_for() is TOP-LEVEL only: decisions +# inside a loop body are not seen (documented limitation, same as CONV009). + +RULE_ID = "UX002" +RULE_NAME = "DecisionsCaptioned" +DESCRIPTION = "Decisions need a caption written as a question; flows of 8+ activities need an annotation or documentation" +CATEGORY = "quality" +SEVERITY = "warning" + +REQUIRES = ["full"] + +SKIP_MODULES = ("System", "Administration") +FLOW_TYPES = ("MICROFLOW", "NANOFLOW") + +# Top-level activity count from which a flow owes the reader an annotation. +ANNOTATE_FROM = 8 + + +def check(): + violations = [] + inspected = 0 + saw_any_activity = False + api_missing = False + + for mf in microflows(): + if mf.module_name in SKIP_MODULES: + continue + if mf.microflow_type not in FLOW_TYPES: + continue + inspected += 1 + + acts = activities_for(mf.qualified_name) + if not acts: + continue + saw_any_activity = True + + bare_decisions = 0 + has_annotation = False + for act in acts: + if act.activity_type == "Annotation": + has_annotation = True + continue + if act.activity_type != "ExclusiveSplit": + continue + auto = getattr(act, "auto_generate_caption", None) + if auto == None: + api_missing = True + break + if auto or act.caption.strip() == "": + bare_decisions += 1 + if api_missing: + break + + loc = location( + module=mf.module_name, + document_type="Microflow", + document_name=mf.qualified_name, + ) + + if bare_decisions > 0: + violations.append(violation( + message="'{}' has {} decision(s) with no caption of their own (auto-generated or empty).".format( + mf.name, bare_decisions + ), + location=loc, + suggestion="Give each decision a caption written as the question it answers: `@caption 'Already approved?'` before the IF in MDL, or the Caption field in Studio Pro.", + )) + + if len(acts) >= ANNOTATE_FROM and not has_annotation and mf.description.strip() == "": + violations.append(violation( + message="'{}' has {} top-level activities, no annotation and no documentation.".format( + mf.name, len(acts) + ), + location=loc, + suggestion="Add `@annotation '...'` before the first activity saying what the flow does and for whom, or a /** ... */ documentation block before CREATE MICROFLOW.", + )) + + # SELF-CHECKS. A rule that cannot see must say so, never report a clean pass. + if api_missing: + violations.append(violation( + message="UX002 cannot run on this mxcli: activities have no auto_generate_caption (added in mxcli 0.25.0). This rule inspected NOTHING.", + location=location(module="_rule", document_type="Microflow", document_name="UX002"), + suggestion="Upgrade mxcli to 0.25.0 or later, or remove this rule deliberately rather than leaving it inert.", + )) + elif inspected > 0 and not saw_any_activity: + violations.append(violation( + message="UX002 could not read any microflow activities ({} flows inspected, all reported zero). activities_for() is returning nothing -- this rule checked NOTHING.".format(inspected), + location=location(module="_rule", document_type="Microflow", document_name="UX002"), + suggestion="Run `REFRESH CATALOG FULL` and lint again; if it stays empty, this mxcli build does not populate activities_for().", + )) + + return violations diff --git a/tests/test-lint-delivery.sh b/tests/test-lint-delivery.sh index 2e0c457d..0a306d2f 100755 --- a/tests/test-lint-delivery.sh +++ b/tests/test-lint-delivery.sh @@ -104,7 +104,7 @@ echo "== 10 caller forgot the manifest: the lib sources it itself ==" P4="$W/proj4"; mkdir -p "$P4/.claude"; echo x > "$P4/PROJECT.md" bash -c "set -u; . '$W/tk/bin/lib/install-lint-rules.sh'; \ mxtk_install_lint_rules '$W/tk' '$P4' 0 ''" >/dev/null 2>&1 -[ "$(ls "$P4/.claude/lint-rules" 2>/dev/null | wc -l | tr -d ' ')" = 4 ] \ +[ "$(ls "$P4/.claude/lint-rules" 2>/dev/null | wc -l | tr -d ' ')" = 6 ] \ && ok "rules landed anyway" || no "silent no-op" echo "== 11 manifest genuinely unavailable: warn, never pretend success ==" From 86d760998bee35d5c03e4f47127e4a70c624af0c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 07:53:24 +0000 Subject: [PATCH 2/6] test(lint): unit suite for UX001/UX002 under the real Starlark interpreter tests/lint-rules/ runs both rules through go.starlark.net at mxcli's pinned version via a small Go harness (starrun) that rebuilds the mxcli projection structs from JSON fixtures and rejects any field the projection does not have. 33 hand-written fixtures cover self-checks, call depth on both sides of the bound, cycles, the destructive-name pattern, module skips, snippet locations and the version probe; a generated 3000-flow / 20000-widget model times each rule (~0.7 s). 35 cases, wired into CI as its own step. The suite caught two defects in UX002 before they shipped: StartEvent/EndEvent objects returned by activities_for() inflated the activity count, and the "needs mxcli >= 0.25.0" probe only fired on flows that had a decision. Both fixed here; UX001's depth comment now states what the bound means. Fixtures encode the projection as read from the mxcli source, not captured output: they prove the rule logic, not the catalog vocabulary. Field calibration on a real model is still owed and the README still says so. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- .github/workflows/checks.yml | 7 + CHANGELOG.md | 2 +- lint-rules/README.md | 8 +- .../ux001_confirm_before_destructive.star | 2 + lint-rules/ux002_decisions_captioned.star | 32 +- .../fixtures/ux001-a-direct-delete.json | 14 + .../fixtures/ux001-b-confirmed.json | 28 ++ .../fixtures/ux001-c-flow-deletes.json | 27 ++ tests/lint-rules/fixtures/ux001-d-depth2.json | 39 +++ tests/lint-rules/fixtures/ux001-e-depth4.json | 59 ++++ .../fixtures/ux001-f-name-irreversible.json | 15 + .../fixtures/ux001-g-name-camelcase.json | 15 + .../lint-rules/fixtures/ux001-h-nanoflow.json | 15 + .../fixtures/ux001-i-system-module.json | 25 ++ .../fixtures/ux001-i2-system-only.json | 14 + .../fixtures/ux001-j-no-flow-actions.json | 25 ++ .../lint-rules/fixtures/ux001-k-snippet.json | 14 + tests/lint-rules/fixtures/ux001-l-cycle.json | 37 +++ tests/lint-rules/fixtures/ux001-m-strip.json | 24 ++ tests/lint-rules/fixtures/ux001-n-empty.json | 3 + .../fixtures/ux001-o-no-action-types.json | 24 ++ .../fixtures/ux001-p-locations.json | 48 +++ .../lint-rules/fixtures/ux002-a-two-bare.json | 32 ++ .../fixtures/ux002-b-captioned.json | 24 ++ .../fixtures/ux002-c-blank-caption.json | 24 ++ tests/lint-rules/fixtures/ux002-d-eight.json | 80 +++++ tests/lint-rules/fixtures/ux002-e-seven.json | 72 +++++ .../fixtures/ux002-f-eight-annotated.json | 80 +++++ .../fixtures/ux002-g-eight-described.json | 80 +++++ tests/lint-rules/fixtures/ux002-h-rule.json | 24 ++ .../fixtures/ux002-i-administration.json | 24 ++ .../fixtures/ux002-j-both-legs.json | 88 ++++++ tests/lint-rules/fixtures/ux002-k-strip.json | 24 ++ .../fixtures/ux002-l-no-activities.json | 29 ++ .../fixtures/ux002-m-no-microflows.json | 4 + .../fixtures/ux002-n-events-counted.json | 96 ++++++ .../fixtures/ux002-o-events-not-counted.json | 88 ++++++ .../fixtures/ux002-p-strip-no-decision.json | 48 +++ tests/lint-rules/gen-perf-fixture.py | 65 +++++ tests/lint-rules/starrun/go.mod | 7 + tests/lint-rules/starrun/go.sum | 8 + tests/lint-rules/starrun/main.go | 275 ++++++++++++++++++ tests/lint-rules/test-ux-rules.sh | 163 +++++++++++ 43 files changed, 1798 insertions(+), 14 deletions(-) create mode 100644 tests/lint-rules/fixtures/ux001-a-direct-delete.json create mode 100644 tests/lint-rules/fixtures/ux001-b-confirmed.json create mode 100644 tests/lint-rules/fixtures/ux001-c-flow-deletes.json create mode 100644 tests/lint-rules/fixtures/ux001-d-depth2.json create mode 100644 tests/lint-rules/fixtures/ux001-e-depth4.json create mode 100644 tests/lint-rules/fixtures/ux001-f-name-irreversible.json create mode 100644 tests/lint-rules/fixtures/ux001-g-name-camelcase.json create mode 100644 tests/lint-rules/fixtures/ux001-h-nanoflow.json create mode 100644 tests/lint-rules/fixtures/ux001-i-system-module.json create mode 100644 tests/lint-rules/fixtures/ux001-i2-system-only.json create mode 100644 tests/lint-rules/fixtures/ux001-j-no-flow-actions.json create mode 100644 tests/lint-rules/fixtures/ux001-k-snippet.json create mode 100644 tests/lint-rules/fixtures/ux001-l-cycle.json create mode 100644 tests/lint-rules/fixtures/ux001-m-strip.json create mode 100644 tests/lint-rules/fixtures/ux001-n-empty.json create mode 100644 tests/lint-rules/fixtures/ux001-o-no-action-types.json create mode 100644 tests/lint-rules/fixtures/ux001-p-locations.json create mode 100644 tests/lint-rules/fixtures/ux002-a-two-bare.json create mode 100644 tests/lint-rules/fixtures/ux002-b-captioned.json create mode 100644 tests/lint-rules/fixtures/ux002-c-blank-caption.json create mode 100644 tests/lint-rules/fixtures/ux002-d-eight.json create mode 100644 tests/lint-rules/fixtures/ux002-e-seven.json create mode 100644 tests/lint-rules/fixtures/ux002-f-eight-annotated.json create mode 100644 tests/lint-rules/fixtures/ux002-g-eight-described.json create mode 100644 tests/lint-rules/fixtures/ux002-h-rule.json create mode 100644 tests/lint-rules/fixtures/ux002-i-administration.json create mode 100644 tests/lint-rules/fixtures/ux002-j-both-legs.json create mode 100644 tests/lint-rules/fixtures/ux002-k-strip.json create mode 100644 tests/lint-rules/fixtures/ux002-l-no-activities.json create mode 100644 tests/lint-rules/fixtures/ux002-m-no-microflows.json create mode 100644 tests/lint-rules/fixtures/ux002-n-events-counted.json create mode 100644 tests/lint-rules/fixtures/ux002-o-events-not-counted.json create mode 100644 tests/lint-rules/fixtures/ux002-p-strip-no-decision.json create mode 100755 tests/lint-rules/gen-perf-fixture.py create mode 100644 tests/lint-rules/starrun/go.mod create mode 100644 tests/lint-rules/starrun/go.sum create mode 100644 tests/lint-rules/starrun/main.go create mode 100755 tests/lint-rules/test-ux-rules.sh diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cf17e2fc..ecb3ac1d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -85,3 +85,10 @@ jobs: bash "$t" || { echo "FAILED: $t"; rc=1; } done exit $rc + + # Lint-rule unit suite. Added 2026-10-08 with UX001/UX002: executes the Starlark rules + # under go.starlark.net at mxcli's pinned version (tests/lint-rules/starrun, built here + # with the runner's Go), so rule logic is proven before any field calibration. Not under + # the glob above because it needs Go and lives in its own directory. + - name: Lint-rule unit suite (tests/lint-rules/test-ux-rules.sh) + run: bash tests/lint-rules/test-ux-rules.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a41e98..54a19529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased -- new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — MendixMau +- new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Logic is unit-tested under the real Starlark interpreter: `tests/lint-rules/test-ux-rules.sh` (35 cases incl. a synthetic 3000-flow perf run, wired into CI) — the suite caught two defects before they shipped: start/end events inflated UX002's activity count, and its version probe only fired on flows that had a decision. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — 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/lint-rules/README.md b/lint-rules/README.md index 8a48eb8c..d592d429 100644 --- a/lint-rules/README.md +++ b/lint-rules/README.md @@ -33,7 +33,13 @@ finding ("needs mxcli >= 0.25.0") and nothing else — `bin/lint-gate.sh` reads never as a clean pass. Their vocabulary (`Forms$DeleteClientAction`, `ExclusiveSplit`, `DeleteObjectAction`) was read from the mxcli source at 0.25.0, not from `write-lint-rules.md`; the first field calibration is still owed — run both on a scratch copy of a real model and -record the counts here before trusting the numbers. +record the counts here before trusting the numbers. Their *logic* is proven: `tests/lint-rules/` +runs both under the real Starlark interpreter (`go.starlark.net` at mxcli's pinned version) over +33 hand-written fixtures — self-checks, call depth both sides of the bound, cycles, the name +pattern, module skips, snippet locations, start/end events not counted — plus a synthetic +3000-flow / 20000-widget model each rule finishes in under a second. Fixtures encode the +projection as read from the mxcli source; they are not captured output, so they prove the +rule, not the vocabulary. `conv010` here is likewise the repaired form of the rule mxcli seeds, and supersedes the version this directory shipped on 11 Aug: one violation per microflow instead of one per diff --git a/lint-rules/ux001_confirm_before_destructive.star b/lint-rules/ux001_confirm_before_destructive.star index be1795e5..1bffaf6f 100644 --- a/lint-rules/ux001_confirm_before_destructive.star +++ b/lint-rules/ux001_confirm_before_destructive.star @@ -58,6 +58,8 @@ DESTRUCTIVE_ACTIONS = ("DeleteObjectAction",) # CamelCase/segment end, so "Approved", "Deleted", "Submitted" never match. DESTRUCTIVE_NAME_PATTERN = "(^|_)(Delete|Remove|Approve|Reject|Submit|Archive|Finalize|Finalise|Publish|Withdraw|Revoke|Discard|Purge)($|_|[A-Z0-9])" +# Flows walked from the button: the target, what it calls, what that calls (3 flows deep). +# A delete four flows away is not seen; the unit suite pins both sides of that line. MAX_CALL_DEPTH = 3 diff --git a/lint-rules/ux002_decisions_captioned.star b/lint-rules/ux002_decisions_captioned.star index e5281bbd..eaddbd7c 100644 --- a/lint-rules/ux002_decisions_captioned.star +++ b/lint-rules/ux002_decisions_captioned.star @@ -14,7 +14,8 @@ # answers ("Order already approved?"), not the auto-generated expression. In MDL: # @caption 'Order already approved?' # IF $Order/Status = 'Approved' THEN ... -# 2. a flow with ANNOTATE_FROM or more top-level activities has at least one +# 2. a flow with ANNOTATE_FROM or more top-level activities (start/end events and +# annotations themselves not counted) has at least one # annotation or a documentation text saying what the flow is for. In MDL: # @annotation 'Rejects the order, notifies the requester, closes the task.' # before an activity, or a /** ... */ doc block before CREATE MICROFLOW. @@ -32,7 +33,9 @@ # getMicroflowObjectType() in mxcli's catalog builder returns (builder_microflows.go); # microflow_type is UPPERCASE ("MICROFLOW" / "NANOFLOW" / "RULE"), verified on 0.24.0 # (skills/lint-that-actually-runs.md). activities_for() is TOP-LEVEL only: decisions -# inside a loop body are not seen (documented limitation, same as CONV009). +# inside a loop body are not seen (documented limitation, same as CONV009). It also +# returns the StartEvent and EndEvent objects, which are not activities and are not +# counted towards ANNOTATE_FROM (found by the unit suite, tests/lint-rules). RULE_ID = "UX002" RULE_NAME = "DecisionsCaptioned" @@ -48,6 +51,9 @@ FLOW_TYPES = ("MICROFLOW", "NANOFLOW") # Top-level activity count from which a flow owes the reader an annotation. ANNOTATE_FROM = 8 +# Objects activities_for() returns that are not activities to a reader. +NOT_COUNTED = ("StartEvent", "EndEvent", "Annotation") + def check(): violations = [] @@ -67,22 +73,24 @@ def check(): continue saw_any_activity = True + # Version probe on the FIRST object of any kind, not only on a decision: a flow + # with no decisions on an old binary must still produce the blindness finding. + if not hasattr(acts[0], "auto_generate_caption"): + api_missing = True + break + bare_decisions = 0 has_annotation = False + counted = 0 for act in acts: if act.activity_type == "Annotation": has_annotation = True - continue + if act.activity_type not in NOT_COUNTED: + counted += 1 if act.activity_type != "ExclusiveSplit": continue - auto = getattr(act, "auto_generate_caption", None) - if auto == None: - api_missing = True - break - if auto or act.caption.strip() == "": + if act.auto_generate_caption or act.caption.strip() == "": bare_decisions += 1 - if api_missing: - break loc = location( module=mf.module_name, @@ -99,10 +107,10 @@ def check(): suggestion="Give each decision a caption written as the question it answers: `@caption 'Already approved?'` before the IF in MDL, or the Caption field in Studio Pro.", )) - if len(acts) >= ANNOTATE_FROM and not has_annotation and mf.description.strip() == "": + if counted >= ANNOTATE_FROM and not has_annotation and mf.description.strip() == "": violations.append(violation( message="'{}' has {} top-level activities, no annotation and no documentation.".format( - mf.name, len(acts) + mf.name, counted ), location=loc, suggestion="Add `@annotation '...'` before the first activity saying what the flow does and for whom, or a /** ... */ documentation block before CREATE MICROFLOW.", diff --git a/tests/lint-rules/fixtures/ux001-a-direct-delete.json b/tests/lint-rules/fixtures/ux001-a-direct-delete.json new file mode 100644 index 00000000..368460b2 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-a-direct-delete.json @@ -0,0 +1,14 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$DeleteClientAction" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-b-confirmed.json b/tests/lint-rules/fixtures/ux001-b-confirmed.json new file mode 100644 index 00000000..92e47957 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-b-confirmed.json @@ -0,0 +1,28 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Order_Delete", + "has_confirmation": true + } + ], + "activities": { + "Sales.ACT_Order_Delete": [ + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "DeleteObjectAction", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux001-c-flow-deletes.json b/tests/lint-rules/fixtures/ux001-c-flow-deletes.json new file mode 100644 index 00000000..5de82dbd --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-c-flow-deletes.json @@ -0,0 +1,27 @@ +{ + "widgets": [ + { + "id": "w-btnRemove", + "name": "btnRemove", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Order_Clear" + } + ], + "activities": { + "Sales.ACT_Order_Clear": [ + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "DeleteObjectAction", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux001-d-depth2.json b/tests/lint-rules/fixtures/ux001-d-depth2.json new file mode 100644 index 00000000..4c95ea3b --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-d-depth2.json @@ -0,0 +1,39 @@ +{ + "widgets": [ + { + "id": "w-btnGo", + "name": "btnGo", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Alpha" + } + ], + "refs_from": { + "Sales.ACT_Alpha": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.ACT_Alpha", + "target_type": "MICROFLOW", + "target_name": "Sales.SUB_Beta", + "ref_kind": "CALL", + "module_name": "Sales" + } + ] + }, + "activities": { + "Sales.SUB_Beta": [ + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "DeleteObjectAction", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux001-e-depth4.json b/tests/lint-rules/fixtures/ux001-e-depth4.json new file mode 100644 index 00000000..e4cd65fd --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-e-depth4.json @@ -0,0 +1,59 @@ +{ + "widgets": [ + { + "id": "w-btnGo", + "name": "btnGo", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Alpha" + } + ], + "refs_from": { + "Sales.ACT_Alpha": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.ACT_Alpha", + "target_type": "MICROFLOW", + "target_name": "Sales.SUB_Beta", + "ref_kind": "CALL", + "module_name": "Sales" + } + ], + "Sales.SUB_Beta": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.SUB_Beta", + "target_type": "MICROFLOW", + "target_name": "Sales.SUB_Gamma", + "ref_kind": "CALL", + "module_name": "Sales" + } + ], + "Sales.SUB_Gamma": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.SUB_Gamma", + "target_type": "MICROFLOW", + "target_name": "Sales.SUB_Delta", + "ref_kind": "CALL", + "module_name": "Sales" + } + ] + }, + "activities": { + "Sales.SUB_Delta": [ + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "DeleteObjectAction", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux001-f-name-irreversible.json b/tests/lint-rules/fixtures/ux001-f-name-irreversible.json new file mode 100644 index 00000000..6b519d03 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-f-name-irreversible.json @@ -0,0 +1,15 @@ +{ + "widgets": [ + { + "id": "w-btnApprove", + "name": "btnApprove", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Order_Approve" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-g-name-camelcase.json b/tests/lint-rules/fixtures/ux001-g-name-camelcase.json new file mode 100644 index 00000000..ac3048b7 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-g-name-camelcase.json @@ -0,0 +1,15 @@ +{ + "widgets": [ + { + "id": "w-btnShow", + "name": "btnShow", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_ShowApprovedOrders" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-h-nanoflow.json b/tests/lint-rules/fixtures/ux001-h-nanoflow.json new file mode 100644 index 00000000..4744b252 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-h-nanoflow.json @@ -0,0 +1,15 @@ +{ + "widgets": [ + { + "id": "w-btnReject", + "name": "btnReject", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$CallNanoflowClientAction", + "nanoflow_ref": "Sales.ACT_Reject" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-i-system-module.json b/tests/lint-rules/fixtures/ux001-i-system-module.json new file mode 100644 index 00000000..deeefd36 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-i-system-module.json @@ -0,0 +1,25 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-System.Some_Page", + "container_qualified_name": "System.Some_Page", + "container_type": "PAGE", + "module_name": "System", + "action_type": "Forms$DeleteClientAction" + }, + { + "id": "w-btnOpen", + "name": "btnOpen", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$ShowPageClientAction", + "page_ref": "Sales.Order_Edit" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-i2-system-only.json b/tests/lint-rules/fixtures/ux001-i2-system-only.json new file mode 100644 index 00000000..c425fefa --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-i2-system-only.json @@ -0,0 +1,14 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-System.Some_Page", + "container_qualified_name": "System.Some_Page", + "container_type": "PAGE", + "module_name": "System", + "action_type": "Forms$DeleteClientAction" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-j-no-flow-actions.json b/tests/lint-rules/fixtures/ux001-j-no-flow-actions.json new file mode 100644 index 00000000..6319afd6 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-j-no-flow-actions.json @@ -0,0 +1,25 @@ +{ + "widgets": [ + { + "id": "w-btnOpen", + "name": "btnOpen", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$ShowPageClientAction", + "page_ref": "Sales.Order_Edit" + }, + { + "id": "w-lblPlain", + "name": "lblPlain", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-k-snippet.json b/tests/lint-rules/fixtures/ux001-k-snippet.json new file mode 100644 index 00000000..79a7040a --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-k-snippet.json @@ -0,0 +1,14 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.SNIPPET_Actions", + "container_qualified_name": "Sales.SNIPPET_Actions", + "container_type": "SNIPPET", + "module_name": "Sales", + "action_type": "Forms$DeleteClientAction" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-l-cycle.json b/tests/lint-rules/fixtures/ux001-l-cycle.json new file mode 100644 index 00000000..0d43ff7d --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-l-cycle.json @@ -0,0 +1,37 @@ +{ + "widgets": [ + { + "id": "w-btnGo", + "name": "btnGo", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Alpha" + } + ], + "refs_from": { + "Sales.ACT_Alpha": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.ACT_Alpha", + "target_type": "MICROFLOW", + "target_name": "Sales.SUB_Beta", + "ref_kind": "CALL", + "module_name": "Sales" + } + ], + "Sales.SUB_Beta": [ + { + "source_type": "MICROFLOW", + "source_name": "Sales.SUB_Beta", + "target_type": "MICROFLOW", + "target_name": "Sales.ACT_Alpha", + "ref_kind": "CALL", + "module_name": "Sales" + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux001-m-strip.json b/tests/lint-rules/fixtures/ux001-m-strip.json new file mode 100644 index 00000000..03b3df78 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-m-strip.json @@ -0,0 +1,24 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$DeleteClientAction" + }, + { + "id": "w-btnOpen", + "name": "btnOpen", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$ShowPageClientAction" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-n-empty.json b/tests/lint-rules/fixtures/ux001-n-empty.json new file mode 100644 index 00000000..88fa1f57 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-n-empty.json @@ -0,0 +1,3 @@ +{ + "widgets": [] +} diff --git a/tests/lint-rules/fixtures/ux001-o-no-action-types.json b/tests/lint-rules/fixtures/ux001-o-no-action-types.json new file mode 100644 index 00000000..63ce6192 --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-o-no-action-types.json @@ -0,0 +1,24 @@ +{ + "widgets": [ + { + "id": "w-a", + "name": "a", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "" + }, + { + "id": "w-b", + "name": "b", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Order_Overview", + "container_qualified_name": "Sales.Order_Overview", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "" + } + ] +} diff --git a/tests/lint-rules/fixtures/ux001-p-locations.json b/tests/lint-rules/fixtures/ux001-p-locations.json new file mode 100644 index 00000000..a7ddb81e --- /dev/null +++ b/tests/lint-rules/fixtures/ux001-p-locations.json @@ -0,0 +1,48 @@ +{ + "widgets": [ + { + "id": "w-btnDelete", + "name": "btnDelete", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Orders.Order_Overview", + "container_qualified_name": "Orders.Order_Overview", + "container_type": "PAGE", + "module_name": "Orders", + "action_type": "Forms$DeleteClientAction" + }, + { + "id": "w-btnRemove", + "name": "btnRemove", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Billing.Invoice_Edit", + "container_qualified_name": "Billing.Invoice_Edit", + "container_type": "PAGE", + "module_name": "Billing", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Billing.ACT_Invoice_Clear" + }, + { + "id": "w-btnApprove", + "name": "btnApprove", + "widget_type": "ACTIONBUTTON", + "container_id": "c-Sales.Quote_View", + "container_qualified_name": "Sales.Quote_View", + "container_type": "PAGE", + "module_name": "Sales", + "action_type": "Forms$MicroflowClientAction", + "microflow_ref": "Sales.ACT_Quote_Approve" + } + ], + "activities": { + "Billing.ACT_Invoice_Clear": [ + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "DeleteObjectAction", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-a-two-bare.json b/tests/lint-rules/fixtures/ux002-a-two-bare.json new file mode 100644 index 00000000..a059c204 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-a-two-bare.json @@ -0,0 +1,32 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Sales.ACT_Check", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-b-captioned.json b/tests/lint-rules/fixtures/ux002-b-captioned.json new file mode 100644 index 00000000..a47720c6 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-b-captioned.json @@ -0,0 +1,24 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Sales.ACT_Check", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": "Already approved?", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-c-blank-caption.json b/tests/lint-rules/fixtures/ux002-c-blank-caption.json new file mode 100644 index 00000000..be8fabf6 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-c-blank-caption.json @@ -0,0 +1,24 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Sales.ACT_Check", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": " ", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-d-eight.json b/tests/lint-rules/fixtures/ux002-d-eight.json new file mode 100644 index 00000000..c90adb95 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-d-eight.json @@ -0,0 +1,80 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-e-seven.json b/tests/lint-rules/fixtures/ux002-e-seven.json new file mode 100644 index 00000000..ac7129b4 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-e-seven.json @@ -0,0 +1,72 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-f-eight-annotated.json b/tests/lint-rules/fixtures/ux002-f-eight-annotated.json new file mode 100644 index 00000000..82a8580b --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-f-eight-annotated.json @@ -0,0 +1,80 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "Annotation", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-g-eight-described.json b/tests/lint-rules/fixtures/ux002-g-eight-described.json new file mode 100644 index 00000000..d40c381d --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-g-eight-described.json @@ -0,0 +1,80 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "Rejects the order." + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-h-rule.json b/tests/lint-rules/fixtures/ux002-h-rule.json new file mode 100644 index 00000000..44e168be --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-h-rule.json @@ -0,0 +1,24 @@ +{ + "microflows": [ + { + "id": "m-RULE_IsValid", + "name": "RULE_IsValid", + "qualified_name": "Sales.RULE_IsValid", + "module_name": "Sales", + "microflow_type": "RULE", + "description": "" + } + ], + "activities": { + "Sales.RULE_IsValid": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-i-administration.json b/tests/lint-rules/fixtures/ux002-i-administration.json new file mode 100644 index 00000000..7814e22a --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-i-administration.json @@ -0,0 +1,24 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Administration.ACT_Check", + "module_name": "Administration", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Administration.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-j-both-legs.json b/tests/lint-rules/fixtures/ux002-j-both-legs.json new file mode 100644 index 00000000..19dfdfb5 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-j-both-legs.json @@ -0,0 +1,88 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + }, + { + "id": "a8", + "name": "a8", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-k-strip.json b/tests/lint-rules/fixtures/ux002-k-strip.json new file mode 100644 index 00000000..e86087d2 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-k-strip.json @@ -0,0 +1,24 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Sales.ACT_Check", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": true + } + ] + } +} diff --git a/tests/lint-rules/fixtures/ux002-l-no-activities.json b/tests/lint-rules/fixtures/ux002-l-no-activities.json new file mode 100644 index 00000000..62ade62b --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-l-no-activities.json @@ -0,0 +1,29 @@ +{ + "microflows": [ + { + "id": "m-ACT_One", + "name": "ACT_One", + "qualified_name": "Sales.ACT_One", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + }, + { + "id": "m-ACT_Two", + "name": "ACT_Two", + "qualified_name": "Sales.ACT_Two", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + }, + { + "id": "m-ACT_Three", + "name": "ACT_Three", + "qualified_name": "Sales.ACT_Three", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": {} +} diff --git a/tests/lint-rules/fixtures/ux002-m-no-microflows.json b/tests/lint-rules/fixtures/ux002-m-no-microflows.json new file mode 100644 index 00000000..1e64c2ad --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-m-no-microflows.json @@ -0,0 +1,4 @@ +{ + "microflows": [], + "activities": {} +} diff --git a/tests/lint-rules/fixtures/ux002-n-events-counted.json b/tests/lint-rules/fixtures/ux002-n-events-counted.json new file mode 100644 index 00000000..2b593549 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-n-events-counted.json @@ -0,0 +1,96 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "StartEvent", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a8", + "name": "a8", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a9", + "name": "a9", + "caption": "Step", + "activity_type": "EndEvent", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} \ No newline at end of file diff --git a/tests/lint-rules/fixtures/ux002-o-events-not-counted.json b/tests/lint-rules/fixtures/ux002-o-events-not-counted.json new file mode 100644 index 00000000..16ba945d --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-o-events-not-counted.json @@ -0,0 +1,88 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "StartEvent", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a4", + "name": "a4", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a5", + "name": "a5", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a6", + "name": "a6", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a7", + "name": "a7", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a8", + "name": "a8", + "caption": "Step", + "activity_type": "EndEvent", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} \ No newline at end of file diff --git a/tests/lint-rules/fixtures/ux002-p-strip-no-decision.json b/tests/lint-rules/fixtures/ux002-p-strip-no-decision.json new file mode 100644 index 00000000..4c8f5a78 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-p-strip-no-decision.json @@ -0,0 +1,48 @@ +{ + "microflows": [ + { + "id": "m-ACT_Big", + "name": "ACT_Big", + "qualified_name": "Sales.ACT_Big", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Big": [ + { + "id": "a0", + "name": "a0", + "caption": "Step", + "activity_type": "StartEvent", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a1", + "name": "a1", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a2", + "name": "a2", + "caption": "Step", + "activity_type": "ActionActivity", + "action_type": "", + "auto_generate_caption": false + }, + { + "id": "a3", + "name": "a3", + "caption": "Step", + "activity_type": "EndEvent", + "action_type": "", + "auto_generate_caption": false + } + ] + } +} \ No newline at end of file diff --git a/tests/lint-rules/gen-perf-fixture.py b/tests/lint-rules/gen-perf-fixture.py new file mode 100755 index 00000000..0fa64a49 --- /dev/null +++ b/tests/lint-rules/gen-perf-fixture.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""gen-perf-fixture.py -- write a synthetic fixture for timing the UX rules. + +usage: gen-perf-fixture.py OUT.json [--flows N=3000] [--widgets M=20000] [--seed S=1] + +N microflows with 10-14 activities each (2 are ExclusiveSplit), a refs_from chain of +depth 2 for every third flow, and M widgets, 10% of them microflow buttons without +confirmation pointing at random flows. Stdlib only. Synthetic, never captured output. +""" +import argparse, json, random + +ap = argparse.ArgumentParser() +ap.add_argument("out") +ap.add_argument("--flows", type=int, default=3000) +ap.add_argument("--widgets", type=int, default=20000) +ap.add_argument("--seed", type=int, default=1) +a = ap.parse_args() +rnd = random.Random(a.seed) +MODS = ["Sales", "Orders", "Billing", "Stock", "Support"] + +def q(i): + return "%s.ACT_Flow%05d" % (MODS[i % len(MODS)], i) + +flows, acts, refs = [], {}, {} +for i in range(a.flows): + n = rnd.randint(10, 14) + qn = q(i) + flows.append(dict(id="m%d" % i, name=qn.split(".")[1], qualified_name=qn, + module_name=MODS[i % len(MODS)], microflow_type="MICROFLOW", + description="", activity_count=n)) + items = [] + for k in range(n): + if k in (2, 5): + items.append(dict(id="a%d_%d" % (i, k), name="d%d" % k, caption="Decision", + activity_type="ExclusiveSplit", auto_generate_caption=True)) + else: + items.append(dict(id="a%d_%d" % (i, k), name="s%d" % k, caption="Step", + activity_type="ActionActivity", + action_type="DeleteObjectAction" if rnd.random() < 0.02 else "CommitObjectsAction" if rnd.random() < 0.2 else "")) + acts[qn] = items + if i % 3 == 0 and i + 2 < a.flows: + def r(s, t): + return dict(source_type="MICROFLOW", source_name=s, target_type="MICROFLOW", + target_name=t, ref_kind="CALL", module_name=s.split(".")[0]) + refs[qn] = [r(qn, q(i + 1))] + refs[q(i + 1)] = [r(q(i + 1), q(i + 2))] + +widgets = [] +for j in range(a.widgets): + mod = MODS[j % len(MODS)] + cont = "%s.Page%04d" % (mod, j // 20) + d = dict(id="w%d" % j, name="w%d" % j, widget_type="ACTIONBUTTON", container_id="p%d" % (j // 20), + container_qualified_name=cont, container_type="PAGE", module_name=mod) + if j % 10 == 0: + d.update(action_type="Forms$MicroflowClientAction", has_confirmation=False, + microflow_ref=q(rnd.randrange(a.flows))) + elif j % 10 < 6: + d.update(action_type="Forms$ShowPageClientAction", page_ref="Sales.Order_Edit") + else: + d.update(action_type="") + widgets.append(d) + +with open(a.out, "w") as f: + json.dump(dict(widgets=widgets, microflows=flows, activities=acts, refs_from=refs), f) +print("wrote %s: %d flows, %d widgets" % (a.out, a.flows, a.widgets)) diff --git a/tests/lint-rules/starrun/go.mod b/tests/lint-rules/starrun/go.mod new file mode 100644 index 00000000..11a69110 --- /dev/null +++ b/tests/lint-rules/starrun/go.mod @@ -0,0 +1,7 @@ +module mxtk/starrun + +go 1.22 + +require go.starlark.net v0.0.0-20260102030733-3fee463870c9 + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/tests/lint-rules/starrun/go.sum b/tests/lint-rules/starrun/go.sum new file mode 100644 index 00000000..d92a3136 --- /dev/null +++ b/tests/lint-rules/starrun/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +go.starlark.net v0.0.0-20260102030733-3fee463870c9 h1:nV1OyvU+0CYrp5eKfQ3rD03TpFYYhH08z31NK1HmtTk= +go.starlark.net v0.0.0-20260102030733-3fee463870c9/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/tests/lint-rules/starrun/main.go b/tests/lint-rules/starrun/main.go new file mode 100644 index 00000000..1ae7ac88 --- /dev/null +++ b/tests/lint-rules/starrun/main.go @@ -0,0 +1,275 @@ +// starrun — run one toolkit Starlark lint rule under the real Go Starlark interpreter +// (go.starlark.net, the engine mxcli embeds) with the mxcli builtins the rule needs +// served from a JSON fixture instead of a live catalog. +// +// Why: lint-rules/*.star have no test path that does not need an mxcli binary and a +// real .mpr. This harness proves a rule's LOGIC and its blindness self-checks against +// fixed input, using the same struct type (starlarkstruct) and the same regexp engine +// (Go RE2 via `matches`) the real linter uses, so a wrong attribute name or a +// Python-only idiom fails here the way it fails in `mxcli lint`. +// +// What it does NOT prove: the vocabulary of a real catalog (action type strings, +// casing). That is the field calibration, still owed per lint-rules/README.md. +// +// Usage: +// go run ./tests/lint-rules/starrun -rule lint-rules/x.star -fixture f.json [-strip-fields a,b] +// Prints a JSON array of {module,document_type,document_name,message,suggestion}. +// Exit 0 on success, 1 on a load/eval error (message on stderr). +// +// Fixture schema (every key optional, missing struct fields default to ""/false/0): +// {"widgets":[{...}], "microflows":[{...}], "pages":[{...}], +// "activities":{"Mod.Flow":[{...}]}, "refs_from":{"Mod.Flow":[{...}]}} +// Field names are the mxcli Starlark projection names (mdl/linter/starlark.go): +// widget: id name widget_type container_id container_qualified_name container_type +// module_name entity_ref attribute_ref microflow_ref nanoflow_ref page_ref +// parent_widget_id depth class_name style dynamic_classes action_type has_confirmation +// microflow: id name qualified_name module_name folder microflow_type description +// return_type parameter_count activity_count total_activity_count complexity +// activity: id name caption activity_type action_type microflow_id +// microflow_qualified_name module_name entity_ref service_ref action_ref +// use_request_timeout timeout_expression auto_generate_caption description +// parent_loop_id loop_depth condition_expression condition_rule error_handling_type +// log_level log_node_expression log_message commit_type with_events retrieve_source queue_ref +// reference: source_type source_id source_name target_type target_id target_name ref_kind module_name +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "regexp" + "strings" + + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +type fixture struct { + Widgets []map[string]any `json:"widgets"` + Microflows []map[string]any `json:"microflows"` + Pages []map[string]any `json:"pages"` + Activities map[string][]map[string]any `json:"activities"` + RefsFrom map[string][]map[string]any `json:"refs_from"` +} + +// Projection field lists with their zero values. Order does not matter to Starlark. +var widgetFields = fields{ + s: []string{"id", "name", "widget_type", "container_id", "container_qualified_name", "container_type", + "module_name", "entity_ref", "attribute_ref", "microflow_ref", "nanoflow_ref", "page_ref", + "parent_widget_id", "class_name", "style", "dynamic_classes", "action_type"}, + b: []string{"has_confirmation"}, + i: []string{"depth"}, +} +var microflowFields = fields{ + s: []string{"id", "name", "qualified_name", "module_name", "folder", "microflow_type", "description", + "return_type", "document_noun", "document_noun_title"}, + i: []string{"parameter_count", "activity_count", "total_activity_count", "complexity"}, +} +var pageFields = fields{ + s: []string{"id", "name", "qualified_name", "module_name", "folder", "title", "url", "description"}, + i: []string{"widget_count"}, +} +var activityFields = fields{ + s: []string{"id", "name", "caption", "activity_type", "action_type", "microflow_id", + "microflow_qualified_name", "module_name", "entity_ref", "service_ref", "action_ref", + "timeout_expression", "description", "parent_loop_id", "condition_expression", "condition_rule", + "error_handling_type", "log_level", "log_node_expression", "log_message", "commit_type", + "retrieve_source", "queue_ref"}, + b: []string{"use_request_timeout", "auto_generate_caption", "with_events"}, + i: []string{"loop_depth"}, +} +var refFields = fields{ + s: []string{"source_type", "source_id", "source_name", "target_type", "target_id", "target_name", + "ref_kind", "module_name"}, +} + +type fields struct{ s, b, i []string } + +var strip = map[string]bool{} + +func toStruct(kind string, f fields, m map[string]any) starlark.Value { + d := starlark.StringDict{} + for _, k := range f.s { + if strip[k] { + continue + } + v, _ := m[k].(string) + d[k] = starlark.String(v) + } + for _, k := range f.b { + if strip[k] { + continue + } + v, _ := m[k].(bool) + d[k] = starlark.Bool(v) + } + for _, k := range f.i { + if strip[k] { + continue + } + v, _ := m[k].(float64) + d[k] = starlark.MakeInt(int(v)) + } + for k := range m { + if _, known := d[k]; !known && !strip[k] { + fmt.Fprintf(os.Stderr, "starrun: fixture %s has unknown field %q (not in the mxcli projection)\n", kind, k) + os.Exit(1) + } + } + return starlarkstruct.FromStringDict(starlark.String(kind), d) +} + +func list(kind string, f fields, rows []map[string]any) *starlark.List { + out := make([]starlark.Value, 0, len(rows)) + for _, r := range rows { + out = append(out, toStruct(kind, f, r)) + } + return starlark.NewList(out) +} + +func str(v starlark.Value) string { + if s, ok := starlark.AsString(v); ok { + return s + } + return "" +} + +func main() { + rule := flag.String("rule", "", "path to the .star rule") + fix := flag.String("fixture", "", "path to the fixture JSON") + stripFlag := flag.String("strip-fields", "", "comma-separated struct fields to omit (simulate an older mxcli)") + flag.Parse() + if *rule == "" || *fix == "" { + fmt.Fprintln(os.Stderr, "usage: starrun -rule file.star -fixture file.json [-strip-fields a,b]") + os.Exit(2) + } + for _, f := range strings.Split(*stripFlag, ",") { + if f = strings.TrimSpace(f); f != "" { + strip[f] = true + } + } + raw, err := os.ReadFile(*fix) + if err != nil { + fmt.Fprintln(os.Stderr, "starrun:", err) + os.Exit(1) + } + var fx fixture + if err := json.Unmarshal(raw, &fx); err != nil { + fmt.Fprintln(os.Stderr, "starrun: fixture:", err) + os.Exit(1) + } + + oneStr := func(name string) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (string, error) { + return func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (string, error) { + var s string + err := starlark.UnpackArgs(name, args, kwargs, "qualified_name", &s) + return s, err + } + } + noArgs := func(name string, v starlark.Value) *starlark.Builtin { + return starlark.NewBuiltin(name, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if len(args) != 0 || len(kwargs) != 0 { + return nil, fmt.Errorf("%s() takes no arguments", name) + } + return v, nil + }) + } + + predeclared := starlark.StringDict{ + "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), + "widgets": noArgs("widgets", list("widget", widgetFields, fx.Widgets)), + "microflows": noArgs("microflows", list("microflow", microflowFields, fx.Microflows)), + "pages": noArgs("pages", list("page", pageFields, fx.Pages)), + "activities_for": starlark.NewBuiltin("activities_for", func(t *starlark.Thread, b *starlark.Builtin, a starlark.Tuple, k []starlark.Tuple) (starlark.Value, error) { + q, err := oneStr("activities_for")(t, b, a, k) + if err != nil { + return nil, err + } + return list("activity", activityFields, fx.Activities[q]), nil + }), + "refs_from": starlark.NewBuiltin("refs_from", func(t *starlark.Thread, b *starlark.Builtin, a starlark.Tuple, k []starlark.Tuple) (starlark.Value, error) { + q, err := oneStr("refs_from")(t, b, a, k) + if err != nil { + return nil, err + } + return list("reference", refFields, fx.RefsFrom[q]), nil + }), + // Same signature and engine as mxcli's builtinMatches: Go regexp (RE2). + "matches": starlark.NewBuiltin("matches", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var s, pattern string + if err := starlark.UnpackArgs("matches", args, kwargs, "s", &s, "pattern", &pattern); err != nil { + return nil, err + } + ok, err := regexp.MatchString(pattern, s) + if err != nil { + return nil, fmt.Errorf("matches: %v", err) + } + return starlark.Bool(ok), nil + }), + "location": starlark.NewBuiltin("location", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var module, docType, docName, docID starlark.String + if err := starlark.UnpackArgs("location", args, kwargs, "module", &module, "document_type", &docType, "document_name", &docName, "document_id?", &docID); err != nil { + return nil, err + } + return starlarkstruct.FromStringDict(starlark.String("location"), starlark.StringDict{ + "module": module, "document_type": docType, "document_name": docName, "document_id": docID}), nil + }), + "violation": starlark.NewBuiltin("violation", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var message, suggestion starlark.String + var loc starlark.Value = starlark.None + if err := starlark.UnpackArgs("violation", args, kwargs, "message", &message, "location?", &loc, "suggestion?", &suggestion); err != nil { + return nil, err + } + return starlarkstruct.FromStringDict(starlark.String("violation"), starlark.StringDict{ + "message": message, "location": loc, "suggestion": suggestion}), nil + }), + } + + thread := &starlark.Thread{Name: "starrun"} + globals, err := starlark.ExecFile(thread, *rule, nil, predeclared) + if err != nil { + fmt.Fprintln(os.Stderr, "starrun: load:", err) + os.Exit(1) + } + check, ok := globals["check"] + if !ok { + fmt.Fprintln(os.Stderr, "starrun: rule defines no check()") + os.Exit(1) + } + res, err := starlark.Call(thread, check, nil, nil) + if err != nil { + fmt.Fprintln(os.Stderr, "starrun: check():", err) + os.Exit(1) + } + type out struct { + Module, DocumentType, DocumentName, Message, Suggestion string + } + rows := []map[string]string{} + iter := res.(starlark.Iterable).Iterate() + defer iter.Done() + var v starlark.Value + for iter.Next(&v) { + st := v.(*starlarkstruct.Struct) + row := map[string]string{"module": "", "document_type": "", "document_name": ""} + if m, err := st.Attr("message"); err == nil { + row["message"] = str(m) + } + if s, err := st.Attr("suggestion"); err == nil { + row["suggestion"] = str(s) + } + if l, err := st.Attr("location"); err == nil { + if ls, ok := l.(*starlarkstruct.Struct); ok { + for _, k := range []string{"module", "document_type", "document_name"} { + if x, err := ls.Attr(k); err == nil { + row[k] = str(x) + } + } + } + } + rows = append(rows, row) + } + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + _ = enc.Encode(rows) +} diff --git a/tests/lint-rules/test-ux-rules.sh b/tests/lint-rules/test-ux-rules.sh new file mode 100755 index 00000000..59db926e --- /dev/null +++ b/tests/lint-rules/test-ux-rules.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# test-ux-rules.sh -- unit tests for lint-rules/ux001_confirm_before_destructive.star and +# lint-rules/ux002_decisions_captioned.star, run under the real Go Starlark interpreter with +# the mxcli builtins mocked from JSON (tests/lint-rules/starrun). +# +# IMPORTANT: every fixture in tests/lint-rules/fixtures/ is a HAND-WRITTEN unit fixture that +# encodes the catalog projection AS READ FROM THE MXCLI SOURCE. It is not captured tool +# output. These tests prove the rule logic and the self-checks; they do NOT prove the +# vocabulary (action_type strings, activity_type strings) matches a real model. That is +# what the field probe in each rule's header is for. +# +# Needs `go` on PATH. A missing toolchain is a FAILURE (exit 1), never a quiet skip: a rule +# test that did not run must not read as a rule test that passed. +# +# Keep bash-3.2 compatible. +# Usage: tests/lint-rules/test-ux-rules.sh Exit 0 only if every assertion holds. + +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TK="$(cd "$HERE/../.." && pwd)" +FX="$HERE/fixtures" +R1="$TK/lint-rules/ux001_confirm_before_destructive.star" +R2="$TK/lint-rules/ux002_decisions_captioned.star" + +if ! command -v go >/dev/null 2>&1; then + echo "FAIL go toolchain not found; these tests cannot run (NOT a pass)." + echo " Install Go (https://go.dev/dl/, or: brew install go / apt install golang) and re-run." + exit 1 +fi +command -v python3 >/dev/null 2>&1 || { echo "FAIL python3 not found"; exit 1; } + +W=$(mktemp -d "${TMPDIR:-/tmp}/uxrules.XXXXXX") +trap 'rm -rf "$W"' EXIT +PASS=0; FAILED=0 +ok(){ printf ' PASS %s\n' "$1"; PASS=$((PASS+1)); } +no(){ printf ' FAIL %s\n' "$1"; FAILED=$((FAILED+1)); } + +cd "$TK" || exit 1 +OUT="$W/out.json"; ERR="$W/err.txt"; RC=0 + +# Build the harness once (its go.mod lives in its own directory, so `go run ./tests/...` +# from the repo root does not resolve). One build also keeps compile time out of the +# perf numbers below. +BIN="$W/starrun" +if ! (cd "$HERE/starrun" && GOFLAGS=-mod=mod go build -o "$BIN" . 2>"$ERR"); then + echo "FAIL could not build tests/lint-rules/starrun: $(head -c 400 "$ERR")"; exit 1 +fi + +# run RULE FIXTURE [STRIP] -> OUT holds stdout, RC the exit code +run(){ + local rule="$1" fix="$2" strip="${3:-}" + if [ -n "$strip" ]; then + "$BIN" -rule "$rule" -fixture "$FX/$fix.json" -strip-fields "$strip" >"$OUT" 2>"$ERR" + else + "$BIN" -rule "$rule" -fixture "$FX/$fix.json" >"$OUT" 2>"$ERR" + fi + RC=$? +} + +# check LABEL PY-EXPR : v is the parsed violation list; the expression must be truthy. +check(){ + local label="$1" expr="$2" + if [ "$RC" -ne 0 ]; then no "$label (harness exit $RC: $(head -c 300 "$ERR"))"; return; fi + if python3 - "$OUT" "$expr" <<'PY' +import json, sys +v = json.load(open(sys.argv[1])) +sys.exit(0 if eval(sys.argv[2]) else 1) +PY + then ok "$label"; else no "$label (got: $(head -c 400 "$OUT"))"; fi +} + +echo "== UX001 ==" +run "$R1" ux001-a-direct-delete +check "a direct delete button -> 1 finding 'deletes directly'" 'len(v)==1 and "deletes directly" in v[0]["message"]' +run "$R1" ux001-b-confirmed +check "b confirmed flow button -> 0 findings" 'len(v)==0' +run "$R1" ux001-c-flow-deletes +check "c unconfirmed, flow deletes -> 1 finding 'deletes objects'" 'len(v)==1 and "deletes objects" in v[0]["message"]' +run "$R1" ux001-d-depth2 +check "d delete reached at call depth 2 -> 1 finding" 'len(v)==1' +run "$R1" ux001-e-depth4 +check "e delete only at depth 4 (MAX_CALL_DEPTH=3) -> 0 findings" 'len(v)==0' +run "$R1" ux001-f-name-irreversible +check "f ACT_Order_Approve -> 1 finding 'irreversible'" 'len(v)==1 and "irreversible" in v[0]["message"]' +run "$R1" ux001-g-name-camelcase +check "g ACT_ShowApprovedOrders -> 0 findings" 'len(v)==0' +run "$R1" ux001-h-nanoflow +check "h nanoflow button to ACT_Reject -> 1 finding" 'len(v)==1' +run "$R1" ux001-i-system-module +check "i System-module delete (plus one Sales show-page button) -> 0 findings" 'len(v)==0' +run "$R1" ux001-i2-system-only +check "i2 System-only widgets -> only the 'no widgets' self-check" 'len(v)==1 and v[0]["module"]=="_rule" and "no widgets" in v[0]["message"]' +run "$R1" ux001-j-no-flow-actions +check "j show-page button + action-less widget -> 0 findings" 'len(v)==0' +run "$R1" ux001-k-snippet +check "k button on SNIPPET -> document_type Snippet" 'len(v)==1 and v[0]["document_type"]=="Snippet"' +run "$R1" ux001-l-cycle +check "l call cycle A->B->A terminates, 0 findings" 'len(v)==0' +run "$R1" ux001-m-strip "action_type,has_confirmation" +check "m stripped projection -> 1 _rule finding mentioning 0.25.0" 'len(v)==1 and v[0]["module"]=="_rule" and "0.25.0" in v[0]["message"]' +run "$R1" ux001-n-empty +check "n empty widgets -> 1 _rule finding 'no widgets'" 'len(v)==1 and v[0]["module"]=="_rule" and "no widgets" in v[0]["message"]' +run "$R1" ux001-o-no-action-types +check "o every action_type empty -> 1 _rule finding" 'len(v)==1 and v[0]["module"]=="_rule" and "not one reports an action type" in v[0]["message"]' +run "$R1" ux001-p-locations +check "p findings located at the widget's module and container" ' +len(v)==3 and sorted((x["module"],x["document_name"],x["document_type"]) for x in v)==[ + ("Billing","Billing.Invoice_Edit","Page"),("Orders","Orders.Order_Overview","Page"),("Sales","Sales.Quote_View","Page")]' + +echo "== UX002 ==" +run "$R2" ux002-a-two-bare +check "a two auto-captioned decisions -> 1 finding '2 decision(s)'" 'len(v)==1 and "2 decision(s)" in v[0]["message"]' +run "$R2" ux002-b-captioned +check "b caption 'Already approved?' -> 0 findings" 'len(v)==0' +run "$R2" ux002-c-blank-caption +check "c blank caption -> 1 finding" 'len(v)==1' +run "$R2" ux002-d-eight +check "d 8 activities, no annotation/description -> '8 top-level activities'" 'len(v)==1 and "8 top-level activities" in v[0]["message"]' +run "$R2" ux002-e-seven +check "e 7 activities -> 0 findings" 'len(v)==0' +run "$R2" ux002-f-eight-annotated +check "f 8 activities incl. Annotation -> 0 findings" 'len(v)==0' +run "$R2" ux002-g-eight-described +check "g 8 activities + description -> 0 findings" 'len(v)==0' +run "$R2" ux002-h-rule +check "h RULE with bare decisions -> 0 findings" 'len(v)==0' +run "$R2" ux002-i-administration +check "i Administration flow -> 0 findings" 'len(v)==0' +run "$R2" ux002-j-both-legs +check "j 9 activities + 3 bare decisions -> 2 findings at the flow" ' +len(v)==2 and all(x["document_name"]=="Sales.ACT_Big" and x["document_type"]=="Microflow" for x in v)' +run "$R2" ux002-k-strip "auto_generate_caption" +check "k stripped projection -> 1 _rule finding mentioning 0.25.0" 'len(v)==1 and v[0]["module"]=="_rule" and "0.25.0" in v[0]["message"]' +run "$R2" ux002-l-no-activities +check "l 3 flows, no activities -> 1 _rule finding 'checked NOTHING'" 'len(v)==1 and v[0]["module"]=="_rule" and "checked NOTHING" in v[0]["message"]' +run "$R2" ux002-m-no-microflows +check "m zero microflows -> 0 findings" 'len(v)==0' +run "$R2" ux002-n-events-counted +check "n 8 activities between StartEvent and EndEvent -> 1 finding saying 8, not 10" 'len(v)==1 and "has 8 top-level" in v[0]["message"]' +run "$R2" ux002-o-events-not-counted +check "o 7 activities + start/end events -> 0 findings (events are not activities)" 'len(v)==0' +run "$R2" ux002-p-strip-no-decision auto_generate_caption +check "p stripped projection, flow with no decision -> still 1 _rule finding" 'len(v)==1 and v[0]["module"]=="_rule" and "0.25.0" in v[0]["message"]' + +echo "== performance (synthetic, 3000 flows / 20000 widgets) ==" +PF="$W/perf.json" +python3 "$HERE/gen-perf-fixture.py" "$PF" >/dev/null || { no "perf fixture generation"; } +TIMEFORMAT='%R' +for pair in "UX001:$R1" "UX002:$R2"; do + nm="${pair%%:*}"; rule="${pair#*:}" + { time "$BIN" -rule "$rule" -fixture "$PF" >"$W/perf-out.json" 2>"$W/perf-err.txt"; } 2>"$W/time.txt" + rc=$? + secs=$(tail -n 1 "$W/time.txt") + if [ "$rc" -eq 0 ] && awk -v t="$secs" 'BEGIN{exit !(t+0 < 60)}'; then + ok "$nm perf run exit 0 in ${secs}s (limit 60s)" + else + no "$nm perf run rc=$rc in ${secs}s (limit 60s): $(head -c 300 "$W/perf-err.txt")" + fi +done + +echo +echo "passed: $PASS failed: $FAILED" +[ "$FAILED" -eq 0 ] From f66f3eccf0560cc556eb00ba1a52283f4a891a6f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 13:50:13 +0000 Subject: [PATCH 3/6] fix(tests): lint-rule suite resolves python through portable.sh check-portability.sh (in CI) rejects a hard-coded `python3`: on Windows a name on PATH may be the Store alias stub, not an interpreter. The suite now sources bin/lib/portable.sh, calls require_py and runs "$PY", like every other script here. 35/35 cases still pass; check-portability clean. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- tests/lint-rules/test-ux-rules.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/lint-rules/test-ux-rules.sh b/tests/lint-rules/test-ux-rules.sh index 59db926e..8f6ad6aa 100755 --- a/tests/lint-rules/test-ux-rules.sh +++ b/tests/lint-rules/test-ux-rules.sh @@ -27,7 +27,9 @@ if ! command -v go >/dev/null 2>&1; then echo " Install Go (https://go.dev/dl/, or: brew install go / apt install golang) and re-run." exit 1 fi -command -v python3 >/dev/null 2>&1 || { echo "FAIL python3 not found"; exit 1; } +# shellcheck source=../../bin/lib/portable.sh +. "$TK/bin/lib/portable.sh" +require_py # sets $PY or exits 2 with an actionable message W=$(mktemp -d "${TMPDIR:-/tmp}/uxrules.XXXXXX") trap 'rm -rf "$W"' EXIT @@ -61,7 +63,7 @@ run(){ check(){ local label="$1" expr="$2" if [ "$RC" -ne 0 ]; then no "$label (harness exit $RC: $(head -c 300 "$ERR"))"; return; fi - if python3 - "$OUT" "$expr" <<'PY' + if "$PY" - "$OUT" "$expr" <<'PY' import json, sys v = json.load(open(sys.argv[1])) sys.exit(0 if eval(sys.argv[2]) else 1) @@ -144,7 +146,7 @@ check "p stripped projection, flow with no decision -> still 1 _rule finding" 'l echo "== performance (synthetic, 3000 flows / 20000 widgets) ==" PF="$W/perf.json" -python3 "$HERE/gen-perf-fixture.py" "$PF" >/dev/null || { no "perf fixture generation"; } +"$PY" "$HERE/gen-perf-fixture.py" "$PF" >/dev/null || { no "perf fixture generation"; } TIMEFORMAT='%R' for pair in "UX001:$R1" "UX002:$R2"; do nm="${pair%%:*}"; rule="${pair#*:}" From 40f2c00409732e27190729cc0ed4768a2b5fa31a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 14:15:52 +0000 Subject: [PATCH 4/6] fix(tests): stock-hashes fixture covers only init-seeded rules test-stock-hashes.sh T2 demanded a STOCK-HASHES.txt entry per version for every file in MXTK_LINT_RULES. mxcli init seeds only three of those; the toolkit-authored ux001/ux002 rules can never have a stock hash, so the fixture went red on every version (3 FAIL) the moment they joined the list. install-manifest.sh gains MXTK_LINT_RULES_STOCK (the three init-seeded rules). The fixture's coverage, listed-name and live-capture checks walk that set, and a new check asserts it is a subset of MXTK_LINT_RULES. Replayed T2's awk against the real list: all three versions cover all three stock rules; a listed non-stock name still fails. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/lib/install-manifest.sh | 10 +++++++++- tests/wave2/test-stock-hashes.sh | 25 ++++++++++++++++--------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a19529..77f54af2 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(tests): **`test-stock-hashes.sh` no longer demands a stock hash for a toolkit-authored rule.** Its coverage check walked all of `MXTK_LINT_RULES`, but `mxcli init` only seeds three of those files, so the day UX001/UX002 joined the list every version went red (`v0.17.0 is missing: ux001… ux002…`, 3 FAIL) for a hash that cannot exist. `bin/lib/install-manifest.sh` gains `MXTK_LINT_RULES_STOCK` (the three init-seeded rules); the fixture covers, lists and live-captures against that set and asserts it is a subset of `MXTK_LINT_RULES`. Verified by replaying the fixture's awk against the real list: all three versions cover all three stock rules, and a listed non-stock name still fails. — MendixMau - new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Logic is unit-tested under the real Starlark interpreter: `tests/lint-rules/test-ux-rules.sh` (35 cases incl. a synthetic 3000-flow perf run, wired into CI) — the suite caught two defects before they shipped: start/end events inflated UX002's activity count, and its version probe only fired on flows that had a decision. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — 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 diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index 66df2248..fddd6e06 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -136,7 +136,8 @@ MXTK_PROJECT_BIN_NOINSTALL="report-disposition-check.sh" # gate-check.sh runs i # lint-rules/README.md. conv010 (CONV010) is the top rule by # volume everywhere it runs; the de-noised version here took PROJECT-C 399 -> 51. # -# The toolkit ships ONLY these three of mxcli's ~29. Copying the rest would pin every project +# The toolkit repairs ONLY these three of mxcli's ~29 (MXTK_LINT_RULES_STOCK below); rules +# after them in the list are toolkit-authored, never seeded by init. Copying the rest would pin every project # to whatever mxcli shipped the day someone copied them. # # MXTK_LINT_RULES_CONFIGURABLE — installed once, then owned by the project. conv020 is not an @@ -151,6 +152,13 @@ MXTK_PROJECT_BIN_NOINSTALL="report-disposition-check.sh" # gate-check.sh runs i # sync-project.sh restores them; lint-rules/STOCK-HASHES.txt is what lets it tell a # reverted-to-stock file (safe to restore) from one a project deliberately tuned (hands off). MXTK_LINT_RULES="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star ux001_confirm_before_destructive.star ux002_decisions_captioned.star" +# MXTK_LINT_RULES_STOCK — the subset of MXTK_LINT_RULES that `mxcli init` ALSO seeds, i.e. the +# only files lint-rules/STOCK-HASHES.txt can ever hold a hash for. The ux001/ux002 rules are +# toolkit-authored: init never writes them, so no stock hash can exist and a coverage check +# that demanded one (tests/wave2/test-stock-hashes.sh, until 2026-10-08) went red on every +# version the day a non-stock rule joined MXTK_LINT_RULES. Every name here must also be in +# MXTK_LINT_RULES; the fixture asserts it. +MXTK_LINT_RULES_STOCK="conv010_act_microflow_content.star data_change_microflows.star entity_business_key.star" MXTK_LINT_RULES_CONFIGURABLE="conv020_action_user_feedback.star" # Files in lint-rules/ deliberately NOT installed. Same contract as MXTK_PROJECT_BIN_NOINSTALL: diff --git a/tests/wave2/test-stock-hashes.sh b/tests/wave2/test-stock-hashes.sh index 056410f7..f9f31196 100755 --- a/tests/wave2/test-stock-hashes.sh +++ b/tests/wave2/test-stock-hashes.sh @@ -10,8 +10,10 @@ # projects saw. # # T1 every entry is <32-hex md5> , no duplicate (rule, version) -# T2 every rule named is one the toolkit owns (MXTK_LINT_RULES), and every listed -# version covers all of them — a half-captured version is caught here +# T2 every rule named is a stock one the toolkit repairs (MXTK_LINT_RULES_STOCK, itself a +# subset of MXTK_LINT_RULES), and every listed version covers all of them — a +# half-captured version is caught here. Toolkit-authored rules (ux001, …) are never +# seeded by `mxcli init`, so no version can list them and none is asked to. # T3 the real reader (_mxtk_lint_is_stock) accepts every entry and rejects a foreign hash # T4 live, only with a binary: `mxcli init` into an EMPTY dir, every seeded rule's md5 is # listed. This is the capture procedure in the file's own header, run as a check. @@ -45,16 +47,21 @@ dup=$(entries | awk '{print $1" "$3}' | sort | uniq -d) echo "== T2 coverage ==" [ -n "${MXTK_LINT_RULES:-}" ] && ok "MXTK_LINT_RULES loaded" || no "MXTK_LINT_RULES empty — manifest not loaded" +[ -n "${MXTK_LINT_RULES_STOCK:-}" ] && ok "MXTK_LINT_RULES_STOCK loaded" || no "MXTK_LINT_RULES_STOCK empty — manifest not loaded" +for f in ${MXTK_LINT_RULES_STOCK:-}; do + case " $MXTK_LINT_RULES " in *" $f "*) ok "stock rule is also toolkit-owned: $f" ;; + *) no "$f is in MXTK_LINT_RULES_STOCK but not MXTK_LINT_RULES — sync would never deliver it" ;; esac +done for f in $(entries | awk '{print $1}' | sort -u); do - case " $MXTK_LINT_RULES " in *" $f "*) ok "listed rule is toolkit-owned: $f" ;; - *) no "$f is listed but not in MXTK_LINT_RULES — nothing ever looks it up" ;; esac + case " ${MXTK_LINT_RULES_STOCK:-} " in *" $f "*) ok "listed rule is a stock one the toolkit repairs: $f" ;; + *) no "$f is listed but not in MXTK_LINT_RULES_STOCK — init never seeds it, nothing ever looks it up" ;; esac done for v in $(entries | awk '{print $3}' | sort -u); do missing="" - for f in $MXTK_LINT_RULES; do + for f in ${MXTK_LINT_RULES_STOCK:-}; do entries | awk -v f="$f" -v v="$v" '$1==f && $3==v {x=1} END {exit !x}' || missing="$missing $f" done - [ -z "$missing" ] && ok "$v covers all toolkit-owned rules" || no "$v is missing:$missing" + [ -z "$missing" ] && ok "$v covers all stock rules" || no "$v is missing:$missing" done echo "== T3 the real reader agrees ==" @@ -82,7 +89,7 @@ else W=$(mktemp -d "${TMPDIR:-/tmp}/stockhash.XXXXXX"); trap 'rm -rf "$W"' EXIT ( cd "$W" && "$MX" init . >/dev/null 2>&1 ) seeded=0 - for f in $MXTK_LINT_RULES; do + for f in $MXTK_LINT_RULES_STOCK; do s="$W/.claude/lint-rules/$f" [ -f "$s" ] || continue seeded=$((seeded+1)) @@ -90,8 +97,8 @@ else _mxtk_lint_is_stock "$f" "$h" "$HASHES" && ok "seeded $f is listed" \ || no "seeded $f ($h) is NOT listed — add it with this mxcli's version" done - [ "$seeded" -gt 0 ] && ok "init seeded $seeded toolkit-owned rule(s)" \ - || no "init seeded none of: $MXTK_LINT_RULES — did it run?" + [ "$seeded" -gt 0 ] && ok "init seeded $seeded stock rule(s)" \ + || no "init seeded none of: $MXTK_LINT_RULES_STOCK — did it run?" fi echo "" From 53c4a36446628a6f7649f414de2a4501dff0d043 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 14:33:31 +0000 Subject: [PATCH 5/6] fix(tests): lint-delivery fixture derives the installed count from the manifest Case 1 asserted a literal 4 files in .claude/lint-rules/ after a fresh install. Two toolkit-authored rules joined MXTK_LINT_RULES, six landed, and the fixture reported them as a NOINSTALL leak. The expected count is now the size of MXTK_LINT_RULES + MXTK_LINT_RULES_CONFIGURABLE, with an explicit check that no MXTK_LINT_RULES_NOINSTALL file was installed. CI's own output on 40f2c00 listed exactly the six manifest rules. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- tests/test-lint-delivery.sh | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f54af2..f59ffeb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased -- fix(tests): **`test-stock-hashes.sh` no longer demands a stock hash for a toolkit-authored rule.** Its coverage check walked all of `MXTK_LINT_RULES`, but `mxcli init` only seeds three of those files, so the day UX001/UX002 joined the list every version went red (`v0.17.0 is missing: ux001… ux002…`, 3 FAIL) for a hash that cannot exist. `bin/lib/install-manifest.sh` gains `MXTK_LINT_RULES_STOCK` (the three init-seeded rules); the fixture covers, lists and live-captures against that set and asserts it is a subset of `MXTK_LINT_RULES`. Verified by replaying the fixture's awk against the real list: all three versions cover all three stock rules, and a listed non-stock name still fails. — MendixMau +- fix(tests): **`test-stock-hashes.sh` no longer demands a stock hash for a toolkit-authored rule.** Its coverage check walked all of `MXTK_LINT_RULES`, but `mxcli init` only seeds three of those files, so the day UX001/UX002 joined the list every version went red (`v0.17.0 is missing: ux001… ux002…`, 3 FAIL) for a hash that cannot exist. `bin/lib/install-manifest.sh` gains `MXTK_LINT_RULES_STOCK` (the three init-seeded rules); the fixture covers, lists and live-captures against that set and asserts it is a subset of `MXTK_LINT_RULES`. Verified by replaying the fixture's awk against the real list: all three versions cover all three stock rules, and a listed non-stock name still fails. Same disease in `tests/test-lint-delivery.sh`: its fresh-install case asserted a literal 4 installed files; it now derives the count from the manifest lists and checks that no `MXTK_LINT_RULES_NOINSTALL` file landed. — MendixMau - new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Logic is unit-tested under the real Starlark interpreter: `tests/lint-rules/test-ux-rules.sh` (35 cases incl. a synthetic 3000-flow perf run, wired into CI) — the suite caught two defects before they shipped: start/end events inflated UX002's activity count, and its version probe only fired on flows that had a decision. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — 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 diff --git a/tests/test-lint-delivery.sh b/tests/test-lint-delivery.sh index 0a306d2f..5d16e9ad 100755 --- a/tests/test-lint-delivery.sh +++ b/tests/test-lint-delivery.sh @@ -48,7 +48,13 @@ RECEIPT="$P/.claude/.mxtk-lint-receipt" echo "== 1 fresh install ==" mxtk_install_lint_rules "$W/tk" "$P" 0 "" >/dev/null { [ -f "$A" ] && [ -f "$C" ] && [ -f "$RECEIPT" ]; } && ok "rules + receipt written" || no "install" -[ "$(ls "$R" | wc -l | tr -d ' ')" = 4 ] && ok "NOINSTALL list respected (4 files)" || no "leaked: $(ls "$R")" +# Expected count comes from the manifest, not a literal: every rule in MXTK_LINT_RULES and +# MXTK_LINT_RULES_CONFIGURABLE lands, nothing from MXTK_LINT_RULES_NOINSTALL does. (A literal +# 4 went red the day two toolkit-authored rules joined the managed list, 2026-10-08.) +want=$(set -- $MXTK_LINT_RULES $MXTK_LINT_RULES_CONFIGURABLE; echo $#) +got=$(ls "$R" | wc -l | tr -d ' ') +[ "$got" = "$want" ] && ok "NOINSTALL list respected ($want files)" || no "expected $want files, got $got: $(ls "$R")" +for f in $MXTK_LINT_RULES_NOINSTALL; do [ -e "$R/$f" ] && no "leaked NOINSTALL file: $f"; done echo "== 2 re-run is silent and rewrites nothing ==" before=$(portable_md5 "$A"); out=$(mxtk_install_lint_rules "$W/tk" "$P" 0 "") From 278fc8d402d3d2791bfbaf6db904615b6a5ae280 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 03:11:10 +0000 Subject: [PATCH 6/6] fix(lint): field-calibrate UX001/UX002 on a real model; fix two vocabulary defects First run of the pair with mxcli 0.26.0 against a scratch copy of a 21-module Mendix 11.12 app (876 microflows, 124 pages): UX001 fired 0 times and UX002's decision leg 0 times across 728 decisions. A catalog probe explained both: - the stored microflow-button action type is Forms$MicroflowAction, not the Forms$MicroflowClientAction read from the source, so UX001 legs 2-3 could never fire for a microflow button. FLOW_CALLS now lists both spellings. - auto_generate_caption is 0 on every ExclusiveSplit; a bare decision carries its condition expression as its caption. UX002 now flags a caption that is empty or equals condition_expression (39 of 39 custom decisions on that model; marketplace decisions carry real questions). Fixtures carry the probed action-type name; new case q pins the caption==condition shape. tests/lint-rules/test-ux-rules.sh: 36/36. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + lint-rules/README.md | 13 ++++--- .../ux001_confirm_before_destructive.star | 11 ++++-- lint-rules/ux002_decisions_captioned.star | 20 +++++++---- .../fixtures/ux001-b-confirmed.json | 2 +- .../fixtures/ux001-c-flow-deletes.json | 2 +- tests/lint-rules/fixtures/ux001-d-depth2.json | 2 +- tests/lint-rules/fixtures/ux001-e-depth4.json | 2 +- .../fixtures/ux001-f-name-irreversible.json | 2 +- .../fixtures/ux001-g-name-camelcase.json | 2 +- tests/lint-rules/fixtures/ux001-l-cycle.json | 2 +- .../fixtures/ux001-p-locations.json | 4 +-- .../fixtures/ux002-b-captioned.json | 3 +- .../ux002-q-caption-is-condition.json | 34 +++++++++++++++++++ tests/lint-rules/gen-perf-fixture.py | 2 +- tests/lint-rules/test-ux-rules.sh | 2 ++ 16 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 tests/lint-rules/fixtures/ux002-q-caption-is-condition.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f59ffeb6..cb6dfa3c 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(lint): **UX001/UX002 field-calibrated on a real model; both had a vocabulary defect the unit suite could not see.** First run of the pair with mxcli 0.26.0 against a scratch copy of a 21-module Mendix 11.12 app (876 microflows, 124 pages, 1.7 s, no load errors): UX001 fired 0 times and UX002's decision leg 0 times across 728 decisions. Catalog probe showed why — the stored microflow-button action type is `Forms$MicroflowAction` (290 rows), not the `Forms$MicroflowClientAction` read from the source, so UX001 legs 2–3 could never fire for a microflow button; and `auto_generate_caption` is 0 on every `ExclusiveSplit`, a bare decision instead carries its condition expression as its caption, so UX002's test could never be true. Fixed: UX001 lists both spellings; UX002 flags a caption that is empty or equals `condition_expression` (39 of 39 custom top-level decisions on that model, while marketplace decisions read "Passwords equal?"). Fixtures now carry the probed action-type name and a new case pins the caption==condition shape (36 cases). Lesson for every rule here: a self-check that only tests "count is non-zero" passes on a wrong vocabulary — the field run is the check. — MendixMau - fix(tests): **`test-stock-hashes.sh` no longer demands a stock hash for a toolkit-authored rule.** Its coverage check walked all of `MXTK_LINT_RULES`, but `mxcli init` only seeds three of those files, so the day UX001/UX002 joined the list every version went red (`v0.17.0 is missing: ux001… ux002…`, 3 FAIL) for a hash that cannot exist. `bin/lib/install-manifest.sh` gains `MXTK_LINT_RULES_STOCK` (the three init-seeded rules); the fixture covers, lists and live-captures against that set and asserts it is a subset of `MXTK_LINT_RULES`. Verified by replaying the fixture's awk against the real list: all three versions cover all three stock rules, and a listed non-stock name still fails. Same disease in `tests/test-lint-delivery.sh`: its fresh-install case asserted a literal 4 installed files; it now derives the count from the manifest lists and checks that no `MXTK_LINT_RULES_NOINSTALL` file landed. — MendixMau - new(lint): **UX001 ConfirmBeforeDestructive and UX002 DecisionsCaptioned.** Two Starlark rules in `lint-rules/`, delivered by the existing install/refresh path: UX001 flags a button that deletes (direct delete action, which has no confirmation setting) or calls a flow that deletes or is named Approve/Reject/Submit/Archive/… without "Ask confirmation"; UX002 flags decisions left on their auto-generated caption and flows of 8+ activities with no annotation or documentation. Both need mxcli ≥ 0.25.0 (widget `action_type`/`has_confirmation`, activity `caption`) and emit a `_rule` blindness finding on anything older. Vocabulary read from the mxcli source, not from the bundled API guide; first field calibration still owed (no mxcli binary reachable in the authoring session) — the README row says so. Logic is unit-tested under the real Starlark interpreter: `tests/lint-rules/test-ux-rules.sh` (35 cases incl. a synthetic 3000-flow perf run, wired into CI) — the suite caught two defects before they shipped: start/end events inflated UX002's activity count, and its version probe only fired on flows that had a decision. Prompted by a CLAUDE.md-only enforcement of the same house rules (confirm before approve, feedback on close/commit) that no session could be held to. — 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 diff --git a/lint-rules/README.md b/lint-rules/README.md index d592d429..920f3d7a 100644 --- a/lint-rules/README.md +++ b/lint-rules/README.md @@ -30,10 +30,15 @@ are excluded at the gate (`bin/lint-gate.sh -e`, from a per-project `caption` / `auto_generate_caption`, which exist from **mxcli 0.25.0** (catalog schema with widget actions and activity captions). On an older binary both rules emit a single `_rule` finding ("needs mxcli >= 0.25.0") and nothing else — `bin/lint-gate.sh` reads that as blindness, -never as a clean pass. Their vocabulary (`Forms$DeleteClientAction`, `ExclusiveSplit`, -`DeleteObjectAction`) was read from the mxcli source at 0.25.0, not from `write-lint-rules.md`; -the first field calibration is still owed — run both on a scratch copy of a real model and -record the counts here before trusting the numbers. Their *logic* is proven: `tests/lint-rules/` +never as a clean pass. Their vocabulary (`Forms$DeleteClientAction`, `Forms$MicroflowAction`, +`ExclusiveSplit`, `DeleteObjectAction`) was read from the mxcli source at 0.25.0, then field-probed +on 2026-10-12 with mxcli 0.26.0 against a scratch copy of a 21-module Mendix 11.12 model (876 +microflows, 124 pages). That probe found two defects the unit suite could not see: the stored +microflow-button action type is `Forms$MicroflowAction`, not the `…ClientAction` the source read +suggested, and `auto_generate_caption` is never set on a decision — a bare decision carries its +condition expression as its caption. Both fixed; after the fix UX002 reports every one of the +model's 39 top-level custom decisions as bare (all were), UX001 stays at 0 there (its 4 microflow +buttons reach no delete and match no verb). Re-probe on your own model before trusting the counts. Their *logic* is proven: `tests/lint-rules/` runs both under the real Starlark interpreter (`go.starlark.net` at mxcli's pinned version) over 33 hand-written fixtures — self-checks, call depth both sides of the bound, cycles, the name pattern, module skips, snippet locations, start/end events not counted — plus a synthetic diff --git a/lint-rules/ux001_confirm_before_destructive.star b/lint-rules/ux001_confirm_before_destructive.star index 1bffaf6f..6b06c37e 100644 --- a/lint-rules/ux001_confirm_before_destructive.star +++ b/lint-rules/ux001_confirm_before_destructive.star @@ -30,7 +30,8 @@ # # Vocabulary below was read from the mxcli catalog builder source (builder_pages.go # widgetPrimaryAction stores the action's stored $Type verbatim; builder_microflows.go -# strips the "Microflows$" prefix from an action's type), NOT from write-lint-rules.md. +# strips the "Microflows$" prefix from an action's type), NOT from write-lint-rules.md, +# then probed on a real model (see FLOW_CALLS: the source read got one name wrong). # Probe it on your model before trusting it: # sqlite3 .mxcli/catalog.db "SELECT DISTINCT ActionType FROM widgets ORDER BY 1;" # sqlite3 .mxcli/catalog.db "SELECT DISTINCT ActionType FROM activities ORDER BY 1;" @@ -49,7 +50,13 @@ SKIP_MODULES = ("System", "Administration") # Stored client-action types on a button / on-click action (widgets.ActionType). DIRECT_DELETE = "Forms$DeleteClientAction" -FLOW_CALLS = ("Forms$MicroflowClientAction", "Forms$CallNanoflowClientAction") +# Field-probed 2026-10-12 on mxcli 0.26.0 against a 21-module Mendix 11.12 model: the stored +# microflow-button value is "Forms$MicroflowAction" (290 rows, 37 with confirmation), NOT the +# "Forms$MicroflowClientAction" the source read suggested -- with the old name legs 2 and 3 +# could never fire for a microflow button, and the self-check did not notice because other +# widgets still reported an action type. Both spellings stay listed; the nanoflow and direct +# delete names were confirmed as written. +FLOW_CALLS = ("Forms$MicroflowAction", "Forms$MicroflowClientAction", "Forms$CallNanoflowClientAction") # Microflow action types that destroy data (activities.ActionType, "Microflows$" stripped). DESTRUCTIVE_ACTIONS = ("DeleteObjectAction",) diff --git a/lint-rules/ux002_decisions_captioned.star b/lint-rules/ux002_decisions_captioned.star index eaddbd7c..07fd2f6a 100644 --- a/lint-rules/ux002_decisions_captioned.star +++ b/lint-rules/ux002_decisions_captioned.star @@ -24,10 +24,11 @@ # findings, not seven), so the ratchet in bin/lint-gate.sh moves one step per flow. # Rules (RULE microflow_type) are skipped: they are expressions, not narratives. # -# REQUIRES mxcli >= 0.25.0: activity caption / auto_generate_caption / description -# (mendixlabs/mxcli#1266, #1267). Before that every activity's caption was the -# placeholder "Activity". On an older binary the rule emits a `_rule` finding instead -# of a silent clean pass. +# REQUIRES mxcli >= 0.25.0: activity caption / auto_generate_caption / description / +# condition_expression (mendixlabs/mxcli#1266, #1267). Before that every activity's +# caption was the placeholder "Activity". On an older binary the rule emits a `_rule` +# finding instead of a silent clean pass. Field note (0.26.0): auto_generate_caption is +# never set on a decision; a bare decision is detected by caption == condition_expression. # # Vocabulary: activity_type "ExclusiveSplit" / "Annotation" are the names # getMicroflowObjectType() in mxcli's catalog builder returns (builder_microflows.go); @@ -89,7 +90,14 @@ def check(): counted += 1 if act.activity_type != "ExclusiveSplit": continue - if act.auto_generate_caption or act.caption.strip() == "": + # Field-probed 2026-10-12 on mxcli 0.26.0 (21-module Mendix 11.12 model, 728 + # decisions): auto_generate_caption is 0 on EVERY ExclusiveSplit, and a decision + # left on its generated caption carries its condition expression AS the caption. + # So "bare" means: empty, or equal to the condition expression (39 of 39 custom + # decisions on that model; the marketplace ones read "Passwords equal?"). + cap = act.caption.strip() + cond = getattr(act, "condition_expression", "").strip() + if act.auto_generate_caption or cap == "" or (cond != "" and cap == cond): bare_decisions += 1 loc = location( @@ -100,7 +108,7 @@ def check(): if bare_decisions > 0: violations.append(violation( - message="'{}' has {} decision(s) with no caption of their own (auto-generated or empty).".format( + message="'{}' has {} decision(s) with no caption of their own (empty, or still the condition expression).".format( mf.name, bare_decisions ), location=loc, diff --git a/tests/lint-rules/fixtures/ux001-b-confirmed.json b/tests/lint-rules/fixtures/ux001-b-confirmed.json index 92e47957..56757976 100644 --- a/tests/lint-rules/fixtures/ux001-b-confirmed.json +++ b/tests/lint-rules/fixtures/ux001-b-confirmed.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Order_Delete", "has_confirmation": true } diff --git a/tests/lint-rules/fixtures/ux001-c-flow-deletes.json b/tests/lint-rules/fixtures/ux001-c-flow-deletes.json index 5de82dbd..9f6099ff 100644 --- a/tests/lint-rules/fixtures/ux001-c-flow-deletes.json +++ b/tests/lint-rules/fixtures/ux001-c-flow-deletes.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Order_Clear" } ], diff --git a/tests/lint-rules/fixtures/ux001-d-depth2.json b/tests/lint-rules/fixtures/ux001-d-depth2.json index 4c95ea3b..762ec52e 100644 --- a/tests/lint-rules/fixtures/ux001-d-depth2.json +++ b/tests/lint-rules/fixtures/ux001-d-depth2.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Alpha" } ], diff --git a/tests/lint-rules/fixtures/ux001-e-depth4.json b/tests/lint-rules/fixtures/ux001-e-depth4.json index e4cd65fd..9f5ce697 100644 --- a/tests/lint-rules/fixtures/ux001-e-depth4.json +++ b/tests/lint-rules/fixtures/ux001-e-depth4.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Alpha" } ], diff --git a/tests/lint-rules/fixtures/ux001-f-name-irreversible.json b/tests/lint-rules/fixtures/ux001-f-name-irreversible.json index 6b519d03..6929decf 100644 --- a/tests/lint-rules/fixtures/ux001-f-name-irreversible.json +++ b/tests/lint-rules/fixtures/ux001-f-name-irreversible.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Order_Approve" } ] diff --git a/tests/lint-rules/fixtures/ux001-g-name-camelcase.json b/tests/lint-rules/fixtures/ux001-g-name-camelcase.json index ac3048b7..178cee69 100644 --- a/tests/lint-rules/fixtures/ux001-g-name-camelcase.json +++ b/tests/lint-rules/fixtures/ux001-g-name-camelcase.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_ShowApprovedOrders" } ] diff --git a/tests/lint-rules/fixtures/ux001-l-cycle.json b/tests/lint-rules/fixtures/ux001-l-cycle.json index 0d43ff7d..bcd16f5f 100644 --- a/tests/lint-rules/fixtures/ux001-l-cycle.json +++ b/tests/lint-rules/fixtures/ux001-l-cycle.json @@ -8,7 +8,7 @@ "container_qualified_name": "Sales.Order_Overview", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Alpha" } ], diff --git a/tests/lint-rules/fixtures/ux001-p-locations.json b/tests/lint-rules/fixtures/ux001-p-locations.json index a7ddb81e..ae5e0790 100644 --- a/tests/lint-rules/fixtures/ux001-p-locations.json +++ b/tests/lint-rules/fixtures/ux001-p-locations.json @@ -18,7 +18,7 @@ "container_qualified_name": "Billing.Invoice_Edit", "container_type": "PAGE", "module_name": "Billing", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Billing.ACT_Invoice_Clear" }, { @@ -29,7 +29,7 @@ "container_qualified_name": "Sales.Quote_View", "container_type": "PAGE", "module_name": "Sales", - "action_type": "Forms$MicroflowClientAction", + "action_type": "Forms$MicroflowAction", "microflow_ref": "Sales.ACT_Quote_Approve" } ], diff --git a/tests/lint-rules/fixtures/ux002-b-captioned.json b/tests/lint-rules/fixtures/ux002-b-captioned.json index a47720c6..a85b7df3 100644 --- a/tests/lint-rules/fixtures/ux002-b-captioned.json +++ b/tests/lint-rules/fixtures/ux002-b-captioned.json @@ -17,7 +17,8 @@ "caption": "Already approved?", "activity_type": "ExclusiveSplit", "action_type": "", - "auto_generate_caption": false + "auto_generate_caption": false, + "condition_expression": "$Order/Status = 'Approved'" } ] } diff --git a/tests/lint-rules/fixtures/ux002-q-caption-is-condition.json b/tests/lint-rules/fixtures/ux002-q-caption-is-condition.json new file mode 100644 index 00000000..12feaa71 --- /dev/null +++ b/tests/lint-rules/fixtures/ux002-q-caption-is-condition.json @@ -0,0 +1,34 @@ +{ + "microflows": [ + { + "id": "m-ACT_Check", + "name": "ACT_Check", + "qualified_name": "Sales.ACT_Check", + "module_name": "Sales", + "microflow_type": "MICROFLOW", + "description": "" + } + ], + "activities": { + "Sales.ACT_Check": [ + { + "id": "a0", + "name": "a0", + "caption": "$Order/Status = 'Approved'", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": false, + "condition_expression": "$Order/Status = 'Approved'" + }, + { + "id": "a1", + "name": "a1", + "caption": "Already approved?", + "activity_type": "ExclusiveSplit", + "action_type": "", + "auto_generate_caption": false, + "condition_expression": "$Order/Status = 'Approved'" + } + ] + } +} diff --git a/tests/lint-rules/gen-perf-fixture.py b/tests/lint-rules/gen-perf-fixture.py index 0fa64a49..888678ca 100755 --- a/tests/lint-rules/gen-perf-fixture.py +++ b/tests/lint-rules/gen-perf-fixture.py @@ -52,7 +52,7 @@ def r(s, t): d = dict(id="w%d" % j, name="w%d" % j, widget_type="ACTIONBUTTON", container_id="p%d" % (j // 20), container_qualified_name=cont, container_type="PAGE", module_name=mod) if j % 10 == 0: - d.update(action_type="Forms$MicroflowClientAction", has_confirmation=False, + d.update(action_type="Forms$MicroflowAction", has_confirmation=False, microflow_ref=q(rnd.randrange(a.flows))) elif j % 10 < 6: d.update(action_type="Forms$ShowPageClientAction", page_ref="Sales.Order_Edit") diff --git a/tests/lint-rules/test-ux-rules.sh b/tests/lint-rules/test-ux-rules.sh index 8f6ad6aa..00b29bac 100755 --- a/tests/lint-rules/test-ux-rules.sh +++ b/tests/lint-rules/test-ux-rules.sh @@ -143,6 +143,8 @@ run "$R2" ux002-o-events-not-counted check "o 7 activities + start/end events -> 0 findings (events are not activities)" 'len(v)==0' run "$R2" ux002-p-strip-no-decision auto_generate_caption check "p stripped projection, flow with no decision -> still 1 _rule finding" 'len(v)==1 and v[0]["module"]=="_rule" and "0.25.0" in v[0]["message"]' +run "$R2" ux002-q-caption-is-condition +check "q caption equal to condition_expression (auto flag false, as on 0.26.0) -> 1 finding '1 decision(s)'" 'len(v)==1 and "1 decision(s)" in v[0]["message"]' echo "== performance (synthetic, 3000 flows / 20000 widgets) ==" PF="$W/perf.json"