From 506de44b9bf556d8719e98d8534e8ec40c490fde Mon Sep 17 00:00:00 2001 From: "Prashant Rane (ALLYIS INC)" Date: Tue, 3 Nov 2026 17:06:43 -0600 Subject: [PATCH] fix(web): bound bulk result values for tenant 10 migration Unintended layout regression: long Azure resource URLs expanded fractional grid tracks at 900 px. Inspected the failed and corrected screenshots; bound text tracks and reuse copy/truncate fields without rewriting visual baselines. Unit RED/GREEN, five built-frontend browser flows, strict added-spec types, production build, coverage and size budgets pass. Full TypeScript diagnostics remain at the verified HEAD baseline. Preserve 0.55.67 migration-only performance exception; customer production is excluded. Corrected immutable-image publication and complete actual target acceptance remain pending. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 29 ++ Session_starter.md | 25 ++ docs/CONTEXT_INSTRUCTIONS.md | 35 ++- .../GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md | 9 +- docs/PHASE_M2_BULK_OPERATIONS.md | 38 ++- ...ENANT_10_MIGRATION_EXECUTION_2026_10_30.md | 105 +++++++ docs/UI_GUIDE.md | 8 + .../ENGINEERING_LESSONS_AND_PATTERNS.md | 24 ++ web/e2e/bulk-and-manual-execution.spec.ts | 268 ++++++++++++++++++ web/src/pages/BulkTab.test.tsx | 29 ++ web/src/pages/BulkTab.tsx | 26 +- 11 files changed, 578 insertions(+), 18 deletions(-) create mode 100644 web/e2e/bulk-and-manual-execution.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 58fcc1d8d..457e6458b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Tenant 10 Bulk result layout repair + +- Fix long resource locations and error details widening the Bulk result + grid at narrow viewports. Bound its flexible tracks with minmax(0,1fr) + and reuse CopyableField for clipped, tooltip-accessible, full-value copying. + Missing locations and empty details do not expose meaningless copy buttons. +- Preserve actual bulk execution, duplicate rejection, failure CSV content, + corrected-row retry and owned-fixture cleanup. Add a deterministic + long-value regression with 1440 px / 900 px bounds, actual truncation and exact + clipboard checks, alongside bulk/manual execution coverage. +- RED: actual Azure browser run 347/348, no skips/flakes, with one measured + layout failure; deterministic long-value case also fails on unchanged + Azure image. GREEN: Bulk component 9 to 10 tests, five built-loopback + browser flows, production build and strict added-spec type check. + Updated full web coverage passes 1,662 tests across 125 files (previously + 1,661): statements 83.06%, branches 75.86%, functions 75.53%, lines 85.74%. + Configured floors pass; branch coverage was previously 75.89%. + Reviewed immutable-image publication remains pending. API unit/E2E + remain unchanged by this frontend-only delta; no + new API run count is claimed here. +- Full web tsc is not green: executable virtual-HEAD comparison verifies + the same 66 diagnostics/two production diagnostics and zero added errors. + No visual baseline was regenerated. Version remains 0.55.67 for the + operator-authorized migration repair; performance remains unqualified + and customer production must not change. +- Self-improvement applied: long-value fixtures catch environment-dependent + grid sizing before deployment. Design disposition accepted: existing + copy/truncate composition; no speculative service or strategy extraction. + ### 0.55.67 tenant 10 migration release under a scoped exception - Record explicit operator authorization diff --git a/Session_starter.md b/Session_starter.md index c010b8a88..66b03e391 100644 --- a/Session_starter.md +++ b/Session_starter.md @@ -1,5 +1,30 @@ ## SCIMServer - Condensed Session Memory +**Current tenant 10 checkpoint:** PR270 merged normally as6102aff4 and its +authoritative scanned image is deployed on dev. Both actual database carries +passed full nine-table fidelity/schema checks. Available-data authorization +supersedes waiting for source freeze; performance remains unqualified. +Dev environment repair has actual-AST RED/GREEN and complete Azure readback. +Live acceptance reports1,841 passes/zero failures, including four explicitly +skipped retired-API placeholders. Browser acceptance has347 passes/one +unintended Bulk layout failure, zero skips/flakes. Canary app remains absent. + +Separate repair branch `fix/tenant10-bulk-result-overflow-20261103` uses +bounded fractional grid tracks and existing CopyableField truncation. +Deterministic long values reproduce RED on unchanged Azure image. Ten +component tests, five built-loopback browser flows with migrated-dev backend, +strict added-spec compilation and production build pass. Actual1440/900px +bounds, clipping and complete clipboard values are asserted; failed and GREEN +screenshots were inspected. No baseline regeneration. Virtual HEAD compiler +comparison verifies unchanged66 errors/two production errors, no additions. +Updated whole-web coverage passes1,662/125 files, with statements83.06%, +branches75.86%, functions75.53%, lines85.74%; all size budgets pass. +Web ESLint remains N/A because no config exists, not a passing lint run. +Review, corrected-image publication, +full actual target acceptance, operator visual confirmation and cutover remain +pending. Customer production is untouched. Older no-app/source-freeze/pending +release statements below are preparation history, not the current state. + **2026-11-03 authorized migration release:** the operator explicitly authorized `T10-0.55.67-PERF-EXCEPTION-2026-11-03` for this 0.55.67 tenant 10 dev/canary migration only. Performance remains unqualified; all other security, diff --git a/docs/CONTEXT_INSTRUCTIONS.md b/docs/CONTEXT_INSTRUCTIONS.md index 5ae0af27b..35ad7b3d5 100644 --- a/docs/CONTEXT_INSTRUCTIONS.md +++ b/docs/CONTEXT_INSTRUCTIONS.md @@ -16,15 +16,32 @@ data-integrity, functional, browser and migration gate remains mandatory; customer production must not change. The existing migration execution document records the exact authorization, scope and required merged-commit/image-digest binding. Historical no-waiver holds below are superseded, not failed receipts. -Use release-only branch `ops/tenant10-release-exception-20261103`; keep -qualification research PR251 draft. Complete API/web trees match integrated -candidate5823a225 after importing four unchanged version-bearing blobs. -No runtime, schema, dependency graph, workflow or normal qualification logic -changes. Root/web dependency restore, unchanged-lock verification, API Prisma -generation and public-registry/sha512 provenance pass. Normal protected review, -final immutable artifact, actual target acceptance, source freeze/final carry -and cutover remain pending. Self-improvement applied: bounded authorization -and honest pending evidence. Design disposition accepted: no general bypass. +PR270 merged normally as6102aff4; authoritative41714210347 produced the +scanned immutable image. Both actual target databases passed complete +nine-table content/schema/constraint/index equality. The operator explicitly +accepts available point-in-time data; no source freeze or synchronized +zero-loss claim is required. Dev is deployed with corrected environment +values/references, a Healthy revision and authenticated0.55.67/Prisma readback. +Live acceptance reports1,841 passes/zero failures, including four explicit +retired-API placeholders that are not exercised checks. Full dev browser +acceptance has347 passes/one unintended Bulk layout failure, zero skips/flakes. + +Current repair branch `fix/tenant10-bulk-result-overflow-20261103` bounds the +Bulk result grid and reuses existing copy/truncate fields. Unit RED/GREEN, +five built-loopback frontend/browser flows with the migrated-dev backend, +strict added-spec compilation and production build pass. Full web tsc remains +non-green at66 diagnostics/two production diagnostics; executable HEAD +comparison proves zero added diagnostics. Updated full coverage passes +1,662 tests / 125 files, with statements83.06%, branches75.86%, +functions75.53%, lines85.74%; all size budgets pass. Web ESLint remains N/A +because no config exists. Protected publication, review, a corrected +immutable image and actual target reruns +remain mandatory. Canary application, operator visual confirmation and role +cutover remain pending. No customer-production changes or performance campaign. +Keep PR251 research draft. Self-improvement applied: deterministic long values, +actual bounds/clipping/full clipboard assertions and immutable scoped reports. +Design disposition accepted: existing primitive composition; no general bypass +or speculative domain-service extraction. **Authoritative current state:** PR267 is merged at84099293570af5ab06f7d8e2f93e7e6ae2c0236f. Reviewed883fb6c6 passes29 protected gates, and exact-tip5757623103 reports diff --git a/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md b/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md index 583efe8f5..d1eab14cf 100644 --- a/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md +++ b/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md @@ -1,6 +1,6 @@ # Global navigation and workflow context -> **Status:** User-facing reference - **Last verified:** 2026-10-16 - **Product version:** `0.55.67` +> **Status:** User-facing reference - **Last verified:** 2026-11-03 - **Product version:** `0.55.67` See [current release notes](RELEASE_NOTES.md) for deployment and acceptance boundaries. Endpoints now uses available window height, Dashboard uses compact @@ -17,6 +17,13 @@ remains under each trust's More menu. See the [isolated integration checkpoint](auth/ENDPOINT_TOKEN_LIFETIME_TDD_HANDOFF.md#current-master-integration-preparation-2026-10-21); this is not a deployed navigation change. +**Tested but unreleased Bulk layout repair (2026-11-03):** copying a result +identifier, location or error detail does not navigate or change endpoint +selection. Long values are bounded and retain their complete clipboard text. +The change remains inside the URL-owned Bulk tab; global Back/Forward and +workflow selection are unchanged. Built loopback frontend checks pass, but +the corrected Azure image and operator visual acceptance remain pending. + ## Purpose The admin UI now exposes one global Back/Forward pair and uses endpoint identity according to workflow intent. The top-level workflows remain separate because they answer different operational questions. diff --git a/docs/PHASE_M2_BULK_OPERATIONS.md b/docs/PHASE_M2_BULK_OPERATIONS.md index cd0bd0828..75157dc33 100644 --- a/docs/PHASE_M2_BULK_OPERATIONS.md +++ b/docs/PHASE_M2_BULK_OPERATIONS.md @@ -1,11 +1,47 @@ # Phase M2 - Bulk Operations UI -> **Date:** 2026-05-15 - **Version:** 0.51.0-alpha.2 - **Predecessor:** v0.51.0-alpha.1 (Phase M1 SCIM Workbench) +> **Original release:** 2026-05-15 - **Version:** 0.51.0-alpha.2 - **Predecessor:** v0.51.0-alpha.1 (Phase M1 SCIM Workbench) > **Origin:** [docs/UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md](UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md) S4.3 > **Scope:** Frontend-only. New per-endpoint Bulk tab + 3 pure modules + 1 free-form mutation hook + the M1-deferred "Save as live-test" emitter wired to Workbench history rows. New live section `9z-AH` adds Bulk UI contract. --- +## Current layout repair: tested, not yet deployed + +Tenant 10 browser acceptance against 0.55.67 exposed result cells overflowing +the card at a 900 px viewport. Long absolute resource locations expanded CSS +Grid's `1fr` tracks because their minimum size was content-driven. This was +an unintended defect; the failure screenshot was inspected and no visual +baseline was regenerated. + +The repair keeps the identifier/status tracks at 80 px and bounds both text +tracks with `minmax(0, 1fr)`. Result identifiers, actual locations and actual +error details use [CopyableField](../web/src/components/primitives/CopyableField.tsx) +with truncation. Full values remain available through tooltips and copying; +missing locations and empty details retain non-copyable placeholders. +Bulk request/response semantics, success/failure badges, CSV failure export +and retry behavior are unchanged. + +| Claim | Verification | +|---|---| +| Complete DOM values and copy affordances | Component regression observed RED, then 10/10 component tests passed | +| Bounded layout independent of hostname | Deterministic long URL/error fixture fails on unchanged Azure image, then passes on built loopback frontend at 1440 px and 900 px | +| Actual truncation, not CSS presence alone | Browser asserts positive client width and scrollWidth greater than clientWidth on each truncation element | +| Full values remain copyable | Browser reads clipboard and compares the entire location and error detail | +| Execution and cleanup still work | Five bulk/manual browser flows pass with migrated-dev backend; real creation, duplicate, retry, CSV content and owned-fixture deletion remain asserted | + +The passing frontend is an owned loopback production build, not a repaired +Azure deployment. Reviewed publication, a new immutable image and complete +actual dev/canary acceptance remain mandatory. Performance remains unqualified +under the migration-only exception; customer production is excluded. + +Self-improvement applied: deterministic long-value coverage catches the +deployment-hostname gap before another migration. Design disposition accepted: +reuse the existing primitive and keep rendering inside the cohesive Bulk tab; +no new domain service, strategy or dependency is justified. + +--- + ## 1. Why this exists [docs/UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md](UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md) S4.3 names the Bulk Operations UI as a Tier 1 Operational Completeness gap. The backend has shipped the full surface since v0.18.0: diff --git a/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md b/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md index f5543418b..93a4d6f58 100644 --- a/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md +++ b/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md @@ -9,6 +9,111 @@ ## Executive summary +### Available-data deployment checkpoint (2026-11-03) + +The operator explicitly updated the migration scope at +`2026-11-03T20:50:41Z`: + +> please proceed with whatever data is available. its alright of we do not have the latest . right now the main point is to have the dev and canary deployed and working + +This permits available point-in-time data rather than a synchronized latest-data +cutover. No source-writer freeze or source mutation is performed, and later +source changes may be omitted. Complete carried-snapshot fidelity, resource +IDs, credentials/settings and all target acceptance gates remain mandatory. +Customer production is untouched; performance remains unqualified under the +separately documented migration-only exception. + +PR270 merged normally as +`6102aff4a1687d2873b1349af3f95e68c3c6aeb5` after all exact-tip hosted checks +passed. Independent review5759073624 reports no open issues, approval +recommended, formal state COMMENTED. Protected Validate29/29 passes in935.5s. +Authoritative merged-source41714210347 passes actual PostgreSQL copy/snapshot +fixtures, build/push, smoke and HIGH/CRITICAL Trivy. Merged CodeQL41714183722 +and dependency-pins41714184178 pass. + +Decision `T10-0.55.67-PERF-EXCEPTION-2026-11-03` is publicly bound in PR270 +comment6614992034 to version0.55.67, that merged source and image +`ghcr.io/pranems/scimserver@sha256:4a00b5421a8d5290506091fe856f3dbb4cc185e0bb799d272cb724651de9aeeb`. +Producer and independent smoke-pull digests agree. The unique tag is +`test-tenant10-release-0.55.67-6102aff4a168`; mutable `latest` was not promoted. + +Both actual target databases now pass complete nine-table content, schema, +constraint and index equality against their carried read-only snapshots. +Native PostgreSQL custom archives and logs are privately protected with +Windows CurrentUser DPAPI. Dev retained all historical request logs in its +760,214,637-byte archive rather than discarding them to shorten validation. +The private receipts are `dev-carry.json` under +`available-dev-aggregate-20261103` and `canary-prod-carry.json` under +`available-canary-20261103T2050`, both below the existing protected tenant 10 +bootstrap directory. These are snapshot receipts, not freshest-data guarantees. + +Dev is created from the exact digest. Its initial environment transformation +was corrected after semantic RED/GREEN, and all actual environment values and +secret references now match the intended preserved configuration. The corrected +revision is Healthy and the unhealthy initial owned revision was deactivated. +Direct HTTP health returns200; the frontend opens its authentication dialog. +Actual version/Prisma-backend preflight passes. Full dev live acceptance +completed with 1,841 reported passing checks and zero failures across 169 +sections and 1,906 flow steps in 793.377s. Four reported passes are explicitly +skipped placeholders for removed Admin Schema/ResourceType APIs (9m-A/B/C +and 9y), not exercised checks. The orphan guard captured 76 pre-existing +endpoints and reported no leaked endpoints. Full actual dev browser +acceptance completed with 347 passes, one failure, zero skips, zero flakes +and no runner errors in 1,037.046s. Its sole failure measured four result +cells overflowing the Bulk card at a narrow viewport. The failure screenshot +was inspected: this is an unintended layout defect, not intended visual drift. +No visual baseline diff was produced and no baseline was regenerated. + +The default minimum width of `1fr` grid columns let long absolute resource +URLs expand the result grid. A separate fix branch, +`fix/tenant10-bulk-result-overflow-20261103`, bounds the two text columns +with `minmax(0, 1fr)` and uses the existing copy/truncate primitive for result +values. The component regression has RED/GREEN (9 to 10 tests), and a +deterministic long-URL/error-message browser case reproduces RED against +the unchanged Azure image. All five bulk/manual browser flows then pass on +the patched, built loopback frontend proxied to migrated dev. They measure +1440/900-pixel layouts, prove actual text overflow is clipped, verify complete +clipboard values, and retain fixture cleanup. The GREEN screenshot was also +inspected. This is tested but unreleased frontend work, not a corrected Azure +image or completed target acceptance. + +Both added browser specs compile under a strict, explicitly owned type-root +configuration. Full web type checking still has 66 diagnostics, including two +production diagnostics. An executable virtual-HEAD comparison against +6102aff4 verifies the same baseline and no added diagnostics without changing +files or borrowing dependencies. The production build and all size budgets +pass. Updated whole-web coverage passes 1,662 tests across 125 files in +619.76s: statements83.06%, branches75.86%, functions75.53%, lines85.74%; +the configured floors pass. Canary creation/acceptance, operator visual +acceptance and registry cutover remain pending. No claim that dev is fully +validated or ready for customer production is made. + +The independent web coverage gate also passed in the owned release worktree: +1,661 tests across 125 files, with statements 83.06%, branches 75.89%, +functions 75.53% and lines 85.74%. The configured floors passed. This is +unit/coverage evidence, not browser layout or operator visual acceptance. + +#### Confirmed execution issues and prevention + +| ID | Type / severity | Symptom and mechanism | Resolution and why it works | Detection / earliest possible gate | +|---|---|---|---|---| +| AD-1 | Azure service / Medium | Two copy jobs reached Failed provisioning; executions API returned InternalServerError although the managed environment itself was Succeeded. | Stop only owned commands, delete only the two owned failed jobs and verify absence. Use direct native PostgreSQL clients without changing source state. Native carries pass complete fidelity/schema checks. | Actual provider/runtime check caught this; the first job readiness result was the earliest possible detection. No repeated blind allocation. | +| AD-2 | Transfer scalability / Medium | Full-row JSON sorting transferred and sorted the large dev request-log corpus rather than using compact row fingerprints. The first attempt was stopped before target writes. | Reuse the existing production server-side nested-MD5 content fingerprint and hash-bind the combined manifest, while retaining full schema/constraint/index equality and the exported snapshot. | Actual dev transfer latency exposed this; corpus-size-aware manifest review could have caught it before execution. Preserve all logs, not a silently smaller data set. | +| AD-3 | Orchestration budget / Low | The40-minute deployment receipt wait expired while the760MB dev transfer continued and progressed through dump/restore/final verification. | Extend only the receipt wait, not rerun the dump or restore. The existing transfer subsequently passes and creation proceeds from its verified receipt. | Wait deadline caught this; actual data-volume/phase budgeting was the earlier opportunity. A wait timeout is not proof that its dependency failed. | +| AD-4 | Deployment configuration / High | PowerShell `switch` changed automatic `$_` from the environment entry to its name string. The default branch read `.value` from the string, emitting empty non-secret values, includingPORT. Ingress8080 therefore did not match dynamic listening port42145. | Capture the original entry before `switch`; preserve its value in the default branch. Actual-AST regression initially fails with `RED: actual deployment transformation did not preserve PORT`, then passes all preserved values, override and secret reference. Repair only the owned exact-image dev app and independently read back every environment value/reference. | Actual startup/health gate caught this. An actual-transformation preservation test before creation was the earliest possible gate and is now applied. Data restoration and secret references were unaffected. | +| AD-5 | Client routing / Medium | Repaired revision was Healthy on8080, but proxy-routed health still timed out. Direct routing returnedHTTP200 in0.131s. | Scope direct routing to the acceptance process; restore request defaults afterwards. No machine-wide proxy or corporate registry setting changes. Actual version/backend preflight then passes and live tests start. | Actual HTTP comparison distinguished client routing from service health. A paired direct/proxy reachability preflight could have found it earlier. | +| AD-6 | UI layout / High | Actual dev browser acceptance has one failure: four bulk-result cells exceed the card at900px. Short loopback URLs had passed earlier measured coverage, while Azure's longer URLs expose the intrinsic minimum width of fractional grid columns. | Unintended defect. Bound flexible tracks with minmax(0,1fr), compose existing CopyableField truncation, and keep placeholders non-copyable. Unit RED/GREEN and five built-frontend browser flows pass, including deterministic long values, two viewport bounds, actual clipping and exact clipboard contents. Review, immutable image production and actual Azure rerun remain pending. | Actual Azure bounded-layout assertion caught it. A deliberately long URL/error fixture in the earliest local browser run could have caught it before release; that fixture is now applied and the general pattern is recorded. | +| AD-7 | Evidence tooling / Low | Parsing the combined line/JSON reporter output as one JSON document failed because a console summary followed the completed JSON envelope. Retry paths also had no distinct-attempt parameter. | Preserve the original protected log; parse its bounded complete envelope for the original outcome. Future runs use a dedicated JSON report, explicit attempt names, no-overwrite guards and encryption roundtrip hashes. Successful browser receipts require nonzero exercised tests and zero skips/failures/flakes/errors, and distinguish loopback preview from deployed-target scope. | Post-run outcome extraction caught it. A dedicated report file and immutable-attempt guard were the earlier opportunity; both are now applied. | +| AD-8 | Validation configuration / Low | A per-session strict browser TypeScript config could not find node/vite type definitions because resolution started outside the owned web package. | Explicit owned @types root and Vite client declaration path give a strict browser-spec exit0. No package install, dependency borrowing or source-config weakening. Existing full-project errors remain explicitly non-green and are compared to HEAD through the compiler. | First focused compiler run caught the resolution issue; explicit config-relative type ownership could have prevented it. | +| AD-9 | Tool selection / Low | A targeted npx ESLint invocation fetched a cached tool but failed because the web package has no eslint.config file. The chained size command therefore did not run. | Classify the existing unconfigured web lint gate as N/A, not PASS. Do not add unrelated lint tooling. Run the existing size command separately: all budgets pass. No source dependency manifests or locks changed. | Targeted lint invocation caught it; checking for the repository-required config before invoking a tool was the earliest opportunity. | + +These entries use retained command results and actual provider observations. +They do not claim that the complete historical session transcript has been +reconciled. Self-improvement is applied through the actual transformation +regression and full provider readback, not a presence-only configuration test. +Design disposition accepted: keep this recovery as scoped operational tooling; +no runtime abstraction or general admission bypass is justified. + ### Authorized performance-admission exception (2026-11-03) **Decision ID:** `T10-0.55.67-PERF-EXCEPTION-2026-11-03`. diff --git a/docs/UI_GUIDE.md b/docs/UI_GUIDE.md index 6b5bba764..0415b8770 100644 --- a/docs/UI_GUIDE.md +++ b/docs/UI_GUIDE.md @@ -273,6 +273,14 @@ These changes are being verified locally in Docker, not yet deployed. The cap is **1000 operations and a 1 MB payload**. Before submitting you get a preview of the first ten operations and a **Copy full envelope as JSON** button, so you can inspect exactly what will be sent. Afterwards, **failure rows are downloadable as CSV** carrying the per-operation `scimType` and `detail` - fix that file and re-submit it rather than re-deriving which rows failed. +**Tested but unreleased tenant 10 layout repair:** result identifiers, resource +locations and error details remain bounded at wide and narrow viewports. +Long values use ellipsis, reveal their full text on hover and copy the entire +value rather than the visible abbreviation. The CSV-input screenshot above +does not illustrate this updated result layout. The repair has passed built +loopback frontend tests with the migrated-dev backend; it is not yet a +validated Azure deployment. See [bulk layout repair evidence](PHASE_M2_BULK_OPERATIONS.md#current-layout-repair-tested-not-yet-deployed). + **Resource types** lists what this endpoint serves, creates custom ones beyond User and Group, and exposes the related discovery/enforcement settings in a collapsed pane above the inventory. An **Effective combined schemas** section flattens each type's core and extension attribute paths with their type, required, cardinality, and mutability characteristics. ![Resource types](screenshots/prod-12-endpoint-resource-types.png) diff --git a/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md b/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md index f8168c44e..fcc9d1836 100644 --- a/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md +++ b/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md @@ -14,6 +14,30 @@ --- +## Long-value fixtures must precede deployment qualification (2026-11-03) + +The tenant 10 dev run measured a Bulk layout defect even though an earlier +loopback browser run had passed the same bounds assertion. The assertion +was meaningful; the short local URL did not exercise the dangerous input. +Long Azure resource locations let CSS Grid's `1fr` tracks retain their +intrinsic minimum width and expand beyond the result card at 900 px. + +Use `minmax(0, 1fr)` for bounded flexible text tracks and compose the existing +copy/truncate primitive. Do not rely on a particular deployment hostname to +exercise overflow. A deterministic browser fixture should supply long +absolute URLs and long unbroken error values, assert every cell's real bounds +at both 1440 px and 900 px, measure actual truncation, and verify that copying +still returns the full value. Keep real API execution/duplicate/retry checks +separate from the fixture-driven layout check. + +This pattern is applied in +[bulk-and-manual-execution.spec.ts](../../web/e2e/bulk-and-manual-execution.spec.ts); +the issue and RED/GREEN provenance are in the +[migration execution record](../TENANT_10_MIGRATION_EXECUTION_2026_10_30.md). +The failure was unintended; failure and GREEN screenshots were inspected, +and no baseline was rewritten. Loopback GREEN still does not prove the +corrected Azure image or replace operator visual approval. + ## Execute the control inventory before claiming coverage (2026-11-02) **Applied in:** [tenant 10 browser acceptance](../TENANT_10_MIGRATION_EXECUTION_2026_10_30.md). diff --git a/web/e2e/bulk-and-manual-execution.spec.ts b/web/e2e/bulk-and-manual-execution.spec.ts new file mode 100644 index 000000000..0c001c06d --- /dev/null +++ b/web/e2e/bulk-and-manual-execution.spec.ts @@ -0,0 +1,268 @@ +import { randomUUID } from 'node:crypto'; +import { expect, test, type Page } from '@playwright/test'; +import { + createFixtureEndpoint, + deleteFixtureEndpoint, + E2E_TOKEN, + seedAuthToken, +} from './endpoint-fixture'; + +const USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User'; +const GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group'; +const headers = { Authorization: `Bearer ${E2E_TOKEN}`, Accept: 'application/scim+json' }; +let endpointId: string | null = null; + +test.beforeEach(async ({ page }) => { + await seedAuthToken(page); + endpointId = await createFixtureEndpoint(page, { namePrefix: 'e2e-execution' }); +}); + +test.afterEach(async ({ page }) => { + if (!endpointId) return; + const ownedId = endpointId; + endpointId = await deleteFixtureEndpoint(page, endpointId); + const response = await page.request.get('/scim/admin/endpoints', { headers }); + expect(response.ok(), 'cleanup inventory must be readable').toBe(true); + const body: { endpoints: Array<{ id: string }> } = await response.json(); + expect(Array.isArray(body.endpoints)).toBe(true); + expect(body.endpoints.map((endpoint) => endpoint.id)).not.toContain(ownedId); +}); + +async function uploadCsv(page: Page, text: string) { + await page.getByTestId('bulk-csv-input').setInputFiles({ + name: 'owned-execution.csv', + mimeType: 'text/csv', + buffer: Buffer.from(text), + }); +} + +async function persistedUser(page: Page, id: string, userName: string) { + const response = await page.request.get(`/scim/endpoints/${id}/Users`, { headers }); + expect(response.status()).toBe(200); + const body: { Resources: Array<{ id: string; userName: string; displayName?: string }> } = + await response.json(); + expect(Array.isArray(body.Resources)).toBe(true); + const matches = body.Resources.filter((user) => user.userName === userName); + expect(matches).toHaveLength(1); + expect(matches[0].id).toMatch(/^[0-9a-f-]{36}$/i); + return matches[0]; +} + +test('Bulk persists successful rows, reports a duplicate, and retries only the corrected row', async ({ page }) => { + const id = endpointId!; + const capability = await page.request.get(`/scim/endpoints/${id}/ServiceProviderConfig`, { headers }); + expect(capability.status()).toBe(200); + expect((await capability.json()).bulk.supported).toBe(true); + const first = `bulk-first-${randomUUID()}`; + const second = `bulk-second-${randomUUID()}`; + const corrected = `bulk-corrected-${randomUUID()}`; + await page.goto(`/endpoints/${id}/bulk`); + await uploadCsv(page, `userName,displayName\n${first},First\n${first},Duplicate\n${second},Second`); + await expect(page.getByTestId('bulk-preview')).toContainText('3 total operations queued.'); + const reply = page.waitForResponse((response) => + response.url().endsWith(`/scim/endpoints/${id}/Bulk`) && response.request().method() === 'POST'); + await page.getByTestId('bulk-submit').click(); + const response = await reply; + expect(response.status()).toBe(200); + const body: { Operations: Array<{ bulkId: string; status: string; location?: string }> } = + await response.json(); + expect(body.Operations.map((operation) => operation.bulkId)).toEqual(['row-1', 'row-2', 'row-3']); + expect(body.Operations.map((operation) => operation.status)).toEqual(['201', '409', '201']); + await expect(page.getByTestId('bulk-result-success-count')).toHaveText('2'); + await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('1'); + await expect(page.getByTestId('bulk-result-row-row-2')).toContainText('409'); + await expect(page.getByTestId('bulk-result-row-row-1')).toContainText(body.Operations[0].location!); + await persistedUser(page, id, first); + await persistedUser(page, id, second); + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.getByTestId('bulk-download-failures').click(), + ]); + expect(await download.failure()).toBeNull(); + const stream = await download.createReadStream(); + if (!stream) throw new Error('Failure CSV download has no readable stream.'); + const chunks: Buffer[] = []; + for await (const chunk of stream) chunks.push(Buffer.from(chunk)); + const failuresCsv = Buffer.concat(chunks).toString('utf8'); + expect(failuresCsv).toContain('bulkId,method,status,scimType,detail'); + expect(failuresCsv).toContain('row-2,POST,409,uniqueness,'); + expect(failuresCsv).toContain(first); + expect(failuresCsv).not.toContain('row-1'); + expect(failuresCsv).not.toContain('row-3'); + + for (const width of [1440, 900]) { + await page.setViewportSize({ width, height: 900 }); + await page.getByTestId('bulk-result').scrollIntoViewIfNeeded(); + const bounds = await page.getByTestId('bulk-result').evaluate((element) => { + const box = element.getBoundingClientRect(); + const rows = Array.from(element.querySelectorAll('[data-testid^="bulk-result-row-"]')); + const cells = rows.flatMap((row) => Array.from(row.children)); + return { + right: box.right, + rows: rows.length, + count: cells.length, + overflowingCells: cells.filter((cell) => cell.getBoundingClientRect().right > box.right + 2).length, + }; + }); + await test.info().attach(`bulk-result-layout-${width}`, { + body: JSON.stringify(bounds, null, 2), + contentType: 'application/json', + }); + expect(bounds.right).toBeLessThanOrEqual(width + 2); + expect(bounds.rows).toBe(3); + expect(bounds.count).toBeGreaterThanOrEqual(9); + expect(bounds.overflowingCells).toBe(0); + } + await uploadCsv(page, `userName,displayName\n${corrected},Corrected`); + const retry = page.waitForResponse((r) => + r.url().endsWith(`/scim/endpoints/${id}/Bulk`) && r.request().method() === 'POST'); + await page.getByTestId('bulk-submit').click(); + expect((await retry).status()).toBe(200); + await expect(page.getByTestId('bulk-result-success-count')).toHaveText('1'); + await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('0'); + await expect(page.getByTestId('bulk-download-failures')).toHaveCount(0); + await persistedUser(page, id, corrected); + await persistedUser(page, id, first); +}); + +test('Bulk bounds long result values at wide and narrow viewports without losing their text', async ({ page }) => { + const id = endpointId!; + const location = `https://${'tenant'.repeat(20)}.example.invalid/Users/${randomUUID()}`; + const detail = `A resource with userName '${'resource'.repeat(40)}' already exists.`; + await page.route(`**/scim/endpoints/${id}/Bulk`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/scim+json', + body: JSON.stringify({ + schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'], + Operations: [ + { bulkId: 'row-1', method: 'POST', status: '201', location }, + { bulkId: 'row-2', method: 'POST', status: '409', response: { scimType: 'uniqueness', detail } }, + ], + }), + }); + }); + await page.goto(`/endpoints/${id}/bulk`); + await uploadCsv(page, 'userName,displayName\nowned-one,One\nowned-two,Two'); + await page.getByTestId('bulk-submit').click(); + await expect(page.getByTestId('bulk-result-success-count')).toHaveText('1'); + await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('1'); + await expect(page.getByTestId('bulk-result-row-row-1')).toContainText(location); + await expect(page.getByTestId('bulk-result-row-row-2')).toContainText(detail); + + for (const width of [1440, 900]) { + await page.setViewportSize({ width, height: 900 }); + const bounds = await page.getByTestId('bulk-result').evaluate((element) => { + const right = element.getBoundingClientRect().right; + const rows = Array.from(element.querySelectorAll('[data-testid^="bulk-result-row-"]')); + return { + right, + rows: rows.length, + overflowingCells: rows.flatMap((row) => Array.from(row.children)) + .filter((cell) => cell.getBoundingClientRect().right > right + 2).length, + }; + }); + await test.info().attach(`long-bulk-result-layout-${width}`, { + body: JSON.stringify(bounds, null, 2), + contentType: 'application/json', + }); + expect(bounds.rows).toBe(2); + expect(bounds.right).toBeLessThanOrEqual(width + 2); + expect(bounds.overflowingCells).toBe(0); + for (const field of ['bulk-result-location-row-1', 'bulk-result-detail-row-2']) { + const truncation = await page.getByTestId(field).evaluate((element) => + Array.from(element.querySelectorAll('span')) + .filter((span) => getComputedStyle(span).textOverflow === 'ellipsis') + .map((span) => ({ clientWidth: span.clientWidth, scrollWidth: span.scrollWidth })), + ); + expect(truncation).toHaveLength(1); + expect(truncation[0].clientWidth).toBeGreaterThan(0); + expect(truncation[0].scrollWidth).toBeGreaterThan(truncation[0].clientWidth); + } + } + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); + await page.getByTestId('bulk-result-location-row-1-copy-button').click(); + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(location); + await page.getByTestId('bulk-result-detail-row-2-copy-button').click(); + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(detail); +}); + +test('Bulk rejects malformed CSV without submitting a mutation', async ({ page }) => { + const id = endpointId!; + let submissions = 0; + page.on('request', (request) => { + if (request.url().endsWith(`/scim/endpoints/${id}/Bulk`) && request.method() === 'POST') submissions++; + }); + await page.goto(`/endpoints/${id}/bulk`); + await uploadCsv(page, 'userName,displayName\n"unterminated'); + await expect(page.getByTestId('bulk-csv-error')).toBeVisible(); + await expect(page.getByTestId('bulk-submit')).toBeDisabled(); + expect(submissions).toBe(0); +}); + +test('Manual Provision creates persisted resources, reports conflict, retries, and clears results on tab change', async ({ page }) => { + const id = endpointId!; + const userName = `manual-${randomUUID()}`; + await page.goto(`/manual-provision?endpointId=${id}&resourceTypeId=User`); + const editor = page.getByTestId('manual-resource-body-input'); + const payload = { schemas: [USER_SCHEMA], userName, displayName: 'Owned manual user', active: true }; + await editor.fill(JSON.stringify(payload, null, 2)); + const submit = page.getByRole('button', { name: 'Create User', exact: true }); + const created = page.waitForResponse((response) => + response.url().endsWith(`/scim/endpoints/${id}/Users`) && response.request().method() === 'POST'); + await submit.click(); + const createResponse = await created; + expect(createResponse.status()).toBe(201); + const resource: { id: string; userName: string } = await createResponse.json(); + expect(resource.userName).toBe(userName); + await expect(page.getByTestId('provision-result-id')).toContainText(resource.id); + await expect(page.getByTestId('provision-result-json')).toContainText(userName); + expect((await persistedUser(page, id, userName)).id).toBe(resource.id); + const duplicate = page.waitForResponse((response) => + response.url().endsWith(`/scim/endpoints/${id}/Users`) && response.request().method() === 'POST'); + await submit.click(); + const conflict = await duplicate; + expect(conflict.status()).toBe(409); + const failure: { detail: string } = await conflict.json(); + await expect(page.getByTestId('provision-result-error')).toContainText(failure.detail); + await expect(page.getByTestId('provision-result-success')).toHaveCount(0); + const retryName = `manual-retry-${randomUUID()}`; + await editor.fill(JSON.stringify({ ...payload, userName: retryName }, null, 2)); + await submit.click(); + await expect(page.getByTestId('provision-result-success')).toBeVisible(); + await expect(page.getByTestId('provision-result-error')).toHaveCount(0); + await persistedUser(page, id, retryName); + await page.getByRole('tab', { name: 'Group', exact: true }).click(); + await expect(page.getByTestId('provision-result-empty')).toBeVisible(); + await expect(page.getByTestId('provision-result-success')).toHaveCount(0); + const groupName = `manual-group-${randomUUID()}`; + await editor.fill(JSON.stringify({ schemas: [GROUP_SCHEMA], displayName: groupName }, null, 2)); + const groupReply = page.waitForResponse((response) => + response.url().endsWith(`/scim/endpoints/${id}/Groups`) && response.request().method() === 'POST'); + await page.getByRole('button', { name: 'Create Group', exact: true }).click(); + const groupResponse = await groupReply; + expect(groupResponse.status()).toBe(201); + const group: { id: string; displayName: string } = await groupResponse.json(); + const stored = await page.request.get(`/scim/endpoints/${id}/Groups/${group.id}`, { headers }); + expect(stored.status()).toBe(200); + expect((await stored.json()).displayName).toBe(groupName); + await page.getByTestId('provision-open-resource').click(); + await expect(page).toHaveURL(new RegExp(`/endpoints/${id}/groups.*detail=${group.id}`)); +}); + +test('Manual Provision cannot submit to an inactive owned endpoint', async ({ page }) => { + const id = endpointId!; + const deactivate = await page.request.patch(`/scim/admin/endpoints/${id}`, { + headers, + data: { active: false }, + }); + expect(deactivate.ok()).toBe(true); + let submissions = 0; + page.on('request', (request) => { + if (request.method() === 'POST' && request.url().includes(`/scim/endpoints/${id}/`)) submissions++; + }); + await page.goto(`/manual-provision?endpointId=${id}`); + await expect(page.getByTestId('manual-provision-inactive-endpoint')).toBeVisible(); + await expect(page.getByTestId('manual-resource-create-form')).toHaveCount(0); + expect(submissions).toBe(0); +}); diff --git a/web/src/pages/BulkTab.test.tsx b/web/src/pages/BulkTab.test.tsx index 6bb3d3acf..785fe5fec 100644 --- a/web/src/pages/BulkTab.test.tsx +++ b/web/src/pages/BulkTab.test.tsx @@ -170,6 +170,35 @@ describe('BulkTab (Phase M2)', () => { // ─── 7. Failure-rows-as-CSV download ─────────────────────────────── + it('preserves complete long result values in copyable fields', async () => { + const location = `https://${'tenant'.repeat(20)}.example.invalid/Users/u1`; + const detail = `A resource with userName '${'resource'.repeat(40)}' already exists.`; + mockMutateAsync.mockResolvedValueOnce({ + status: 200, + durationMs: 100, + body: { + schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'], + Operations: [ + { bulkId: 'row-1', status: '201', location }, + { bulkId: 'row-2', status: '409', response: { scimType: 'uniqueness', detail } }, + ], + }, + }); + renderWithProviders(); + const input = screen.getByTestId('bulk-csv-input'); + Object.defineProperty(input, 'files', { value: [csvFile(sampleCsv)] }); + fireEvent.change(input); + await waitFor(() => expect(screen.getByTestId('bulk-preview')).toBeInTheDocument()); + fireEvent.click(screen.getByTestId('bulk-submit')); + await waitFor(() => expect(screen.getByTestId('bulk-result')).toBeInTheDocument()); + + expect(screen.getByTestId('bulk-result-location-row-1')).toHaveTextContent(location); + expect(screen.getByTestId('bulk-result-detail-row-2')).toHaveTextContent(detail); + expect(screen.getByTestId('bulk-result-id-row-1')).toHaveTextContent('row-1'); + expect(screen.getByTestId('bulk-result-location-row-1-copy-button')).toBeInTheDocument(); + expect(screen.getByTestId('bulk-result-detail-row-2-copy-button')).toBeInTheDocument(); + }); + it('Download failure rows triggers CSV with detail + scimType columns', async () => { mockMutateAsync.mockResolvedValueOnce({ status: 200, diff --git a/web/src/pages/BulkTab.tsx b/web/src/pages/BulkTab.tsx index 0b538f061..c328e15e2 100644 --- a/web/src/pages/BulkTab.tsx +++ b/web/src/pages/BulkTab.tsx @@ -51,7 +51,7 @@ import { } from '../utils/bulk-builder'; import { toCsv, triggerCsvDownload } from '../utils/csv-export'; import { ScimErrorMessage } from '../components/primitives/ScimErrorMessage'; -import { CopyableJsonBlock, CopyJsonButton } from '../components/primitives'; +import { CopyableField, CopyableJsonBlock, CopyJsonButton } from '../components/primitives'; type BulkResource = 'Users' | 'Groups'; @@ -89,7 +89,7 @@ const useStyles = makeStyles({ }, resultRow: { display: 'grid', - gridTemplateColumns: '80px 80px 1fr 1fr', + gridTemplateColumns: '80px 80px minmax(0, 1fr) minmax(0, 1fr)', columnGap: '12px', padding: '6px 8px', borderBottom: `1px solid ${tokens.colorNeutralStroke3}`, @@ -390,14 +390,26 @@ export const BulkTab: React.FC = ({ endpointId }) => { className={classes.resultRow} data-testid={`bulk-result-row-${op.bulkId ?? 'unknown'}`} > - {op.bulkId ?? '-'} + {op.status ?? '-'} - - {op.location ?? '-'} - - {op.response?.detail ?? ''} + {op.location ? : -} + {op.response?.detail ? : } ))}