Files
cve-dashboard/.kiro/specs/compliance-list-stale-after-sidebar-edit/tasks.md
Jordan Ramos a61d254ff9 Sync .kiro/ from master — v2.2.0 release batch
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
2026-06-04 11:27:31 -06:00

13 KiB
Raw Blame History

Implementation Plan: Compliance List Stale After Sidebar Edit

Overview

Fix the frontend state-refresh defect described in GitLab issue #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 onNoteAddedrefresh 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

  • 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. 1200)
    • 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
  • 2. Fix the stale list after sidebar metadata save

    • 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
    • 2.2 Wire onMetadataSaved to refresh in CompliancePage

      • In frontend/src/components/pages/CompliancePage.js, at the <ComplianceDetailPanel> render site (around lines 696702), 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
    • 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
  • 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
  • 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

{
  "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 883890) already renders them correctly when present.
  • The fix is intentionally minimal and mirrors the existing onNoteAddedrefresh pattern rather than introducing a new refresh mechanism, satisfying the Regression Prevention clauses (3.13.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.