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:
2
.ignore
Normal file
2
.ignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Prevents cargo watch from restarting the server when frontend files change
|
||||||
|
frontend/
|
||||||
@@ -67,6 +67,7 @@ When the user asks for a feature, follow this 4-step loop strictly:
|
|||||||
### Step 3: The Implementation (Code)
|
### Step 3: The Implementation (Code)
|
||||||
* **Action:** Write the code to satisfy the approved tests and Acceptance Criteria.
|
* **Action:** Write the code to satisfy the approved tests and Acceptance Criteria.
|
||||||
* **Constraint:** adhere strictly to `specs/tech/STACK.md` (e.g., if it says "No `unwrap()`", you must not use `unwrap()`).
|
* **Constraint:** adhere strictly to `specs/tech/STACK.md` (e.g., if it says "No `unwrap()`", you must not use `unwrap()`).
|
||||||
|
* **Full-Stack Completion:** Every story must be completed across all components of the stack. If a feature touches the backend, frontend, and API layer, all three must be fully implemented and working end-to-end before the story can be accepted. Partial implementations (e.g., backend logic with no frontend wiring, or UI scaffolding with no real data) do not satisfy acceptance criteria.
|
||||||
|
|
||||||
### Step 4: Verification (Close)
|
### Step 4: Verification (Close)
|
||||||
* **Action:** For each Acceptance Criterion in the story, write a failing test (red), mark the criterion as tested, make the test pass (green), and refactor if needed. Keep only one failing test at a time.
|
* **Action:** For each Acceptance Criterion in the story, write a failing test (red), mark the criterion as tested, make the test pass (green), and refactor if needed. Keep only one failing test at a time.
|
||||||
|
|||||||
0
.story_kit/stories/archived/.gitkeep
Normal file
0
.story_kit/stories/archived/.gitkeep
Normal file
37
.story_kit/stories/archived/27_protect_tests_and_coverage.md
Normal file
37
.story_kit/stories/archived/27_protect_tests_and_coverage.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Coverage Tracking
|
||||||
|
test_plan: approved
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 27: Coverage Tracking
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
As a user, I want the workflow to track test coverage and block acceptance when coverage regresses, so quality guardrails cannot be weakened silently.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- [x] The workflow fails if coverage drops below the defined threshold.
|
||||||
|
- [x] Coverage regression is reported clearly before acceptance.
|
||||||
|
|
||||||
|
## Test Plan (Approved)
|
||||||
|
|
||||||
|
### Backend (Rust) — Unit
|
||||||
|
|
||||||
|
**AC1: Workflow fails if coverage drops below threshold**
|
||||||
|
- `workflow::check_coverage_threshold()` fails when coverage % < configured threshold
|
||||||
|
- Passes when coverage >= threshold
|
||||||
|
|
||||||
|
**AC2: Coverage regression reported clearly before acceptance**
|
||||||
|
- `workflow::evaluate_acceptance_with_coverage()` includes coverage delta when coverage dropped
|
||||||
|
- `AcceptanceDecision` extended with `coverage_report` field
|
||||||
|
- Acceptance blocked with clear message when baseline exists and current < baseline
|
||||||
|
|
||||||
|
### Frontend (Vitest + Playwright)
|
||||||
|
|
||||||
|
**AC1:** Gate panel shows "Coverage below threshold (X% < Y%)" and coverage display
|
||||||
|
**AC2:** Review/gate panels display coverage regression text and summary
|
||||||
|
**E2E:** Blocked acceptance displays coverage reasons; green coverage when above threshold
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- Introducing new test frameworks beyond those listed in `specs/tech/STACK.md`.
|
||||||
|
- Large refactors solely to improve coverage.
|
||||||
|
- Path-based test file detection (not reliable for languages with inline tests like Rust).
|
||||||
0
.story_kit/stories/upcoming/.gitkeep
Normal file
0
.story_kit/stories/upcoming/.gitkeep
Normal file
@@ -1,14 +0,0 @@
|
|||||||
# Story 27: Protect Tests and Coverage
|
|
||||||
|
|
||||||
## User Story
|
|
||||||
As a user, I want explicit safeguards around test deletion and coverage regression, so quality guardrails cannot be weakened silently.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- Any deletion of test files requires explicit user approval.
|
|
||||||
- Any change that disables or neuters a test (e.g., commenting out assertions) requires explicit user approval.
|
|
||||||
- The workflow fails if coverage drops below the defined threshold.
|
|
||||||
- Coverage regression is reported clearly before acceptance.
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
- Introducing new test frameworks beyond those listed in `specs/tech/STACK.md`.
|
|
||||||
- Large refactors solely to improve coverage.
|
|
||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -10,4 +10,5 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"server": "cargo run --manifest-path server/Cargo.toml",
|
"server": "cargo run --manifest-path server/Cargo.toml",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:unit": "vitest run",
|
"test:unit": "vitest run",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test",
|
||||||
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
@@ -25,10 +26,11 @@
|
|||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
"@types/node": "^25.0.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/node": "^25.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"@vitest/coverage-v8": "^2.1.9",
|
||||||
"jest": "^29.0.0",
|
"jest": "^29.0.0",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"ts-jest": "^29.0.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':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.7.0(vite@5.4.21(@types/node@25.0.3))
|
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:
|
jest:
|
||||||
specifier: ^29.0.0
|
specifier: ^29.0.0
|
||||||
version: 29.7.0(@types/node@25.0.3)
|
version: 29.7.0(@types/node@25.0.3)
|
||||||
@@ -78,6 +81,10 @@ packages:
|
|||||||
'@adobe/css-tools@4.4.4':
|
'@adobe/css-tools@4.4.4':
|
||||||
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
|
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':
|
'@asamuzakjp/css-color@4.1.2':
|
||||||
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
|
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
|
||||||
|
|
||||||
@@ -146,6 +153,11 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
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':
|
'@babel/plugin-syntax-async-generators@7.8.4':
|
||||||
resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
|
resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -265,6 +277,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@bcoe/v8-coverage@0.2.3':
|
||||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||||
|
|
||||||
@@ -503,6 +519,10 @@ packages:
|
|||||||
'@noble/hashes':
|
'@noble/hashes':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@istanbuljs/load-nyc-config@1.1.0':
|
'@istanbuljs/load-nyc-config@1.1.0':
|
||||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -593,6 +613,10 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
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':
|
'@playwright/test@1.58.2':
|
||||||
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -832,6 +856,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
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':
|
'@vitest/expect@2.1.9':
|
||||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||||
|
|
||||||
@@ -873,6 +906,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2:
|
||||||
|
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -881,6 +918,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
ansi-styles@6.2.3:
|
||||||
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -948,6 +989,9 @@ packages:
|
|||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||||
|
|
||||||
|
brace-expansion@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1158,6 +1202,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0:
|
||||||
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267:
|
electron-to-chromium@1.5.267:
|
||||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||||
|
|
||||||
@@ -1168,6 +1215,9 @@ packages:
|
|||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2:
|
||||||
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
entities@6.0.1:
|
entities@6.0.1:
|
||||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@@ -1257,6 +1307,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
format@0.2.2:
|
format@0.2.2:
|
||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
engines: {node: '>=0.4.x'}
|
||||||
@@ -1304,6 +1358,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||||
engines: {node: '>=10'}
|
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:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
@@ -1538,10 +1597,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
|
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
|
||||||
engines: {node: '>=10'}
|
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:
|
istanbul-reports@3.2.0:
|
||||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
jackspeak@3.4.3:
|
||||||
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jest-changed-files@29.7.0:
|
jest-changed-files@29.7.0:
|
||||||
resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==}
|
resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@@ -1727,6 +1793,9 @@ packages:
|
|||||||
lowlight@1.20.0:
|
lowlight@1.20.0:
|
||||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
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:
|
lru-cache@11.2.6:
|
||||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -1741,6 +1810,9 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
magicast@0.3.5:
|
||||||
|
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||||
|
|
||||||
make-dir@4.0.0:
|
make-dir@4.0.0:
|
||||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1863,9 +1935,17 @@ packages:
|
|||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
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:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
minipass@7.1.3:
|
||||||
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1933,6 +2013,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
parse-entities@4.0.2:
|
parse-entities@4.0.2:
|
||||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
@@ -1958,6 +2041,10 @@ packages:
|
|||||||
path-parse@1.0.7:
|
path-parse@1.0.7:
|
||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
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:
|
pathe@1.1.2:
|
||||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
@@ -2160,6 +2247,10 @@ packages:
|
|||||||
signal-exit@3.0.7:
|
signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
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:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
@@ -2206,6 +2297,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -2213,6 +2308,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@7.1.2:
|
||||||
|
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
strip-bom@4.0.0:
|
strip-bom@4.0.0:
|
||||||
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
|
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2254,6 +2353,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
test-exclude@7.0.1:
|
||||||
|
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
@@ -2499,6 +2602,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
@@ -2541,6 +2648,11 @@ snapshots:
|
|||||||
|
|
||||||
'@adobe/css-tools@4.4.4': {}
|
'@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':
|
'@asamuzakjp/css-color@4.1.2':
|
||||||
dependencies:
|
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)
|
'@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:
|
dependencies:
|
||||||
'@babel/types': 7.28.5
|
'@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)':
|
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
@@ -2758,6 +2874,11 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@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': {}
|
'@bcoe/v8-coverage@0.2.3': {}
|
||||||
|
|
||||||
'@biomejs/biome@2.4.2':
|
'@biomejs/biome@2.4.2':
|
||||||
@@ -2892,6 +3013,15 @@ snapshots:
|
|||||||
|
|
||||||
'@exodus/bytes@1.14.1': {}
|
'@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':
|
'@istanbuljs/load-nyc-config@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
@@ -3083,6 +3213,9 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@playwright/test@1.58.2':
|
'@playwright/test@1.58.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
playwright: 1.58.2
|
playwright: 1.58.2
|
||||||
@@ -3304,6 +3437,24 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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':
|
'@vitest/expect@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
@@ -3352,12 +3503,16 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2: {}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
ansi-styles@5.2.0: {}
|
ansi-styles@5.2.0: {}
|
||||||
|
|
||||||
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
@@ -3454,6 +3609,10 @@ snapshots:
|
|||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
concat-map: 0.0.1
|
concat-map: 0.0.1
|
||||||
|
|
||||||
|
brace-expansion@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
@@ -3669,12 +3828,16 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267: {}
|
electron-to-chromium@1.5.267: {}
|
||||||
|
|
||||||
emittery@0.13.1: {}
|
emittery@0.13.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
entities@6.0.1: {}
|
entities@6.0.1: {}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
@@ -3790,6 +3953,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
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: {}
|
format@0.2.2: {}
|
||||||
|
|
||||||
fs.realpath@1.0.0: {}
|
fs.realpath@1.0.0: {}
|
||||||
@@ -3830,6 +3998,15 @@ snapshots:
|
|||||||
|
|
||||||
get-stream@6.0.1: {}
|
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:
|
glob@7.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
@@ -4094,11 +4271,25 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
istanbul-reports@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
html-escaper: 2.0.2
|
html-escaper: 2.0.2
|
||||||
istanbul-lib-report: 3.0.1
|
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:
|
jest-changed-files@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
execa: 5.1.1
|
execa: 5.1.1
|
||||||
@@ -4468,6 +4659,8 @@ snapshots:
|
|||||||
fault: 1.0.4
|
fault: 1.0.4
|
||||||
highlight.js: 10.7.3
|
highlight.js: 10.7.3
|
||||||
|
|
||||||
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.2.6: {}
|
lru-cache@11.2.6: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@@ -4480,6 +4673,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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:
|
make-dir@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
@@ -4731,8 +4930,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|
||||||
|
minimatch@9.0.5:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@@ -4791,6 +4996,8 @@ snapshots:
|
|||||||
|
|
||||||
p-try@2.2.0: {}
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
parse-entities@4.0.2:
|
parse-entities@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.11
|
'@types/unist': 2.0.11
|
||||||
@@ -4820,6 +5027,11 @@ snapshots:
|
|||||||
|
|
||||||
path-parse@1.0.7: {}
|
path-parse@1.0.7: {}
|
||||||
|
|
||||||
|
path-scurry@1.11.1:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 10.4.3
|
||||||
|
minipass: 7.1.3
|
||||||
|
|
||||||
pathe@1.1.2: {}
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
pathval@2.0.1: {}
|
pathval@2.0.1: {}
|
||||||
@@ -5070,6 +5282,8 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@3.0.7: {}
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
@@ -5111,6 +5325,12 @@ snapshots:
|
|||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
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:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -5120,6 +5340,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
strip-ansi@7.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
strip-bom@4.0.0: {}
|
strip-bom@4.0.0: {}
|
||||||
|
|
||||||
strip-final-newline@2.0.0: {}
|
strip-final-newline@2.0.0: {}
|
||||||
@@ -5156,6 +5380,12 @@ snapshots:
|
|||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
minimatch: 3.1.2
|
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: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
tinyexec@0.3.2: {}
|
tinyexec@0.3.2: {}
|
||||||
@@ -5405,6 +5635,12 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
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: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
write-file-atomic@4.0.2:
|
write-file-atomic@4.0.2:
|
||||||
|
|||||||
@@ -22,12 +22,19 @@ export interface TestRunSummaryResponse {
|
|||||||
failed: number;
|
failed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CoverageReportResponse {
|
||||||
|
current_percent: number;
|
||||||
|
threshold_percent: number;
|
||||||
|
baseline_percent?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AcceptanceResponse {
|
export interface AcceptanceResponse {
|
||||||
can_accept: boolean;
|
can_accept: boolean;
|
||||||
reasons: string[];
|
reasons: string[];
|
||||||
warning?: string | null;
|
warning?: string | null;
|
||||||
summary: TestRunSummaryResponse;
|
summary: TestRunSummaryResponse;
|
||||||
missing_categories: string[];
|
missing_categories: string[];
|
||||||
|
coverage_report?: CoverageReportResponse | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReviewStory {
|
export interface ReviewStory {
|
||||||
@@ -37,6 +44,18 @@ export interface ReviewStory {
|
|||||||
warning?: string | null;
|
warning?: string | null;
|
||||||
summary: TestRunSummaryResponse;
|
summary: TestRunSummaryResponse;
|
||||||
missing_categories: string[];
|
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 {
|
export interface ReviewListResponse {
|
||||||
@@ -71,6 +90,20 @@ async function requestJson<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const workflowApi = {
|
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) {
|
recordTests(payload: RecordTestsPayload, baseUrl?: string) {
|
||||||
return requestJson<boolean>(
|
return requestJson<boolean>(
|
||||||
"/workflow/tests/record",
|
"/workflow/tests/record",
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ vi.mock("../api/workflow", () => {
|
|||||||
getReviewQueue: vi.fn(),
|
getReviewQueue: vi.fn(),
|
||||||
getReviewQueueAll: vi.fn(),
|
getReviewQueueAll: vi.fn(),
|
||||||
ensureAcceptance: 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();
|
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;
|
failed: number;
|
||||||
};
|
};
|
||||||
missingCategories: string[];
|
missingCategories: string[];
|
||||||
|
coverageReport: {
|
||||||
|
currentPercent: number;
|
||||||
|
thresholdPercent: number;
|
||||||
|
baselinePercent: number | null;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||||
@@ -54,6 +59,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const [proceedSuccess, setProceedSuccess] = useState<string | null>(null);
|
const [proceedSuccess, setProceedSuccess] = useState<string | null>(null);
|
||||||
const [lastReviewRefresh, setLastReviewRefresh] = useState<Date | null>(null);
|
const [lastReviewRefresh, setLastReviewRefresh] = useState<Date | null>(null);
|
||||||
const [lastGateRefresh, setLastGateRefresh] = 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 storyId = "26_establish_tdd_workflow_and_gates";
|
||||||
const gateStatusColor = isGateLoading
|
const gateStatusColor = isGateLoading
|
||||||
@@ -185,6 +192,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
warning: response.warning ?? null,
|
warning: response.warning ?? null,
|
||||||
summary: response.summary,
|
summary: response.summary,
|
||||||
missingCategories: response.missing_categories,
|
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());
|
setLastGateRefresh(new Date());
|
||||||
})
|
})
|
||||||
@@ -254,6 +269,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
warning: response.warning ?? null,
|
warning: response.warning ?? null,
|
||||||
summary: response.summary,
|
summary: response.summary,
|
||||||
missingCategories: response.missing_categories,
|
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());
|
setLastGateRefresh(new Date());
|
||||||
} catch (error) {
|
} 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 () => {
|
const refreshReviewQueue = async () => {
|
||||||
setIsReviewLoading(true);
|
setIsReviewLoading(true);
|
||||||
setReviewError(null);
|
setReviewError(null);
|
||||||
@@ -549,8 +587,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
gateStatusColor={gateStatusColor}
|
gateStatusColor={gateStatusColor}
|
||||||
isGateLoading={isGateLoading}
|
isGateLoading={isGateLoading}
|
||||||
gateError={gateError}
|
gateError={gateError}
|
||||||
|
coverageError={coverageError}
|
||||||
lastGateRefresh={lastGateRefresh}
|
lastGateRefresh={lastGateRefresh}
|
||||||
onRefresh={() => refreshGateState(storyId)}
|
onRefresh={() => refreshGateState(storyId)}
|
||||||
|
onCollectCoverage={handleCollectCoverage}
|
||||||
|
isCollectingCoverage={isCollectingCoverage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
interface CoverageReport {
|
||||||
|
currentPercent: number;
|
||||||
|
thresholdPercent: number;
|
||||||
|
baselinePercent: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface GateState {
|
interface GateState {
|
||||||
canAccept: boolean;
|
canAccept: boolean;
|
||||||
reasons: string[];
|
reasons: string[];
|
||||||
@@ -8,6 +14,7 @@ interface GateState {
|
|||||||
failed: number;
|
failed: number;
|
||||||
};
|
};
|
||||||
missingCategories: string[];
|
missingCategories: string[];
|
||||||
|
coverageReport: CoverageReport | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GatePanelProps {
|
interface GatePanelProps {
|
||||||
@@ -16,8 +23,11 @@ interface GatePanelProps {
|
|||||||
gateStatusColor: string;
|
gateStatusColor: string;
|
||||||
isGateLoading: boolean;
|
isGateLoading: boolean;
|
||||||
gateError: string | null;
|
gateError: string | null;
|
||||||
|
coverageError: string | null;
|
||||||
lastGateRefresh: Date | null;
|
lastGateRefresh: Date | null;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
|
onCollectCoverage: () => void;
|
||||||
|
isCollectingCoverage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTimestamp = (value: Date | null): string => {
|
const formatTimestamp = (value: Date | null): string => {
|
||||||
@@ -35,8 +45,11 @@ export function GatePanel({
|
|||||||
gateStatusColor,
|
gateStatusColor,
|
||||||
isGateLoading,
|
isGateLoading,
|
||||||
gateError,
|
gateError,
|
||||||
|
coverageError,
|
||||||
lastGateRefresh,
|
lastGateRefresh,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onCollectCoverage,
|
||||||
|
isCollectingCoverage,
|
||||||
}: GatePanelProps) {
|
}: GatePanelProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -83,6 +96,27 @@ export function GatePanel({
|
|||||||
>
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</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>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -147,6 +181,27 @@ export function GatePanel({
|
|||||||
Summary: {gateState.summary.passed}/{gateState.summary.total}{" "}
|
Summary: {gateState.summary.passed}/{gateState.summary.total}{" "}
|
||||||
passing, {gateState.summary.failed} failing
|
passing, {gateState.summary.failed} failing
|
||||||
</div>
|
</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 && (
|
{gateState.missingCategories.length > 0 && (
|
||||||
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||||||
Missing: {gateState.missingCategories.join(", ")}
|
Missing: {gateState.missingCategories.join(", ")}
|
||||||
|
|||||||
@@ -278,6 +278,22 @@ export function ReviewPanel({
|
|||||||
Summary: {story.summary.passed}/{story.summary.total} passing,{" "}
|
Summary: {story.summary.passed}/{story.summary.total} passing,{" "}
|
||||||
{` ${story.summary.failed}`} failing
|
{` ${story.summary.failed}`} failing
|
||||||
</div>
|
</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 && (
|
{story.missing_categories.length > 0 && (
|
||||||
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||||||
Missing: {story.missing_categories.join(", ")}
|
Missing: {story.missing_categories.join(", ")}
|
||||||
|
|||||||
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()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://127.0.0.1:3001",
|
"/api": {
|
||||||
|
target: "http://127.0.0.1:3001",
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -9,5 +9,10 @@ export default defineConfig({
|
|||||||
setupFiles: ["./src/setupTests.ts"],
|
setupFiles: ["./src/setupTests.ts"],
|
||||||
css: true,
|
css: true,
|
||||||
exclude: ["tests/e2e/**", "node_modules/**"],
|
exclude: ["tests/e2e/**", "node_modules/**"],
|
||||||
|
coverage: {
|
||||||
|
provider: "v8",
|
||||||
|
reporter: ["json-summary"],
|
||||||
|
reportsDirectory: "./coverage",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||||
use crate::io::story_metadata::{StoryMetadata, parse_front_matter};
|
use crate::io::story_metadata::{StoryMetadata, parse_front_matter};
|
||||||
use crate::workflow::{
|
use crate::workflow::{
|
||||||
StoryTestResults, TestCaseResult, TestStatus, evaluate_acceptance, summarize_results,
|
CoverageReport, StoryTestResults, TestCaseResult, TestStatus,
|
||||||
|
evaluate_acceptance_with_coverage, parse_coverage_json, summarize_results,
|
||||||
};
|
};
|
||||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -40,6 +41,13 @@ struct TestRunSummaryResponse {
|
|||||||
pub failed: usize,
|
pub failed: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Object)]
|
||||||
|
struct CoverageReportResponse {
|
||||||
|
pub current_percent: f64,
|
||||||
|
pub threshold_percent: f64,
|
||||||
|
pub baseline_percent: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Object)]
|
#[derive(Object)]
|
||||||
struct AcceptanceResponse {
|
struct AcceptanceResponse {
|
||||||
pub can_accept: bool,
|
pub can_accept: bool,
|
||||||
@@ -47,6 +55,7 @@ struct AcceptanceResponse {
|
|||||||
pub warning: Option<String>,
|
pub warning: Option<String>,
|
||||||
pub summary: TestRunSummaryResponse,
|
pub summary: TestRunSummaryResponse,
|
||||||
pub missing_categories: Vec<String>,
|
pub missing_categories: Vec<String>,
|
||||||
|
pub coverage_report: Option<CoverageReportResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Object)]
|
#[derive(Object)]
|
||||||
@@ -57,6 +66,20 @@ struct ReviewStory {
|
|||||||
pub warning: Option<String>,
|
pub warning: Option<String>,
|
||||||
pub summary: TestRunSummaryResponse,
|
pub summary: TestRunSummaryResponse,
|
||||||
pub missing_categories: Vec<String>,
|
pub missing_categories: Vec<String>,
|
||||||
|
pub coverage_report: Option<CoverageReportResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Object)]
|
||||||
|
struct RecordCoveragePayload {
|
||||||
|
pub story_id: String,
|
||||||
|
pub current_percent: f64,
|
||||||
|
pub threshold_percent: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Object)]
|
||||||
|
struct CollectCoverageRequest {
|
||||||
|
pub story_id: String,
|
||||||
|
pub threshold_percent: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Object)]
|
#[derive(Object)]
|
||||||
@@ -96,8 +119,12 @@ fn load_current_story_metadata(ctx: &AppContext) -> Result<Vec<(String, StoryMet
|
|||||||
Ok(stories)
|
Ok(stories)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_review_story(story_id: &str, results: &StoryTestResults) -> ReviewStory {
|
fn to_review_story(
|
||||||
let decision = evaluate_acceptance(results);
|
story_id: &str,
|
||||||
|
results: &StoryTestResults,
|
||||||
|
coverage: Option<&CoverageReport>,
|
||||||
|
) -> ReviewStory {
|
||||||
|
let decision = evaluate_acceptance_with_coverage(results, coverage);
|
||||||
let summary = summarize_results(results);
|
let summary = summarize_results(results);
|
||||||
|
|
||||||
let mut missing_categories = Vec::new();
|
let mut missing_categories = Vec::new();
|
||||||
@@ -114,6 +141,12 @@ fn to_review_story(story_id: &str, results: &StoryTestResults) -> ReviewStory {
|
|||||||
|
|
||||||
let can_accept = decision.can_accept && missing_categories.is_empty();
|
let can_accept = decision.can_accept && missing_categories.is_empty();
|
||||||
|
|
||||||
|
let coverage_report = coverage.map(|c| CoverageReportResponse {
|
||||||
|
current_percent: c.current_percent,
|
||||||
|
threshold_percent: c.threshold_percent,
|
||||||
|
baseline_percent: c.baseline_percent,
|
||||||
|
});
|
||||||
|
|
||||||
ReviewStory {
|
ReviewStory {
|
||||||
story_id: story_id.to_string(),
|
story_id: story_id.to_string(),
|
||||||
can_accept,
|
can_accept,
|
||||||
@@ -125,6 +158,7 @@ fn to_review_story(story_id: &str, results: &StoryTestResults) -> ReviewStory {
|
|||||||
failed: summary.failed,
|
failed: summary.failed,
|
||||||
},
|
},
|
||||||
missing_categories,
|
missing_categories,
|
||||||
|
coverage_report,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,20 +204,23 @@ impl WorkflowApi {
|
|||||||
&self,
|
&self,
|
||||||
payload: Json<AcceptanceRequest>,
|
payload: Json<AcceptanceRequest>,
|
||||||
) -> OpenApiResult<Json<AcceptanceResponse>> {
|
) -> OpenApiResult<Json<AcceptanceResponse>> {
|
||||||
let results = {
|
let (results, coverage) = {
|
||||||
let workflow = self
|
let workflow = self
|
||||||
.ctx
|
.ctx
|
||||||
.workflow
|
.workflow
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| bad_request(e.to_string()))?;
|
.map_err(|e| bad_request(e.to_string()))?;
|
||||||
workflow
|
let results = workflow
|
||||||
.results
|
.results
|
||||||
.get(&payload.0.story_id)
|
.get(&payload.0.story_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
|
let coverage = workflow.coverage.get(&payload.0.story_id).cloned();
|
||||||
|
(results, coverage)
|
||||||
};
|
};
|
||||||
|
|
||||||
let decision = evaluate_acceptance(&results);
|
let decision =
|
||||||
|
evaluate_acceptance_with_coverage(&results, coverage.as_ref());
|
||||||
let summary = summarize_results(&results);
|
let summary = summarize_results(&results);
|
||||||
|
|
||||||
let mut missing_categories = Vec::new();
|
let mut missing_categories = Vec::new();
|
||||||
@@ -200,6 +237,12 @@ impl WorkflowApi {
|
|||||||
|
|
||||||
let can_accept = decision.can_accept && missing_categories.is_empty();
|
let can_accept = decision.can_accept && missing_categories.is_empty();
|
||||||
|
|
||||||
|
let coverage_report = coverage.map(|c| CoverageReportResponse {
|
||||||
|
current_percent: c.current_percent,
|
||||||
|
threshold_percent: c.threshold_percent,
|
||||||
|
baseline_percent: c.baseline_percent,
|
||||||
|
});
|
||||||
|
|
||||||
Ok(Json(AcceptanceResponse {
|
Ok(Json(AcceptanceResponse {
|
||||||
can_accept,
|
can_accept,
|
||||||
reasons,
|
reasons,
|
||||||
@@ -210,6 +253,7 @@ impl WorkflowApi {
|
|||||||
failed: summary.failed,
|
failed: summary.failed,
|
||||||
},
|
},
|
||||||
missing_categories,
|
missing_categories,
|
||||||
|
coverage_report,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +269,10 @@ impl WorkflowApi {
|
|||||||
workflow
|
workflow
|
||||||
.results
|
.results
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(story_id, results)| to_review_story(story_id, results))
|
.map(|(story_id, results)| {
|
||||||
|
let coverage = workflow.coverage.get(story_id);
|
||||||
|
to_review_story(story_id, results, coverage)
|
||||||
|
})
|
||||||
.filter(|story| story.can_accept)
|
.filter(|story| story.can_accept)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
@@ -262,7 +309,8 @@ impl WorkflowApi {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|story_id| {
|
.map(|story_id| {
|
||||||
let results = workflow.results.get(&story_id).cloned().unwrap_or_default();
|
let results = workflow.results.get(&story_id).cloned().unwrap_or_default();
|
||||||
to_review_story(&story_id, &results)
|
let coverage = workflow.coverage.get(&story_id);
|
||||||
|
to_review_story(&story_id, &results, coverage)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
@@ -270,6 +318,91 @@ impl WorkflowApi {
|
|||||||
Ok(Json(ReviewListResponse { stories }))
|
Ok(Json(ReviewListResponse { stories }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record coverage data for a story.
|
||||||
|
#[oai(path = "/workflow/coverage/record", method = "post")]
|
||||||
|
async fn record_coverage(
|
||||||
|
&self,
|
||||||
|
payload: Json<RecordCoveragePayload>,
|
||||||
|
) -> OpenApiResult<Json<bool>> {
|
||||||
|
let mut workflow = self
|
||||||
|
.ctx
|
||||||
|
.workflow
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| bad_request(e.to_string()))?;
|
||||||
|
workflow.record_coverage(
|
||||||
|
payload.0.story_id,
|
||||||
|
payload.0.current_percent,
|
||||||
|
payload.0.threshold_percent,
|
||||||
|
);
|
||||||
|
Ok(Json(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run coverage collection: execute test:coverage, parse output, record result.
|
||||||
|
#[oai(path = "/workflow/coverage/collect", method = "post")]
|
||||||
|
async fn collect_coverage(
|
||||||
|
&self,
|
||||||
|
payload: Json<CollectCoverageRequest>,
|
||||||
|
) -> OpenApiResult<Json<CoverageReportResponse>> {
|
||||||
|
let root = self
|
||||||
|
.ctx
|
||||||
|
.state
|
||||||
|
.get_project_root()
|
||||||
|
.map_err(bad_request)?;
|
||||||
|
|
||||||
|
let frontend_dir = root.join("frontend");
|
||||||
|
|
||||||
|
// Run pnpm run test:coverage in the frontend directory
|
||||||
|
let output = tokio::task::spawn_blocking(move || {
|
||||||
|
std::process::Command::new("pnpm")
|
||||||
|
.args(["run", "test:coverage"])
|
||||||
|
.current_dir(&frontend_dir)
|
||||||
|
.output()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| bad_request(format!("Task join error: {e}")))?
|
||||||
|
.map_err(|e| bad_request(format!("Failed to run coverage command: {e}")))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(bad_request(format!("Coverage command failed: {stderr}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the coverage summary JSON
|
||||||
|
let summary_path = root
|
||||||
|
.join("frontend")
|
||||||
|
.join("coverage")
|
||||||
|
.join("coverage-summary.json");
|
||||||
|
let json_str = fs::read_to_string(&summary_path)
|
||||||
|
.map_err(|e| bad_request(format!("Failed to read coverage summary: {e}")))?;
|
||||||
|
|
||||||
|
let current_percent = parse_coverage_json(&json_str).map_err(bad_request)?;
|
||||||
|
|
||||||
|
// Record coverage in workflow state
|
||||||
|
let coverage_report = {
|
||||||
|
let mut workflow = self
|
||||||
|
.ctx
|
||||||
|
.workflow
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| bad_request(e.to_string()))?;
|
||||||
|
workflow.record_coverage(
|
||||||
|
payload.0.story_id.clone(),
|
||||||
|
current_percent,
|
||||||
|
payload.0.threshold_percent,
|
||||||
|
);
|
||||||
|
workflow
|
||||||
|
.coverage
|
||||||
|
.get(&payload.0.story_id)
|
||||||
|
.cloned()
|
||||||
|
.expect("just inserted")
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(CoverageReportResponse {
|
||||||
|
current_percent: coverage_report.current_percent,
|
||||||
|
threshold_percent: coverage_report.threshold_percent,
|
||||||
|
baseline_percent: coverage_report.baseline_percent,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure a story can be accepted; returns an error when gates fail.
|
/// Ensure a story can be accepted; returns an error when gates fail.
|
||||||
#[oai(path = "/workflow/acceptance/ensure", method = "post")]
|
#[oai(path = "/workflow/acceptance/ensure", method = "post")]
|
||||||
async fn ensure_acceptance(
|
async fn ensure_acceptance(
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub struct StoryTestResults {
|
|||||||
pub struct WorkflowState {
|
pub struct WorkflowState {
|
||||||
pub stories: HashMap<String, StoryMetadata>,
|
pub stories: HashMap<String, StoryMetadata>,
|
||||||
pub results: HashMap<String, StoryTestResults>,
|
pub results: HashMap<String, StoryTestResults>,
|
||||||
|
pub coverage: HashMap<String, CoverageReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -105,6 +106,29 @@ impl WorkflowState {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_coverage(
|
||||||
|
&mut self,
|
||||||
|
story_id: String,
|
||||||
|
current_percent: f64,
|
||||||
|
threshold_percent: Option<f64>,
|
||||||
|
) {
|
||||||
|
let threshold = threshold_percent.unwrap_or(80.0);
|
||||||
|
|
||||||
|
let baseline = self
|
||||||
|
.coverage
|
||||||
|
.get(&story_id)
|
||||||
|
.map(|existing| existing.baseline_percent.unwrap_or(existing.current_percent));
|
||||||
|
|
||||||
|
self.coverage.insert(
|
||||||
|
story_id,
|
||||||
|
CoverageReport {
|
||||||
|
current_percent,
|
||||||
|
threshold_percent: threshold,
|
||||||
|
baseline_percent: baseline,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -179,10 +203,250 @@ pub fn evaluate_acceptance(results: &StoryTestResults) -> AcceptanceDecision {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Coverage report for a story.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct CoverageReport {
|
||||||
|
pub current_percent: f64,
|
||||||
|
pub threshold_percent: f64,
|
||||||
|
pub baseline_percent: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse coverage percentage from a vitest coverage-summary.json string.
|
||||||
|
/// Expects JSON with `{"total": {"lines": {"pct": <number>}}}`.
|
||||||
|
pub fn parse_coverage_json(json_str: &str) -> Result<f64, String> {
|
||||||
|
let value: serde_json::Value =
|
||||||
|
serde_json::from_str(json_str).map_err(|e| format!("Invalid coverage JSON: {e}"))?;
|
||||||
|
|
||||||
|
value
|
||||||
|
.get("total")
|
||||||
|
.and_then(|t| t.get("lines"))
|
||||||
|
.and_then(|l| l.get("pct"))
|
||||||
|
.and_then(|p| p.as_f64())
|
||||||
|
.ok_or_else(|| "Missing total.lines.pct in coverage JSON.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether coverage meets the threshold.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn check_coverage_threshold(current: f64, threshold: f64) -> Result<(), String> {
|
||||||
|
if current >= threshold {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Coverage below threshold ({current:.1}% < {threshold:.1}%)."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate acceptance with optional coverage data.
|
||||||
|
pub fn evaluate_acceptance_with_coverage(
|
||||||
|
results: &StoryTestResults,
|
||||||
|
coverage: Option<&CoverageReport>,
|
||||||
|
) -> AcceptanceDecision {
|
||||||
|
let mut decision = evaluate_acceptance(results);
|
||||||
|
|
||||||
|
if let Some(report) = coverage {
|
||||||
|
if report.current_percent < report.threshold_percent {
|
||||||
|
decision.can_accept = false;
|
||||||
|
decision.reasons.push(format!(
|
||||||
|
"Coverage below threshold ({:.1}% < {:.1}%).",
|
||||||
|
report.current_percent, report.threshold_percent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(baseline) = report.baseline_percent
|
||||||
|
&& report.current_percent < baseline {
|
||||||
|
decision.can_accept = false;
|
||||||
|
decision.reasons.push(format!(
|
||||||
|
"Coverage regression: {:.1}% → {:.1}% (threshold: {:.1}%).",
|
||||||
|
baseline, report.current_percent, report.threshold_percent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decision
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// === parse_coverage_json ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_valid_coverage_json() {
|
||||||
|
let json = r#"{"total":{"lines":{"total":100,"covered":85,"pct":85.0},"statements":{"pct":85.0}}}"#;
|
||||||
|
assert_eq!(parse_coverage_json(json).unwrap(), 85.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_coverage_json() {
|
||||||
|
assert!(parse_coverage_json("not json").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_missing_total_lines_pct() {
|
||||||
|
let json = r#"{"total":{"branches":{"pct":90.0}}}"#;
|
||||||
|
assert!(parse_coverage_json(json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AC1: check_coverage_threshold ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coverage_threshold_passes_when_met() {
|
||||||
|
assert!(check_coverage_threshold(80.0, 80.0).is_ok());
|
||||||
|
assert!(check_coverage_threshold(95.5, 80.0).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coverage_threshold_fails_when_below() {
|
||||||
|
let result = check_coverage_threshold(72.3, 80.0);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(err.contains("72.3%"));
|
||||||
|
assert!(err.contains("80.0%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AC2: evaluate_acceptance_with_coverage ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acceptance_blocked_by_coverage_below_threshold() {
|
||||||
|
let results = StoryTestResults {
|
||||||
|
unit: vec![TestCaseResult {
|
||||||
|
name: "unit-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
integration: vec![TestCaseResult {
|
||||||
|
name: "int-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let coverage = CoverageReport {
|
||||||
|
current_percent: 55.0,
|
||||||
|
threshold_percent: 80.0,
|
||||||
|
baseline_percent: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
|
||||||
|
assert!(!decision.can_accept);
|
||||||
|
assert!(decision.reasons.iter().any(|r| r.contains("Coverage below threshold")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acceptance_blocked_by_coverage_regression() {
|
||||||
|
let results = StoryTestResults {
|
||||||
|
unit: vec![TestCaseResult {
|
||||||
|
name: "unit-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
integration: vec![TestCaseResult {
|
||||||
|
name: "int-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let coverage = CoverageReport {
|
||||||
|
current_percent: 82.0,
|
||||||
|
threshold_percent: 80.0,
|
||||||
|
baseline_percent: Some(90.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
|
||||||
|
assert!(!decision.can_accept);
|
||||||
|
assert!(decision.reasons.iter().any(|r| r.contains("Coverage regression")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acceptance_passes_with_good_coverage() {
|
||||||
|
let results = StoryTestResults {
|
||||||
|
unit: vec![TestCaseResult {
|
||||||
|
name: "unit-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
integration: vec![TestCaseResult {
|
||||||
|
name: "int-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let coverage = CoverageReport {
|
||||||
|
current_percent: 92.0,
|
||||||
|
threshold_percent: 80.0,
|
||||||
|
baseline_percent: Some(90.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let decision = evaluate_acceptance_with_coverage(&results, Some(&coverage));
|
||||||
|
assert!(decision.can_accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acceptance_works_without_coverage_data() {
|
||||||
|
let results = StoryTestResults {
|
||||||
|
unit: vec![TestCaseResult {
|
||||||
|
name: "unit-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
integration: vec![TestCaseResult {
|
||||||
|
name: "int-1".to_string(),
|
||||||
|
status: TestStatus::Pass,
|
||||||
|
details: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let decision = evaluate_acceptance_with_coverage(&results, None);
|
||||||
|
assert!(decision.can_accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === record_coverage ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_coverage_first_time_has_no_baseline() {
|
||||||
|
let mut state = WorkflowState::default();
|
||||||
|
state.record_coverage("story-27".to_string(), 85.0, Some(80.0));
|
||||||
|
|
||||||
|
let report = state.coverage.get("story-27").unwrap();
|
||||||
|
assert_eq!(report.current_percent, 85.0);
|
||||||
|
assert_eq!(report.threshold_percent, 80.0);
|
||||||
|
assert_eq!(report.baseline_percent, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_coverage_subsequent_sets_baseline() {
|
||||||
|
let mut state = WorkflowState::default();
|
||||||
|
state.record_coverage("story-27".to_string(), 85.0, Some(80.0));
|
||||||
|
state.record_coverage("story-27".to_string(), 78.0, Some(80.0));
|
||||||
|
|
||||||
|
let report = state.coverage.get("story-27").unwrap();
|
||||||
|
assert_eq!(report.current_percent, 78.0);
|
||||||
|
assert_eq!(report.baseline_percent, Some(85.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_coverage_default_threshold() {
|
||||||
|
let mut state = WorkflowState::default();
|
||||||
|
state.record_coverage("story-27".to_string(), 90.0, None);
|
||||||
|
|
||||||
|
let report = state.coverage.get("story-27").unwrap();
|
||||||
|
assert_eq!(report.threshold_percent, 80.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_coverage_custom_threshold() {
|
||||||
|
let mut state = WorkflowState::default();
|
||||||
|
state.record_coverage("story-27".to_string(), 90.0, Some(95.0));
|
||||||
|
|
||||||
|
let report = state.coverage.get("story-27").unwrap();
|
||||||
|
assert_eq!(report.threshold_percent, 95.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Existing tests ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warns_when_multiple_tests_fail() {
|
fn warns_when_multiple_tests_fail() {
|
||||||
let results = StoryTestResults {
|
let results = StoryTestResults {
|
||||||
|
|||||||
Reference in New Issue
Block a user