23 KiB
Local Pre-CI Patch Report (Single Scope)
Date: 2026-02-17 Scope: Add a local pre-CI patch report to Definition of Done (DoD) unit-testing flow for both backend and frontend.
1) Objective
Add one executable local workflow that computes patch coverage from current branch changes and publishes a consolidated report before CI runs.
The report must consume backend and frontend coverage inputs, use origin/main...HEAD as the patch baseline, and produce human-readable and machine-readable artifacts in test-results/.
2) In Scope / Out of Scope
In Scope
- Local patch report generation.
- Backend + frontend DoD unit-testing integration.
- VS Code task wiring for repeatable local execution.
- Non-blocking warning policy for initial rollout.
Out of Scope
- CI gate changes.
- Encryption-key or unrelated reliability/security remediation.
- Historical Codecov placeholder gates and unrelated patch-closure matrices.
3) Required Inputs and Baseline
Coverage Inputs
- Backend coverage profile:
backend/coverage.txt - Frontend coverage profile:
frontend/coverage/lcov.info
Diff Baseline
- Git diff range:
origin/main...HEAD
Preconditions
origin/mainis fetchable locally.- Backend and frontend coverage artifacts exist before report generation.
4) Required Output Artifacts
- Markdown report:
test-results/local-patch-report.md - JSON report:
test-results/local-patch-report.json
Both artifacts are mandatory per run. Missing either artifact is a failed local report run.
5) Initial Policy (Rollout)
Initial Policy (Non-Blocking)
- Local patch report does not fail DoD on low patch coverage during initial rollout.
- Local runner emits warnings (stdout + markdown/json status fields) when thresholds are not met.
- DoD requires the report to run and artifacts to exist, even in warning mode.
- Execution and final merge checks in this plan follow this same warn-mode policy during rollout.
Threshold Defaults and Source Precedence
- Coverage thresholds are resolved with this precedence:
- Environment variables (highest precedence)
- Built-in defaults (fallback)
- Threshold environment variables:
CHARON_OVERALL_PATCH_COVERAGE_MINCHARON_BACKEND_PATCH_COVERAGE_MINCHARON_FRONTEND_PATCH_COVERAGE_MIN
- Built-in defaults for this rollout:
- Overall patch coverage minimum:
90 - Backend patch coverage minimum:
85 - Frontend patch coverage minimum:
85
- Overall patch coverage minimum:
- Parsing/validation:
- Values must be numeric percentages in
[0, 100]. - Invalid env values are ignored with a warning, and the corresponding default is used.
- Values must be numeric percentages in
Future Policy (Optional Hard Gate)
- Optional future switch to hard gate (non-zero exit on threshold breach).
- Gate behavior is controlled by a dedicated flag/env (to be added during implementation).
- Hard-gate enablement is explicitly deferred and not part of this rollout.
6) Technical Specification
6.1 Script
Implement a new local report script:
- Path:
scripts/local-patch-report.sh - Responsibilities:
- Validate required inputs exist (
backend/coverage.txt,frontend/coverage/lcov.info). - Resolve patch files/lines from
origin/main...HEAD. - Correlate changed lines with backend/frontend coverage data.
- Compute patch summary by component and overall.
- Resolve thresholds using env-var-first precedence, then defaults (
90/85/85). - Evaluate statuses against resolved thresholds:
overall.status=passwhenoverall.patch_coverage_pct >= overall_threshold, elsewarn.backend.status=passwhenbackend.patch_coverage_pct >= backend_threshold, elsewarn.frontend.status=passwhenfrontend.patch_coverage_pct >= frontend_threshold, elsewarn.
- Emit warning status when any scope is below its resolved threshold.
- Write required outputs:
test-results/local-patch-report.mdtest-results/local-patch-report.json
- Validate required inputs exist (
6.2 Report Contract
Minimum JSON fields:
baseline:origin/main...HEADgenerated_atmode:warn(initial rollout)thresholds:overall_patch_coverage_minbackend_patch_coverage_minfrontend_patch_coverage_min
threshold_sources:overall(env|default)backend(env|default)frontend(env|default)
overall:changed_linescovered_linespatch_coverage_pctstatus(pass|warn)
backendandfrontendobjects with same coverage counters and statusfiles_needing_coverage(required array for execution baselines), where each item includes at minimum:pathuncovered_changed_linespatch_coverage_pct
artifactswith emitted file paths
Minimum Markdown sections:
- Run metadata (timestamp, baseline)
- Input paths used
- Resolved thresholds and their sources (env/default)
- Coverage summary table (overall/backend/frontend)
- Warning section (if any)
- Artifact paths
6.3 Task Wiring
Add VS Code task entries in .vscode/tasks.json:
Test: Local Patch Report- Runs report generation script only.
Test: Backend DoD + Local Patch Report- Runs backend unit test coverage flow, then local patch report.
Test: Frontend DoD + Local Patch Report- Runs frontend unit test coverage flow, then local patch report.
Test: Full DoD Unit + Local Patch Report- Runs backend + frontend unit coverage flows, then local patch report.
Task behavior:
- Reuse existing coverage scripts/tasks where available.
- Keep command order deterministic: coverage generation first, patch report second.
7) Implementation Tasks
Phase 1 — Script Foundation
- Create
scripts/local-patch-report.sh. - Add input validation + clear error messages.
- Add diff parsing for
origin/main...HEAD.
Phase 2 — Coverage Correlation
- Parse backend
coverage.txtand map covered lines. - Parse frontend
coverage/lcov.infoand map covered lines. - Compute per-scope and overall patch coverage counters.
Phase 3 — Artifact Emission
- Generate
test-results/local-patch-report.jsonwith required schema. - Generate
test-results/local-patch-report.mdwith summary + warnings. - Ensure
test-results/creation if missing.
Phase 4 — Task Wiring
- Add
Test: Local Patch Reportto.vscode/tasks.json. - Add backend/frontend/full DoD task variants with report execution.
- Verify tasks run successfully from workspace root.
Phase 5 — Documentation Alignment
- Update DoD references in applicable docs/instructions only where this local report is now required.
- Remove stale references to unrelated placeholder gates in active plan context.
8) Validation Commands
Run from repository root unless noted.
- Generate backend coverage input:
cd backend && go test ./... -coverprofile=coverage.txt
- Generate frontend coverage input:
cd frontend && npm run test:coverage
- Generate local patch report directly:
./scripts/local-patch-report.sh
- Generate local patch report via task:
# VS Code task: Test: Local Patch Report
- Validate artifacts exist:
test -f test-results/local-patch-report.md
test -f test-results/local-patch-report.json
- Validate baseline recorded in JSON:
jq -r '.baseline' test-results/local-patch-report.json
# expected: origin/main...HEAD
9) Acceptance Criteria
- Plan remains single-scope: local pre-CI patch report for DoD unit testing only.
- Inputs are explicit and used:
backend/coverage.txtfrontend/coverage/lcov.infoorigin/main...HEAD
- Outputs are generated on every successful run:
test-results/local-patch-report.mdtest-results/local-patch-report.json
- Initial policy is non-blocking warning mode.
- Default thresholds are explicit:
- Overall patch coverage:
90 - Backend patch coverage:
85 - Frontend patch coverage:
85
- Overall patch coverage:
- Threshold source precedence is explicit: env vars first, then defaults.
- Future hard-gate mode is documented as optional and deferred.
- Concrete script + task wiring tasks are present and executable.
- Validation commands are present and reproducible.
- Stale unrelated placeholder gates are removed from this active spec.
10) Concrete Execution Plan — Patch Gap Closure (PR Merge Objective)
Single-scope objective: close current patch gaps for this PR merge by adding targeted tests and iterating local patch reports until changed-line coverage is merge-ready under DoD.
Authoritative Gap Baseline (2026-02-17)
Use this list as the only planning baseline for this execution cycle:
backend/cmd/localpatchreport/main.go: 0%, 200 uncovered changed lines, ranges46-59,61-73,75-79,81-85,87-96,98-123,125-156,158-165,167-172,175-179,182-187,190-198,201-207,210-219,222-254,257-264,267-269frontend/src/pages/UsersPage.tsx: 30.8%, 9 uncovered (152-160)frontend/src/pages/CrowdSecConfig.tsx: 36.8%, 12 uncovered (975-977,1220,1248-1249,1281-1282,1316,1324-1325,1335)frontend/src/pages/DNSProviders.tsx: 70.6%, 10 uncoveredfrontend/src/pages/AuditLogs.tsx: 75.0%, 1 uncoveredfrontend/src/components/ProxyHostForm.tsx: 75.5%, 12 uncoveredbackend/internal/api/middleware/auth.go: 86.4%, 3 uncoveredfrontend/src/pages/Notifications.tsx: 88.9%, 3 uncoveredbackend/internal/cerberus/rate_limit.go: 91.9%, 12 uncovered
DoD Entry Gate (Mandatory Before Phase 1)
All execution phases are blocked until this gate is completed in order:
- E2E first:
cd /projects/Charon && npx playwright test --project=firefox
- Local patch preflight (baseline refresh trigger):
cd /projects/Charon && bash scripts/local-patch-report.sh
- Baseline refresh checkpoint (must pass before phase execution):
cd /projects/Charon && jq -r '.files_needing_coverage[].path' test-results/local-patch-report.json | sort > /tmp/charon-baseline-files.txt
cd /projects/Charon && while read -r f; do git diff --name-only origin/main...HEAD -- "$f" | grep -qx "$f" || echo "baseline file missing from current diff: $f"; done < /tmp/charon-baseline-files.txt
- If checkpoint output is non-empty, refresh this baseline list to match the latest
test-results/local-patch-report.jsonbefore starting Phase 1.
Ordered Phases (Highest Impact First)
Phase 1 — Backend Local Patch Report CLI (Highest Delta)
Targets:
backend/cmd/localpatchreport/main.go(all listed uncovered ranges)
Suggested test file:
backend/cmd/localpatchreport/main_test.go
Test focus:
- argument parsing and mode selection
- coverage input validation paths
- baseline/diff resolution flow
- report generation branches (markdown/json)
- warning/error branches for missing inputs and malformed coverage
Pass criteria:
- maximize reduction of uncovered changed lines in
backend/cmd/localpatchreport/main.gofrom the200baseline, with priority on highest-impact uncovered ranges and no new uncovered changed lines introduced - backend targeted test command passes
Targeted test command:
cd /projects/Charon/backend && go test ./cmd/localpatchreport -coverprofile=coverage.txt
Phase 2 — Frontend Lowest-Coverage, Highest-Uncovered Pages
Targets:
frontend/src/pages/CrowdSecConfig.tsx(975-977,1220,1248-1249,1281-1282,1316,1324-1325,1335)frontend/src/pages/UsersPage.tsx(152-160)frontend/src/pages/DNSProviders.tsx(10 uncovered changed lines)
Suggested test files:
frontend/src/pages/__tests__/CrowdSecConfig.patch-gap.test.tsxfrontend/src/pages/__tests__/UsersPage.patch-gap.test.tsxfrontend/src/pages/__tests__/DNSProviders.patch-gap.test.tsx
Test focus:
- branch/error-state rendering tied to uncovered lines
- conditional action handlers and callback guards
- edge-case interaction states not hit by existing tests
Pass criteria:
- maximize reduction of changed-line gaps for the three targets, prioritize highest-impact uncovered lines first, and avoid introducing new uncovered changed lines
- frontend targeted test command passes
Targeted test command:
cd /projects/Charon/frontend && npm run test:coverage -- src/pages/__tests__/CrowdSecConfig.patch-gap.test.tsx src/pages/__tests__/UsersPage.patch-gap.test.tsx src/pages/__tests__/DNSProviders.patch-gap.test.tsx
Phase 3 — Backend Residual Middleware/Security Gaps
Targets:
backend/internal/api/middleware/auth.go(3 uncovered changed lines)backend/internal/cerberus/rate_limit.go(12 uncovered changed lines)
Suggested test targets/files:
- extend
backend/internal/api/middleware/auth_test.go - extend
backend/internal/cerberus/rate_limit_test.go
Test focus:
- auth middleware edge branches (token/context failure paths)
- rate-limit boundary and deny/allow branch coverage
Pass criteria:
- maximize reduction of changed-line gaps for both backend files, prioritize highest-impact uncovered lines first, and avoid introducing new uncovered changed lines
- backend targeted test command passes
Targeted test command:
cd /projects/Charon/backend && go test ./internal/api/middleware ./internal/cerberus -coverprofile=coverage.txt
Phase 4 — Frontend Component + Residual Page Gaps
Targets:
frontend/src/components/ProxyHostForm.tsx(12 uncovered changed lines)frontend/src/pages/AuditLogs.tsx(1 uncovered changed line)frontend/src/pages/Notifications.tsx(3 uncovered changed lines)
Suggested test files:
frontend/src/components/__tests__/ProxyHostForm.patch-gap.test.tsxfrontend/src/pages/__tests__/AuditLogs.patch-gap.test.tsxfrontend/src/pages/__tests__/Notifications.patch-gap.test.tsx
Test focus:
- form branch paths and validation fallbacks
- single-line residual branch in audit logs
- notification branch handling for low-frequency states
Pass criteria:
- maximize reduction of changed-line gaps for all three targets, prioritize highest-impact uncovered lines first, and avoid introducing new uncovered changed lines
- frontend targeted test command passes
Targeted test command:
cd /projects/Charon/frontend && npm run test:coverage -- src/components/__tests__/ProxyHostForm.patch-gap.test.tsx src/pages/__tests__/AuditLogs.patch-gap.test.tsx src/pages/__tests__/Notifications.patch-gap.test.tsx
Execution Commands
Run from repository root unless stated otherwise.
- Backend coverage:
cd backend && go test ./... -coverprofile=coverage.txt
- Frontend coverage:
cd frontend && npm run test:coverage
- Local patch report iteration:
bash scripts/local-patch-report.sh
- Iteration loop (repeat until all target gaps are closed):
cd backend && go test ./... -coverprofile=coverage.txt
cd /projects/Charon/frontend && npm run test:coverage
cd /projects/Charon && bash scripts/local-patch-report.sh
Phase Completion Checks
- After each phase, rerun
bash scripts/local-patch-report.shand confirm that only the next planned target set remains uncovered. - Do not advance phases when a phase target still shows uncovered changed lines.
Final Merge-Ready Gate (DoD-Aligned, Warn-Mode Rollout)
This PR is merge-ready only when all conditions are true:
- local patch report runs in warn mode and required artifacts are generated
- practical merge objective: drive a significant reduction in authoritative baseline uncovered changed lines in this PR, prioritizing highest-impact files;
0remains aspirational and is not a warn-mode merge blocker - required artifacts exist and are current:
test-results/local-patch-report.mdtest-results/local-patch-report.json
- backend and frontend coverage commands complete successfully
- DoD checks remain satisfied (E2E first, local patch report preflight, required security/coverage/type/build validations)
Flaky Test Stabilization Plan: TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPath (2026-02-17)
1) Scope and Objective
Stabilize backend flake in backend/internal/api/handlers/settings_wave4_test.go for:
TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPath
Scope is limited to this flaky path and directly adjacent test/lifecycle hardening required to make behavior deterministic across CI contexts.
2) Investigation Findings (Root Cause)
Evidence from CI and local repro (go test -race -count=20 -run 'TestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrors|TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPath' ./internal/api/handlers):
- Race is reported by Go race detector during execution of
TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPath. - Conflicting operations:
- Read path: background goroutine from
services.NewSecurityService()performingdb.Create()inpersistAuditWithRetry()/processAuditEvents(). - Write path: test cleanup removing GORM create callback (
db.Callback().Create().Remove(...)) inregisterCreatePermissionDeniedHookcleanup.
- Read path: background goroutine from
- This race is triggered by preceding test
TestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrors, which creates aSecurityService(spawns goroutine) and does not shut it down before callback cleanup mutates callback registry.
Primary cause is shared mutable callback registry + still-running background audit goroutine (order-dependent teardown), not business logic in PatchConfig itself.
3) Dependency Map (Files and Symbols)
Test path
backend/internal/api/handlers/settings_wave4_test.goTestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPathTestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrorsregisterCreatePermissionDeniedHooksetupSettingsWave3DB
Handler/runtime path
backend/internal/api/handlers/settings_handler.goPatchConfigUpdateSetting
backend/internal/api/handlers/permission_helpers.gorespondPermissionErrorlogPermissionAudit
backend/internal/services/security_service.goNewSecurityServiceLogAuditprocessAuditEventsCloseFlush
CI execution context
scripts/go-test-coverage.sh(always runs backend tests with-race).github/workflows/codecov-upload.yml(usesscripts/go-test-coverage.shfor both push and PR)
4) Flake Vector Assessment
- Timing/Goroutines: High confidence root cause. Background audit goroutine outlives test branch and races with callback registry mutation.
- Shared state/global hooks: High confidence root cause. GORM callback registry is mutable shared state per DB instance.
- Order dependence: High confidence root cause. Preceding wave4 permission-error test influences subsequent test via asynchronous cleanup timing.
- DB locking/no-such-table noise: Secondary contributor (observed
security_auditsmissing logs), but not primary failure trigger. - Env vars (PR vs push): Low confidence as root cause for this test; same script and
-racepath are used in both contexts. - Log buffering: Not a primary root cause; race detector output indicates memory race in callback internals.
5) Stabilization Strategy (Minimal and Deterministic)
Recommended approach
-
Deterministic lifecycle shutdown for
SecurityServicein wave4 permission-error test- In
TestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrors, explicitly manage the service used forh.SecuritySvcand register teardown to flush/close it before callback removal side effects complete. - Ensure cleanup order prevents callback registry mutation while audit goroutine is still active.
- In
-
Reduce unnecessary async audit side effects in this wave4 path
- For tests that only assert HTTP permission error response (not audit persistence), avoid creating live async service when not required by assertion semantics.
- Keep behavior coverage for response contract while eliminating unnecessary goroutine work in this flaky sequence.
-
Harden test DB schema for adjacent audit paths
- In
setupSettingsWave3DB, includemodels.SecurityAuditmigration to remove noisyno such table: security_auditswrites from concurrent worker paths. - This reduces background retry/noise and improves determinism under race mode.
- In
-
Guard callback hook helper usage
- Keep callback registration/removal confined to narrow tests and avoid overlap with asynchronous writers on same DB handle.
- Maintain unique callback naming per test branch to prevent accidental collisions when future subtests are added.
6) EARS Requirements
- WHEN wave4 permission-error tests register temporary GORM callbacks, THE SYSTEM SHALL ensure all asynchronous
SecurityServiceaudit workers are fully stopped before callback removal occurs. - WHEN
TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPathruns with-race, THE SYSTEM SHALL complete without data race reports. - IF a test path uses
SecurityService.LogAudit, THEN the test DB setup SHALL include required audit schema to avoid asynchronous write failures due to missing tables. - WHILE running backend coverage in CI contexts (push and PR), THE SYSTEM SHALL produce deterministic pass/fail outcomes for this test sequence.
7) Implementation Tasks (Single-Scope)
-
Update
backend/internal/api/handlers/settings_wave4_test.go- Add explicit
SecurityServicelifecycle management inTestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrors. - Ensure teardown ordering is deterministic relative to callback cleanup.
- Keep
TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPathassertions unchanged (status + reload/cache call counts).
- Add explicit
-
Update
backend/internal/api/handlers/settings_wave3_test.go- Extend
setupSettingsWave3DBmigrations to includemodels.SecurityAudit.
- Extend
-
Validation
- Targeted race test loop:
cd backend && CHARON_ENCRYPTION_KEY="$(openssl rand -base64 32)" go test -race -count=50 -run 'TestSettingsHandlerWave4_UpdateSetting_ACLPathsPermissionErrors|TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPath' ./internal/api/handlers
- Targeted package race pass:
cd backend && CHARON_ENCRYPTION_KEY="$(openssl rand -base64 32)" go test -race -run 'TestSettingsHandlerWave4_' ./internal/api/handlers
- Standard backend CI-equivalent coverage command:
bash scripts/go-test-coverage.sh
- Targeted race test loop:
8) PR Slicing Strategy
- Decision: Single PR (small, isolated, low blast radius).
- Trigger rationale: Changes are constrained to wave4 settings tests and adjacent test helper DB schema.
- Slice PR-1:
- Scope: lifecycle/order hardening + helper schema migration only.
- Files:
backend/internal/api/handlers/settings_wave4_test.gobackend/internal/api/handlers/settings_wave3_test.go
- Validation gate: no race detector output in targeted loop; package tests stable under
-race; no assertion behavior drift in target flaky test.
- Rollback: Revert PR-1 if unintended changes appear in broader handlers suite; no production code path changes expected.
9) Acceptance Criteria
TestSettingsHandlerWave4_PatchConfig_SecurityReloadSuccessLogsPathis stable under repeated-raceruns.- No race detector warnings involving GORM callback compile/remove and
SecurityServiceaudit goroutine in this test sequence. - Test remains behaviorally equivalent (same API contract and assertions).
- Scope remains limited to this flaky test sequence and adjacent stabilization only.