toga-ai 1.0.768 → 1.0.769

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -380,6 +380,14 @@ across clients, establish **which app** the missing control is in. Detail:
380
380
  [Surface Resolver](surface-resolver.md) and
381
381
  [toga25-supply side navigation](../../toga25-supply/features/side-navigation-and-default-route.md).
382
382
 
383
+ > **Narrow scope precisely: the FLAGS are inert, the ENDPOINT is not gone.** "Dead config" above is
384
+ > about the `navigation-*` `AclActions` flags having no effect on the 2.5 UI — that stands, verified
385
+ > prod-wide. It is **not** a claim that `toga25-supply` has stopped calling `Page::meta`: verified
386
+ > 2026-09-03, five of its view models still call `GET /v2/pages/meta` (nothing in `src/` reads a
387
+ > field off the result). So a change to `Page::meta`'s request path can still reach the 2.5 app even
388
+ > though its ACL flags cannot reach the 2.5 nav. Detail:
389
+ > [Surface Frontend](../../toga25-supply/features/surface-frontend.md).
390
+
383
391
  Worked example (Quad, 2026-08-26): the vendor-items **Add and Edit buttons both** read
384
392
  `acl['vendor-items']['navigation-edit-on-vendor-items']` (AclActions id 7). `Client_Quad` had a row
385
393
  for id 5 only, so the page was reachable but button-less, even though record 19 already granted role
@@ -598,6 +606,10 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
598
606
  and every repo is on the **same branch** so the generated model matches the DB.
599
607
 
600
608
  ## Change history
609
+ - **2026-09-03** — Scoped the "dead config in the surface-driven 2.5 app" claim: the `navigation-*`
610
+ **flags** are inert (unchanged), but `toga25-supply` still **calls `GET /v2/pages/meta`** from five
611
+ view models, so `Page::meta` is not out of that app's request path. Additive clarification only.
612
+ (apeterson)
601
613
  - **2026-09-01** — Four additions from a `PurchaseOrderItems._qty*` grant investigation.
602
614
  (1) **Read `Core.Records.aclDatabase` FIRST** — both databases carry an `AclFieldPermissions`
603
615
  table, so checking Core for a `CLIENT`-acl record (here `purchase-order-items`, record 18) returns
@@ -5,16 +5,16 @@
5
5
  | [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/providers/TalosStreamProvider.tsx |
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.test.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalFlowDetailInputs.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASS/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASSCANADA/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
7
7
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
8
- | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
8
+ | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/resolveRole.ts, toga25-supply/src/globalFieldsConfig/clientLoginFields.json, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
9
9
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
12
12
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
13
13
  | [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
14
14
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
15
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
15
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
16
16
  | [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
17
17
  | [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
18
18
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/.env.development |
19
- | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
19
+ | [Cypress Testing Harness (multi-tenant, fully stubbed)](workflows/cypress-testing.md) | The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus a stub layer that answers **every** `/v2` call so no test touches | toga25-supply/cypress.config.ts, toga25-supply/cypress/README.md, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/tenants.ts, toga25-supply/cypress/support/api/stubApi.ts, toga25-supply/cypress/support/api/envelope.ts, toga25-supply/cypress/support/api/users.ts, toga25-supply/cypress/support/api/surfaces.ts, toga25-supply/cypress/support/api/session.ts, toga25-supply/cypress/e2e/login.cy.ts, toga25-supply/cypress/e2e/emptyNavigation.cy.ts, toga25-supply/cypress/component/harness.cy.tsx, toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json, toga25-supply/scripts/cypress.mjs, toga25-supply/tsconfig.node.json, toga25-supply/package.json |
20
20
  | [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
@@ -19,6 +19,7 @@ files:
19
19
  - toga25-supply/src/hooks/useTableCellInteractions.ts
20
20
  - toga25-supply/src/providers/TalosStreamProvider.tsx
21
21
  related:
22
+ - workflows/cypress-testing.md
22
23
  - features/talos-integration.md
23
24
  - features/client-configurable-fields.md
24
25
  - features/meta-driven-table-data.md
@@ -70,6 +71,18 @@ toga-blox table/modal templates.
70
71
  - **UI library:** `@agilant/toga-blox` provides `PrimaryTable*Layout`, `TableRecordModal`,
71
72
  `BaseInput`, `BaseButton`, `DetailSection`, the `useTableData` hook, FontAwesome helpers, etc.
72
73
 
74
+ ### Testing
75
+
76
+ Two harnesses, both fully stubbed — **no front-end test ever talks to a backend**:
77
+
78
+ - **Vitest + React Testing Library** (`npm test`) for logic: hooks, view models, auth context,
79
+ table URL-state, error classification.
80
+ - **Cypress** (`npm run cy:e2e:<tenant>`, `npm run cy:ct`) for the **tenant x role x feature**
81
+ matrix, driven by per-tenant surface **fixtures** rather than real logins or a seeded database.
82
+
83
+ The full design, run commands, guardrails and traps live in
84
+ [cypress-testing](workflows/cypress-testing.md) — read that before writing or changing a test.
85
+
73
86
  ## Directory layout (`src/`)
74
87
 
75
88
  - `pages/<Entity>/` — page component (thin) + `viewModel/` (the `use*PageViewModel` hook) +
@@ -6,10 +6,12 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-01
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/fieldsConfig/index.ts
13
+ - toga25-supply/src/fieldsConfig/resolveRole.ts
14
+ - toga25-supply/src/globalFieldsConfig/clientLoginFields.json
13
15
  - toga25-supply/src/fieldsConfig/useClientFields.ts
14
16
  - toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/
15
17
  - toga25-supply/src/pages/Inventory/viewModel/FIELDS/
@@ -61,6 +63,37 @@ and the `useClientFields()` hook.
61
63
  const tenantFields = clientFields.salesOrdersPageFields; // your key
62
64
  ```
63
65
 
66
+ ### How `resolveRole` decides the role (verified 2026-09-03)
67
+
68
+ `src/fieldsConfig/resolveRole.ts` takes `(clientSlug, user)` and reads **boolean-ish string flags
69
+ off the `/v2/users` record** — `"1"` means set. The flag set and the returned role names **differ
70
+ per client**, and the checks are ordered, so the first match wins:
71
+
72
+ | clientSlug | flag (in order) | role returned |
73
+ |---|---|---|
74
+ | `COMPASS`, `COMPASSCANADA` | `_isAdmin` | `ADMIN` |
75
+ | | `_isSuperAdmin` | `SUPERUSER` |
76
+ | | `_isSupervisor` | `MANAGER` |
77
+ | | none set | `USER` |
78
+ | `QUAD` | `_isGlobalAdmin` | `GLOBALADMIN` |
79
+ | | `_isBuyer` | `BUYER` |
80
+ | | `_isItShopper` | `ITSHOPPER` |
81
+ | | none set | `USER` |
82
+ | any other client | — | `DEFAULT` |
83
+
84
+ Two things follow that catch people out:
85
+
86
+ - **Which flags are even requested is per-client**, declared in
87
+ `src/globalFieldsConfig/clientLoginFields.json` (`userApiFields`). A flag the client does not
88
+ request is never fetched, so `resolveRole` cannot see it. `NYCHH`, for example, has no entry, so
89
+ no role flags are requested and the role is always `DEFAULT`.
90
+ - **An ungranted or unrequested field comes back ABSENT, not null**, from the API (see the
91
+ [ACL permission chain](../../_underscore/features/acl-permission-chain.md)) — so `user._isAdmin`
92
+ being `undefined` is the normal shape, not a bug.
93
+
94
+ The Cypress harness mirrors this table as `roleFlags` per tenant in `cypress/support/tenants.ts`;
95
+ keep the two in step. See [Cypress testing harness](../workflows/cypress-testing.md).
96
+
64
97
  ### By client × role vs. by client only
65
98
 
66
99
  - **Client × role** (e.g. `salesOrdersPageFields` — ADMIN vs MANAGER see different filter buttons):
@@ -168,6 +201,11 @@ modal's chrome from the same per-client JSON, not from the component:
168
201
  `vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
169
202
 
170
203
  ## Change history
204
+ - 2026-09-03 — Documented the actual `resolveRole` flag → role table (COMPASS/COMPASSCANADA:
205
+ `_isAdmin`/`_isSuperAdmin`/`_isSupervisor`, in that order, else `USER`; QUAD:
206
+ `_isGlobalAdmin`/`_isBuyer`/`_isItShopper`, else `USER`; every other client `DEFAULT`) and the
207
+ fact that which flags are requested is itself per-client, via
208
+ `globalFieldsConfig/clientLoginFields.json` — so NYCHH is always `DEFAULT`. (apeterson)
171
209
  - 2026-07-20 — Extracted the **API-fetch projection** out of these presentation JSON blobs into a
172
210
  new per-page `apiFields.json` + `useClientApiFields` mechanism (see
173
211
  [client-api-fetch-fields](client-api-fetch-fields.md)). The `salesOrderDetailsConfig` block was
@@ -28,6 +28,11 @@ files:
28
28
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
29
29
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx
30
30
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
31
+ - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
32
+ - toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
33
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
34
+ - toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx
35
+ - toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
31
36
  - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
32
37
  - toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx
33
38
  - toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx
@@ -78,6 +83,7 @@ files:
78
83
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
79
84
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
80
85
  related:
86
+ - ../workflows/cypress-testing.md
81
87
  - ../../_underscore/features/surface-resolver.md
82
88
  - ../../_underscore/features/sales-order-denial-reason.md
83
89
  - meta-driven-table-data.md
@@ -965,7 +971,43 @@ Final: the parameterized FE map above.
965
971
  button except **View Log** renders and does nothing on click. Whether the SR modal gets its own
966
972
  actions surface is **still an open decision**, not an oversight.
967
973
 
974
+ ## ⚠ The app still calls the legacy `GET /v2/pages/meta`
975
+
976
+ Verified 2026-09-03. The Surface layer replaced `Page::meta` **as the source of nav/ACL/label
977
+ config** — that part of the story holds — but the **endpoint is still being called**. Five view
978
+ models call blox's `useFetchPageMeta` → `getPageMeta` → `apiGet("/pages/meta", {page, app})`:
979
+
980
+ - `src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx` (fires **twice** per landing-page load)
981
+ - `src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx`
982
+ - `src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx`
983
+ - `src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx`
984
+ - `src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx`
985
+
986
+ **Nothing in `src/` reads any field off the result.** The only references to `pageMeta.acl` are
987
+ *comments* in `src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts` noting it was replaced. So
988
+ these are almost certainly dead weight left over from the Surface migration — but until they are
989
+ removed, the app still makes the call, and a request-level change to `/pages/meta` can still hit
990
+ this app.
991
+
992
+ Two consequences:
993
+
994
+ - Any test/stub layer must answer `/pages/meta` or the calls fall through (this is how it was
995
+ found — the Cypress fall-through guardrail flagged it on its **first** run, not a code read).
996
+ See [Cypress testing harness](../workflows/cypress-testing.md).
997
+ - **Removing the five calls is separate, unstarted work.** Stubbing them is not the fix.
998
+
999
+ Related: [legacy page meta](../../_underscore/features/page-meta-context-field-settings.md) and the
1000
+ [ACL permission chain](../../_underscore/features/acl-permission-chain.md) (whose "dead config"
1001
+ claim is about the `navigation-*` **flags** being inert — still true — not about the endpoint being
1002
+ unused).
1003
+
968
1004
  ## Change history
1005
+ - 2026-09-03 — Recorded that the 2.5 app **still calls `GET /v2/pages/meta`** from five view
1006
+ models (SalesOrders — twice per load — VendorItems, Inventory, Bundles, ItemFulfillmentModal)
1007
+ even though nothing in `src/` reads a field off the result. Found by the new Cypress
1008
+ fall-through guardrail. Qualifies, does not overturn, the "Page::meta is replaced" claim: the
1009
+ nav/ACL config really is surface-driven; the endpoint call is leftover. (apeterson)
1010
+
969
1011
  - 2026-09-03 — **Built the stacked-Total `+` joiner in the Approval Workflow modal**
970
1012
  (`withStackedCurrencyJoiner`, new `RecordApprovalModal/helpers/stackedCurrencyJoiner.ts` + 13
971
1013
  vitest cases; one-line wiring in `RecordApprovalModalLayout` `renderField`, workflow modal only —
@@ -1,143 +1,280 @@
1
1
  ---
2
- title: Cypress Testing Harness (component + e2e)
2
+ title: Cypress Testing Harness (multi-tenant, fully stubbed)
3
3
  framework: "2.0"
4
4
  repo: toga25-supply
5
5
  project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-06-30
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/cypress.config.ts
13
+ - toga25-supply/cypress/README.md
13
14
  - toga25-supply/cypress/tsconfig.json
14
- - toga25-supply/cypress/support/component.tsx
15
- - toga25-supply/cypress/support/component-index.html
16
15
  - toga25-supply/cypress/support/e2e.ts
17
16
  - toga25-supply/cypress/support/commands.ts
18
- - toga25-supply/cypress/support/fixtures.ts
19
- - toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts
20
- - toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx
21
- - toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx
22
- - toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx
23
- - toga25-supply/cypress/e2e/salesOrderApproval.cy.ts
17
+ - toga25-supply/cypress/support/component.tsx
18
+ - toga25-supply/cypress/support/component-index.html
19
+ - toga25-supply/cypress/support/tenants.ts
20
+ - toga25-supply/cypress/support/api/stubApi.ts
21
+ - toga25-supply/cypress/support/api/envelope.ts
22
+ - toga25-supply/cypress/support/api/users.ts
23
+ - toga25-supply/cypress/support/api/surfaces.ts
24
+ - toga25-supply/cypress/support/api/session.ts
25
+ - toga25-supply/cypress/e2e/login.cy.ts
26
+ - toga25-supply/cypress/e2e/emptyNavigation.cy.ts
27
+ - toga25-supply/cypress/component/harness.cy.tsx
28
+ - toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json
29
+ - toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json
30
+ - toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json
31
+ - toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json
32
+ - toga25-supply/scripts/cypress.mjs
33
+ - toga25-supply/tsconfig.node.json
34
+ - toga25-supply/package.json
24
35
  related:
25
- - ../features/record-modals-and-nested-tables.md
26
- - ../features/action-button-rule-engine.md
36
+ - ../features/surface-frontend.md
37
+ - ../features/client-configurable-fields.md
38
+ - ../features/side-navigation-and-default-route.md
39
+ - ../features/sso-redirect-and-session-gating.md
40
+ - ../features/force-logout-on-deployment.md
27
41
  - ../architecture.md
42
+ - ../../../2.0/standards/frontend.md
28
43
  ---
29
44
 
30
45
  ## What it is
31
46
 
32
- The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch
33
- (`cypress` was already a dependency but there was no config, no `cypress/` dir, and zero
34
- tests anywhere in the repo). It defines **both** a `component` project (Vite devServer,
35
- mounts a single component) and an `e2e` project, plus reusable support code for mounting
36
- components with the app's providers, mocking a component's data hook without a network or
37
- tenant config, and sharing fixtures. The first specs cover the Sales Order **approval
38
- modal flow**. Use this doc when adding any new test to the repo.
39
-
40
- ## How the harness is wired
41
-
42
- ### `cypress.config.ts` — two projects
43
-
44
- - **`component`** — `devServer: { framework: "react", bundler: "vite" }` with a `viteConfig`
45
- that **mirrors the app's `vite.config.ts`**: the `@` → `src` alias, the regex aliases that
46
- force a single React copy, `optimizeDeps.exclude: ["@agilant/toga-blox"]` +
47
- `optimizeDeps.include: ["dayjs"]`. This is the load-bearing part — toga-blox must resolve
48
- the same way it does under `npm run dev`, or mounted components fail to bundle or get a
49
- second React copy.
50
- - **`e2e`** — standard `baseUrl` + `supportFile` setup for full-journey specs.
51
-
52
- ### `cypress/support/component.tsx` + `commands.ts` — mounting with providers
53
-
54
- `cy.mountWithProviders` is a custom command that wraps the mounted component in
55
- `QueryClientProvider` + `ToasterProvider`. It accepts an **injectable `queryClient`** so a
56
- spec can `cy.spy()` on `removeQueries` / `invalidateQueries` and assert the cache routine a
57
- component runs on success. The default QueryClient is configured with `retry: false` for
58
- determinism. Support also imports `@/index.css` (styling) and `@/api/api` — the latter so
59
- toga-blox's `apiPost` / `apiPut` have a configured axios instance available during component
60
- tests.
61
-
62
- ### Window-backed hook-mock pattern (key technique)
63
-
64
- `cypress/support/mocks/useApprovalModalViewModel.ts` lets you unit-test a data-binding
65
- component **without** a network or tenant config. The component devServer aliases the
66
- component's **relative** import of its view model to the mock:
67
-
68
- ```ts
69
- // in the component viteConfig resolve.alias
70
- { find: /^\.\/viewModel\/useApprovalModalViewModel$/, replacement: "<abs>/cypress/support/mocks/useApprovalModalViewModel.ts" }
71
- ```
72
-
73
- The mock returns whatever the spec parked on `window.__approvalModalViewModel`. In Cypress
74
- component testing the spec and the component share the **same `window`**, so a plain
75
- assignment before `cy.mount` is visible to the hook. Helpers `setApprovalModalViewModel` /
76
- `resetApprovalModalViewModel` set and clear it. To mock a different component's hook, add a
77
- parallel regex alias + a parallel `window.__<name>` mock.
78
-
79
- ### `cypress/support/fixtures.ts` — shared fixtures
80
-
81
- Imports the **real shipped** tenant config
82
- `src/pages/SalesOrders/.../viewModel/FIELDS/QUAD/approvalActionFields.json` as
83
- `recordActionFields`, so tests track the actual field / label / guardrail config the app
84
- ships (not a hand-rolled copy that drifts). Adds synthetic order / approval-stages / PO
85
- fixtures alongside it. (The QUAD file is reused only as a representative real config — this
86
- harness is generic test infra, not Quad-specific behavior.)
87
-
88
- ## What the specs cover
89
-
90
- - **`SalesOrderApprovalModalsLayout.cy.tsx`** (component) — the sales-order binding:
91
- null-guards, `isApproveButtonEnabled` derivation, and the documented `onSuccess` cache
92
- routine asserted via the spied QueryClient.
93
- - **`RecordApprovalModalLayout.cy.tsx`** (component) — the generic approval modal flow: deny
94
- requires a reason (approve optional), submit fires the decision API + `onSuccess`/`onClose`,
95
- discard-reason guardrail on Cancel/backdrop, workflow approve-enablement, deep workflow
96
- stage editing (deny one stage with a required note → submits only that stage; an empty
97
- required note blocks submit), and the **decision-failure path** (500 → modal stays open,
98
- `onSuccess`/`onClose` not called).
99
- - **`EnterPoNumberModal.cy.tsx`** (component) — the PO-number action: create requires a PO#,
100
- POST `/purchase-orders/`, credit-card flag `0`/`1`, edit → PUT.
101
- - **`salesOrderApproval.cy.ts`** (e2e) — full journey: seed auth via
102
- `localStorage.accessToken`, row-action menu → Deny → submit, stubbing only the write.
103
-
104
- ## Conventions for writing tests here
105
-
106
- - **Component test of a data-binding component** → use the window-backed hook mock so you
107
- never need a real network/tenant config; park state on `window.__<hook>` before `cy.mount`.
108
- - **Asserting a cache side-effect** (invalidate/remove queries on success) → inject a spied
109
- `queryClient` into `cy.mountWithProviders`.
110
- - **Testing an async error path** → use `cy.on("uncaught:exception", ...)` to swallow the
111
- rejected `mutateAsync` so the rejection doesn't fail the spec, then assert the modal stayed
112
- open and `onSuccess`/`onClose` were not called.
113
- - **e2e auth** → seed `localStorage.accessToken`; stub only the write request, let reads hit
114
- the configured backend (document any backend prerequisites in the spec).
115
- - **Keep the component devServer config in sync with `vite.config.ts`** — when the app's
116
- Vite aliases or `optimizeDeps` change, mirror the change into `cypress.config.ts` or
117
- component mounts break.
118
-
119
- ## package.json scripts
120
-
121
- `cy:open`, `cy:run:ct` (component), `cy:run:e2e`, `test:ct`.
122
- `.gitignore` excludes `cypress/videos|screenshots|downloads`.
47
+ The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus
48
+ a stub layer that answers **every** `/v2` call so no test touches a backend. It exists to test
49
+ the thing that actually breaks in this app — **tenant × role × feature** — without needing real
50
+ logins or a seeded database.
51
+
52
+ Current state (2026-09-03, branch `TRUE-81735`, working tree only): **26 tests green** —
53
+ 6 e2e specs × 4 tenants (COMPASS, COMPASSCANADA, NYCHH, QUAD) + 2 component tests.
54
+ `cypress/README.md` is the hands-on version of this doc for a developer working without Claude
55
+ (run commands, recipes, traps). Read that to *write* a test; read this to understand *why* it is
56
+ shaped this way.
57
+
58
+ > **This replaces the earlier harness.** A first harness (approval-modal component specs, a
59
+ > window-backed hook-mock pattern) was built on branch **`TRUE-79813`** (commit `962a41d`
60
+ > "Save") and **never merged** — it is not on `_main`, `_production`, or any other branch, and
61
+ > its mirrored Vite config is now stale (hardcoded `optimizeDeps.exclude` for blox, 3-member
62
+ > `dedupe`). The decision was to **build fresh on `TRUE-81735`** rather than port it. If you go
63
+ > looking for `cy.mountWithProviders` or `cypress/support/mocks/`, that is where it lives — a
64
+ > stranded branch, not the live harness.
65
+
66
+ ## The three multi-tenant axes (the core design)
67
+
68
+ The app is multi-tenant along three axes, and **each has a different source of truth**, so each
69
+ is modelled in a different place. Get this and the rest of the harness follows.
70
+
71
+ | Axis | Source of truth in the app | Modelled in the harness as |
72
+ |---|---|---|
73
+ | **Client** | `window.location.hostname` — `useHostnameStore` takes the first label, upper-cased | the **`baseUrl`** Cypress visits (`TENANT` env var → `TENANT_HOSTS` in `cypress.config.ts`) |
74
+ | **Role** | flags on the `/users` record, resolved client-side by `src/fieldsConfig/resolveRole.ts` | `roleFlags` per tenant in `cypress/support/tenants.ts` (mirrors that function) |
75
+ | **Features / permissions** | 100% backend — the **Surface** bundles (there is no front-end roster any more) | per-tenant JSON **fixtures** under `cypress/fixtures/tenants/<SLUG>/surfaces/` |
76
+
77
+ **One tenant per run.** Because the client *is* the URL, and Cypress allows one superdomain per
78
+ spec (`compass.togasupply` and `nychh.togasupply` are different superdomains), the runner targets
79
+ a single tenant per invocation: `npm run cy:e2e:compass`, `…:compasscanada`, `…:nychh`, `…:quad`,
80
+ and `cy:e2e:all` chains them. `cypress/support/e2e.ts` has a `before()` that fails the run if
81
+ `Cypress.env("tenant")` and `baseUrl` disagree.
82
+
83
+ **One dev server serves every tenant.** It binds `127.0.0.1` and `vite.config.ts` sets
84
+ `allowedHosts: ['.togasupply']`. So `npm run dev` **once**, then run any tenant. Each tenant host
85
+ needs a `127.0.0.1` line in `/etc/hosts`.
86
+
87
+ **Adding a tenant = 3 edits, no spec changes:** one entry in `TENANTS` (`support/tenants.ts`),
88
+ one fixture folder, and one **static** import + map entry in `support/api/surfaces.ts` (Cypress
89
+ bundles the support file with esbuild, so the imports cannot be dynamic). The specs read the
90
+ registry, so they pick it up.
91
+
92
+ ## Derived vs fixtured — the split, and why
93
+
94
+ - **`/users` records are DERIVED.** `support/api/users.ts` builds them from the tenant's
95
+ `roleFlags` **plus the app's own `src/globalFieldsConfig/clientLoginFields.json`**, so a new
96
+ login field there is picked up automatically instead of drifting. Critically, the builder
97
+ returns **only the fields that client requests** — an ungranted or unrequested field comes back
98
+ **ABSENT (not null)** from the real API, so a stub that always returned every flag would hide
99
+ that whole class of bug.
100
+ - **Surface bundles are FIXTURED.** They carry real per-client config that cannot be derived
101
+ from anything on the client — they *are* the thing being simulated. Copy a real
102
+ `GET /v2/surfaces/meta` bundle into the fixture folder and edit it.
103
+ - **A slug with no fixture resolves to an EMPTY bundle** (`emptyBundle()` in
104
+ `support/api/surfaces.ts`) — which is the real "client not seeded / not granted" state and
105
+ exercises the FE's design-literal fallbacks. Leaving a slug unfixtured is the *correct* way to
106
+ test an un-onboarded client (`e2e/emptyNavigation.cy.ts` does exactly this).
107
+
108
+ ## The stub layer
109
+
110
+ `cy.stubApi({ tenant?, role?, surfaces?, user?, domainIsValid? })` registers a catch-all plus a
111
+ handler for every endpoint the app calls: `auth/public`, `auth/refresh`, `auth/login`, `users`,
112
+ `surfaces/meta`, `surfaces/meta-group`, `core-parameters`, `pages/meta`, `table-views/meta`,
113
+ `domains`, `client-authentications/singleSignOnServiceUrl`.
114
+
115
+ `support/api/envelope.ts` builds the V2 wrapper (`v2Envelope` / `v2Empty` / `v2Failure`). blox's
116
+ `apiGet`/`apiPost` return `response.data` — i.e. the **whole envelope** — so app code reads
117
+ `envelope.data.<route>`; a stub that puts the payload anywhere else reads back `undefined` and
118
+ the failure looks like a front-end bug.
119
+
120
+ Three custom commands: `cy.stubApi()`, `cy.visitAsGuest(path)` (storage cleared → anonymous),
121
+ `cy.loginAs(role, path)` (seeds a session, lands signed in), plus `cy.getByTestId()` and
122
+ `cy.allowUnstubbedApiCalls()`. Selector convention: prefer `data-testid`, but use an existing
123
+ stable semantic attribute (`input[name]`, `button[type=submit]`, `a[href]`) rather than adding a
124
+ testid for its own sake.
125
+
126
+ ## Guardrails built into the suite
127
+
128
+ ### The fall-through check (the main one)
129
+
130
+ A catch-all intercept **scoped to the `/v2` path** answers anything not modelled — with an empty
131
+ **success** envelope — and **records the URL**. A global `afterEach` in `support/e2e.ts` fails the
132
+ test if that list is non-empty.
133
+
134
+ - **Why a catch-all at all:** an un-stubbed request would otherwise escape to the real backend —
135
+ slow, non-deterministic, and it can **write real data**.
136
+ - **Why it must be registered FIRST:** Cypress matches intercepts in **reverse order of
137
+ definition**.
138
+ - **Why it is path-scoped:** a bare `**` would also swallow the Vite dev server's own HTML/JS/CSS.
139
+ - **Why the check is worth it:** one check catches **four** bugs — a non-matching intercept
140
+ pattern, a typo'd path, a wrong HTTP method, and an endpoint the app newly calls that nobody
141
+ stubbed. All four are otherwise **completely silent**: the catch-all answers 200 with an empty
142
+ envelope, so the app just behaves as though the record did not exist and the spec goes green on
143
+ wrong behaviour. It was chosen over a lint/text-grep rule because it checks the **real failure**
144
+ ("a call fell through") rather than one way of causing it.
145
+ - **Trade-offs, know them:** (a) a failed `afterEach` **aborts the remaining tests in that spec
146
+ file** (Mocha behaviour), so the first fall-through hides the other results; (b) it is strict —
147
+ when the app starts calling something new the suite fails until someone stubs it. That is the
148
+ intent.
149
+ - **Escape hatch:** `cy.allowUnstubbedApiCalls()` opts **one** test out. Use it only when the
150
+ un-modelled call *is* the point.
151
+
152
+ ### The once-per-test check
153
+
154
+ `cy.stubApi()` must be called **exactly once per test** — a second call registers a **second
155
+ catch-all**, and the resulting match order stops the specific intercepts from winning, so a call
156
+ that **is** stubbed falls through anyway. This bit the author's own spec: `stubApi()` ran in both
157
+ `beforeEach` and the test body, `/surfaces/meta?slug=login` fell through, and **the test still
158
+ passed — for the wrong reason**. Fixed by giving that test its own `describe` + `beforeEach`;
159
+ `stubApi()` now **throws** on a second call with the fix in the message. Per-test state is reset
160
+ **only** from the global `beforeEach`, deliberately *not* inside `stubApi()`, so a double call is
161
+ detectable instead of silently self-healing.
162
+
163
+ ### The component canary
164
+
165
+ `cypress/component/harness.cy.tsx` mounts a blox component and asserts one React + one
166
+ QueryClient. It fails when the component-test Vite config drifts from `vite.config.ts`.
167
+
168
+ ### Not built: CI
169
+
170
+ There is **no** CI for this yet. Worth knowing before you plan it: a workflow must add the four
171
+ `*.togasupply` `/etc/hosts` entries as a step (the tenant is resolved from the hostname) and serve
172
+ a build (`vite preview` / `start-server-and-test`).
173
+
174
+ ## How to run it
175
+
176
+ 1. `npm run dev` once (any tenant — one server serves all of them).
177
+ 2. `npm run cy:e2e:compass` (or `:compasscanada`, `:nychh`, `:quad`, or `cy:e2e:all`).
178
+ 3. `npm run cy:ct` for component tests; `cy:open` / `cy:open:<tenant>` for the GUI.
179
+ 4. `npm run test:types:cypress` type-checks the specs (`tsc --noEmit -p cypress/tsconfig.json`).
180
+
181
+ Every `cy:*` script goes through **`node scripts/cypress.mjs`** — see the first gotcha below.
123
182
 
124
183
  ## Gotchas
125
184
 
126
- - **Cypress's bundled Electron cannot launch in the sandboxed macOS dev environment** — it
127
- fails the pre-run smoke test (`bad option: --no-sandbox / --smoke-test / --ping`), which
128
- blocks **all** specs before any run. The specs in this harness were validated by
129
- `tsc -p cypress/tsconfig.json` (0 errors) and `eslint cypress` (clean) but **could not be
130
- executed in that sandbox** — run them locally with `npm run cy:run:ct`.
131
- - **`eslint.config.js` is protected by a config-protection hook.** When a spec/support file
132
- trips a lint rule (e.g. `no-explicit-any`), **fix the source to satisfy the linter** (use
133
- `unknown` / typed shapes) — do **not** weaken the shared eslint config to make tests pass.
134
- The override edit to `eslint.config.js` is blocked by the hook by design.
135
- - **`@agilant/toga-blox` consumes `dist/`** — same as the app, the harness sees built
136
- toga-blox, so a toga-blox source change needs a rebuild before tests observe it.
185
+ - **⚠ `bad option: --no-sandbox` is `ELECTRON_RUN_AS_NODE`, NOT a macOS sandbox problem.**
186
+ Cypress fails in *"Verifying Cypress can run"* with
187
+ `.../Cypress.app/Contents/MacOS/Cypress: bad option: --no-sandbox` (also `--smoke-test`,
188
+ `--ping=NNN`). **Cause:** VS Code exports `ELECTRON_RUN_AS_NODE=1` into its integrated terminal
189
+ and into shells its extensions spawn; Cypress's bundled Electron reads it and runs as plain
190
+ Node, which rejects Electron's own flags. **The tell is the wording** — `bad option:` is
191
+ *Node's* format for an unknown CLI flag. Verified 2026-09-03: disabling the tool sandbox does
192
+ **not** help; `env -u ELECTRON_RUN_AS_NODE npx cypress run` works immediately. **Fixed
193
+ permanently** by `scripts/cypress.mjs`, which deletes the variable before spawning (the same
194
+ trap exists on Windows, hence a Node wrapper rather than `env -u`), and all `cy:*` scripts route
195
+ through it. *(This doc previously recorded it as an unfixable environment blocker — it is a
196
+ one-line fix.)*
197
+ - **⚠ NEVER match an intercept with a URL glob — match on `pathname`.** Every request carries a
198
+ query string (blox's axios interceptor appends `transactionId`; `assembleOptions` serializes
199
+ `fields`/`join`/`where` into it) and **some values contain a slash**. Real captured `/domains`
200
+ example: `…AND,SelfDomains.domain:like:http://compass%2Etogasupply)&transactionId=…`.
201
+ minimatch's `*` does not cross `/`, so `**/v2/domains*` **silently fails to match** and the
202
+ catch-all answers instead — which made the login form show *"Error validating domain"* and made
203
+ one test pass for the wrong reason. Use
204
+ `cy.intercept({ method: "GET", pathname: apiPath("domains") }, …)`. Confirmed by mutation:
205
+ reverting one intercept to the glob form made the fall-through guardrail fail and print the URL.
206
+ - **The session key is `user`, not `accessToken`.** blox's `fetchPublicToken` writes an
207
+ `accessToken` (and `refreshToken`) for **logged-out** requests, so a token alone makes an
208
+ anonymous visitor look signed in. `AuthContext` and `useAuthenticationFlow` both key on
209
+ **`user`**. A seeded session must write `accessToken`, `refreshToken`, `user`, `zu-user` (the
210
+ zustand `useUserStore` persist key — the side-nav footer reads name/initials there) and
211
+ `deployment-version` (keeps `useDeploymentGuard` inert).
212
+ - **Seed in `cy.visit`'s `onBeforeLoad`.** Zustand `persist` and the persisted React Query cache
213
+ both rehydrate on **page load only** — an in-app navigation will not pick up storage you wrote
214
+ mid-test.
215
+ - **Clear the persisted query cache between tests.** It lives in localStorage under
216
+ `supply-chain-query-cache`. `src/App.tsx` excludes `surface-meta` / `surface-meta-group` from
217
+ persistence, but **`currentUser` — which carries the ROLE FLAGS — is persisted**, so one test's
218
+ role bleeds into the next unless storage is cleared.
219
+ - **Nav links exist but are not visible.** The side nav is a native `popover="auto"` element (blox
220
+ `SideNav.Content`), so assert `exist`, **not** `be.visible`, unless the test opens the hamburger.
221
+ Items render as `<a href="/path">` because `SideNav.NavigationItem` passes
222
+ `{ as: NavLink, to: path }` through blox's polymorphic `BaseButton`.
223
+ - **Stub `/table-views/meta` with `isSuccess: false`.** blox's `getDataTableMeta` maps that to
224
+ `{ table: null }`, which `useFetchTablePageMeta` documents as the "meta not ready" path. An
225
+ empty **success** envelope instead leaves `table` **undefined**, slips past every downstream
226
+ guard, and crashes in `buildUrl` (`route.includes` on `undefined`).
227
+ - **The envelope's data key is the route name in camelCase:** `purchase-orders` →
228
+ `purchaseOrders`, `core-parameters` → `coreParameters`, `client-authentications` →
229
+ `clientAuthentications`, `table-views` → `tableViews`.
230
+ - **`GET /v2/domains` has two callers with different field sets** — blox `validateEmailDomain`
231
+ (the login form) and `useAuthenticationFlow.checkSsoAuth`, which reads
232
+ `domains[0].ClientAuthentications.type` and **redirects to the IdP** when it is `SSO`. One
233
+ stubbed row typed `PASSWORD` satisfies both and keeps the suite on the local login form. Also:
234
+ `useAuthenticationFlow` returns early when `pathname === "/login"`, so visiting `/login`
235
+ directly never triggers the SSO check.
236
+ - **`surfaces/meta-group`: do not add a sibling key whose value is `null`.** `findSurfaceMap`
237
+ treats a map containing an explicit null as the slug-keyed one, so a `group: null` sibling wins
238
+ over the real map.
239
+ - **Config files type-check green by never being compiled.** `cypress.config.ts` and
240
+ `vitest.config.ts` were checked by **nothing** until both were added to
241
+ `tsconfig.node.json`'s `include` — the same false-green class already documented for this repo
242
+ (the root `tsconfig.json` is references-only, so a bare `npx tsc --noEmit` always exits 0).
243
+ - **`cypress.config.ts` is loaded as NATIVE ESM** by Cypress (`package.json` is `type: module`),
244
+ unlike `vite.config.ts` which Vite transpiles — so `__dirname` does not exist there. Derive it
245
+ with `fileURLToPath(import.meta.url)`.
246
+ - **The component-test Vite config MUST mirror `vite.config.ts`, including the `isBloxLinked`
247
+ branch.** Otherwise mounted components get a second copy of React / react-query — "Invalid hook
248
+ call", or "No QueryClient set" with the provider plainly visible in the stack.
249
+ `component/harness.cy.tsx` is the canary that fails when it drifts.
250
+ - **Scrub captured payloads.** Copying a real body out of Chrome DevTools (Network → filter
251
+ `/v2/` → Response → Copy response) is the fastest way to build a fixture — but those payloads
252
+ carry **real customer data**. Remove emails, names, addresses and tokens before committing.
253
+
254
+ ## Rest of the repo's test state (for context)
255
+
256
+ - **Vitest is the other half.** `npm test` (`vitest run`) currently has **one failing test file**:
257
+ `src/hooks/useTalosSurface.test.ts` fails with
258
+ `TypeError: Unknown file extension ".css"` on `@agilant/toga-blox/dist/global.css`. 89 tests pass
259
+ across the other 13 files. **Pre-existing** — the file was last touched in commit `604a779` and
260
+ the Cypress work changed neither it nor `vitest.config.ts`. Needs a separate fix (Vitest CSS
261
+ handling for blox imports).
262
+ - **No other mechanical guardrails exist in this repo:** no `.github/workflows`, no husky, no
263
+ lint-staged. The only `.git/hooks/pre-commit` is a machine-local `code-review-graph` shim that
264
+ always exits 0 and is not checked in.
265
+ - **Green FE tests do not mean a client gets a feature.** Real rollout is a role-scoped
266
+ `SurfaceOverrides IS_VISIBLE` row in `dbchanges2`; the fixture only simulates the grant.
137
267
 
138
268
  ## Change history
139
- - 2026-06-30 — Bootstrapped the Cypress harness from scratch (dual component + e2e config,
140
- `cy.mountWithProviders`, window-backed hook-mock pattern, shared fixtures off the real
141
- QUAD `approvalActionFields.json`) and added the first specs for the Sales Order approval
142
- modal flow. Documented the sandbox-Electron blocker and the protected-eslint decision.
143
- (apeterson)
269
+ - 2026-09-03 — **Rebuilt the harness from scratch on `TRUE-81735`** (the `TRUE-79813` harness this
270
+ doc used to describe was never merged and is stranded there). New shape: all `/v2` calls stubbed,
271
+ four tenants (COMPASS/COMPASSCANADA/NYCHH/QUAD), the three-axes model (client = baseUrl, role =
272
+ `roleFlags` mirroring `resolveRole`, features = surface fixtures), derived `/users` vs fixtured
273
+ surface bundles, and the fall-through + once-per-`stubApi()` guardrails. 26 tests green.
274
+ **Corrected** the old "Cypress's Electron cannot launch in the sandboxed macOS environment"
275
+ gotcha: it is `ELECTRON_RUN_AS_NODE=1` inherited from the VS Code shell, fixed by
276
+ `scripts/cypress.mjs`. Recorded the `pathname`-not-glob intercept rule, the `user`-key session
277
+ facts, and the pre-existing `useTalosSurface.test.ts` Vitest failure. (apeterson)
278
+ - 2026-06-30 — First harness bootstrapped on branch `TRUE-79813` (approval-modal component specs,
279
+ `cy.mountWithProviders`, window-backed hook mocks, fixtures off the real QUAD
280
+ `approvalActionFields.json`). **Never merged**; superseded by the 2026-09-03 rebuild. (apeterson)
@@ -672,20 +672,92 @@ consuming app, guard against them and move on.
672
672
 
673
673
  ---
674
674
 
675
+ ## 23. Front-end test layering — **MUST / SHOULD**
676
+
677
+ Pick the layer by **what the test is actually checking**, not by habit. Three layers, three jobs:
678
+
679
+ | Layer | Use it for | Tooling |
680
+ |---|---|---|
681
+ | **Unit / logic** | hooks, view models, resolvers, error classification, URL-state | **Vitest + React Testing Library** (jsdom, co-located `*.test.ts(x)`) |
682
+ | **Component** | the **tenant x role x feature** matrix — the thing that actually breaks | **Cypress component tests** |
683
+ | **e2e** | a handful of real user journeys, end to end | **Cypress e2e** |
684
+
685
+ **MUST — the tenant x role x feature matrix belongs in COMPONENT tests, not e2e.** This is the
686
+ rule most often gotten wrong. Multi-tenant, role-scoped rendering is a *rendering* question: mount
687
+ the component with a tenant + role + surface bundle and assert what appears. Driving that same
688
+ matrix through e2e multiplies runtime and flake for no extra signal, and e2e can only cover one
689
+ tenant per spec anyway (the client *is* the hostname, and Cypress allows one superdomain per spec).
690
+
691
+ **SHOULD — reserve e2e for a few genuine journeys** (log in, land on the default route, navigate).
692
+ Rule of thumb: **no URL change or page transition => it is not an e2e test.** If the assertion is
693
+ "this field/button/column renders for this client and role," that is a component test.
694
+
695
+ **MUST — STUB the surface bundles; never point a front-end test at a backend.** Features and
696
+ permissions are 100% server-driven (the Surface bundles — there is no front-end roster any more),
697
+ so a front-end test must supply them as **fixtures**. A test that reaches a real API is slow,
698
+ non-deterministic, and **can write real data**. Two consequences worth internalizing:
699
+
700
+ - Answer **every** API call, including a **path-scoped catch-all registered first**, and fail the
701
+ test when anything falls through. One check catches four otherwise-silent bugs: a non-matching
702
+ intercept pattern, a typo'd path, a wrong HTTP method, and an endpoint the app newly calls that
703
+ nobody stubbed. All four are silent because the catch-all answers 200 with an empty envelope, so
704
+ the app behaves as if the record did not exist and the spec goes **green on wrong behaviour**.
705
+ - **Derive what you can, fixture only what you must.** Build user/permission records from the
706
+ app's own config so a new field is picked up instead of drifting; fixture only the payloads that
707
+ cannot be derived from anything on the client. Return a field **absent, not null**, when the real
708
+ API would omit it — a stub that always returns everything hides that entire class of bug.
709
+
710
+ **MUST — every new test is SEEN TO FAIL before it is allowed to pass.** A test written straight to
711
+ green has proven nothing; on a stubbed harness a test can easily pass for the wrong reason. Break
712
+ the thing under test on purpose, watch the test go red, then restore it. Two worked examples from
713
+ the reference implementation:
714
+
715
+ 1. **Intercept matching.** Revert one intercept from `{ pathname: apiPath("domains") }` to the URL
716
+ glob `**/v2/domains*` and re-run: the fall-through guard **must** fail and print the URL.
717
+ (Real query strings contain a slash — `…SelfDomains.domain:like:http://compass%2Etogasupply)…` —
718
+ and minimatch's `*` does not cross `/`, so the glob silently never matches and the catch-all
719
+ answers instead.) If the suite stays green, the guard is not wired.
720
+ 2. **Config drift canary.** Delete the `isBloxLinked` branch from the component-test Vite config
721
+ and re-run the canary spec: it **must** go red with "Invalid hook call" or "No QueryClient set"
722
+ (a second copy of React / react-query). If it stays green, the canary is not asserting anything.
723
+
724
+ **MUST — every config file is type-checked.** A config that no `tsconfig` includes is compiled by
725
+ **nothing** and is green by default — the same false-green class as running bare `npx tsc --noEmit`
726
+ against a references-only root `tsconfig.json` (§2). Add `vite.config.ts`, `vitest.config.ts` and
727
+ `cypress.config.ts` to `tsconfig.node.json`'s `include`, and type-check test specs with their own
728
+ project (`tsc --noEmit -p cypress/tsconfig.json`).
729
+
730
+ - ⚠ **`cypress.config.ts` is loaded as NATIVE ESM** by Cypress when `package.json` is
731
+ `type: module` — unlike `vite.config.ts`, which Vite transpiles. So **`__dirname` does not exist
732
+ there**; derive it with `fileURLToPath(import.meta.url)`.
733
+
734
+ > ⚠ **Green front-end tests do NOT mean a client gets a feature.** The fixture only *simulates*
735
+ > the grant. Real rollout is a **role-scoped `SurfaceOverrides IS_VISIBLE` row in `dbchanges2`**.
736
+ > Never close a client-enablement ticket on a passing front-end suite.
737
+
738
+ **Reference implementation:** the worked harness for all of the above is
739
+ [toga25-supply Cypress testing](../apps/toga25-supply/workflows/cypress-testing.md) — three-axis
740
+ tenant model, the stub layer, the fall-through and once-per-`stubApi()` guardrails, and the traps.
741
+ Copy from there rather than reinventing.
742
+
743
+ **Do not** conflate any of this with the team's unresolved **backend** testing question — that is
744
+ **PHP-only** and settled nothing here.
745
+
746
+ ---
747
+
675
748
  # Part 3 — Open items
676
749
 
677
750
  *(Described and flagged. NOT prescribed as settled.)*
678
751
 
679
- - **Front-end testing — SHOULD-lean, not yet a settled standard.** The reality (verified
680
- 2026-08-18): `toga25-supply` **and** `toga-blox-npm` both run **Vitest + React Testing Library**
681
- (jsdom, `globals: true`, a setup file, co-located `*.test.ts(x)`). Supply has **real tests on the
682
- tricky bits** — auth context, table URL-state, error classification — with `@vitest/coverage-v8`;
683
- blox configures istanbul coverage. `toga25-desk` has **no test script and an empty Cypress
684
- scaffold**. Recommendation: **Vitest + RTL for unit / logic-hook tests** — a firm SHOULD, since it
685
- is real in 2 of 3 sources and mirrors how `backend-testing.md` prescribes a path even without a
686
- formal harness — with **Cypress reserved for E2E** (acknowledged empty today). A dedicated
687
- `frontend-testing.md` sibling is the likely eventual home. **Do not** conflate this with the
688
- team's unresolved *backend* testing question — that is PHP-only.
752
+ - **Front-end testing — SETTLED; moved to §23.** No longer an open item. Test layering (Vitest +
753
+ RTL for logic, Cypress **component** tests for the tenant x role x feature matrix, Cypress e2e
754
+ for a few real journeys), fully-stubbed surface bundles, the see-it-fail rule and config
755
+ type-checking are now prescribed in
756
+ [§23 Front-end test layering](#23-front-end-test-layering--must--should), with the worked harness
757
+ in [toga25-supply Cypress testing](../apps/toga25-supply/workflows/cypress-testing.md). A
758
+ dedicated `frontend-testing.md` sibling remains the likely eventual home if §23 outgrows this
759
+ doc. Still **not** to be conflated with the unresolved *backend* testing question — that is
760
+ PHP-only.
689
761
  - **`eslint-plugin-jsx-a11y` not configured** (§10) — a11y is convention-only, not lint-enforced in
690
762
  any of the three repos. Recommend adopting it.
691
763
  - **Client error-reporting gap.** Verified 2026-08-18: all three repos depend on `@sentry/react`
@@ -709,6 +781,18 @@ consuming app, guard against them and move on.
709
781
 
710
782
  ## Change history
711
783
 
784
+ - 2026-09-03 — **Front-end testing promoted from an open item to §23 (MUST/SHOULD).** Settles the
785
+ layering: Vitest + RTL for logic, Cypress **component** tests for the tenant x role x feature
786
+ matrix (**not** e2e), e2e for a few real journeys only, with the rule of thumb "no URL or page
787
+ transition => not an e2e test". Adds the MUSTs to **stub the surface bundles and never point a
788
+ front-end test at a backend** (path-scoped catch-all registered first + fall-through failure;
789
+ derive what you can, fixture what you must, return absent-not-null), to have **every new test
790
+ seen to fail before it passes** (two worked mutation examples: glob-vs-`pathname` intercept
791
+ matching, and the blox-linked component-config drift canary), and to **type-check every config
792
+ file** — including that `cypress.config.ts` loads as native ESM so `__dirname` is unavailable.
793
+ Warns that green front-end tests do not enable a feature for a client — that is a role-scoped
794
+ `SurfaceOverrides IS_VISIBLE` row in `dbchanges2`. Reference implementation:
795
+ `../apps/toga25-supply/workflows/cypress-testing.md`. (apeterson)
712
796
  - 2026-09-03 — §11: replaced supply's stale three-member `dedupe` list with the verified
713
797
  seven-member one and added `react-hook-form` (it was in neither `dedupe` nor `alias` while 24
714
798
  `src` files import it and blox's `BaseInput` calls `useFormContext()`). Added a SHOULD for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.768",
3
+ "version": "1.0.769",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",