diff --git a/.gitignore b/.gitignore index c7c5be5..516f1a1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ test-results/ # UI kit / console (U1+) node_modules/ ui/**/dist/ + +# Coverage reports are BUILD OUTPUT: regenerated by `pnpm test`, timestamped, never reviewed. +coverage/ diff --git a/docs/rfc/goldpath-console.md b/docs/rfc/goldpath-console.md index 1da22d2..5d57653 100644 --- a/docs/rfc/goldpath-console.md +++ b/docs/rfc/goldpath-console.md @@ -62,7 +62,9 @@ custom-develop ON, with the same kit, the same way they add features to the back landing page: operators open consoles to answer "is anything wrong", not to browse. - **D3 — Proof bar (UI is claims-are-proofs too).** Kit: component tests (vitest) on the composites (keyset table paging, verb button's - refusal surface, state mapping). Console: the smoke drives THREE real apps — one open, + refusal surface, state mapping), under a COVERAGE FLOOR that CI enforces (kit 95/90, + console 97/85 statements/branches) — the floor exists because the gaps that hid real + bugs here were branches nobody had thought to exercise, not tests anyone had deleted. Console: the smoke drives THREE real apps — one open, one behind the auth floor, one tenant-scoped — because how the console behaves when it is REFUSED is as much a claim as how it behaves when it is welcome; plus a service that dies mid-session. Runnable on demand (`.github/workflows/console-smoke.yml`), not only diff --git a/ui/console/package.json b/ui/console/package.json index d53b1c4..8d6ccb6 100644 --- a/ui/console/package.json +++ b/ui/console/package.json @@ -6,7 +6,7 @@ "type": "module", "main": "src/index.ts", "scripts": { - "test": "vitest run", + "test": "vitest run --coverage --coverage.thresholds.statements=97 --coverage.thresholds.branches=85 --coverage.thresholds.functions=75 --coverage.thresholds.lines=97", "typecheck": "tsc --noEmit", "dev": "vite", "e2e": "playwright test" @@ -16,21 +16,22 @@ "react-dom": "^19.0.0" }, "devDependencies": { + "@goldpath/kit": "link:../kit", + "@playwright/test": "1.50.1", + "@tailwindcss/vite": "^4.3.2", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "14.6.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "2.1.8", "jsdom": "^25.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "tailwindcss": "^4.0.0", "typescript": "^5.7.0", "vite": "^8.1.4", - "vitest": "^2.1.8", - "@goldpath/kit": "link:../kit", - "@tailwindcss/vite": "^4.3.2", - "tailwindcss": "^4.0.0", - "@playwright/test": "1.50.1" + "vitest": "^2.1.8" } } diff --git a/ui/console/pnpm-lock.yaml b/ui/console/pnpm-lock.yaml index 61f1657..44fefb6 100644 --- a/ui/console/pnpm-lock.yaml +++ b/ui/console/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@8.1.5(jiti@2.7.0)) + '@vitest/coverage-v8': + specifier: 2.1.8 + version: 2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.33.0)) jsdom: specifier: ^25.0.1 version: 25.0.1 @@ -62,6 +65,10 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -152,6 +159,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -327,6 +337,14 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -352,6 +370,10 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.50.1': resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==} engines: {node: '>=18'} @@ -757,6 +779,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@2.1.8': + resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==} + peerDependencies: + '@vitest/browser': 2.1.8 + vitest: 2.1.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -794,10 +825,22 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -812,11 +855,25 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.11.3: resolution: {integrity: sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew==} engines: {node: '>=6.0.0'} hasBin: true + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + browserslist@4.28.7: resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -841,6 +898,13 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -848,6 +912,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -900,9 +968,18 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.396: resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -955,6 +1032,10 @@ packages: picomatch: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -984,6 +1065,11 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -991,6 +1077,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1007,6 +1097,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1023,9 +1116,35 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1216,6 +1335,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1232,6 +1358,18 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1247,9 +1385,20 @@ packages: nwsapi@2.2.24: resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1336,9 +1485,26 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1349,10 +1515,30 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1363,6 +1549,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1539,11 +1729,24 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.21.1: resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} @@ -1570,6 +1773,11 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -1692,6 +1900,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -1797,6 +2007,17 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1825,6 +2046,9 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.50.1': dependencies: playwright: 1.50.1 @@ -2110,6 +2334,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.33.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(jsdom@25.0.1)(lightningcss@1.33.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -2154,8 +2396,16 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -2166,8 +2416,20 @@ snapshots: asynckit@0.4.0: {} + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + baseline-browser-mapping@2.11.3: {} + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.11.3 @@ -2195,12 +2457,24 @@ snapshots: check-error@2.1.3: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 convert-source-map@2.0.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -2239,8 +2513,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.396: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -2303,6 +2583,11 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -2339,10 +2624,21 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + gopd@1.2.0: {} graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2357,6 +2653,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2377,8 +2675,39 @@ snapshots: indent-string@4.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} js-tokens@4.0.0: {} @@ -2527,6 +2856,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + math-intrinsics@1.1.0: {} mime-db@1.52.0: {} @@ -2537,6 +2876,16 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minipass@7.1.3: {} + ms@2.1.3: {} nanoid@3.3.16: {} @@ -2545,10 +2894,19 @@ snapshots: nwsapi@2.2.24: {} + package-json-from-dist@1.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + pathe@1.1.2: {} pathval@2.0.1: {} @@ -2661,24 +3019,64 @@ snapshots: semver@6.3.1: {} + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} stackback@0.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + symbol-tree@3.2.4: {} tailwindcss@4.3.3: {} tapable@2.3.3: {} + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.5 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -2809,11 +3207,27 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + ws@8.21.1: {} xml-name-validator@5.0.0: {} diff --git a/ui/console/src/ArchivalPanel.test.tsx b/ui/console/src/ArchivalPanel.test.tsx index 08f8800..b200220 100644 --- a/ui/console/src/ArchivalPanel.test.tsx +++ b/ui/console/src/ArchivalPanel.test.tsx @@ -261,4 +261,45 @@ describe("the archival panel (chain health, retrieval, lifecycle)", () => { // The previous "verifies" verdict is GONE — an unknown chain must not read as a proven one. expect(screen.queryByTestId("chain-findings")).toBeNull(); }); + + it("a lifecycle verb that never reached the server is NOT reported as applied", async () => { + const { client: api } = client({ verb: { status: 503, body: {} } }); + render(); + + await retrieve(); + await userEvent.click(await screen.findByRole("button", { name: "lift-hold" })); + await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "lift-hold" })); + + expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument(); + }); + + it("switching archive clears the open entry — a key means nothing outside its archive", async () => { + const { client: api } = client({ + definitions: [ + { name: "policies", entries: 10, dueBacklog: 0, activeHolds: 0, chainHead: 10, purgedThrough: 0 }, + { name: "claims", entries: 4, dueBacklog: 0, activeHolds: 0, chainHead: 4, purgedThrough: 0 }, + ], + }); + render(); + + await retrieve(); + await screen.findByTestId("archive-entry"); + + await userEvent.selectOptions(screen.getByLabelText("archive"), "claims"); + + // The policies entry must not linger under the claims archive's name. + expect(screen.queryByTestId("archive-entry")).toBeNull(); + expect(screen.queryByText(/No entry for/)).toBeNull(); + }); + + it("a lifted hold shows who lifted it and when — the row stays as evidence", async () => { + const { client: api } = client({ + holds: [hold({ liftedBy: "legal@example.com", liftedAt: "2026-07-27T05:00:00Z" })], + }); + render(); + + const row = await screen.findByRole("row", { name: /CASE-2026-7/ }); + expect(row).toHaveTextContent("lifted by legal@example.com at 2026-07-27T05:00:00Z"); + expect(within(row).queryByText("Held")).toBeNull(); + }); }); diff --git a/ui/console/src/BulkPanel.test.tsx b/ui/console/src/BulkPanel.test.tsx index 8773478..546b950 100644 --- a/ui/console/src/BulkPanel.test.tsx +++ b/ui/console/src/BulkPanel.test.tsx @@ -259,4 +259,40 @@ describe("the bulk intake panel (upload → report → four-eyes gate)", () => { await waitFor(() => expect(screen.queryByRole("button", { name: "approve" })).toBeNull()); expect(screen.getByText("approved")).toBeInTheDocument(); }); + + it("a decision that never reached the server is NOT reported as recorded", async () => { + // The verb envelope never came back — 503 is neither an ok nor a refusal, and the + // operator must not walk away believing the gate moved. + const { client: api } = client({ verb: { status: 503, body: {} } }); + render(); + + await openBatch(); + await userEvent.click(await screen.findByRole("button", { name: "approve" })); + await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "approve" })); + + expect(await screen.findByText(/did not reach the server — it may not have been recorded/)).toBeInTheDocument(); + }); + + it("a decided batch carries its evidence: who, when, and why", async () => { + const decided = batch({ + state: "Rejected", + decidedAt: "2026-07-27T10:00:00Z", + decidedBy: "ops@example.com", + decisionNote: "line 3 is malformed", + }); + const { client: api } = client({ batch: decided, batches: [decided] }); + render(); + + await openBatch(); + const detail = await screen.findByTestId("batch-detail"); + expect(detail).toHaveTextContent("Rejected by ops@example.com at 2026-07-27T10:00:00Z"); + expect(detail).toHaveTextContent("line 3 is malformed"); + }); + + it("a definition with nothing in it says so, instead of showing a bare name", async () => { + const { client: api } = client({ definitions: [definition({ batchesByState: {}, awaitingApproval: 0 })] }); + render(); + + expect(await screen.findByText("no batches yet")).toBeInTheDocument(); + }); }); diff --git a/ui/console/src/CampaignPanel.test.tsx b/ui/console/src/CampaignPanel.test.tsx index 8083696..f99fb5f 100644 --- a/ui/console/src/CampaignPanel.test.tsx +++ b/ui/console/src/CampaignPanel.test.tsx @@ -298,4 +298,26 @@ describe("the campaign governor panel", () => { await waitFor(() => expect(screen.getByTestId("campaign-detail").textContent).not.toBe(before)); }); + + it("a verb that never reached the server is NOT reported as applied", async () => { + const { client: api } = client({ verb: { status: 503, body: {} } }); + render(); + + await open(); + await userEvent.click(await screen.findByRole("button", { name: "pause" })); + await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "pause" })); + + expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument(); + }); + + it("a campaign with no window releases around the clock, and says exactly that", async () => { + const always = campaign({ windowStart: null, windowEnd: null, dailyQuota: null }); + const { client: api } = client({ detail: always, list: [always] }); + render(); + + await open(); + const detail = await screen.findByTestId("campaign-detail"); + expect(detail).toHaveTextContent("around the clock"); + expect(detail).toHaveTextContent("Daily quotanone"); + }); }); diff --git a/ui/console/src/NotificationPanel.test.tsx b/ui/console/src/NotificationPanel.test.tsx index 71d7934..2a48c41 100644 --- a/ui/console/src/NotificationPanel.test.tsx +++ b/ui/console/src/NotificationPanel.test.tsx @@ -44,6 +44,18 @@ function client(routes: Routes = {}) { const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + // The by-id route must be matched BEFORE the list route it is nested under. + if (/\/notifications\/[^/?]+$/.test(url)) { + const id = url.split("/").pop(); + const all = [ + notification(), + notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" }), + notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" }), + ...(Array.isArray(routes.list) ? (routes.list as NotificationInfo[]) : []), + ]; + return json(all.find((row) => row.id === id) ?? notification()); + } + if (url.includes("/failures")) return json(routes.failures ?? [notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" })]); if (url.includes("/suppressions")) return json(routes.suppressions ?? [notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" })]); if (url.includes("/notifications")) return json(routes.list ?? [notification()]); @@ -157,7 +169,7 @@ describe("the notification evidence panel (read-only by contract)", () => { }); it("says when the body was deleted — the retention promise, kept and recorded", async () => { - const { client: api } = client({ list: [notification({ bodyDeletedAt: "2026-10-25T00:00:00Z" })] }); + const { client: api } = client({ list: [notification({ id: "deleted-1", bodyDeletedAt: "2026-10-25T00:00:00Z" })] }); render(); await userEvent.click(await screen.findByRole("button", { name: "a***@e***" })); @@ -171,4 +183,26 @@ describe("the notification evidence panel (read-only by contract)", () => { // The table reports its own failure too — the panel-level banner is the first one. expect((await screen.findAllByRole("alert"))[0]).toHaveTextContent(/notification templates could not be loaded/i); }); + + it("tenant and correlation id appear when the row carries them — and never as empty labels", async () => { + const { client: api } = client({ + list: [notification({ id: "traced-1", tenant: "acme", correlationId: "0af7651916cd43dd8448eb211c80319c" })], + }); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "a***@e***" })); + const detail = await screen.findByTestId("notification-detail"); + expect(detail).toHaveTextContent("acme"); + expect(detail).toHaveTextContent("0af7651916cd43dd8448eb211c80319c"); + }); + + it("a row WITHOUT them shows no tenant or correlation block at all", async () => { + const { client: api } = client(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "a***@e***" })); + const detail = await screen.findByTestId("notification-detail"); + expect(within(detail).queryByText("Tenant")).toBeNull(); + expect(within(detail).queryByText("Correlation")).toBeNull(); + }); }); diff --git a/ui/console/src/NotificationPanel.tsx b/ui/console/src/NotificationPanel.tsx index d124da8..215e498 100644 --- a/ui/console/src/NotificationPanel.tsx +++ b/ui/console/src/NotificationPanel.tsx @@ -45,6 +45,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) { const [lens, setLens] = useState("all"); const [state, setState] = useState(""); const [template, setTemplate] = useState(""); + const [selectedId, setSelectedId] = useState(null); const [selected, setSelected] = useState(null); const [error, setError] = useState(null); const [refreshToken, setRefreshToken] = useState(0); @@ -77,6 +78,24 @@ export function NotificationPanel({ client }: NotificationPanelProps) { [client, lens, state, template, refreshToken], ); + // The open row RE-READS by id: a notification is evidence in MOTION (Requested → Sent + // or Failed), so a row captured minutes ago is not what the operator should judge. + useEffect(() => { + if (!selectedId) { + setSelected(null); + return; + } + + let live = true; + client + .notification(selectedId) + .then((found) => live && setSelected(found)) + .catch(() => live && setError(`notification ${selectedId} could not be read`)); + return () => { + live = false; + }; + }, [client, selectedId, refreshToken]); + const waiting = (templates ?? []).filter((entry) => (entry.oldestRequestedSeconds ?? 0) > 0); return ( @@ -127,7 +146,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) { }`} onClick={() => { setLens(entry.key); - setSelected(null); + setSelectedId(null); }} > {entry.label} @@ -145,7 +164,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) { value={state} onChange={(event) => { setState(event.target.value); - setSelected(null); + setSelectedId(null); }} > @@ -165,7 +184,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) { value={template} onChange={(event) => { setTemplate(event.target.value); - setSelected(null); + setSelectedId(null); }} > @@ -192,7 +211,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) { { header: "Recipient (masked)", cell: (row) => ( - ), diff --git a/ui/console/src/RunConsole.test.tsx b/ui/console/src/RunConsole.test.tsx index ee8fd48..7d317fa 100644 --- a/ui/console/src/RunConsole.test.tsx +++ b/ui/console/src/RunConsole.test.tsx @@ -141,4 +141,46 @@ describe("the run console (console RFC §2 — a client of the frozen contract)" expect(await screen.findByRole("alert")).toHaveTextContent(/fleet list could not be loaded/i); }); + + it("choosing another fleet loads ITS jobs and closes the run that belonged to the first", async () => { + const fetcher = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); + if (init?.method === "POST") return json({ ok: true, message: "done" }); + if (url.includes("/jobs/runs/")) return json(detail()); + if (url.includes("/runs?")) return json(url.includes("night-cluster") ? [] : [run()]); + if (url.endsWith("/jobs")) return json(url.includes("night-cluster") ? [{ name: "nightly-sweep" }] : [{ name: "eod-reconciliation" }]); + return json([ + { schedulerName: "it-cluster", jobCount: 1, nodes: [{ instanceId: "node-a" }] }, + { schedulerName: "night-cluster", jobCount: 1, nodes: [] }, + ]); + }) as typeof fetch; + + render(); + + await userEvent.click(await screen.findByRole("button", { name: "run-9f21" })); + await screen.findByTestId("run-detail"); + + await userEvent.click(screen.getByRole("button", { name: /night-cluster/ })); + + // The open run belonged to the other fleet: keeping it on screen would put one + // fleet's job list next to another fleet's run. + await waitFor(() => expect(screen.queryByTestId("run-detail")).toBeNull()); + expect(await screen.findByText("nightly-sweep")).toBeInTheDocument(); + }); + + it("a PAUSED job offers resume, and resuming posts the frozen route", async () => { + const { client: api, posted } = client({ jobs: [{ name: "eod-reconciliation", paused: true }] }); + render(); + + // A paused job must never offer "pause" — the console reads the job's own flag. + expect(await screen.findByRole("button", { name: "resume" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "pause" })).toBeNull(); + + await userEvent.click(screen.getByRole("button", { name: "resume" })); + await userEvent.click(screen.getByRole("alertdialog").querySelector("button")!); + + await waitFor(() => expect(posted).toHaveLength(1)); + expect(posted[0].url).toContain("/goldpath/admin/jobs/fleets/it-cluster/jobs/eod-reconciliation/resume"); + }); }); diff --git a/ui/console/src/adminClient.test.ts b/ui/console/src/adminClient.test.ts index 2907b89..9d5bb02 100644 --- a/ui/console/src/adminClient.test.ts +++ b/ui/console/src/adminClient.test.ts @@ -132,4 +132,41 @@ describe("the admin client (the console's only door — the FROZEN contract)", ( expect(calls[0].init?.credentials).toBe("include"); }); + + it("every campaign verb posts ITS frozen route — including the ones the panel shows rarely", async () => { + const { fetcher, calls } = fakeFetch({ "/campaign/": json({ ok: true, message: "done" }) }); + const client = new AdminClient({ fetcher }); + + await client.resumeCampaign("c-1"); + await client.pauseCampaign("c-1"); + + expect(calls[0].url).toContain("/goldpath/admin/campaign/c-1/resume"); + expect(calls[1].url).toContain("/goldpath/admin/campaign/c-1/pause"); + }); + + it("ids with slashes and spaces are ENCODED — a key is data, never part of the path", async () => { + const { fetcher, calls } = fakeFetch({ "/goldpath/admin/": json({}) }); + const client = new AdminClient({ fetcher }); + + await client.notification("a/b c"); + await client.archiveEntry("policies", "P/42 x"); + + expect(calls[0].url).toContain("/notifications/a%2Fb%20c"); + expect(calls[1].url).toContain("/entries/policies/P%2F42%20x"); + }); + + it("rerun posts the run's own route — the repair verbs are all one shape", async () => { + const { fetcher, calls } = fakeFetch({ "/jobs/runs/": json({ ok: true, message: "rerun queued" }) }); + + const result = await new AdminClient({ fetcher }).rerun("run-9f21"); + + expect(calls[0].url).toContain("/goldpath/admin/jobs/runs/run-9f21/rerun"); + expect(result).toEqual({ ok: true, message: "rerun queued" }); + }); + + it("a verb answered with an unexpected status THROWS — it is never read as an outcome", async () => { + const { fetcher } = fakeFetch({ "/jobs/runs/": new Response("", { status: 503 }) }); + + await expect(new AdminClient({ fetcher }).rerun("run-9f21")).rejects.toThrow(/503/); + }); }); diff --git a/ui/console/vitest.config.ts b/ui/console/vitest.config.ts index e2ee1ff..1daa3b8 100644 --- a/ui/console/vitest.config.ts +++ b/ui/console/vitest.config.ts @@ -9,6 +9,13 @@ export default defineConfig({ }, test: { environment: "jsdom", + coverage: { + // Only the SOURCE is judged: the gallery and the dev entry point are worked + // examples, and a barrel file has nothing to test. + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/index.ts", "src/test-setup.ts", "src/**/*.{test,spec}.{ts,tsx}"], + }, + globals: true, setupFiles: ["./src/test-setup.ts"], // The e2e suite belongs to Playwright (`pnpm e2e`); vitest owns src/ only. diff --git a/ui/kit/package.json b/ui/kit/package.json index e2989b0..b7c30ee 100644 --- a/ui/kit/package.json +++ b/ui/kit/package.json @@ -6,7 +6,7 @@ "type": "module", "main": "src/index.ts", "scripts": { - "test": "vitest run", + "test": "vitest run --coverage --coverage.thresholds.statements=95 --coverage.thresholds.branches=90 --coverage.thresholds.functions=75 --coverage.thresholds.lines=95", "typecheck": "tsc --noEmit", "gallery": "vite" }, @@ -22,6 +22,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "2.1.8", "jsdom": "^25.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/ui/kit/pnpm-lock.yaml b/ui/kit/pnpm-lock.yaml index 15f7637..a714192 100644 --- a/ui/kit/pnpm-lock.yaml +++ b/ui/kit/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@8.1.4(jiti@2.7.0)) + '@vitest/coverage-v8': + specifier: 2.1.8 + version: 2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.32.0)) jsdom: specifier: ^25.0.1 version: 25.0.1 @@ -56,6 +59,10 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -146,6 +153,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -321,6 +331,14 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -346,6 +364,10 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@rolldown/binding-android-arm64@1.1.5': resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -743,6 +765,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@2.1.8': + resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==} + peerDependencies: + '@vitest/browser': 2.1.8 + vitest: 2.1.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -780,10 +811,22 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -798,11 +841,25 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.43: resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} engines: {node: '>=6.0.0'} hasBin: true + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + browserslist@4.28.6: resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -827,6 +884,13 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -834,6 +898,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -886,9 +954,18 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.389: resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} @@ -941,6 +1018,10 @@ packages: picomatch: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -965,6 +1046,11 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -972,6 +1058,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -988,6 +1078,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1004,9 +1097,35 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -1123,6 +1242,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1139,6 +1265,18 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1154,9 +1292,20 @@ packages: nwsapi@2.2.24: resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1233,9 +1382,26 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1246,10 +1412,30 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1260,6 +1446,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1436,11 +1626,24 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -1467,6 +1670,11 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -1589,6 +1797,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -1694,6 +1904,17 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1722,6 +1943,9 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -2002,6 +2226,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.32.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(jsdom@25.0.1)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -2046,8 +2288,16 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -2058,8 +2308,20 @@ snapshots: asynckit@0.4.0: {} + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.43: {} + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 @@ -2087,12 +2349,24 @@ snapshots: check-error@2.1.3: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 convert-source-map@2.0.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -2131,8 +2405,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.389: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 @@ -2195,6 +2475,11 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -2228,10 +2513,21 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + gopd@1.2.0: {} graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2246,6 +2542,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2266,8 +2564,39 @@ snapshots: indent-string@4.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} js-tokens@4.0.0: {} @@ -2367,6 +2696,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + math-intrinsics@1.1.0: {} mime-db@1.52.0: {} @@ -2377,6 +2716,16 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minipass@7.1.3: {} + ms@2.1.3: {} nanoid@3.3.16: {} @@ -2385,10 +2734,19 @@ snapshots: nwsapi@2.2.24: {} + package-json-from-dist@1.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + pathe@1.1.2: {} pathval@2.0.1: {} @@ -2493,24 +2851,64 @@ snapshots: semver@6.3.1: {} + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} stackback@0.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + symbol-tree@3.2.4: {} tailwindcss@4.3.2: {} tapable@2.3.3: {} + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.5 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -2641,11 +3039,27 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + ws@8.21.0: {} xml-name-validator@5.0.0: {} diff --git a/ui/kit/src/components/AppShell.test.tsx b/ui/kit/src/components/AppShell.test.tsx index f22c170..a4f88e3 100644 --- a/ui/kit/src/components/AppShell.test.tsx +++ b/ui/kit/src/components/AppShell.test.tsx @@ -124,4 +124,28 @@ describe("the app shell (ui-standard-v1 §3 — the surface scrolls, never the p rerender(

c

); expect(screen.queryByText("0")).toBeNull(); }); + + it("the service picker defaults to the first service and switches to the one chosen", async () => { + const chosen: string[] = []; + render( + chosen.push("payments") }, + { name: "claims", onSelect: () => chosen.push("claims") }, + ]} + > +

body

+
, + ); + + // No activeService given: the picker shows the first, rather than an empty box. + const picker = screen.getByLabelText(/service/i); + expect(picker).toHaveValue("payments"); + + await userEvent.selectOptions(picker, "claims"); + expect(chosen).toEqual(["claims"]); + }); }); diff --git a/ui/kit/src/components/KeysetTable.test.tsx b/ui/kit/src/components/KeysetTable.test.tsx index ed1808e..f227459 100644 --- a/ui/kit/src/components/KeysetTable.test.tsx +++ b/ui/kit/src/components/KeysetTable.test.tsx @@ -100,4 +100,53 @@ describe("the keyset table (ui-standard-v1 §4 — cursor pager, never offsets)" expect(load).toHaveBeenNthCalledWith(1, null, 50); expect(load).toHaveBeenNthCalledWith(2, null, 50); // same page, fresh walk }); + + it("a superseded load never overwrites the newer one — the page the operator sees is the last they asked for", async () => { + // Page 1 resolves LAST: without the generation guard it would land on top of the + // second page and the table would silently show stale rows. + const gates: ((rows: string[]) => void)[] = []; + const loadPage = (cursor: string | null) => + new Promise>((resolve) => { + gates.push((rows) => resolve({ items: rows, nextCursor: cursor === null ? "c1" : null })); + }); + + const { rerender } = render( + + columns={[{ header: "Row", cell: (row) => row }]} + loadPage={loadPage} + rowKey={(row) => row} + />, + ); + + // A second load supersedes the first (a filter change does exactly this). + rerender( + + columns={[{ header: "Row", cell: (row) => row }]} + loadPage={(cursor) => loadPage(cursor)} + rowKey={(row) => row} + />, + ); + + await waitFor(() => expect(gates).toHaveLength(2)); + gates[1](["fresh"]); + gates[0](["stale"]); + + expect(await screen.findByText("fresh")).toBeInTheDocument(); + await waitFor(() => expect(screen.queryByText("stale")).toBeNull()); + }); + + it("right-aligned columns align their header AND their cells", () => { + render( + + columns={[{ header: "Count", align: "right", cell: (row) => row.n }]} + loadPage={async () => ({ items: [{ n: 7 }], nextCursor: null })} + rowKey={(row) => String(row.n)} + />, + ); + + return waitFor(() => { + expect(screen.getByRole("columnheader", { name: "Count" })).toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: "7" })).toHaveClass("text-right"); + }); + }); }); diff --git a/ui/kit/src/components/RunProgress.test.tsx b/ui/kit/src/components/RunProgress.test.tsx index 4fc1f61..f8ef0f9 100644 --- a/ui/kit/src/components/RunProgress.test.tsx +++ b/ui/kit/src/components/RunProgress.test.tsx @@ -31,27 +31,39 @@ describe("the run progress composite (ui-standard-v1 §4)", () => { expect(itemsPerSecond(finished, now)).toBeCloseTo(2000); // 100k in 50s, not in 100s }); - it("judges the deadline: live runs on the prediction, finished runs on reality", () => { - expect(deadlineVerdict(base)).toBe("none"); // no deadline - expect(deadlineVerdict({ ...base, deadlineAt: "2026-07-26T11:00:00Z" })).toBe("on-track"); + it("judges the deadline: live runs on the clock and the prediction, finished runs on reality", () => { + // The clock is INJECTED: a verdict that silently reads the wall clock cannot be tested + // and would drift under a skewed browser. + const now = new Date("2026-07-26T10:10:00Z"); + expect(deadlineVerdict(base, now)).toBe("none"); // no deadline + expect(deadlineVerdict({ ...base, deadlineAt: "2026-07-26T11:00:00Z" }, now)).toBe("on-track"); expect(deadlineVerdict({ ...base, deadlineAt: "2026-07-26T10:30:00Z", predictedFinishAt: "2026-07-26T10:45:00Z", - })).toBe("overrun-predicted"); + }, now)).toBe("overrun-predicted"); + + // Still running AFTER its deadline: it has overrun, prediction or not. Anything else + // would tell the operator the one thing that is certainly false. + expect(deadlineVerdict({ ...base, deadlineAt: "2026-07-26T10:05:00Z" }, now)).toBe("overrun"); + expect(deadlineVerdict({ + ...base, + deadlineAt: "2026-07-26T10:05:00Z", + predictedFinishAt: "2026-07-26T10:04:00Z", + }, now)).toBe("overrun"); expect(deadlineVerdict({ ...base, status: "Completed", deadlineAt: "2026-07-26T10:30:00Z", predictedFinishAt: "2026-07-26T10:45:00Z", finishedAt: "2026-07-26T10:20:00Z", - })).toBe("on-track"); // reality beat the prediction + }, now)).toBe("on-track"); // reality beat the prediction expect(deadlineVerdict({ ...base, status: "Completed", deadlineAt: "2026-07-26T10:30:00Z", finishedAt: "2026-07-26T10:44:00Z", - })).toBe("overrun"); + }, now)).toBe("overrun"); }); it("renders chunk progress, the rate and the predicted-overrun warning", () => { @@ -99,4 +111,31 @@ describe("the run progress composite (ui-standard-v1 §4)", () => { expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "0"); expect(screen.getByText("0/0 chunks")).toBeInTheDocument(); }); + + it("an overrun that ALREADY happened reads danger; one merely predicted reads warning", () => { + const overrun = render( + , + ); + expect(overrun.container.querySelector(".text-danger")).not.toBeNull(); + overrun.unmount(); + + const predicted = render( + , + ); + expect(predicted.container.querySelector(".text-warning")).not.toBeNull(); + expect(predicted.container.querySelector(".text-danger")).toBeNull(); + }); }); diff --git a/ui/kit/src/components/RunProgress.tsx b/ui/kit/src/components/RunProgress.tsx index 1120033..55ab99e 100644 --- a/ui/kit/src/components/RunProgress.tsx +++ b/ui/kit/src/components/RunProgress.tsx @@ -34,14 +34,18 @@ export function itemsPerSecond(run: RunProgressData, now: Date): number | null { export type DeadlineVerdict = "none" | "on-track" | "overrun-predicted" | "overrun"; /** - * The deadline verdict: a FINISHED run is judged on when it actually ended; a live run - * on its prediction. The console warns BEFORE the deadline passes — that is the whole - * point of carrying a prediction in the run row. + * The deadline verdict: a FINISHED run is judged on when it actually ended; a live run on + * the CLOCK first and its prediction second. The console warns BEFORE the deadline passes + * — that is the point of carrying a prediction — but a run that is still going after its + * deadline has already overrun, prediction or not. Reading that as "on track" (which it + * did until the UI coverage sweep of 2026-07-27) tells the operator the one thing that is + * certainly false. */ -export function deadlineVerdict(run: RunProgressData): DeadlineVerdict { +export function deadlineVerdict(run: RunProgressData, now: Date = new Date()): DeadlineVerdict { if (!run.deadlineAt) return "none"; const deadline = new Date(run.deadlineAt).getTime(); if (run.finishedAt) return new Date(run.finishedAt).getTime() > deadline ? "overrun" : "on-track"; + if (now.getTime() > deadline) return "overrun"; if (!run.predictedFinishAt) return "on-track"; return new Date(run.predictedFinishAt).getTime() > deadline ? "overrun-predicted" : "on-track"; } @@ -64,7 +68,7 @@ function formatRate(rate: number): string { export function RunProgress({ run, now = new Date() }: RunProgressProps) { const pct = run.totalChunks > 0 ? Math.round((run.completedChunks / run.totalChunks) * 100) : 0; const rate = itemsPerSecond(run, now); - const verdict = deadlineVerdict(run); + const verdict = deadlineVerdict(run, now); const barTone = verdict === "overrun" || run.failedChunks > 0 ? "bg-danger" diff --git a/ui/kit/src/components/VerbButton.test.tsx b/ui/kit/src/components/VerbButton.test.tsx index 50d6a92..ef361aa 100644 --- a/ui/kit/src/components/VerbButton.test.tsx +++ b/ui/kit/src/components/VerbButton.test.tsx @@ -137,4 +137,21 @@ describe("the verb button (ui-standard-v1 §3/§4 — confirm-before-verb, verba await waitFor(() => expect(seen).toEqual([ok("approved")])); expect(screen.queryByText("approved")).toBeNull(); }); + + it("an unexpected STATUS is named — the operator gets a number to take to the logs", async () => { + render( + Promise.resolve({ kind: "error", status: 503 } as VerbOutcome)} + />, + ); + + await userEvent.click(screen.getByRole("button", { name: "trigger" })); + await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "trigger" })); + + // Status 0 means "never left the browser"; a real status means the service answered + // something unexpected — two different stories, told differently. + expect(await screen.findByText(/unexpected 503 — check the service logs/)).toBeInTheDocument(); + }); }); diff --git a/ui/kit/vitest.config.ts b/ui/kit/vitest.config.ts index af50570..4eb2dbd 100644 --- a/ui/kit/vitest.config.ts +++ b/ui/kit/vitest.config.ts @@ -5,6 +5,13 @@ export default defineConfig({ plugins: [react()], test: { environment: "jsdom", + coverage: { + // Only the SOURCE is judged: the gallery and the dev entry point are worked + // examples, and a barrel file has nothing to test. + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/index.ts", "src/test-setup.ts", "src/**/*.{test,spec}.{ts,tsx}"], + }, + globals: true, setupFiles: ["./src/test-setup.ts"], },