From 3842ef7138fbd96b314d7964c61907d795da1e5a Mon Sep 17 00:00:00 2001 From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:05:56 +0200 Subject: [PATCH] Address second round of Jesper Schulz-Wedde's review on PR #157 - log-writes-must-survive-rollback.good.al: fixed invalid trigger OnRun(var Rec: ...) declaration; Rec is implicit when TableNo is set. - exposed-objects-must-be-in-a-permission-set.md: distinguished the three exposure mechanisms (page/query web service or API, codeunit published as a web service, [ServiceEnabled] bound action on a page) and their actual permission targets (page/query "..." = X vs codeunit "..." = X). - code-must-not-change-workdate.md: scoped from an absolute "never" to "not as a side effect of unrelated logic" - verified real WorkDate(x) setter usage in BCApps demo-data generators and test codeunits. - bcpt-scenarios-must-be-app-specific.md: SingleInstance and StartScenario/EndScenario reframed as context-dependent patterns, not mandatory requirements - BCPT Create Customer uses neither. - test-feature-scenario-tags.good.al/.bad.al: replaced the invented LibrarySales.CreateCustomerWithPrice/"Item Price Mgt." calls with a real, verified price-list-line test using Library - Sales/Library - Inventory/ Library - Price Calculation. - page-design-must-match-bc-page-type-conventions.md: scoped the missing UsageCategory anti-pattern to pages intended as searchable entry points. - defensive-vs-offensive-code-must-match-blast-radius.md/.good.al/.bad.al: replaced the VAT registration number "low blast radius" example with a genuinely cosmetic field (customer home page URL). - source-organized-by-feature-not-object-type.md: anti-pattern reframed as inconsistency with a repo's own convention, not the object-type scheme itself. - pictures-must-use-media-not-blob.md: removed leftover "image variants" wording contradicting the already-corrected MediaSet description. Proactively fixed while sweeping all fixtures for invented APIs: - given-blocks-must-cover-full-precondition-chain.bad.al: PostSalesOrder called with wrong arity and referenced an undeclared variable. - ui-test-codeunit-naming.good.al/.bad.al: replaced the same fake "Item Price Mgt."/TestPage "Item Price" with real Library - Sales calls and the real Customer Card TestPage. Worklist completeness: added review-skill cues for the 12 of 18 new rules that had none (al-appsource-review.md, al-data-modeling-review.md, al-error-handling-review.md, al-security-review.md, al-style-review.md x3, al-testing-review.md x2, al-ui-review.md, al-upgrade-review.md, al-web-services-review.md), and fixed test-feature-scenario-tags' cue, which only matched the compliant (tagged) shape instead of the anti-pattern (untagged/generic-named test). Co-Authored-By: Claude Sonnet 5 --- .../code-must-not-change-workdate.md | 46 +++++++++++++------ .../pictures-must-use-media-not-blob.md | 4 +- ...ensive-code-must-match-blast-radius.bad.al | 4 +- ...nsive-code-must-match-blast-radius.good.al | 6 +-- ...-offensive-code-must-match-blast-radius.md | 2 +- .../log-writes-must-survive-rollback.good.al | 2 +- ...sed-objects-must-be-in-a-permission-set.md | 10 +++- ...ce-organized-by-feature-not-object-type.md | 18 ++++++-- .../bcpt-scenarios-must-be-app-specific.md | 2 +- ...-must-cover-full-precondition-chain.bad.al | 5 +- .../testing/test-feature-scenario-tags.bad.al | 24 +++++++--- .../test-feature-scenario-tags.good.al | 27 +++++++---- .../testing/test-one-when-per-test.bad.al | 31 ++++++++++--- .../testing/test-one-when-per-test.good.al | 46 ++++++++++++++----- .../testing/ui-test-codeunit-naming.bad.al | 19 ++++---- .../testing/ui-test-codeunit-naming.good.al | 33 ++++++++----- ...ign-must-match-bc-page-type-conventions.md | 10 ++-- .../skills/review/al-appsource-review.md | 1 + .../skills/review/al-data-modeling-review.md | 1 + .../skills/review/al-error-handling-review.md | 1 + microsoft/skills/review/al-security-review.md | 1 + microsoft/skills/review/al-style-review.md | 4 ++ microsoft/skills/review/al-testing-review.md | 8 ++-- microsoft/skills/review/al-ui-review.md | 1 + microsoft/skills/review/al-upgrade-review.md | 1 + .../skills/review/al-web-services-review.md | 1 + 26 files changed, 219 insertions(+), 89 deletions(-) diff --git a/microsoft/knowledge/data-modeling/code-must-not-change-workdate.md b/microsoft/knowledge/data-modeling/code-must-not-change-workdate.md index 428e6db..daf8259 100644 --- a/microsoft/knowledge/data-modeling/code-must-not-change-workdate.md +++ b/microsoft/knowledge/data-modeling/code-must-not-change-workdate.md @@ -13,28 +13,46 @@ application-area: [all] The work date is a per-user session setting the user controls from the client (the date shown in the top-right corner, used to default posting -dates and date filters). Application code must never call the `WorkDate` -function to set a new value. Doing so changes what the user sees and -defaults to for the rest of their session, as a side effect of running -unrelated business logic — a surprising, hard-to-trace behavior change the -user never asked for and has no visibility into. +dates and date filters). Business logic unrelated to that setting must not +call `WorkDate(NewDate)` as a side effect of doing something else — that +silently changes what the user sees and defaults to for the rest of their +session, a surprising, hard-to-trace behavior change the user never asked +for and has no visibility into. This is not a blanket ban on the setter +itself: BCApps' own demo-data generators legitimately save the current +work date, set a specific one to backdate the data they create, and +restore it afterward (see `CreateDemoEDocsBE.Codeunit.al`'s +`WorkDate(SampleInvoiceDate)` / `WorkDate(SavedWorkDate)` pair), and test +codeunits routinely set `WorkDate` deliberately to control the date context +a test runs under (hundreds of calls across BCApps' test suite, for +example `SustainabilityPostingTest.Codeunit.al`). Both are the code's +*actual purpose*, not a side effect of something unrelated. -This is a call-direction distinction: reading the current work date via -`WorkDate` (or `WorkDate()` with no argument) is fine and common — it is -only the assignment form, `WorkDate(NewDate)`, that is the anti-pattern. +This is a call-direction distinction for the read side: reading the +current work date via `WorkDate` (or `WorkDate()` with no argument) is +always fine. ## Best Practice -Read the work date to default a value; never write to it. +Read the work date to default a value. Only write to it when changing it +*is* the operation being performed — implementing the user's own +work-date/settings action, or a test or demo-data routine that deliberately +establishes a date context (saving and restoring the prior value if the +routine must leave the session as it found it). Business logic that exists +to do something else must never write `WorkDate` as an incidental side +effect; if a calculation needs a specific date, pass or compute that date +as a local variable instead. See sample: `code-must-not-change-workdate.good.al`. ## Anti Pattern -Setting the work date from within a codeunit, report, or page action -changes session state the user owns, for the duration of a call that has -nothing to do with the user's date preference. If a scenario genuinely -needs a specific date for a calculation, pass or compute that date as a -local variable — never repurpose the session's `WorkDate`. +Setting the work date from within a codeunit, report, or page action whose +purpose is unrelated to the user's date preference — for example, a +posting or calculation routine that calls `WorkDate(SomeDate)` to make its +own logic simpler. This changes session state the user owns for the +duration of a call that was never about the work date, and never restores +it. This is a different case from a test or demo-data routine explicitly +declaring a date context: the anti-pattern is unrelated logic silently +mutating state it does not own, not the setter form itself. See sample: `code-must-not-change-workdate.bad.al`. diff --git a/microsoft/knowledge/data-modeling/pictures-must-use-media-not-blob.md b/microsoft/knowledge/data-modeling/pictures-must-use-media-not-blob.md index 195c4a8..b9db6bd 100644 --- a/microsoft/knowledge/data-modeling/pictures-must-use-media-not-blob.md +++ b/microsoft/knowledge/data-modeling/pictures-must-use-media-not-blob.md @@ -30,8 +30,8 @@ file attachment blob unrelated to picture rendering). ## Best Practice -Use `Media` (or `MediaSet` for multiple image variants) for any field that -holds a picture. +Use `Media` for a single image, or `MediaSet` for multiple independent +images, for any field that holds a picture. See sample: `pictures-must-use-media-not-blob.good.al`. diff --git a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.bad.al b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.bad.al index fb822b3..6f2835f 100644 --- a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.bad.al +++ b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.bad.al @@ -1,6 +1,6 @@ // Both fields guarded the same way, out of habit rather than analysis. -if SalesHeader.Get(SalesHeader."Document Type"::Order, DocumentNo) then - VATRegNo := SalesHeader."VAT Registration No."; // low blast radius - fine +if Customer.Get(SalesHeader."Sell-to Customer No.") then + CustomerHomePage := Customer."Home Page"; // low blast radius - fine // but the same pattern, unexamined, was also applied here: if SalesHeader.Get(SalesHeader."Document Type"::Order, DocumentNo) then diff --git a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.good.al b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.good.al index 38fbc88..19fa2ad 100644 --- a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.good.al +++ b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.good.al @@ -1,8 +1,8 @@ // Low blast radius: guard, with an explicit chosen fallback. -if SalesHeader.Get(SalesHeader."Document Type"::Order, DocumentNo) then - VATRegNo := SalesHeader."VAT Registration No."; +if Customer.Get(SalesHeader."Sell-to Customer No.") then + CustomerHomePage := Customer."Home Page"; // Blank is an acceptable, deliberately-considered default here - the field -// is informational and a reviewer sees it before the document ships. +// is purely a display convenience and a reviewer sees it before the document ships. // High blast radius: let it fail loud, because this feeds posted VAT. SalesHeader.Get(SalesHeader."Document Type"::Order, DocumentNo); diff --git a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.md b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.md index fab6850..14b5217 100644 --- a/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.md +++ b/microsoft/knowledge/error-handling/defensive-vs-offensive-code-must-match-blast-radius.md @@ -21,6 +21,6 @@ See sample: `defensive-vs-offensive-code-must-match-blast-radius.good.al`. ## Anti Pattern -Guarding two fields the same way purely out of habit, without analyzing what each one feeds. A low-blast-radius field, such as a VAT registration number shown only on a printed document, and a high-blast-radius field, such as the VAT posting group that determines VAT actually applied to a posted transaction, are both wrapped in the same `if Header.Get(...) then ... else` pattern with a blank/zero fallback — leaving the posting-critical field free to post with a silently wrong value. +Guarding two fields the same way purely out of habit, without analyzing what each one feeds. A low-blast-radius field, such as a customer's home page URL shown only for convenience on a printed document, and a high-blast-radius field, such as the VAT posting group that determines VAT actually applied to a posted transaction, are both wrapped in the same `if Header.Get(...) then ... else` pattern with a blank/zero fallback — leaving the posting-critical field free to post with a silently wrong value. A VAT registration number is not a safe stand-in for the low-risk side of this example: it is legally relevant, often validated, and can feed external VAT services or mandated document output, so it belongs on the offensive/fail-fast side alongside the posting group, not next to it as the "safe" contrast. See sample: `defensive-vs-offensive-code-must-match-blast-radius.bad.al`. diff --git a/microsoft/knowledge/error-handling/log-writes-must-survive-rollback.good.al b/microsoft/knowledge/error-handling/log-writes-must-survive-rollback.good.al index 5c455fd..c0f7d65 100644 --- a/microsoft/knowledge/error-handling/log-writes-must-survive-rollback.good.al +++ b/microsoft/knowledge/error-handling/log-writes-must-survive-rollback.good.al @@ -15,7 +15,7 @@ codeunit 50100 "Sample Error Log Writer" // session — there is no shared memory with the caller's instance. TableNo = "Sample Error Log Buffer"; - trigger OnRun(var Rec: Record "Sample Error Log Buffer") + trigger OnRun() var ErrorLogEntry: Record "Sample Error Log"; begin diff --git a/microsoft/knowledge/security/exposed-objects-must-be-in-a-permission-set.md b/microsoft/knowledge/security/exposed-objects-must-be-in-a-permission-set.md index f7a05ff..3b325c4 100644 --- a/microsoft/knowledge/security/exposed-objects-must-be-in-a-permission-set.md +++ b/microsoft/knowledge/security/exposed-objects-must-be-in-a-permission-set.md @@ -11,11 +11,17 @@ application-area: [all] ## Description -An object that is reachable from outside the app's own UI is only usable if it is also granted execute access through a permission set. Three distinct mechanisms make an object reachable this way, and each needs to be checked on its own terms: a page or query published through the **Web Services** configuration page; a custom REST endpoint declared with `PageType = API` / `QueryType = API`; or an individual codeunit method exposed with the `[ServiceEnabled]` attribute (a method-level attribute — it does not apply to pages or queries as a property). When such an object is left out of every permission set, it becomes both unusable (no caller, human or service, can reach it) and invisible in review: nobody deliberately decided who may call it. Exposure without a matching grant is not a safe default; it is an endpoint nobody is governing. +An object that is reachable from outside the app's own UI is only usable if it is also granted execute access through a permission set. Three distinct mechanisms make an object reachable this way, each with its own permission target: + +- A page or query published through the **Web Services** configuration page, or a custom REST endpoint declared with `PageType = API` / `QueryType = API` — both need a `page "..." = X` / `query "..." = X` entry for that object. +- A codeunit published through **Web Services** exposes *every* public procedure on it as an OData/SOAP operation automatically — there is no per-method attribute to add. The permission target is the codeunit itself: `codeunit "..." = X`. +- `[ServiceEnabled]` is a method-level attribute used on a *page* procedure to expose it as an OData v4 bound action (for example a `Post` action on an invoice page) — it does not apply to pages, queries, or codeunits as an object-level property, and it does not create its own permission target. The action is still a call into that page object, so the page's own `page "..." = X` entry is what governs it. + +When such an object is left out of every permission set, it becomes both unusable (no caller, human or service, can reach it) and invisible in review: nobody deliberately decided who may call it. Exposure without a matching grant is not a safe default; it is an endpoint nobody is governing. ## Best Practice -Give every exposed object an explicit execute entry (`page "..." = X`, `query "..." = X`) in a permission set shipped by the app. Route sensitive endpoints into a dedicated, non-default admin permission set so reaching them requires a deliberate grant rather than being included by default. If an object should never be reachable from outside the app, remove the exposure itself (drop `PageType = API` / `ServiceEnabled`) rather than leaving an orphaned endpoint with no permission-set membership. +Give every exposed object an explicit execute entry in a permission set shipped by the app: `page "..." = X` / `query "..." = X` for a published or API page/query (including one that exposes a `[ServiceEnabled]` bound action), and `codeunit "..." = X` for a codeunit published as a web service. Route sensitive endpoints into a dedicated, non-default admin permission set so reaching them requires a deliberate grant rather than being included by default. If an object should never be reachable from outside the app, remove the exposure itself (drop `PageType = API` / the Web Services registration) rather than leaving an orphaned endpoint with no permission-set membership. See sample: `exposed-objects-must-be-in-a-permission-set.good.al`. diff --git a/microsoft/knowledge/style/source-organized-by-feature-not-object-type.md b/microsoft/knowledge/style/source-organized-by-feature-not-object-type.md index 990a93b..c698093 100644 --- a/microsoft/knowledge/style/source-organized-by-feature-not-object-type.md +++ b/microsoft/knowledge/style/source-organized-by-feature-not-object-type.md @@ -28,9 +28,21 @@ Each feature folder holds every object type it needs; shared code has one dedica ## Anti Pattern +A repository that documents or has established feature-based organization +as its convention, but then mixes in object-type folders for new work +anyway: + src/ - ├── Tables/ - ├── Pages/ + ├── Sales/ + │ └── Invoice/ + ├── Tables/ <- new objects land here instead of a feature folder └── Codeunits/ -Finding everything related to one feature now requires searching multiple folders and mentally reassembling it from scattered pieces. +The anti-pattern is inconsistency with the project's own chosen convention, +not the object-type scheme itself — a repository that deliberately and +consistently organizes by object type throughout is exercising the other +reasonable choice described above, not violating this rule. What actually +costs a reader time is a codebase where some features live under their own +folder and others are scattered across type folders, so finding everything +related to one feature means checking both schemes and reassembling it from +wherever each object happened to land. diff --git a/microsoft/knowledge/testing/bcpt-scenarios-must-be-app-specific.md b/microsoft/knowledge/testing/bcpt-scenarios-must-be-app-specific.md index 8dae0b7..1269b3e 100644 --- a/microsoft/knowledge/testing/bcpt-scenarios-must-be-app-specific.md +++ b/microsoft/knowledge/testing/bcpt-scenarios-must-be-app-specific.md @@ -15,7 +15,7 @@ A PerformanceTest app that ships with only the generic Microsoft BCPT samples (c ## Best Practice -For every major business flow the extension adds, create a matching `BCPT*` scenario codeunit: `SingleInstance = true`, implementing `"BCPT Test Param. Provider"`, wrapping the operation under test in `BCPTTestContext.StartScenario()` / `EndScenario()`, and building its own test data in a local `InitTest()` procedure rather than depending on hardcoded records. Give each distinct step its own named scenario so a regression in one step doesn't hide inside a coarser measurement. +For every major business flow the extension adds, create a matching `BCPT*` scenario codeunit implementing `"BCPT Test Param. Provider"`, building its own test data in a local `InitTest()` procedure rather than depending on hardcoded records. Beyond that shared shape, the interface details are context-dependent, not fixed requirements: most of Microsoft's own shipped BCPT samples declare `SingleInstance = true`, but `codeunit "BCPT Create Customer"` does not, relying instead on `OnRun` calling `InitTest()` unconditionally every run. Likewise, wrapping the operation under test in `BCPTTestContext.StartScenario()` / `EndScenario()` is a real, available pattern for splitting one codeunit's run into several separately measured steps — useful when a regression in one step should not hide inside a coarser, whole-`OnRun` measurement — but it is not what every sample does; `"BCPT Create Customer"` measures its entire `OnRun` as a single implicit scenario and never calls `StartScenario`/`EndScenario` at all. Choose per-step scenarios when step-level granularity matters to the flow being tested; otherwise a single measured `OnRun` is a legitimate, simpler choice. See sample: `bcpt-scenarios-must-be-app-specific.good.al`. diff --git a/microsoft/knowledge/testing/given-blocks-must-cover-full-precondition-chain.bad.al b/microsoft/knowledge/testing/given-blocks-must-cover-full-precondition-chain.bad.al index 4357b40..0181fdb 100644 --- a/microsoft/knowledge/testing/given-blocks-must-cover-full-precondition-chain.bad.al +++ b/microsoft/knowledge/testing/given-blocks-must-cover-full-precondition-chain.bad.al @@ -2,11 +2,14 @@ procedure PostSalesOrder_CreatesInvoice() var SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + InvoiceNo: Code[20]; begin // [GIVEN] a sales order — posting groups left to whatever exists in the test company LibrarySales.CreateSalesOrder(SalesHeader); // [WHEN] - LibrarySales.PostSalesOrder(SalesHeader, false, true); + InvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, false, true); // [THEN] + SalesInvoiceHeader.Get(InvoiceNo); Assert.RecordIsNotEmpty(SalesInvoiceHeader); end; diff --git a/microsoft/knowledge/testing/test-feature-scenario-tags.bad.al b/microsoft/knowledge/testing/test-feature-scenario-tags.bad.al index fa9d2c0..06e5d54 100644 --- a/microsoft/knowledge/testing/test-feature-scenario-tags.bad.al +++ b/microsoft/knowledge/testing/test-feature-scenario-tags.bad.al @@ -3,7 +3,9 @@ codeunit 50102 "Item Price Testing" Subtype = Test; var - ItemPriceMgt: Codeunit "Item Price Mgt."; + LibrarySales: Codeunit "Library - Sales"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryPriceCalculation: Codeunit "Library - Price Calculation"; Assert: Codeunit "Library Assert"; [Test] @@ -11,11 +13,21 @@ codeunit 50102 "Item Price Testing" var Customer: Record Customer; Item: Record Item; - Price, Disc: Decimal; + PriceListHeader: Record "Price List Header"; + PriceListLine: Record "Price List Line"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; begin - // setup mixed with assertions, no clear layers - Customer.Insert(false); - ItemPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc); - Assert.AreEqual(100, Price, ''); + // setup mixed with assertions, no clear layers, no FEATURE/SCENARIO/GIVEN/WHEN/THEN tags + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibraryPriceCalculation.CreatePriceHeader( + PriceListHeader, PriceListHeader."Price Type"::Sale, "Price Source Type"::Customer, Customer."No."); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); + LibrarySales.CreateSalesDocumentWithItem( + SalesHeader, SalesLine, SalesHeader."Document Type"::Order, Customer."No.", Item."No.", 1, '', 0D); + Assert.AreEqual(PriceListLine."Unit Price", SalesLine."Unit Price", ''); end; } diff --git a/microsoft/knowledge/testing/test-feature-scenario-tags.good.al b/microsoft/knowledge/testing/test-feature-scenario-tags.good.al index 5079b6d..fdbb76b 100644 --- a/microsoft/knowledge/testing/test-feature-scenario-tags.good.al +++ b/microsoft/knowledge/testing/test-feature-scenario-tags.good.al @@ -5,7 +5,8 @@ codeunit 50103 "Item Price Testing" var LibrarySales: Codeunit "Library - Sales"; - ItemPriceMgt: Codeunit "Item Price Mgt."; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryPriceCalculation: Codeunit "Library - Price Calculation"; Assert: Codeunit "Library Assert"; [Test] @@ -13,14 +14,24 @@ codeunit 50103 "Item Price Testing" var Customer: Record Customer; Item: Record Item; - UnitPrice, LineDiscPct: Decimal; + PriceListHeader: Record "Price List Header"; + PriceListLine: Record "Price List Line"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; begin // [SCENARIO] Customer with a specific price list line gets that unit price - // [GIVEN] a customer with a price list line at 100 LCY - LibrarySales.CreateCustomerWithPrice(Customer, Item, '', 100); - // [WHEN] - ItemPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); - // [THEN] - Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list'); + // [GIVEN] a customer and an item with a customer-specific sales price list line + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibraryPriceCalculation.CreatePriceHeader( + PriceListHeader, PriceListHeader."Price Type"::Sale, "Price Source Type"::Customer, Customer."No."); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); + // [WHEN] a sales line is created for that customer and item + LibrarySales.CreateSalesDocumentWithItem( + SalesHeader, SalesLine, SalesHeader."Document Type"::Order, Customer."No.", Item."No.", 1, '', 0D); + // [THEN] the sales line picks up the customer's price list line + Assert.AreEqual(PriceListLine."Unit Price", SalesLine."Unit Price", 'Unit price must match customer price list'); end; } diff --git a/microsoft/knowledge/testing/test-one-when-per-test.bad.al b/microsoft/knowledge/testing/test-one-when-per-test.bad.al index d24da5d..c49696c 100644 --- a/microsoft/knowledge/testing/test-one-when-per-test.bad.al +++ b/microsoft/knowledge/testing/test-one-when-per-test.bad.al @@ -1,15 +1,32 @@ [Test] -procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues() +procedure GetPrice_ThenGetPriceLines_ReturnsCorrectValues() var - TempBuffer: Record "Item Price Tier Buffer" temporary; - UnitPrice, LineDiscPct: Decimal; + Customer: Record Customer; + Item: Record Item; + PriceListHeader: Record "Price List Header"; + PriceListLine: Record "Price List Line"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; begin // [GIVEN] ... + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibraryPriceCalculation.CreatePriceHeader( + PriceListHeader, PriceListHeader."Price Type"::Sale, "Price Source Type"::Customer, Customer."No."); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); // [WHEN] first action - ItemPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct); + LibrarySales.CreateSalesDocumentWithItem( + SalesHeader, SalesLine, SalesHeader."Document Type"::Order, Customer."No.", Item."No.", 1, '', 0D); // [WHEN] second action — this is a second test in disguise - ItemPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer); + PriceListLine.Validate("Minimum Quantity", 10); + PriceListLine.Modify(true); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); // [THEN] asserting two unrelated things - Assert.AreEqual(100, UnitPrice, ''); - Assert.IsFalse(TempBuffer.IsEmpty(), ''); + Assert.AreEqual(PriceListLine."Unit Price", SalesLine."Unit Price", ''); + PriceListLine.SetRange("Price List Code", PriceListHeader.Code); + Assert.AreEqual(2, PriceListLine.Count(), ''); end; diff --git a/microsoft/knowledge/testing/test-one-when-per-test.good.al b/microsoft/knowledge/testing/test-one-when-per-test.good.al index 320bffd..cb61708 100644 --- a/microsoft/knowledge/testing/test-one-when-per-test.good.al +++ b/microsoft/knowledge/testing/test-one-when-per-test.good.al @@ -3,27 +3,51 @@ procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice() var Customer: Record Customer; Item: Record Item; - UnitPrice, LineDiscPct: Decimal; + PriceListHeader: Record "Price List Header"; + PriceListLine: Record "Price List Line"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; begin - // [GIVEN] a customer with a price list line at 100 - LibrarySales.CreateCustomerWithPrice(Customer, Item, '', 100); + // [GIVEN] a customer with a price list line for the item + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibraryPriceCalculation.CreatePriceHeader( + PriceListHeader, PriceListHeader."Price Type"::Sale, "Price Source Type"::Customer, Customer."No."); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); // [WHEN] - ItemPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + LibrarySales.CreateSalesDocumentWithItem( + SalesHeader, SalesLine, SalesHeader."Document Type"::Order, Customer."No.", Item."No.", 1, '', 0D); // [THEN] - Assert.AreEqual(100, UnitPrice, 'Unit price must match price list'); + Assert.AreEqual(PriceListLine."Unit Price", SalesLine."Unit Price", 'Unit price must match price list'); end; [Test] -procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine() +procedure GetPriceLines_TwoMinimumQuantityLines_ReturnsBoth() var Customer: Record Customer; Item: Record Item; - TempBuffer: Record "Item Price Tier Buffer" temporary; + PriceListHeader: Record "Price List Header"; + PriceListLine: Record "Price List Line"; begin - // [GIVEN] a customer with a tier price at min qty 10 - LibrarySales.CreateCustomerWithTierPrice(Customer, Item, '', 10, 90); + // [GIVEN] a customer price list with two minimum-quantity price lines for the same item + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibraryPriceCalculation.CreatePriceHeader( + PriceListHeader, PriceListHeader."Price Type"::Sale, "Price Source Type"::Customer, Customer."No."); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); + PriceListLine.Validate("Minimum Quantity", 10); + PriceListLine.Modify(true); + LibraryPriceCalculation.CreateSalesPriceLine( + PriceListLine, PriceListHeader.Code, "Price Source Type"::Customer, Customer."No.", + "Price Asset Type"::Item, Item."No."); + PriceListLine.Validate("Minimum Quantity", 50); + PriceListLine.Modify(true); // [WHEN] - ItemPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer); + PriceListLine.SetRange("Price List Code", PriceListHeader.Code); // [THEN] - Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected'); + Assert.AreEqual(2, PriceListLine.Count(), 'Exactly two price lines expected'); end; diff --git a/microsoft/knowledge/testing/ui-test-codeunit-naming.bad.al b/microsoft/knowledge/testing/ui-test-codeunit-naming.bad.al index 98fc2b2..2acba18 100644 --- a/microsoft/knowledge/testing/ui-test-codeunit-naming.bad.al +++ b/microsoft/knowledge/testing/ui-test-codeunit-naming.bad.al @@ -3,22 +3,25 @@ codeunit 50104 "Item Price Testing" Subtype = Test; [Test] - procedure GetPrice_LogicTest() + procedure ApplyDiscount_LogicTest() var - Customer: Record Customer; - Item: Record Item; - UnitPrice, LineDiscPct: Decimal; + Assert: Codeunit "Library Assert"; begin // logic test — fine on its own, but not paired with a UI test below - ItemPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + Assert.AreEqual(90, ApplyDiscount(100, 10), 'A 10% discount on 100 must yield 90'); + end; + + local procedure ApplyDiscount(UnitPrice: Decimal; DiscountPct: Decimal): Decimal + begin + exit(UnitPrice - (UnitPrice * DiscountPct / 100)); end; [Test] - procedure Page_ShowsPrice_UT() + procedure CustomerCard_Opens_UT() var - ItemPricePage: TestPage "Item Price"; + CustomerCard: TestPage "Customer Card"; begin // UI test mixed into a logic-test codeunit, and the codeunit lacks the _UT suffix - ItemPricePage.OpenNew(); + CustomerCard.OpenNew(); end; } diff --git a/microsoft/knowledge/testing/ui-test-codeunit-naming.good.al b/microsoft/knowledge/testing/ui-test-codeunit-naming.good.al index 5f216b0..6c343d8 100644 --- a/microsoft/knowledge/testing/ui-test-codeunit-naming.good.al +++ b/microsoft/knowledge/testing/ui-test-codeunit-naming.good.al @@ -3,13 +3,18 @@ codeunit 50105 "Item Price Testing" Subtype = Test; [Test] - procedure GetPrice_CustomerPrice_ReturnsUnitPrice() + procedure ApplyDiscount_ReducesUnitPrice() var - Customer: Record Customer; - Item: Record Item; - UnitPrice, LineDiscPct: Decimal; + Assert: Codeunit "Library Assert"; + DiscountedPrice: Decimal; begin - ItemPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + DiscountedPrice := ApplyDiscount(100, 10); + Assert.AreEqual(90, DiscountedPrice, 'A 10% discount on 100 must yield 90'); + end; + + local procedure ApplyDiscount(UnitPrice: Decimal; DiscountPct: Decimal): Decimal + begin + exit(UnitPrice - (UnitPrice * DiscountPct / 100)); end; } @@ -18,16 +23,20 @@ codeunit 50106 "Item Price Testing_UT" Subtype = Test; [Test] - procedure Page_EnterCustomerAndItem_FactBoxShowsPrice() + procedure CustomerCard_SetName_UpdatesField() var Customer: Record Customer; - Item: Record Item; - ItemPricePage: TestPage "Item Price"; + CustomerCard: TestPage "Customer Card"; Assert: Codeunit "Library Assert"; + LibrarySales: Codeunit "Library - Sales"; begin - ItemPricePage.OpenNew(); - ItemPricePage.CustomerNo.SetValue(Customer."No."); - ItemPricePage.ItemNo.SetValue(Item."No."); - Assert.AreEqual('100.00', ItemPricePage.PriceInfo.UnitPrice.Value(), ''); + LibrarySales.CreateCustomer(Customer); + CustomerCard.OpenEdit(); + CustomerCard.GoToRecord(Customer); + CustomerCard.Name.SetValue('Updated Name'); + CustomerCard.Close(); + + Customer.Get(Customer."No."); + Assert.AreEqual('Updated Name', Customer.Name, 'Name must be updated through the page'); end; } diff --git a/microsoft/knowledge/ui/page-design-must-match-bc-page-type-conventions.md b/microsoft/knowledge/ui/page-design-must-match-bc-page-type-conventions.md index b579f69..d3628d8 100644 --- a/microsoft/knowledge/ui/page-design-must-match-bc-page-type-conventions.md +++ b/microsoft/knowledge/ui/page-design-must-match-bc-page-type-conventions.md @@ -75,9 +75,11 @@ page with no `CardPageID` even though a Card page exists for the same table — signals a design step was skipped, not a stylistic choice. A Card page over a composite-key table is not automatically this anti pattern; check whether the table supplements a master record first. Also watch -for: a Worksheet or List page showing primary-key fields it shouldn't (or -hiding them when it should show them), and a page with no -`UsageCategory` set, which makes it invisible to Tell Me search even -though it otherwise works. +for a Worksheet or List page showing primary-key fields it shouldn't (or +hiding them when it should show them). A page with no `UsageCategory` set +is not automatically a defect either: supporting pages, subpages, dialogs, +and pages intended only to be reached through another workflow correctly +have no `UsageCategory` — flag its absence only on a page intended as a +searchable entry point in its own right. See sample: `page-design-must-match-bc-page-type-conventions.bad.al`. diff --git a/microsoft/skills/review/al-appsource-review.md b/microsoft/skills/review/al-appsource-review.md index 3f6a221..63ebbd5 100644 --- a/microsoft/skills/review/al-appsource-review.md +++ b/microsoft/skills/review/al-appsource-review.md @@ -52,6 +52,7 @@ The following targeted checks cover every current `appsource` article across the - A page or report that repository context identifies as a direct user entry point omits `UsageCategory` or sets it to `None` — `set-usagecategory-on-searchable-entry-points`. Do not select this article based only on object type; exclude supporting parts, dialogs, API pages, and objects intentionally reached through another page. - A `DateTime` assignment adds or subtracts a fixed duration to represent an assumed regional offset — `do-not-hard-code-time-zone-offsets`. Require contextual evidence such as an hour-sized constant, offset-oriented name, or time-zone comment; do not flag deadlines, schedules, or elapsed-time calculations. - For BC v27 or later, `app.json` adds or changes the `help` URL to a path deeper than two levels, or a changed Copilot/context-sensitive help arrangement would ground the app under an overly broad truncated parent — `keep-copilot-help-url-to-two-path-levels`. +- A release/submission pipeline change (`AL-Go-Settings.json`, a publish/release workflow) or an `app.json` version bump is present without the new complete version being strictly greater than the previously submitted one, or the change asserts a hand-edited build/revision or every-merge-is-a-release policy as a universal AppSource rule rather than a project-specific workflow choice — `release-must-update-app-version`. Require repository/pipeline context to know the previously submitted version; a single `app.json` diff cannot prove ordering on its own. Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`. diff --git a/microsoft/skills/review/al-data-modeling-review.md b/microsoft/skills/review/al-data-modeling-review.md index 12d555c..d3444ad 100644 --- a/microsoft/skills/review/al-data-modeling-review.md +++ b/microsoft/skills/review/al-data-modeling-review.md @@ -47,6 +47,7 @@ The following targeted checks cover every current `data-modeling` article. Treat - A `* Setup` table or its page changes singleton structure, uses a nonblank or generated key, permits insert/delete, uses a List page, or does not ensure the blank-keyed row exists — `setup-table-is-a-singleton`. - A new field is typed `Media`, `MediaSet`, or `BLOB` and the field's caption/name suggests a picture or image — `pictures-must-use-media-not-blob`. +- Code outside a test codeunit or a demo-data generator calls `WorkDate(NewDate)` (the assignment form, not a bare `WorkDate()` read) as part of logic whose purpose is unrelated to the work date itself — `code-must-not-change-workdate`. A test deliberately setting a date context, or a demo-data routine that saves, sets, and restores the work date to backdate the data it creates, is not this anti-pattern. - A new or extended table declares its `keys` block, primary-key field list, or naming suffix (`Ledger Entry`, `Journal Line`, `Header`/`Line`, `Setup`) — `table-design-must-match-bc-table-type-conventions`. - A custom master table changes its primary key, `No.`/`No. Series` fields, or `OnInsert` without assigning a blank `No.` from setup through a number series — `master-table-no-from-number-series-in-oninsert`. - BC v22 or later code introduces or retains `NoSeriesManagement`, `InitSeries`, `SelectSeries`, or `SetSeries`, or number assignment/manual-entry checks do not use codeunit `"No. Series"` methods such as `GetNextNo`, `IsManual`, or `TestManual` — `use-no-series-codeunit-not-noseriesmanagement`. diff --git a/microsoft/skills/review/al-error-handling-review.md b/microsoft/skills/review/al-error-handling-review.md index 98213ba..778e4e2 100644 --- a/microsoft/skills/review/al-error-handling-review.md +++ b/microsoft/skills/review/al-error-handling-review.md @@ -48,6 +48,7 @@ The following targeted checks cover every current `error-handling` article: - `[ErrorBehavior(ErrorBehavior::Collect)]`, `ErrorInfo.Collectible`, `HasCollectedErrors`, `GetCollectedErrors`, or `ClearCollectedErrors` is added or changed, especially when errors are collected without later surfacing/clearing them — `collect-validation-errors-with-errorbehavior`. - New or changed code calls `Session.StartSession` from within error/duration logging around a web-service call, background job, or other operation expected to fail — `log-writes-must-survive-rollback`. +- A guarded lookup (`if Record.Get(...) then ... else` or similar) sets a value used later, and the same guard shape (with the same blank/zero fallback style) is applied to a field that feeds a posted amount, a tax/VAT calculation, a quantity or price actually used in a transaction, or a legally/compliance-facing output — `defensive-vs-offensive-code-must-match-blast-radius`. The signal is a posting-critical or compliance-facing field guarded defensively with a silent fallback, not the mere presence of a guarded lookup. - Developer-only invariant text is raised with default client visibility, or a user-actionable validation is hidden as `ErrorType::Internal` — `errortype-internal-vs-client-for-diagnostics`. - `FieldError` receives a complete capitalized sentence, repeats the field caption/value, or ends the predicate with punctuation — `fielderror-default-message-logic`. - An unguarded `FieldError` is used as though it performed a comparison, or `TestField` is forced onto a complex rule needing a tailored predicate — `fielderror-vs-testfield`. diff --git a/microsoft/skills/review/al-security-review.md b/microsoft/skills/review/al-security-review.md index e5390fb..c6b63ca 100644 --- a/microsoft/skills/review/al-security-review.md +++ b/microsoft/skills/review/al-security-review.md @@ -49,6 +49,7 @@ For secret values, select the most specific sink owner: - When a `Text`/`Code` credential is declared, passed, returned, or unwrapped without a visible HTTP URI/header/body sink, use `secrettext-for-credentials.md`. - When that value is interpolated into a URI, authorization header, or HTTP body and sent through `HttpClient`, use `secrettext-with-httpclient.md` as the primary finding. It supersedes the generic credential-type article at that location; keep the latter only as a supporting reference when useful. +- When a page/query is registered in Web Services, declares `PageType = API`/`QueryType = API`, a codeunit is registered in Web Services, or `[ServiceEnabled]` is added to a page procedure, and no permission set in the app grants a matching `page "..." = X` / `query "..." = X` / `codeunit "..." = X` entry for that specific object — use `exposed-objects-must-be-in-a-permission-set.md`. The anti-pattern is the exposed object missing its own execute entry, even when the underlying table's `tabledata` permissions look complete; require repository-level permission-set context, since one file cannot prove an entry is absent elsewhere in the app. Once the candidate worklist is known, resolve layer-precedence conflicts per READ. Drop lower-precedence files whose normative guidance (`## Best Practice` or `## Anti Pattern`) directly contradicts a higher-precedence candidate, and record each dropped file in `suppressed` with `reason: "layer-precedence"`. Files that would have been candidates but are hidden because their layer is disabled in consumer configuration are recorded with `reason: "configuration"`. Files that never became candidates are NOT recorded in `suppressed`. diff --git a/microsoft/skills/review/al-style-review.md b/microsoft/skills/review/al-style-review.md index 5b0b6b6..986c73e 100644 --- a/microsoft/skills/review/al-style-review.md +++ b/microsoft/skills/review/al-style-review.md @@ -51,6 +51,10 @@ Apply these high-signal mappings before fuzzy topic ranking: - A `Label` or `TextConst` contains multiple or ambiguous placeholders but has no `Comment`, or its Comment does not explain every placeholder — `label-comment-explains-placeholders.md`. A single placeholder whose meaning is explicit in the text, such as `Customer %1`, is allowed without a Comment and must not be flagged. - A normal two-argument `Evaluate` has a resolved `DateFormula` destination and a hard-coded non-angle-bracket date-formula literal, directly or through a visible constant — `dateformula-evaluate-needs-language-independent-literals.md`. Do not use this cue for dynamic/localized external input, already invariant `<...>` input, or direct `CalcDate(Text, ...)` calls. +- `function-call-parentheses-required.md` applies only to a zero-argument invocation written without `()`. Never worklist it from an invocation that already has parentheses or supplies arguments, including `Error(Label, Arg1, Arg2)`. +- A new or changed comment restates what the adjacent code already makes obvious from its own names and structure (a comment that just repeats a variable/field/method name in prose) rather than explaining a non-obvious constraint, invariant, or workaround — `al-comments-must-not-restate-what-code-already-shows.md`. A comment absent entirely is not this anti-pattern; only a present-but-redundant comment is. +- A `page`/`pageextension` adds or changes a procedure body that performs a calculation, validation, or record mutation belonging to a business operation reused across entry points, rather than presentation-specific state or a call into a codeunit — `pages-must-not-contain-business-logic.md`. A page calling a codeunit procedure, or a page's own presentation-only state and formatting, is not this anti-pattern; nor is a data invariant that belongs on the table itself. +- Changed source files are added under an object-type folder (`Tables/`, `Pages/`, `Codeunits/`, etc.) in a repository whose existing structure is predominantly feature-based, or vice versa — `source-organized-by-feature-not-object-type.md`. The anti-pattern is inconsistency with the repository's own established convention, not the choice of either scheme; a repository consistently organized by object type throughout is not a violation. Require repository-level folder context; a single new file's path cannot prove the project's convention alone. Once the candidate worklist is known, resolve layer-precedence conflicts per READ and record suppressions. diff --git a/microsoft/skills/review/al-testing-review.md b/microsoft/skills/review/al-testing-review.md index 057bca0..66c8d25 100644 --- a/microsoft/skills/review/al-testing-review.md +++ b/microsoft/skills/review/al-testing-review.md @@ -46,9 +46,11 @@ A file enters the candidate worklist when its `keywords` intersect the extracted The following targeted checks cover every current `testing` article. Treat each as a candidate-selection cue: when the signal appears in changed code, add the named article to the worklist and evaluate it in Action. - A method in a `Subtype = Test` codeunit adds or changes `[TransactionModel(...)]`, exercises code that calls `Commit` under `AutoRollback`, defaults broadly to `AutoCommit`, or chooses `None` for a writing test — `transactionmodel-attribute-governs-test-transactions`. -- A test procedure or comment adds `[FEATURE]`/`[SCENARIO]`/`[GIVEN]`/`[WHEN]`/`[THEN]` tags — `test-feature-scenario-tags`. -- A test codeunit calls `TestPage` methods (`OpenNew`, `OpenView`, `OpenEdit`) — `ui-test-codeunit-naming`. -- A `[GIVEN]`-tagged setup precedes a posting call or report execution — `given-blocks-must-cover-full-precondition-chain`. +- A new or changed `[Test]` procedure is added, whether or not it already carries `[FEATURE]`/`[SCENARIO]`/`[GIVEN]`/`[WHEN]`/`[THEN]` tags — `test-feature-scenario-tags`. A procedure with no tags at all, or a generic name like `Test1`, is the anti-pattern signal; presence of the tags is the compliant shape, not the thing to search for. +- A test codeunit calls `TestPage` methods (`OpenNew`, `OpenView`, `OpenEdit`) alongside `[Test]` procedures in the same codeunit that call business-logic procedures directly with no `TestPage` involved — `ui-test-codeunit-naming`. The anti-pattern signal is both kinds of test mixed into one codeunit (or, on a project using the `_UT` convention, a UI-layer codeunit missing the suffix); a codeunit containing only `TestPage`-driven tests is not itself a violation. +- A `[GIVEN]`-tagged setup precedes a posting call or report execution and does not visibly set up posting-group/VAT setup records, an explicit date, or (for a report test) both an included and an excluded record — `given-blocks-must-cover-full-precondition-chain`. +- A test procedure contains more than one `[WHEN]` block, or more than one distinct action not labelled `[GIVEN]`, without the procedure name declaring a flow/defect-then-fix shape — `test-one-when-per-test`. +- A `BCPT*` scenario codeunit is added and the PerformanceTest app's only other scenario codeunits are copies of Microsoft's shipped BCPT samples (`BCPT Create Customer`, `BCPT Create Item Journal`, `BCPT Post GL Entries`, etc.) with no scenario exercising the extension's own codeunits, FlowFields, or pages — `bcpt-scenarios-must-be-app-specific`. - An `AutoCommit` test runs under a `Subtype = TestRunner` codeunit that omits `TestIsolation` or sets it to `Disabled`, leaving committed data between tests — `testisolation-belongs-on-the-test-runner`. Require runner/repository context; a standalone test file cannot prove which runner executes it. - A permission-sensitive test uses `TestPermissions = Disabled`, claims to test a restricted user without `"Permissions Mock"`/`"Library - Lower Permissions"`, or declares `[TestPermissions(...)]` without applying that context — `permission-tests-must-lower-the-execution-context`. - Test fixture code manually calls `Init`/`Insert`, invents keys or prerequisite records, or bypasses available `LibrarySales`, `LibraryPurchase`, `LibraryERM`, `LibraryInventory`, `LibraryRandom`, or equivalent library codeunits — `use-library-codeunits-for-test-fixtures`. diff --git a/microsoft/skills/review/al-ui-review.md b/microsoft/skills/review/al-ui-review.md index f665543..f54c0c9 100644 --- a/microsoft/skills/review/al-ui-review.md +++ b/microsoft/skills/review/al-ui-review.md @@ -40,6 +40,7 @@ Discard files that are not applicable. Retain conditionally applicable files onl Narrow the relevant files to the subset that applies to the changes under review. - **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to JavaScript/CSS/HTML that implements a control add-in's rendering or Business Central communication. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files. +- A new or changed page's name/suffix, primary-key handling, `CardPageID`, `SubPageLink`, `AutoSplitKey`, or `UsageCategory` doesn't match the conventions of its own declared `PageType` — `page-design-must-match-bc-page-type-conventions.md`. A Card page over a composite-key table that supplements a master record, or a supporting/subpage/dialog page intended only to be reached through another workflow and correctly omitting `UsageCategory`, is not this anti-pattern on its own; check whether the page is actually mixing conventions or is meant as a searchable entry point before flagging. - For each relevant knowledge file, compute overlap against changed page declarations and control add-in files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, promoted action definitions, field importance, page background tasks, DOM creation, ARIA attributes, keyboard/focus handlers, packaged-resource AJAX, and calls from JavaScript into AL. - Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Importance`, `Promoted`, `Additional`, `area(Promoted)`, `actionref`, `PromotedCategory`, `PromotedOnly`, `PromotedIsBig`, `ShowAs`, `SplitButton`, `fieldgroups`, `DropDown`, `UpdatePropagation`, `EnqueueBackgroundTask`, `OnAfterGetCurrRecord`, `OnAfterGetRecord`, `OnPageBackgroundTaskCompleted`, `OnPageBackgroundTaskError`, `RunPageBackgroundTask`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions, `CardPageID`, `AutoSplitKey`, `PageType = Card`, `PageType = List`, `PageType = Worksheet`, `PageType = Document`). diff --git a/microsoft/skills/review/al-upgrade-review.md b/microsoft/skills/review/al-upgrade-review.md index 9ae61f8..d47ae50 100644 --- a/microsoft/skills/review/al-upgrade-review.md +++ b/microsoft/skills/review/al-upgrade-review.md @@ -45,6 +45,7 @@ Narrow the relevant files to the subset that applies to the changes under review - Worklist the install-versus-upgrade rule when migration helpers are reachable only from an install codeunit. - Worklist `install-and-upgrade-codeunits-have-no-order.md` when a change adds multiple install or upgrade codeunits whose same-phase triggers share state or depend on one another. - Worklist `appversion-meaning-depends-on-execution-context.md` when install or upgrade code branches on `ModuleInfo.AppVersion()` or confuses it with `DataVersion()`. +- An upgrade-tag procedure nests a record loop or a multi-branch business-data condition inside the tag-check/exit guard, going past the tag-check-then-exit-then-single-upgrade-action shape, or one procedure mixes the gated logic for more than one distinct upgrade tag — `upgrade-tag-logic-must-not-nest-deeply.md`. A single `if UpgradeTag.HasUpgradeTag(...) then exit;` guard followed by one flat upgrade action (even one that loops over records to apply that single action) is the compliant shape, not the signal to flag — the anti-pattern is a buried, separately-conditioned business decision nested inside that action. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. diff --git a/microsoft/skills/review/al-web-services-review.md b/microsoft/skills/review/al-web-services-review.md index 19d0735..fd7df8d 100644 --- a/microsoft/skills/review/al-web-services-review.md +++ b/microsoft/skills/review/al-web-services-review.md @@ -40,6 +40,7 @@ Narrow the relevant files to the subset that applies to the changes under review - The changed AL object names and types — especially pages declared with `PageType = API`, API page `part` controls, queries declared with `QueryType = API`, and procedures that expose bound actions. - The changed properties and triggers, weighted toward API page metadata (`APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SourceTable`, `SourceTableTemporary`), navigation metadata (`SubPageLink`, `Multiplicity`, and visible singleton or collection semantics), CRUD guards (`InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`), the `OnOpenPage` trigger, and `OnValidate` triggers on exposed fields. - Webhook subscriber handlers and subscription lifecycle code, especially code that creates or renews subscriptions, handles `validationToken`, schedules from `expirationDateTime`, or targets resources whose eligibility is visible in the diff. +- An API page (`PageType = API`) is added or changed and its `InsertAllowed`/`ModifyAllowed`/`DeleteAllowed` properties are left at their default `true` (or explicitly set `true`) for an operation the endpoint's stated purpose does not need — `api-page-least-privilege-write-access.md`. The signal is an operation left enabled beyond what the endpoint's own described purpose requires, not the mere presence of these properties; a page that genuinely needs full CRUD and grants it deliberately is not a violation. - Tokens extracted from the diff that relate to API surface and behaviour (`PageType`, `QueryType`, `API`, `api-page`, `page-part`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SystemId`, `SubPageLink`, `subpagelink`, `Multiplicity`, `multiplicity`, `Many`, `ZeroOrOne`, `SourceTableTemporary`, `Job Queue Entry`, `webhook`, `webhookSupportedResources`, `webhook-supported-resources`, `subscriptions`, `notificationUrl`, `validationToken`, `validationtoken`, `expirationDateTime`, `expirationdatetime`, `ServiceEnabled`, `WebServiceActionContext`, `SetActionResponse`, `ReadIsolation`, `IsolationLevel`, `ReadCommitted`, `InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`, `SourceTable`). A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone.