Story 27: Coverage tracking (full-stack)
Add end-to-end coverage tracking: backend collects vitest coverage, records metrics with threshold/baseline tracking, and blocks acceptance on regression. Frontend displays coverage in gate/review panels with a "Collect Coverage" button. Includes 20 Rust tests, 17 Vitest tests, and 14 Playwright E2E tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -10,4 +10,5 @@ lerna-debug.log*
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"server": "cargo run --manifest-path server/Cargo.toml",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:e2e": "playwright test"
|
||||
"test:e2e": "playwright test",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -25,10 +26,11 @@
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/node": "^25.0.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"jest": "^29.0.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"ts-jest": "^29.0.0",
|
||||
|
||||
236
frontend/pnpm-lock.yaml
generated
236
frontend/pnpm-lock.yaml
generated
@@ -51,6 +51,9 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.6.0
|
||||
version: 4.7.0(vite@5.4.21(@types/node@25.0.3))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^2.1.9
|
||||
version: 2.1.9(vitest@2.1.9(@types/node@25.0.3)(jsdom@28.1.0))
|
||||
jest:
|
||||
specifier: ^29.0.0
|
||||
version: 29.7.0(@types/node@25.0.3)
|
||||
@@ -78,6 +81,10 @@ packages:
|
||||
'@adobe/css-tools@4.4.4':
|
||||
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@asamuzakjp/css-color@4.1.2':
|
||||
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
|
||||
|
||||
@@ -146,6 +153,11 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/plugin-syntax-async-generators@7.8.4':
|
||||
resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
|
||||
peerDependencies:
|
||||
@@ -265,6 +277,10 @@ packages:
|
||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@0.2.3':
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
|
||||
@@ -503,6 +519,10 @@ packages:
|
||||
'@noble/hashes':
|
||||
optional: true
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@istanbuljs/load-nyc-config@1.1.0':
|
||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -593,6 +613,10 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -832,6 +856,15 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@vitest/coverage-v8@2.1.9':
|
||||
resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 2.1.9
|
||||
vitest: 2.1.9
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||
|
||||
@@ -873,6 +906,10 @@ 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'}
|
||||
@@ -881,6 +918,10 @@ packages:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -948,6 +989,9 @@ packages:
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1158,6 +1202,9 @@ 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.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
@@ -1168,6 +1215,9 @@ packages:
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -1257,6 +1307,10 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
format@0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
@@ -1304,6 +1358,11 @@ packages:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
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
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
@@ -1538,10 +1597,17 @@ packages:
|
||||
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
|
||||
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==}
|
||||
|
||||
jest-changed-files@29.7.0:
|
||||
resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -1727,6 +1793,9 @@ packages:
|
||||
lowlight@1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.6:
|
||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -1741,6 +1810,9 @@ 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'}
|
||||
@@ -1863,9 +1935,17 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
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==}
|
||||
|
||||
@@ -1933,6 +2013,9 @@ packages:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
|
||||
@@ -1958,6 +2041,10 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
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==}
|
||||
|
||||
@@ -2160,6 +2247,10 @@ packages:
|
||||
signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -2206,6 +2297,10 @@ packages:
|
||||
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'}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@@ -2213,6 +2308,10 @@ packages:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-bom@4.0.0:
|
||||
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2254,6 +2353,10 @@ packages:
|
||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
test-exclude@7.0.1:
|
||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -2499,6 +2602,10 @@ packages:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
@@ -2541,6 +2648,11 @@ snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.4': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@asamuzakjp/css-color@4.1.2':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
@@ -2638,6 +2750,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/parser@7.29.0':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -2758,6 +2874,11 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@bcoe/v8-coverage@0.2.3': {}
|
||||
|
||||
'@biomejs/biome@2.4.2':
|
||||
@@ -2892,6 +3013,15 @@ snapshots:
|
||||
|
||||
'@exodus/bytes@1.14.1': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@istanbuljs/load-nyc-config@1.1.0':
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
@@ -3083,6 +3213,9 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
dependencies:
|
||||
playwright: 1.58.2
|
||||
@@ -3304,6 +3437,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@25.0.3)(jsdom@28.1.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.1
|
||||
tinyrainbow: 1.2.0
|
||||
vitest: 2.1.9(@types/node@25.0.3)(jsdom@28.1.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
@@ -3352,12 +3503,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: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
@@ -3454,6 +3609,10 @@ snapshots:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
@@ -3669,12 +3828,16 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
|
||||
emittery@0.13.1: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
@@ -3790,6 +3953,11 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
@@ -3830,6 +3998,15 @@ snapshots:
|
||||
|
||||
get-stream@6.0.1: {}
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.3
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -4094,11 +4271,25 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
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
|
||||
|
||||
jest-changed-files@29.7.0:
|
||||
dependencies:
|
||||
execa: 5.1.1
|
||||
@@ -4468,6 +4659,8 @@ snapshots:
|
||||
fault: 1.0.4
|
||||
highlight.js: 10.7.3
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.6: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -4480,6 +4673,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.3.5:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
@@ -4731,8 +4930,14 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@@ -4791,6 +4996,8 @@ snapshots:
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
@@ -4820,6 +5027,11 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.3
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
@@ -5070,6 +5282,8 @@ snapshots:
|
||||
|
||||
signal-exit@3.0.7: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
@@ -5111,6 +5325,12 @@ snapshots:
|
||||
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.1.2
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@@ -5120,6 +5340,10 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
strip-bom@4.0.0: {}
|
||||
|
||||
strip-final-newline@2.0.0: {}
|
||||
@@ -5156,6 +5380,12 @@ snapshots:
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.5.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -5405,6 +5635,12 @@ snapshots:
|
||||
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.1.2
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
write-file-atomic@4.0.2:
|
||||
|
||||
@@ -22,12 +22,19 @@ export interface TestRunSummaryResponse {
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export interface CoverageReportResponse {
|
||||
current_percent: number;
|
||||
threshold_percent: number;
|
||||
baseline_percent?: number | null;
|
||||
}
|
||||
|
||||
export interface AcceptanceResponse {
|
||||
can_accept: boolean;
|
||||
reasons: string[];
|
||||
warning?: string | null;
|
||||
summary: TestRunSummaryResponse;
|
||||
missing_categories: string[];
|
||||
coverage_report?: CoverageReportResponse | null;
|
||||
}
|
||||
|
||||
export interface ReviewStory {
|
||||
@@ -37,6 +44,18 @@ export interface ReviewStory {
|
||||
warning?: string | null;
|
||||
summary: TestRunSummaryResponse;
|
||||
missing_categories: string[];
|
||||
coverage_report?: CoverageReportResponse | null;
|
||||
}
|
||||
|
||||
export interface RecordCoveragePayload {
|
||||
story_id: string;
|
||||
current_percent: number;
|
||||
threshold_percent?: number | null;
|
||||
}
|
||||
|
||||
export interface CollectCoverageRequest {
|
||||
story_id: string;
|
||||
threshold_percent?: number | null;
|
||||
}
|
||||
|
||||
export interface ReviewListResponse {
|
||||
@@ -71,6 +90,20 @@ async function requestJson<T>(
|
||||
}
|
||||
|
||||
export const workflowApi = {
|
||||
collectCoverage(payload: CollectCoverageRequest, baseUrl?: string) {
|
||||
return requestJson<CoverageReportResponse>(
|
||||
"/workflow/coverage/collect",
|
||||
{ method: "POST", body: JSON.stringify(payload) },
|
||||
baseUrl,
|
||||
);
|
||||
},
|
||||
recordCoverage(payload: RecordCoveragePayload, baseUrl?: string) {
|
||||
return requestJson<boolean>(
|
||||
"/workflow/coverage/record",
|
||||
{ method: "POST", body: JSON.stringify(payload) },
|
||||
baseUrl,
|
||||
);
|
||||
},
|
||||
recordTests(payload: RecordTestsPayload, baseUrl?: string) {
|
||||
return requestJson<boolean>(
|
||||
"/workflow/tests/record",
|
||||
|
||||
@@ -33,6 +33,8 @@ vi.mock("../api/workflow", () => {
|
||||
getReviewQueue: vi.fn(),
|
||||
getReviewQueueAll: vi.fn(),
|
||||
ensureAcceptance: vi.fn(),
|
||||
recordCoverage: vi.fn(),
|
||||
collectCoverage: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -390,4 +392,122 @@ describe("Chat review panel", () => {
|
||||
|
||||
expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows coverage below threshold in gate panel (AC3)", async () => {
|
||||
mockedWorkflow.getAcceptance.mockResolvedValueOnce({
|
||||
can_accept: false,
|
||||
reasons: ["Coverage below threshold (55.0% < 80.0%)."],
|
||||
warning: null,
|
||||
summary: { total: 3, passed: 3, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 55.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
});
|
||||
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByText("Blocked")).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Coverage: 55\.0%/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/threshold: 80\.0%/)).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText("Coverage below threshold (55.0% < 80.0%)."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows coverage regression in review panel (AC4)", async () => {
|
||||
const story: ReviewStory = {
|
||||
story_id: "27_protect_tests_and_coverage",
|
||||
can_accept: false,
|
||||
reasons: ["Coverage regression: 90.0% → 82.0% (threshold: 80.0%)."],
|
||||
warning: null,
|
||||
summary: { total: 4, passed: 4, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 82.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: 90.0,
|
||||
},
|
||||
};
|
||||
|
||||
mockedWorkflow.getReviewQueueAll.mockResolvedValueOnce({
|
||||
stories: [story],
|
||||
});
|
||||
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"Coverage regression: 90.0% → 82.0% (threshold: 80.0%).",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Coverage: 82\.0%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows green coverage when above threshold (AC3)", async () => {
|
||||
mockedWorkflow.getAcceptance.mockResolvedValueOnce({
|
||||
can_accept: true,
|
||||
reasons: [],
|
||||
warning: null,
|
||||
summary: { total: 5, passed: 5, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 92.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: 90.0,
|
||||
},
|
||||
});
|
||||
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByText("Ready to accept")).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Coverage: 92\.0%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("collect coverage button triggers collection and refreshes gate", async () => {
|
||||
const mockedCollectCoverage = vi.mocked(workflowApi.collectCoverage);
|
||||
mockedCollectCoverage.mockResolvedValueOnce({
|
||||
current_percent: 85.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
});
|
||||
|
||||
mockedWorkflow.getAcceptance
|
||||
.mockResolvedValueOnce({
|
||||
can_accept: false,
|
||||
reasons: ["No test results recorded for the story."],
|
||||
warning: null,
|
||||
summary: { total: 0, passed: 0, failed: 0 },
|
||||
missing_categories: ["unit", "integration"],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
can_accept: true,
|
||||
reasons: [],
|
||||
warning: null,
|
||||
summary: { total: 5, passed: 5, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 85.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
});
|
||||
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
const collectButton = await screen.findByRole("button", {
|
||||
name: "Collect Coverage",
|
||||
});
|
||||
await userEvent.click(collectButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedCollectCoverage).toHaveBeenCalledWith({
|
||||
story_id: "26_establish_tdd_workflow_and_gates",
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByText(/Coverage: 85\.0%/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,11 @@ interface GateState {
|
||||
failed: number;
|
||||
};
|
||||
missingCategories: string[];
|
||||
coverageReport: {
|
||||
currentPercent: number;
|
||||
thresholdPercent: number;
|
||||
baselinePercent: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
@@ -54,6 +59,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [proceedSuccess, setProceedSuccess] = useState<string | null>(null);
|
||||
const [lastReviewRefresh, setLastReviewRefresh] = useState<Date | null>(null);
|
||||
const [lastGateRefresh, setLastGateRefresh] = useState<Date | null>(null);
|
||||
const [isCollectingCoverage, setIsCollectingCoverage] = useState(false);
|
||||
const [coverageError, setCoverageError] = useState<string | null>(null);
|
||||
|
||||
const storyId = "26_establish_tdd_workflow_and_gates";
|
||||
const gateStatusColor = isGateLoading
|
||||
@@ -185,6 +192,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
warning: response.warning ?? null,
|
||||
summary: response.summary,
|
||||
missingCategories: response.missing_categories,
|
||||
coverageReport: response.coverage_report
|
||||
? {
|
||||
currentPercent: response.coverage_report.current_percent,
|
||||
thresholdPercent: response.coverage_report.threshold_percent,
|
||||
baselinePercent:
|
||||
response.coverage_report.baseline_percent ?? null,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
setLastGateRefresh(new Date());
|
||||
})
|
||||
@@ -254,6 +269,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
warning: response.warning ?? null,
|
||||
summary: response.summary,
|
||||
missingCategories: response.missing_categories,
|
||||
coverageReport: response.coverage_report
|
||||
? {
|
||||
currentPercent: response.coverage_report.current_percent,
|
||||
thresholdPercent: response.coverage_report.threshold_percent,
|
||||
baselinePercent:
|
||||
response.coverage_report.baseline_percent ?? null,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
setLastGateRefresh(new Date());
|
||||
} catch (error) {
|
||||
@@ -268,6 +291,21 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollectCoverage = async () => {
|
||||
setIsCollectingCoverage(true);
|
||||
setCoverageError(null);
|
||||
try {
|
||||
await workflowApi.collectCoverage({ story_id: storyId });
|
||||
await refreshGateState(storyId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to collect coverage.";
|
||||
setCoverageError(message);
|
||||
} finally {
|
||||
setIsCollectingCoverage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshReviewQueue = async () => {
|
||||
setIsReviewLoading(true);
|
||||
setReviewError(null);
|
||||
@@ -549,8 +587,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
gateStatusColor={gateStatusColor}
|
||||
isGateLoading={isGateLoading}
|
||||
gateError={gateError}
|
||||
coverageError={coverageError}
|
||||
lastGateRefresh={lastGateRefresh}
|
||||
onRefresh={() => refreshGateState(storyId)}
|
||||
onCollectCoverage={handleCollectCoverage}
|
||||
isCollectingCoverage={isCollectingCoverage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
interface CoverageReport {
|
||||
currentPercent: number;
|
||||
thresholdPercent: number;
|
||||
baselinePercent: number | null;
|
||||
}
|
||||
|
||||
interface GateState {
|
||||
canAccept: boolean;
|
||||
reasons: string[];
|
||||
@@ -8,6 +14,7 @@ interface GateState {
|
||||
failed: number;
|
||||
};
|
||||
missingCategories: string[];
|
||||
coverageReport: CoverageReport | null;
|
||||
}
|
||||
|
||||
interface GatePanelProps {
|
||||
@@ -16,8 +23,11 @@ interface GatePanelProps {
|
||||
gateStatusColor: string;
|
||||
isGateLoading: boolean;
|
||||
gateError: string | null;
|
||||
coverageError: string | null;
|
||||
lastGateRefresh: Date | null;
|
||||
onRefresh: () => void;
|
||||
onCollectCoverage: () => void;
|
||||
isCollectingCoverage: boolean;
|
||||
}
|
||||
|
||||
const formatTimestamp = (value: Date | null): string => {
|
||||
@@ -35,8 +45,11 @@ export function GatePanel({
|
||||
gateStatusColor,
|
||||
isGateLoading,
|
||||
gateError,
|
||||
coverageError,
|
||||
lastGateRefresh,
|
||||
onRefresh,
|
||||
onCollectCoverage,
|
||||
isCollectingCoverage,
|
||||
}: GatePanelProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -83,6 +96,27 @@ export function GatePanel({
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCollectCoverage}
|
||||
disabled={isCollectingCoverage || isGateLoading}
|
||||
style={{
|
||||
padding: "4px 10px",
|
||||
borderRadius: "999px",
|
||||
border: "1px solid #333",
|
||||
background:
|
||||
isCollectingCoverage || isGateLoading ? "#2a2a2a" : "#2f2f2f",
|
||||
color: isCollectingCoverage || isGateLoading ? "#777" : "#aaa",
|
||||
cursor:
|
||||
isCollectingCoverage || isGateLoading
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
fontSize: "0.75em",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{isCollectingCoverage ? "Collecting..." : "Collect Coverage"}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -147,6 +181,27 @@ export function GatePanel({
|
||||
Summary: {gateState.summary.passed}/{gateState.summary.total}{" "}
|
||||
passing, {gateState.summary.failed} failing
|
||||
</div>
|
||||
{gateState.coverageReport && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color:
|
||||
gateState.coverageReport.currentPercent <
|
||||
gateState.coverageReport.thresholdPercent
|
||||
? "#ff7b72"
|
||||
: "#7ee787",
|
||||
}}
|
||||
>
|
||||
Coverage: {gateState.coverageReport.currentPercent.toFixed(1)}%
|
||||
(threshold: {gateState.coverageReport.thresholdPercent.toFixed(1)}
|
||||
%)
|
||||
</div>
|
||||
)}
|
||||
{coverageError && (
|
||||
<div style={{ fontSize: "0.85em", color: "#ff7b72" }}>
|
||||
Coverage error: {coverageError}
|
||||
</div>
|
||||
)}
|
||||
{gateState.missingCategories.length > 0 && (
|
||||
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||||
Missing: {gateState.missingCategories.join(", ")}
|
||||
|
||||
@@ -278,6 +278,22 @@ export function ReviewPanel({
|
||||
Summary: {story.summary.passed}/{story.summary.total} passing,{" "}
|
||||
{` ${story.summary.failed}`} failing
|
||||
</div>
|
||||
{story.coverage_report && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color:
|
||||
story.coverage_report.current_percent <
|
||||
story.coverage_report.threshold_percent
|
||||
? "#ff7b72"
|
||||
: "#7ee787",
|
||||
}}
|
||||
>
|
||||
Coverage: {story.coverage_report.current_percent.toFixed(1)}%
|
||||
(threshold:{" "}
|
||||
{story.coverage_report.threshold_percent.toFixed(1)}%)
|
||||
</div>
|
||||
)}
|
||||
{story.missing_categories.length > 0 && (
|
||||
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||||
Missing: {story.missing_categories.join(", ")}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("App boot smoke test", () => {
|
||||
test("renders the project selection screen", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
test("renders the project selection screen", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByText("StorkIt")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("/path/to/project")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open Project" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
await expect(page.getByText("StorkIt")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("/path/to/project")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open Project" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
247
frontend/tests/e2e/test-protection.spec.ts
Normal file
247
frontend/tests/e2e/test-protection.spec.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type {
|
||||
AcceptanceResponse,
|
||||
ReviewListResponse,
|
||||
} from "../../src/api/workflow";
|
||||
|
||||
/**
|
||||
* Helper: mock all API routes needed to render the Chat view.
|
||||
*/
|
||||
function mockChatApis(
|
||||
page: import("@playwright/test").Page,
|
||||
overrides: {
|
||||
acceptance?: AcceptanceResponse;
|
||||
reviewQueue?: ReviewListResponse;
|
||||
} = {},
|
||||
) {
|
||||
const acceptance: AcceptanceResponse = overrides.acceptance ?? {
|
||||
can_accept: false,
|
||||
reasons: ["No test results recorded for the story."],
|
||||
warning: null,
|
||||
summary: { total: 0, passed: 0, failed: 0 },
|
||||
missing_categories: ["unit", "integration"],
|
||||
};
|
||||
|
||||
const reviewQueue: ReviewListResponse = overrides.reviewQueue ?? {
|
||||
stories: [],
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
page.route("**/api/projects", (route) =>
|
||||
route.fulfill({ json: ["/tmp/test-project"] }),
|
||||
),
|
||||
page.route("**/api/io/fs/home", (route) => route.fulfill({ json: "/tmp" })),
|
||||
page.route("**/api/project", (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({ json: "/tmp/test-project" });
|
||||
}
|
||||
if (route.request().method() === "DELETE") {
|
||||
return route.fulfill({ json: true });
|
||||
}
|
||||
return route.fulfill({ json: null });
|
||||
}),
|
||||
page.route("**/api/ollama/models**", (route) =>
|
||||
route.fulfill({ json: ["llama3.1"] }),
|
||||
),
|
||||
page.route("**/api/anthropic/key/exists", (route) =>
|
||||
route.fulfill({ json: false }),
|
||||
),
|
||||
page.route("**/api/anthropic/models", (route) =>
|
||||
route.fulfill({ json: [] }),
|
||||
),
|
||||
page.route("**/api/model", (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
return route.fulfill({ json: true });
|
||||
}
|
||||
return route.fulfill({ json: null });
|
||||
}),
|
||||
page.route("**/api/workflow/acceptance", (route) => {
|
||||
if (route.request().url().includes("/ensure")) return route.fallback();
|
||||
return route.fulfill({ json: acceptance });
|
||||
}),
|
||||
page.route("**/api/workflow/review/all", (route) =>
|
||||
route.fulfill({ json: reviewQueue }),
|
||||
),
|
||||
page.route("**/api/workflow/acceptance/ensure", (route) =>
|
||||
route.fulfill({ json: true }),
|
||||
),
|
||||
page.route("**/api/io/fs/list/absolute**", (route) =>
|
||||
route.fulfill({ json: [] }),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async function openProject(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByPlaceholder("/path/to/project").fill("/tmp/test-project");
|
||||
await page.getByRole("button", { name: "Open Project" }).click();
|
||||
await expect(page.getByText("Workflow Gates", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Coverage threshold (AC1)", () => {
|
||||
test("shows blocked status when coverage is below threshold", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockChatApis(page, {
|
||||
acceptance: {
|
||||
can_accept: false,
|
||||
reasons: ["Coverage below threshold (55.0% < 80.0%)."],
|
||||
warning: null,
|
||||
summary: { total: 3, passed: 3, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 55.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await openProject(page);
|
||||
|
||||
await expect(page.getByText("Blocked").first()).toBeVisible();
|
||||
await expect(page.getByText(/Coverage: 55\.0%/)).toBeVisible();
|
||||
await expect(page.getByText(/threshold: 80\.0%/)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Coverage below threshold (55.0% < 80.0%)."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows green coverage when above threshold", async ({ page }) => {
|
||||
await mockChatApis(page, {
|
||||
acceptance: {
|
||||
can_accept: true,
|
||||
reasons: [],
|
||||
warning: null,
|
||||
summary: { total: 5, passed: 5, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 92.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: 90.0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await openProject(page);
|
||||
|
||||
await expect(page.getByText("Ready to accept")).toBeVisible();
|
||||
await expect(page.getByText(/Coverage: 92\.0%/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Coverage regression reporting (AC2)", () => {
|
||||
test("review panel shows coverage regression reason", async ({ page }) => {
|
||||
await mockChatApis(page, {
|
||||
reviewQueue: {
|
||||
stories: [
|
||||
{
|
||||
story_id: "27_protect_tests_and_coverage",
|
||||
can_accept: false,
|
||||
reasons: ["Coverage regression: 90.0% → 82.0% (threshold: 80.0%)."],
|
||||
warning: null,
|
||||
summary: { total: 4, passed: 4, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 82.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: 90.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await openProject(page);
|
||||
|
||||
await expect(
|
||||
page.getByText(/Coverage regression.*90\.0%.*82\.0%/),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/Coverage: 82\.0%/)).toBeVisible();
|
||||
|
||||
const blockedButton = page.getByRole("button", { name: "Blocked" });
|
||||
await expect(blockedButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("blocked acceptance with coverage and test failures combined", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockChatApis(page, {
|
||||
acceptance: {
|
||||
can_accept: false,
|
||||
reasons: [
|
||||
"1 test(s) are failing; acceptance is blocked.",
|
||||
"Coverage below threshold (65.0% < 80.0%).",
|
||||
],
|
||||
warning: null,
|
||||
summary: { total: 4, passed: 3, failed: 1 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 65.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await openProject(page);
|
||||
|
||||
await expect(page.getByText("Blocked").first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Coverage below threshold (65.0% < 80.0%)."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/Coverage: 65\.0%/)).toBeVisible();
|
||||
await expect(page.getByText(/1 test\(s\) are failing/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Coverage collection (E2E)", () => {
|
||||
test("collect coverage button triggers collection and displays result", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockChatApis(page);
|
||||
|
||||
// Mock the collect coverage endpoint
|
||||
await page.route("**/api/workflow/coverage/collect", (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
current_percent: 85.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await openProject(page);
|
||||
|
||||
// Click the Collect Coverage button
|
||||
const collectButton = page.getByRole("button", {
|
||||
name: "Collect Coverage",
|
||||
});
|
||||
await expect(collectButton).toBeVisible();
|
||||
|
||||
// Override acceptance to return coverage data after collection
|
||||
await page.unroute("**/api/workflow/acceptance");
|
||||
await page.route("**/api/workflow/acceptance", (route) => {
|
||||
if (route.request().url().includes("/ensure")) return route.fallback();
|
||||
return route.fulfill({
|
||||
json: {
|
||||
can_accept: true,
|
||||
reasons: [],
|
||||
warning: null,
|
||||
summary: { total: 5, passed: 5, failed: 0 },
|
||||
missing_categories: [],
|
||||
coverage_report: {
|
||||
current_percent: 85.0,
|
||||
threshold_percent: 80.0,
|
||||
baseline_percent: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await collectButton.click();
|
||||
|
||||
await expect(page.getByText(/Coverage: 85\.0%/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,10 @@ export default defineConfig(() => ({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:3001",
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:3001",
|
||||
timeout: 120000,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -9,5 +9,10 @@ export default defineConfig({
|
||||
setupFiles: ["./src/setupTests.ts"],
|
||||
css: true,
|
||||
exclude: ["tests/e2e/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["json-summary"],
|
||||
reportsDirectory: "./coverage",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user