New specs: archer-template-library, ccp-metrics-view-restructure, compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date, compliance-remediation-display-fix, flexible-jira-ticket-creation, forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix, multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown New steering: archer-template-gen.md Updated: migration-registration-check hook, remediation-plan-history spec, gitlab-workflow, tech, versioning steering files
117 lines
13 KiB
Markdown
117 lines
13 KiB
Markdown
# Implementation Plan: Compliance List Stale After Sidebar Edit
|
||
|
||
## Overview
|
||
|
||
Fix the frontend state-refresh defect described in GitLab issue [#23](http://steam-gitlab.charterlab.com/steam/cve-dashboard/-/issues/23). After a successful metadata save in `ComplianceDetailPanel` (`PATCH /api/compliance/items/:hostname/metadata`), the host list on `CompliancePage` keeps showing the pre-edit Resolution Date / Remediation Plan until an unrelated refresh trigger fires. The backend list endpoint already returns the correct data (fixed in the predecessor spec `compliance-remediation-display-fix`); the defect is purely that the sidebar never notifies the parent after a metadata save, so `fetchDevices` is never re-issued.
|
||
|
||
The fix mirrors the existing `onNoteAdded` → `refresh` pattern: `handleSaveMetadata()` invokes a new parent callback (`onMetadataSaved`) after a successful `PATCH`, and `CompliancePage` wires that callback to `refresh` (`fetchSummary` + `fetchDevices`). The backend is out of scope.
|
||
|
||
All tasks below are coding/test tasks. Tests live in the frontend and run via `react-scripts test` (`CI=true npm test`), using `@testing-library/react`, `@testing-library/jest-dom`, and `fast-check` (already permitted by `jest.transformIgnorePatterns` in `frontend/package.json`).
|
||
|
||
## Tasks
|
||
|
||
- [x] 1. Write bug condition exploration property test
|
||
- **Property 1: Bug Condition** - List Row Stays Stale After Sidebar Metadata Save
|
||
- **CRITICAL**: This test MUST FAIL on the current unfixed code — failure confirms the bug exists
|
||
- **DO NOT attempt to fix the test or the code when it fails** — the failure is the expected, desired outcome at this step
|
||
- **NOTE**: This test encodes the expected behavior (Property 1 below) — it will validate the fix when it passes after implementation
|
||
- **GOAL**: Surface counterexamples demonstrating that after a successful sidebar metadata save the parent `CompliancePage` list does NOT reflect the saved value without a manual refresh
|
||
- **Test file**: `frontend/src/components/pages/__tests__/compliance-list-stale-after-sidebar-edit.exploration.property.test.js`
|
||
- **Header comment**: reference spec `.kiro/specs/compliance-list-stale-after-sidebar-edit/` and issue #23, and state "EXPECTED TO FAIL on unfixed code — failure confirms the bug" (mirrors `backend/__tests__/compliance-remediation-display-fix.exploration.property.test.js` tagging convention)
|
||
- **Bug Condition** (from bugfix.md Current Behavior): `isBugCondition(input) = a successful PATCH /api/compliance/items/:hostname/metadata occurred from the sidebar` — under this condition the parent list is never re-fetched (1.1), `onClose` only clears `selectedHost` (1.2), and the row shows the stale value (1.3)
|
||
- **Scoped PBT Approach**: use `fast-check` to generate varied saved-metadata values across runs and assert the universal property holds for each:
|
||
- `fc.assert(fc.asyncProperty(arbResolutionDate, arbRemediationPlan, async (resolutionDate, remediationPlan) => { ... }), { numRuns: ... })`
|
||
- `arbResolutionDate`: build `YYYY-MM-DD` strings from integer tuples (do NOT call `toISOString` on shrunk values — follow the date-generator pattern in the predecessor exploration test)
|
||
- `arbRemediationPlan`: non-empty trimmed strings, length-bounded (e.g. 1–200)
|
||
- **Property under test**: _for any_ saved metadata value, after a successful sidebar save the parent list row for that hostname displays the saved value without a manual filter/team/tab change or manual refresh click
|
||
- **Test structure**:
|
||
- Mock `useAuth` from `../../contexts/AuthContext` (e.g. `jest.mock`) to return a stub: `canWrite: () => true`, `isAdmin: () => false`, `getAvailableTeams: () => ['STEAM']`, `adminScope: null`
|
||
- Mock `global.fetch` with a URL-routing implementation responding to:
|
||
- `GET /compliance/summary?team=STEAM` → minimal summary `{ entries: [], overall_scores: {}, upload: { report_date: '2025-01-01' } }`
|
||
- `GET /compliance/items?team=STEAM&status=active` → `{ devices: [oneDevice] }` where the device starts with a stale value (e.g. `resolution_date: null`, `remediation_plan: null`) on the FIRST call, and returns the generated saved value on SUBSEQUENT calls (so a re-fetch is observable)
|
||
- `GET /compliance/items/:hostname` → detail object with one active metric so the panel renders the Resolution Date / Remediation Plan inputs and Save button
|
||
- `PATCH /compliance/items/:hostname/metadata` → `{ ok: true }` success response
|
||
- Render `<CompliancePage onNavigate={() => {}} />`, wait for the device row to appear, click the row to open `ComplianceDetailPanel`
|
||
- Set the Resolution Date input and/or Remediation Plan textarea to the generated value(s) and click **Save** (use `@testing-library/user-event` or `fireEvent`, wrapped in `act`/`waitFor`)
|
||
- **Primary assertion (encodes Property 1)**: `await waitFor(() => expect(within(row).getByText(<generated value, sliced to 10 for the date>)).toBeInTheDocument())` — the list row reflects the saved value
|
||
- **Supporting assertion**: the list endpoint `GET /compliance/items?...` is called more than once (initial load + post-save refresh); on unfixed code it is called exactly once
|
||
- Run on UNFIXED code: `cd frontend && CI=true npm test -- --testPathPattern compliance-list-stale-after-sidebar-edit.exploration`
|
||
- **EXPECTED OUTCOME**: Test FAILS — `handleSaveMetadata()` never invokes a parent callback, so `fetchDevices` is not re-issued and the row keeps the stale value (the list GET is called only once)
|
||
- Document the counterexample found (e.g. "after saving resolution_date='2026-03-04', the list row still shows '—'; GET /compliance/items called 1 time, expected ≥ 2")
|
||
- Mark this task complete when the test is written, run, and the failure is documented
|
||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2_
|
||
|
||
- [x] 2. Fix the stale list after sidebar metadata save
|
||
|
||
- [x] 2.1 Invoke a parent refresh callback from `handleSaveMetadata()` after a successful PATCH
|
||
- In `frontend/src/components/pages/ComplianceDetailPanel.js`, add `onMetadataSaved` to the component props: `export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, onMetadataSaved, onNavigate })`
|
||
- In `handleSaveMetadata()`, after the `PATCH` succeeds and `await fetchDetail()` completes (inside the `try`, after the existing success handling), add: `if (onMetadataSaved) onMetadataSaved();`
|
||
- Place the call so it only runs on success (not in the `catch`), mirroring how `handleAddNote()` calls `if (onNoteAdded) onNoteAdded();` only after a successful note save
|
||
- Keep the existing `await fetchDetail()` call so the panel's own view/history still refreshes (do not replace it)
|
||
- _Bug_Condition: isBugCondition(input) = successful PATCH /api/compliance/items/:hostname/metadata from the sidebar_
|
||
- _Expected_Behavior: a parent refresh callback is invoked on save success so the parent list re-fetches (bugfix.md 2.1)_
|
||
- _Preservation: only the success path is touched; the `catch` path that sets `metaError` is unchanged (3.3); `fetchDetail()` still runs (3.6)_
|
||
- _Requirements: 2.1, 3.3, 3.6_
|
||
|
||
- [x] 2.2 Wire `onMetadataSaved` to `refresh` in `CompliancePage`
|
||
- In `frontend/src/components/pages/CompliancePage.js`, at the `<ComplianceDetailPanel>` render site (around lines 696–702), add the prop `onMetadataSaved={refresh}` alongside the existing `onNoteAdded={refresh}`
|
||
- Reuse the existing `refresh` function (`fetchSummary(activeTeam)` + `fetchDevices(activeTeam, activeTab)`) — do not introduce a new refresh path
|
||
- Leave `onClose={() => setSelectedHost(null)}` unchanged (close still just clears selection)
|
||
- Do not leave any unused variables; if a value is intentionally unused prefix it with `_` per the ESLint `no-unused-vars` warning budget
|
||
- _Bug_Condition: isBugCondition(input) = successful sidebar metadata save while the panel is open from CompliancePage_
|
||
- _Expected_Behavior: the parent list re-fetches via fetchDevices so the updated Resolution Date / Remediation Plan appears in the row without a manual refresh (bugfix.md 2.1, 2.2)_
|
||
- _Preservation: onNoteAdded still maps to refresh (3.1); team/tab/upload/manual-refresh triggers unchanged (3.2); onClose unchanged (3.4); other row fields unchanged (3.5)_
|
||
- _Requirements: 2.1, 2.2_
|
||
|
||
- [x] 2.3 Verify the bug condition exploration test now passes
|
||
- **Property 1: Expected Behavior** - List Row Reflects Saved Value Without Manual Refresh
|
||
- **IMPORTANT**: Re-run the SAME test from Task 1 — do NOT write a new test
|
||
- The Task 1 test encodes the expected behavior; when it passes it confirms the fix satisfies Property 1
|
||
- Run: `cd frontend && CI=true npm test -- --testPathPattern compliance-list-stale-after-sidebar-edit.exploration`
|
||
- **EXPECTED OUTCOME**: Test PASSES — after a successful save the list re-fetches (`GET /compliance/items?...` called ≥ 2 times) and the row displays the saved value
|
||
- _Requirements: 2.1, 2.2_
|
||
|
||
- [x] 3. Write preservation / regression property test
|
||
- **Property 2: Preservation** - Unchanged Behaviors Hold After the Fix
|
||
- **IMPORTANT**: Follow observation-first methodology — the unchanged behaviors below are observed to hold on UNFIXED code and must continue to hold after the fix; only the new "list updates after save" guard is expected to flip from failing to passing
|
||
- **Test file**: `frontend/src/components/pages/__tests__/compliance-list-stale-after-sidebar-edit.preservation.property.test.js`
|
||
- **Header comment**: reference spec `.kiro/specs/compliance-list-stale-after-sidebar-edit/` and issue #23 (mirrors the predecessor preservation test tagging convention)
|
||
- **Test structure**: reuse the same `useAuth` mock and URL-routing `global.fetch` mock approach as Task 1; use `fast-check` to generate varied note text and metadata values across runs
|
||
- **Regression guard (new behavior, post-fix)**: _for any_ saved metadata value, after a successful save the parent list row displays the saved value (same assertion family as Property 1, kept as a standing regression guard)
|
||
- **Preservation properties (must hold before and after the fix)**:
|
||
- **Note-add refresh still works**: when a note is added in the sidebar (`POST /compliance/notes` succeeds), the existing `onNoteAdded` callback still triggers a list re-fetch — assert `GET /compliance/items?...` is re-issued after a note add (bugfix.md 3.1)
|
||
- **Failed save shows error and does NOT falsely update the list**: when `PATCH /compliance/items/:hostname/metadata` returns a non-OK response, assert the panel surfaces `metaError` AND the list row value is unchanged / no spurious list re-fetch displays a value that was never persisted (bugfix.md 3.3)
|
||
- **Close-without-change just clears selection**: clicking the close (`X`) / backdrop without saving removes the detail panel (selection cleared) and does not error; no metadata save occurs (bugfix.md 3.4)
|
||
- **Other row fields render correctly**: _for any_ generated device, the row still displays hostname, IP address, device type, failing metrics, and seen count unchanged (bugfix.md 3.5)
|
||
- Run before the fix to confirm the preservation properties pass on unfixed code; run after the fix to confirm they still pass and the regression guard now passes
|
||
- Command: `cd frontend && CI=true npm test -- --testPathPattern compliance-list-stale-after-sidebar-edit.preservation`
|
||
- **EXPECTED OUTCOME (post-fix)**: All preservation properties PASS (no regressions) and the regression guard PASSES
|
||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
|
||
|
||
- [x] 4. Checkpoint - Run tests and confirm the production build
|
||
- Run both new tests together: `cd frontend && CI=true npm test -- --testPathPattern compliance-list-stale-after-sidebar-edit`
|
||
- **EXPECTED OUTCOME**: exploration test PASSES (bug fixed), preservation test PASSES (no regressions)
|
||
- Run the production build to confirm it compiles and stays within the ESLint 25-warning budget: `cd frontend && npm run build`
|
||
- **EXPECTED OUTCOME**: build succeeds and the warning count remains at or below 25 (the CI `lint`/`build` stage fails above 25) — prefix any intentionally-unused variables with `_` if needed
|
||
- Ensure all tests pass; ask the user if questions arise.
|
||
|
||
## Task Dependency Graph
|
||
|
||
```json
|
||
{
|
||
"waves": [
|
||
{ "id": 0, "tasks": ["1"] },
|
||
{ "id": 1, "tasks": ["2.1", "2.2"] },
|
||
{ "id": 2, "tasks": ["2.3", "3"] },
|
||
{ "id": 3, "tasks": ["4"] }
|
||
]
|
||
}
|
||
```
|
||
|
||
## Notes
|
||
|
||
- This is a frontend-only fix. The backend list endpoint (`GET /api/compliance/items`) already returns `resolution_date` and `remediation_plan` (predecessor spec `compliance-remediation-display-fix`), and `DeviceRow` in `CompliancePage.js` (lines 883–890) already renders them correctly when present.
|
||
- The fix is intentionally minimal and mirrors the existing `onNoteAdded` → `refresh` pattern rather than introducing a new refresh mechanism, satisfying the Regression Prevention clauses (3.1–3.6).
|
||
- Both tests render `CompliancePage` with a mocked `useAuth` and a URL-routing `global.fetch` mock; the list GET mock returns the stale value on first call and the saved value on later calls so a post-save re-fetch is observable in the rendered row.
|
||
- `fast-check` is already allowed through `jest.transformIgnorePatterns` in `frontend/package.json`; generate dates as `YYYY-MM-DD` strings from integer tuples (avoid `toISOString` on shrunk values) as in the predecessor exploration test.
|