diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index b69f75cee1..66acdf3a20 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -868,6 +868,7 @@ {"area": "mdl/executor", "date": "2026-10-05", "symptom": "On Mendix 11.15, `TestMxCheck_DoctypeScripts/40-message-definition-examples.mdl` fails with CE0270 \"No root element could be found in the schema\" at MsgTest.IMM_Order. On a converted or Studio Pro 11.15 project, `list message definition collections` finds nothing, and `describe import mapping` prints no source", "cause": "Mendix 11.15 replaced `MessageDefinitions$MessageDefinitionCollection` with one `MessageDefinitions$MessageDefinition2` document per definition. `mx convert` turns the collection into a `Projects$Folder` with the same unit ID and name, and the ExposedEntity tree is byte-identical apart from $IDs. mxcli read and wrote only collections, so on 11.15 there was nothing to resolve a mapping against", "file": "`mdl/backend/modelsdk/messagedefinition_document.go` (read/write, reusing the collection's exposedNodeFromGen/messageNodeToGen), `mdl/executor/cmd_messagedefinition_documents.go` (create/describe/drop/list + version refusals), `mdl/executor/mapping_messagedefinition.go` (`findMessageDefinition(ctx, ref)` returns the stored reference), `cmd_messagedefinitions.go` (alter target resolution, association-drop guard), grammar `createMessageDefinitionStatement` / `DROP|DESCRIBE MESSAGE DEFINITION` / `LIST MESSAGE DEFINITIONS`", "insight": "**`mx convert` is the oracle for a storage change.** Converting a known 11.14 project gives a Studio Pro-authored 11.15 reference for the new shape and for how references move, so no guessing is needed. **Under the revert, 11.15 fails earlier than CE0270.** A two-part name written into the old `MessageDefinition` key makes mxbuild refuse to load the project (`StorageLoadException: ... is not a valid OldMessageDefinitionIdentifier`). The old key is typed as a three-part identifier even though 11.15 no longer reads it. The doctype script uses `-- @version: ..11.14` / `11.15+` sections, and the 11.15 mapping needs a different name from the older section's, because a plain `check` sees both sections and reports MDL-DUPDEF", "refs": ["ako/mxcli#987"]} {"area": "mdl/executor", "date": "2026-10-06", "symptom": "v0.25.0: `describe microflow` 4-8x slower than v0.24.0 on a flow arranged by hand in Studio Pro, and the cost grows faster than the flow (60 if/else blocks: 4.7 s -> 35.6 s); flows laid out by mxcli barely change", "cause": "The canonical describe's layout derivation (#748) re-renders the description every round, and a hand-laid flow takes gatedLayoutRounds+2 = 8 rounds. Each render re-ran the stored flow's split/merge analysis (findSplitMergePoints, labelCrossedMerges) and the body warnings (postDominators via microflowgraph.Analyze) - superlinear, and independent of which annotations the round keeps", "file": "`mdl/executor/cmd_microflows_derived_layout.go` (flowAnalysisMemo, installed by useDerivedFlowLayout, rebuildOnly view for the rounds), `mdl/executor/cmd_microflows_show.go` (findSplitMergePoints / labels / microflowBodyWarnings go through it)", "insight": "**Profile before believing the issue's theory.** The report blamed the round count, but rounds were already capped at 8; the rebuild itself was cheap. The CPU profile showed the cost was the *render* inside each round - the stored flow's graph analysis, redone per round. Memoising it per describe (keyed on the stored collection pointer) and leaving the `-- WARNING` comment lines out of rounds (the rebuild never reads them) gave 14.2 s -> 1.7 s at 60 blocks, with the printed description byte-identical to the unfixed build. A wall-clock assertion would be flaky; the test counts split/merge analyses per describe (18 over 8 rounds before, 2 after), with the round count as its control", "refs": ["mendixlabs/mxcli#1301"]} {"area":"mdl-executor","date":"2026-10-05","symptom":"A view entity selecting a **non-localized** DateTime column (Studio Pro's \"Localize\" unticked — the normal choice for a calendar date) passes `mxcli check --references` and exec, then `mx check` fails CE6770 \"View Entity is out of sync with the OQL Query.\" The view attribute was always written LocalizeDate = true, and MDL has no spelling for the flag, so describe -> exec re-broke it on every run","cause":"execCreateViewEntity built every view attribute with convertDataType, whose DateTime is Mendix's default LocalizeDate = true; nothing looked at the attribute the column reads","file":"`mdl/executor/oql_view_localize_date.go` (viewDateTimeLocalize), `mdl/executor/cmd_entities.go` (execCreateViewEntity); test `mdl/executor/view_entity_localize_date_test.go`; bug-test `mdl-examples/bug-tests/1297-view-entity-non-localized-datetime.mdl`","insight":"**Derive, don't add syntax**: on a view entity the column's localization is a property of the query, so the fix reads it from the source attribute and describe -> exec round-trips (`Unchanged`) with nothing new to spell — same reasoning as the view association (the column is the declaration). **Measure the shapes before choosing the scope**: one project, one view per shape, mxbuild 11.12.5 — pass-through, `s/Attr`, MIN, MAX and CASE over a non-localized source are ALL CE6770 when written localized and 0 errors when patched to false, so a pass-through-only rule (the obvious one, mirroring the string-length rule) would have fixed the report and left MAX(date) broken. Sources that disagree (coalesce of a localized and a non-localized column) are unmeasured and keep the default. **Repro needs a Studio Pro-authored flag**: mxcli writes every persistent DateTime localized, so the bug is invisible from MDL alone — the reporter's pymongo patch flipping Sale.SaleDate is the cheapest stand-in. Control: stubbing the assignment fails 5 of 6 cases with `LocalizeDate = true`; pre-fix binary on the same project gives 5 × CE6770, fixed gives 0","refs":["mendixlabs/mxcli#1297"]} +{"area": "mdl-executor", "date": "2026-10-07", "symptom": "`retrieve … where starts-with(Name, 'MS-' + $Key)` passes `check --references` and `exec`, then mx check CE0161 \"Error(s) in XPath constraint.\" — the same `+` at the constraint's top level (`Name = 'X-' + $Key`) builds clean", "cause": "MDL091 only matched expression-only function NAMES (startsWith/endsWith/getKey) by regex over the rendered XPath string; an operator inside a function ARGUMENT is a different expression construct the string regex never looked for", "file": "`mdl/executor/validate_microflow.go` (`xpathFunctionArgumentOperators`, `checkXPathFunctionArgumentOperators`, MDL091)", "insight": "Walk the `RetrieveStmt.Where` AST, not the XPath string: a call's argument that IS a `+`/`-` BinaryExpr is CE0161 (after unwrapping ParenExpr AND `SourceExpr` — the retrieve where-clause is wrapped in a SourceExpr, and a walker that misses it silently matches nothing, which is how the first cut passed nothing while compiling clean). Only the unbracketed form reaches the writer: the bracketed `[…]` grammar already refuses `+` in a function argument as a parse error. Measured on mxbuild 11.14.0 with a stubbed-out check to force the fault in: `+` and `-` in a string argument CE0161 (also nested in `not()`), `year-from-dateTime(Due) = $N - 1` clean. `*`/`div`/`mod` deliberately NOT flagged — the only argument they fit is numeric, and the control `contains(Name, $N)` is already CE0161 with no operator, so no measurement isolates the operator; MDL091 is exec-enforced, so an unmeasured operator would be a write barrier on a guess. Tests `TestValidateMicroflow_XPathFunctionArgumentOperator`, `TestCheckExecAgree_RetrieveConstraintFunctionArgumentOperator`; repro `mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl`", "refs": ["mendixlabs/mxcli#1326", "mendixlabs/mxcli#1213"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "`set $Cursor = $Next;` where both are OBJECT variables (Reference retrieves followed from the FROM entity) passes `check --references` and `exec` (\"Created microflow\"), is written as a Microflows$ChangeVariableAction, and mx check reports [CE7247] \"Variable 'Cursor' does not have a primitive type.\" Natural to write when walking a parent chain in a `while` loop", "cause": "`set` on a plain variable had two outcomes: Change list Replace for a known list (ako/mxcli#949) and Change variable for everything else, so an object fell into the primitive-only action. Mendix has NO action that reassigns an object variable. Check could not have caught it even with a guard: the check-time validator (validateFlowBody) typed every association retrieve as `List of `, because it had no association multiplicity, so the forward-Reference object read as a list", "file": "`mdl/executor/cmd_microflows_builder_actions.go` (`objectVariableType`, `refuseSetOnObject`), called from `cmd_microflows_builder_graph.go` (exec) and `cmd_microflows_builder_validate.go` (check); `validate.go` passes `checkAssociationShapes(ctx, sc)` into `validateFlowBody` so `forwardReferenceTarget` types a forward Reference retrieve as an object", "insight": "A refusal is only as good as the variable typing under it, and check and exec type variables differently: exec asks the backend for the association, check guessed `List of`. A guard added to the check validator alone passes the reported repro because the object it should refuse is, to check, a list. Write the check-path test against the REPORTED shape (association retrieve), not an object parameter, or it goes green while the issue stays open. Reuse `checkAssociationShapes` (project + script associations, built for MDL-ASSOCDS01) rather than a new lookup. Type only the unambiguous shape (Reference followed from FROM, not a self-association): Mendix types a self-association retrieve as a list, and that `set` is a valid Change list Replace. Measured on 11.14.0: faulted build exec -> CE7247; the self-association and recursive sub-microflow forms -> 0 errors. Plain `check` with no -p does not run validateFlowBody at all, so a `.fail.mdl` cannot pin this", "refs": ["mendixlabs/mxcli#1323", "ako/mxcli#949"], "ce": ["CE7247"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "Plain `mxcli check` (no -p) passed `set $X = $Y;` on an OBJECT variable (entity parameter, create, retrieve first, loop iterator, head) even after check --references and exec refused it; the build then failed with CE7247. The bug-test could not be a .fail.mdl because check-mdl runs plain check", "cause": "Plain check runs only the MDL0xx rule set (ValidateProgram -> ValidateMicroflow/ValidateNanoflow); validateFlowBody, where the first #1323 refusal lived, runs only under --references (validate.go) and in exec. A refusal added to one validator is invisible to the other path", "file": "`mdl/executor/validate_microflow_set_object.go` (`checkSetOnObjectVariable`, MDL-SET01), wired from `validate_microflow.go` (validate) and `validate_nanoflow.go`; `cmd_microflows_builder_validate.go` now reports only `assocObjectVars`", "insight": "Know which validator each entry point runs before adding a refusal: plain check = ValidateMicroflow; --references = that PLUS validateFlowBody; exec = the builder (plus execEnforcedMicroflowRules). Putting the rule in both check validators made --references print it twice; split by what each can see — the rule takes every object visible in the text, validateFlowBody only the association-typed ones that need the project. Measure every producer a rule judges, with the faulted build: all five plus a nanoflow gave CE7247 on 11.14.0, but a PARAMETER gives different wording, \"Parameter 'A' cannot be changed.\" — and that wording also fires for a PRIMITIVE parameter (`set $N = 1` on `$N: Integer`), a separate unrefused gap the object-only rule does not cover. Leave `find` out of the object producers: it is also the String function", "refs": ["mendixlabs/mxcli#1323", "ako/mxcli#1011"], "ce": ["CE7247"], "rules": ["MDL-SET01"]} {"area": "mdl-executor", "date": "2026-10-06", "symptom": "`create constant M.Flag (Type: Boolean, DefaultValue: true)` (or `True`) stores DefaultValue \"true\". Studio Pro stores \"True\"/\"False\"; its constant dialog shows a stored \"true\" as False while the runtime reads it as true, so the developer sees one value and the app runs with another. `mx check` is silent and `describe constant` prints `true` for both, so only the stored unit shows it. Quoted `'True'` was the workaround. `alter settings constant @M.Flag value true` (or 'true') wrote the configuration override the same way", "cause": "createConstant rendered the AST literal with fmt.Sprintf(\"%v\"), so a Go bool became Go's lowercase \"true\"; the quoted string passed through verbatim, which is why only 'True' was right. The settings path stored the visitor's token text verbatim and never looked at the constant's type", "file": "`mdl/executor/cmd_constants.go` (storedConstantDefault, used by the create and create-or-modify branches, and by `alterSettingsConstant` in `mdl/executor/cmd_settings.go` via settingsConstantType); test `mdl/executor/cmd_constant_boolean_default_test.go`; bug-test `mdl-examples/bug-tests/1321-boolean-constant-default-case.mdl`", "insight": "**`%v` on an AST literal is Go's spelling, not Mendix's**: any place that stringifies a parsed value into a stored property must normalise to the platform's form. The case-insensitive reader (formatDefaultValue's EqualFold) hid the writer's defect from describe, so a round-trip test could not catch it; assert on the value handed to the backend. Neither `mx check` nor the runtime distinguishes the two spellings — only Studio Pro's dialog does — so a build cannot verify this; the evidence is the reporter's Studio Pro 11.12.4 measurement plus the stored unit. Control: test written before the fix failed 5 of 6 spellings with `stored as \"true\"`/`\"false\"`, the quoted 'True' control passing; exec on a copy of testdata/pedapp then stores True/True/True/False. **Enumerate every write path for the value, not just the reported one**: the configuration override holds the same typed value and had the same defect; it needs the constant's type looked up (a String constant holding 'true' must stay lowercase). Control: the settings test failed 3 of 5 cases with `stored as \"true\"`/`\"FALSE\"` before the fix, the 'True' and String-constant controls passing; the stored Settings unit on a pedapp copy read 'true' for `value true` and `value 'true'` before, 'True' after, the String override 'true' both times", "refs": ["mendixlabs/mxcli#1321"]} diff --git a/.claude/skills/mendix/xpath-constraints/SKILL.md b/.claude/skills/mendix/xpath-constraints/SKILL.md index 50299a1034..634d04def1 100644 --- a/.claude/skills/mendix/xpath-constraints/SKILL.md +++ b/.claude/skills/mendix/xpath-constraints/SKILL.md @@ -215,7 +215,10 @@ where [Displayed = false()] Supported functions: `contains()`, `starts-with()`, `not()`, `true()`, `false()` The expression functions `startsWith()` / `endsWith()` are not XPath: in a -constraint they are CE0161, and `check` reports them as **MDL091**. A member the +constraint they are CE0161, and `check` reports them as **MDL091**. So is an +operator inside a function argument — `starts-with(Name, 'MS-' + $Key)` is +CE0161 although `Name = 'X-' + $Key` is fine: compute the value into a variable +first (`declare $P String = 'MS-' + $Key;`, then `starts-with(Name, $P)`). A member the entity does not have is a reference error in `check -p`, and so is a system member written the way `describe` prints the attribute: XPath spells it `createdDate`, `changedDate`, `owner`, `changedBy` — `[CreatedDate > …]` is CE0161. diff --git a/mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl b/mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl new file mode 100644 index 0000000000..5a13185c0d --- /dev/null +++ b/mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl @@ -0,0 +1,38 @@ +-- Issue mendixlabs/mxcli#1326: an operator inside an XPath function argument. +-- +-- `mxcli check mdl-examples/bug-tests/1326-xpath-function-argument-operator.fail.mdl` +-- reports MDL091 on SUB_PlusInFunc, and exec refuses it. Before the fix it passed +-- check and exec, and mxbuild 11.14.0 rejected it: CE0161 "Error(s) in XPath +-- constraint." The two microflows after it are the controls, 0 errors on the +-- same mxbuild, and must stay clean. + +create or modify module G1326; + +create or modify persistent entity G1326.Item ( + Name: String(50) +); + +-- MDL091: the concatenation is inside the starts-with() argument. +create or modify microflow G1326.SUB_PlusInFunc ($Key: String) returns Integer as $N +begin + retrieve $L from G1326.Item where starts-with(Name, 'MS-' + $Key); + declare $N Integer = length($L); + return $N; +end; + +-- Clean: the same operator at the top level of the constraint. +create or modify microflow G1326.SUB_PlusTopLevel ($Key: String) returns Integer as $N +begin + retrieve $L from G1326.Item where Name = 'X-' + $Key; + declare $N Integer = length($L); + return $N; +end; + +-- Clean: the value computed beforehand — the fix MDL091 suggests. +create or modify microflow G1326.SUB_PreComputed ($Key: String) returns Integer as $N +begin + declare $P String = 'MS-' + $Key; + retrieve $L from G1326.Item where starts-with(Name, $P); + declare $N Integer = length($L); + return $N; +end; diff --git a/mdl/executor/validate_microflow.go b/mdl/executor/validate_microflow.go index 9f2b3a7b1e..9263a38b52 100644 --- a/mdl/executor/validate_microflow.go +++ b/mdl/executor/validate_microflow.go @@ -393,6 +393,7 @@ func (v *microflowValidator) walkBody(body []ast.MicroflowStatement) { xp := expressionToXPath(stmt.Where) v.checkXPathAssociationEmpty(stmt.Variable, xp) v.checkXPathFunctionNames(stmt.Variable, xp) + v.checkXPathFunctionArgumentOperators(stmt.Variable, stmt.Where) v.checkXPathIdConstraint(stmt.Variable, xp) v.checkXPathVariableTraversal(stmt.Variable, xp) } @@ -830,6 +831,89 @@ func (v *microflowValidator) checkXPathFunctionNames(variable, xpath string) { } } +// xpathArithmeticOperators are the operators an XPath function argument cannot +// carry. Measured on mxbuild 11.14.0 (mendixlabs/mxcli#1326): +// +// starts-with(Name, 'MS-' + $Key) CE0161 "Error(s) in XPath constraint" +// not(contains(Name, $Key + $Key)) CE0161 +// contains(Name, $Key - 'a') CE0161 +// Name = 'X-' + $Key clean — the same operator at top level +// year-from-dateTime(Due) = $N - 1 clean — an operator beside a call, not in it +// starts-with(Name, $P) clean — the value computed beforehand +// +// `*`, `div` and `mod` are left out on purpose: the only argument they could +// appear in is numeric, and `contains(Name, $N)` with an Integer $N is already +// CE0161 with no operator at all, so no measurement isolates the operator. This +// rule blocks exec, so it covers only what was shown to fail. +var xpathArithmeticOperators = map[string]bool{"+": true, "-": true} + +// xpathFunctionArgumentOperators returns, for each XPath function call in a +// retrieve constraint whose argument is itself a `+` or `-` operation, +// the function name and the operator. Only an argument that IS the operation +// counts: `not(Price > $A + 1)` has a comparison for an argument and is not +// flagged — that shape was not measured. +func xpathFunctionArgumentOperators(expr ast.Expression) (hits [][2]string) { + var walk func(ast.Expression) + walk = func(e ast.Expression) { + switch n := e.(type) { + case *ast.FunctionCallExpr: + for _, arg := range n.Arguments { + inner := unwrapXPathArgument(arg) + if b, ok := inner.(*ast.BinaryExpr); ok && xpathArithmeticOperators[strings.ToLower(b.Operator)] { + hits = append(hits, [2]string{mendixFunctionName(n.Name), b.Operator}) + } + walk(arg) + } + case *ast.BinaryExpr: + walk(n.Left) + walk(n.Right) + case *ast.UnaryExpr: + walk(n.Operand) + case *ast.ParenExpr: + walk(n.Inner) + case *ast.SourceExpr: + walk(n.Expression) + case *ast.XPathPathExpr: + for _, s := range n.Steps { + walk(s.Expr) + walk(s.Predicate) + } + } + } + walk(expr) + return hits +} + +// unwrapXPathArgument strips the parentheses and source wrappers around an +// argument, down to the expression that is its value. +func unwrapXPathArgument(e ast.Expression) ast.Expression { + for { + switch n := e.(type) { + case *ast.ParenExpr: + e = n.Inner + case *ast.SourceExpr: + e = n.Expression + default: + return e + } + } +} + +// checkXPathFunctionArgumentOperators flags (MDL091) an operator inside an XPath +// function argument — the other way an expression construct gets into a +// retrieve constraint and out as CE0161 (mendixlabs/mxcli#1326). +func (v *microflowValidator) checkXPathFunctionArgumentOperators(variable string, where ast.Expression) { + for _, h := range xpathFunctionArgumentOperators(where) { + v.addViolation("MDL091", linter.SeverityError, + fmt.Sprintf("retrieve '$%s' constraint computes `%s` inside an argument of `%s()` — XPath does not "+ + "evaluate an operator in a function argument, and mxbuild reports CE0161 \"Error(s) in XPath constraint\"", + variable, h[1], h[0]), + fmt.Sprintf("Compute the value into a variable first, then pass the variable: "+ + "`declare $P String = ;` and `%s(, $P)`. An operator at the top "+ + "level of the constraint (`Name = 'X-' + $Key`) is fine.", h[0])) + } +} + // xpathVarTraversalRe matches a path rooted at a $variable with TWO OR MORE // segments (`$P/Mod.Assoc/Name`). One segment is deliberately not matched: both // `$P/Code` (the parameter's own attribute) and `$P/Mod.Assoc` (one hop to the diff --git a/mdl/executor/xpath_function_operator_test.go b/mdl/executor/xpath_function_operator_test.go new file mode 100644 index 0000000000..6b31e4625f --- /dev/null +++ b/mdl/executor/xpath_function_operator_test.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" +) + +// mendixlabs/mxcli#1326: an operator inside an XPath function argument passed +// check and exec, then failed the build with CE0161 on mxbuild 11.14.0 — the +// measured pairs are on xpathArithmeticOperators. The bracketed `[…]` form +// does not parse an operator in a function argument at all, so only the +// unbracketed form reached the writer. +func TestValidateMicroflow_XPathFunctionArgumentOperator(t *testing.T) { + cases := []struct { + name string + where string + wantMDL bool + }{ + {"concatenation in starts-with", "starts-with(Name, 'MS-' + $Key)", true}, + {"parenthesised argument", "contains(Name, ($Key + 'x'))", true}, + {"inside not()", "not(ends-with(Name, $Key + '-X'))", true}, + {"subtraction in contains", "contains(Name, $Key - 'a')", true}, + {"inside and", "Name != empty and ends-with(Name, '-' + $Key)", true}, + {"top-level concatenation is fine", "Name = 'X-' + $Key", false}, + {"operator beside a call is fine", "year-from-dateTime(Due) = $N - 1", false}, + {"pre-computed argument is fine", "starts-with(Name, $Key)", false}, + {"operator in a literal is text", "starts-with(Name, 'a + b')", false}, + {"comparison inside not() is fine", "not(Name = $Key)", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + src := "create microflow M.F ($Key: String, $N: Integer)\nreturns list of M.Item\nbegin\n retrieve $L from M.Item where " + tc.where + ";\n return $L;\nend;" + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("parse errors: %v", errs) + } + mf := prog.Statements[0].(*ast.CreateMicroflowStmt) + var msgs []string + for _, vi := range ValidateMicroflow(mf) { + if vi.RuleID == "MDL091" { + msgs = append(msgs, vi.Message) + } + } + if got := len(msgs) > 0; got != tc.wantMDL { + t.Fatalf("MDL091 fired=%v, want %v (where: %q) %v", got, tc.wantMDL, tc.where, msgs) + } + if tc.wantMDL && !strings.Contains(strings.Join(msgs, "\n"), "CE0161") { + t.Errorf("message must name CE0161: %v", msgs) + } + }) + } +} + +// check and exec must agree: the report had both accepting the constraint. +func TestCheckExecAgree_RetrieveConstraintFunctionArgumentOperator(t *testing.T) { + const head = `mdl 1; +create module ModG; +create persistent entity ModG.Item (Name: String(50)); +create microflow ModG.SUB_PlusInFunc ($Key: String) +begin + retrieve $L from ModG.Item where %s; +end;` + exec, _, dir := openPedAppCopy(t) + bad := strings.Replace(head, "%s", "starts-with(Name, 'MS-' + $Key)", 1) + got := strings.Join(agreeCheck(t, exec, dir, bad), "\n") + if !strings.Contains(got, "MDL091") || !strings.Contains(got, "starts-with()") { + t.Errorf("check -p must report MDL091 for the operator, reported:\n%s", got) + } + if err := agreeExec(t, exec, bad); err == nil || !strings.Contains(err.Error(), "MDL091") { + t.Errorf("exec must refuse the operator with MDL091, got: %v", err) + } + + // The two shapes the report measured clean must stay accepted. + for _, ok := range []string{"Name = 'X-' + $Key", "starts-with(Name, $Key)"} { + exec2, _, dir2 := openPedAppCopy(t) + assertAgree(t, exec2, dir2, strings.Replace(head, "%s", ok, 1), "") + } +}