## PR2 testing and release-gate plan ### Current baseline - **Backend:** 842 tracked `test_*` functions under `tests/`; CI runs the complete suite once. - **Frontend:** 187 tracked Vitest cases; CI runs Vitest, `vue-tsc`, and the production build. - **CI workflow:** `.github/workflows/portfolio-phase3-integration.yml` - Backend full suite - Frontend unit tests/typecheck/build - Migration, OpenAPI, security, Ruff, compile, and repository-safety controls - **No browser E2E gate exists:** no Playwright config, test project, or CI browser job. - I could not execute the suites locally because this worktree has neither an installed frontend dependency tree nor a usable Python virtualenv/`uv`. Analysis is grounded in tracked tests, source, Makefile, and CI definitions. - Worktree remained clean; **no files modified**. ## Important existing gaps 1. **Filters are not shared** - `HouseholdOverviewPage.vue` owns only `month`. - `HouseholdTransactionsPage.vue` independently owns period/account/type/category/merchant/source/review and directly manipulates `window.history`. - Budget pages separately hardcode `2026-05` or `2026`. - `useQueryFilters` exists and tests URL restoration, but these household/cockpit surfaces do not use it. 2. **Reconciliation is only partial** - `test_financial_semantics_reconcile_refunds_transfers_categories_and_net` already proves overview/category totals and transaction pagination. - It does not assert one acceptance identity across **KPI → category sums → budget actuals → filtered transaction effects**, nor the ±CHF 0.01 tolerance boundary. 3. **Chart behavior is not tested semantically** - Current tests mainly assert chart containers exist. - `BudgetStatusPage.vue::barHeight()` scales each month independently against `max(actual, budget, 1)`, not one shared chart maximum. - Zero values are rendered as a minimum **6px bar**, so zero is visually non-zero. - The UI says “Linie = Monatsbudget,” but no budget-line element is rendered. - All 12 months are returned as numeric zeroes; future months are not distinguished from observed zero months. 4. **Household transaction pagination is comparatively strong** - Backend tests cover cursor pagination and stale snapshot rejection. - Frontend tests cover append behavior, true totals, stale-cursor restart, and unmount races. - Missing: filter preservation through page 2, duplicate prevention at tied sort keys, and browser-level loading at narrow widths. --- # Exact regression tests to add ## 1. Canonical synthetic acceptance fixture Create `tests/fixtures/household_cockpit_synthetic.py` as a fixture builder rather than committing a database. Seed **May 2026** with: | Semantic row | CHF effect | |---|---:| | Salary income | +6,500.00 income | | Housing expense | 1,200.00 expense | | Food expense | 120.01 expense | | Linked food refund | −20.00 expense | | Health expense | 49.99 expense | | Internal transfer pair | 900.00 neutral volume | | June future expense | excluded from May | Expected May identities: - Income: **CHF 6,500.00** - Expense: **CHF 1,350.00** - Balance/net: **CHF 5,150.00** - Expense categories: - Housing: 1,200.00 - Food: 100.01 - Health: 49.99 - Category expense sum: **CHF 1,350.00** - Neutral transfer volume: **CHF 900.00** - Transfers contribute exactly zero to income, expense, and net. Add a separate pagination builder producing **101 confirmed transactions** with: - repeated dates and timestamps, - stable unique IDs, - at least three accounts/categories/sources, - one filter matching 53 rows, - no productive/raw identifiers in expected HTTP responses. ## 2. Backend reconciliation tests Add `tests/unit/test_household_cockpit_v1.py`. ### `test_cockpit_reconciles_kpis_categories_budgets_and_transactions_to_one_cent` For identical canonical filters: - Call the new/extended cockpit contract. - Call `get_household_financial_summary`. - Sum category expense values using `Decimal`. - Sum returned filtered transaction financial effects. - Compare budget actual for each category. - Assert all identities differ by no more than `Decimal("0.01")`. - Assert the exact fixture values above. - Assert transfers are visible but neutral. - Assert the June transaction is absent. ### `test_cockpit_reconciliation_boundary_accepts_one_cent_and_rejects_two` Parameterized probes: - KPI/category difference `0.00` → reconciled - `0.01` and `-0.01` → reconciled - `0.02` and `-0.02` → mismatch/fail-closed Do not use float conversion; assert `Decimal` quantization to `0.01`. ### `test_shared_filters_apply_identically_to_every_cockpit_section` Parameterize: - month - date range - account - transaction type - category - merchant - source - review status For each filter assert: - response echoes normalized `filters_applied`, - KPI values equal filtered transaction effects, - category rows contain only matching effects, - budget actuals use the same scope, - transaction total/page scope matches, - chart series derives from the same rows. Also assert malformed month/date/type is HTTP 422 rather than silently widened. ### `test_chart_contract_has_shared_scale_observed_zero_future_null_and_budget_line` Expected chart contract should include explicit metadata, for example: - `scale.max_chf` - `points[].actual_chf` - `points[].observation_status` - `budget_line_chf` Assertions: - one maximum is used for every bar; - observed zero is numeric `"0.00"` with status `observed`; - future month is `actual_chf: null`, status `future`; - future transactions do not leak into current/as-of totals; - the budget line is present even when actual is zero; - max includes both actuals and budget-line values; - empty/all-zero data uses a deterministic non-zero axis domain without inventing an actual bar. ### `test_category_drilldown_reconciles_header_breakdown_and_transactions` For Food/May: - header actual = `100.01`; - breakdown sum = `100.01`; - transaction effect sum = `100.01`; - linked refund inherits the original expense category; - returned transaction link carries the exact shared filter scope; - another category/account cannot leak into the drilldown. ### `test_household_transactions_cursor_pages_are_complete_stable_and_filter_bound` Using the 101-row fixture: - page size 48 yields 48/48/5; - concatenated tokens are unique and equal the expected ordered set; - tied date/timestamp rows are ordered by ID without loss or duplication; - `filtered_total == 53`, while `total_all == 101`; - every subsequent request retains all shared filters; - a cursor reused under different filters returns 409; - mutation between pages returns 409. ### `test_household_cockpit_openapi_contract_and_read_only_behavior` Assert: - exact GET paths and response fields are in OpenAPI; - all cockpit/filter/drilldown/page reads leave `conn.total_changes` unchanged; - opaque tokens only—no DB IDs, source references, file names, or raw fingerprints. ## 3. Frontend contract/component tests Create focused files instead of expanding the 61KB `HouseholdUx.test.ts` further: ### `frontend/src/pages/HouseholdCockpitFilters.test.ts` Tests: - URL initializes every shared filter. - Changing a filter updates one canonical query. - KPI, categories, chart, drilldown, and transactions receive the same filter object. - back/forward and reload restore the same state. - changing any filter clears pagination and obsolete drilldown state. - late responses from old filters cannot overwrite the current cockpit. - aliases such as `category_id` normalize to the canonical key. Extend `useQueryFilters.test.ts` for: - household-specific enum values (`reviewed/open/ignored`); - alias conflict precedence; - preserving unrelated router state/hash; - empty/default filter serialization. ### `frontend/src/pages/HouseholdCockpitReconciliation.test.ts` Mount with the canonical fixture and assert visible values: - CHF 6,500.00 - CHF 1,350.00 - CHF 5,150.00 - category values sum to CHF 1,350.00 - budget actual equals category actual - transfer CHF 900.00 is labeled neutral - mismatch > CHF 0.01 renders a blocking integrity state, not silently rounded agreement. ### `frontend/src/components/household/HouseholdChart.test.ts` Prefer extracting a pure chart-model builder and test it directly: - all bars divide by the same `max_chf`; - observed zero gets zero height—not a 6px data bar; - future point is omitted/gapped and labeled “Noch nicht eingetreten,” not zero; - budget line dataset exists and spans the intended months; - zero-only series remains renderable; - long category labels remain available in accessible text; - tooltip values preserve two CHF decimals. ### `frontend/src/pages/HouseholdCategoryDrilldown.test.ts` Assert: - chart/category activation sends the active shared filters plus category ID; - header, breakdown, and transactions show the same total; - linked refund is displayed in the original category; - “open transactions” URL targets `/household/transactions` with canonical filters; - closing restores focus; - stale drilldown responses are ignored. ### Extend `HouseholdUx.test.ts` pagination coverage Add: - all active filters are present in the page-2 API request, not only cursor; - applying/resetting a filter while page 2 is pending discards the old response; - duplicate transaction tokens across pages are not rendered twice or produce a contract error; - load-more error keeps page 1 visible and supports retry; - `aria-busy`/button loading state is correct. ## 4. Browser evidence and E2E Add: - `frontend/playwright.config.ts` - `frontend/e2e/household-cockpit.spec.ts` - deterministic API fixture routing or a synthetic temporary backend database - ignored output directories for screenshots/traces Run the same acceptance flow at exact CSS viewports: - **1440 × 900** - **820 × 1180** - **390 × 844** Required assertions at each viewport: 1. Load the canonical May URL. 2. Verify all shared controls and exact KPI values. 3. Apply account/category/source filters. 4. Verify URL and all sections update consistently. 5. Inspect chart: - same visual scale, - observed-zero baseline, - future gap, - visible budget line. 6. Open category drilldown and reconcile total/breakdown/transactions. 7. Open household transactions and load page 2. 8. Use browser back/forward and verify state restoration. 9. Assert: - no horizontal document overflow; - no clipped filter actions or pagination button; - minimum 44px interactive targets; - no console errors, failed requests, or uncaught exceptions. Store evidence as CI artifacts, not tracked product files: - `test-results/household-cockpit/1440.png` - `test-results/household-cockpit/820.png` - `test-results/household-cockpit/390.png` - Playwright trace on failure --- # Existing tests likely to break High-confidence update targets: - **`frontend/src/pages/HouseholdUx.test.ts`** - Overview card labels/counts and independent month behavior. - Existing transaction filter and pagination call expectations. - Direct `window.history` assumptions if moved to router/query composable. - **`frontend/src/api/household.test.ts`** - New cockpit/drilldown paths, filter serialization, or response envelope. - **`frontend/src/composables/useQueryFilters.test.ts`** - Schema/default/alias changes. - **`frontend/src/pages/BudgetAnalyticsDashboardV2.test.ts`** - Hardcoded old dashboard mocks and container-only chart expectations. - **`frontend/src/pages/BudgetPhase1.test.ts`** - Current native bar assumptions and “Linie = Monatsbudget” test despite no actual line. - **`frontend/src/pages/MobileUx.test.ts`** - Mock status-row shape and breakpoint/layout assumptions. - **`frontend/src/stores/budgetStores.test.ts`** - `loadCockpit('2026-05')` if the cockpit store moves to shared filters. - **`tests/unit/test_budget_analytics_dashboard_v2.py`** - Old cockpit/status/chart response shapes and hardcoded numeric expectations. - **`tests/unit/test_household_import_v1_golden.py`** - Exact read-model keys, overview shape, and pagination/filter contracts. - **`tests/unit/test_household_review_corrections_v1.py`** - Category corrections must invalidate/recompute the shared cockpit and drilldown versions. - Any schema-version assertion such as `get_schema_version(conn) == 48` if PR2 adds a migration. Do not weaken the existing stale-cursor, fail-closed FX/refund, transfer neutrality, or privacy assertions while updating fixtures. --- # Release gates ## Required PR checks 1. **Focused backend** ```bash python -m pytest -q \ tests/unit/test_household_cockpit_v1.py \ tests/unit/test_household_import_v1_golden.py \ tests/unit/test_household_review_corrections_v1.py \ tests/unit/test_budget_analytics_dashboard_v2.py ``` 2. **Focused frontend** ```bash npm --prefix frontend test -- \ src/pages/HouseholdCockpitFilters.test.ts \ src/pages/HouseholdCockpitReconciliation.test.ts \ src/components/household/HouseholdChart.test.ts \ src/pages/HouseholdCategoryDrilldown.test.ts \ src/pages/HouseholdUx.test.ts ``` 3. **Complete existing gates** ```bash make PYTHON=.venv/bin/python verify git diff --check ``` 4. **Browser gate** ```bash npm --prefix frontend run test:e2e -- household-cockpit.spec.ts ``` 5. **CI controls update** - Add Sprint 17B files/tests to the Ruff controls list. - Add a separate Playwright job with locked browser installation and uploaded screenshots/traces. - Replace the stale job label **“Backend – 667 tests”** with a non-counted name or update the floor deliberately; the current source already contains substantially more test functions. - Keep browser evidence separate from the fast Vitest suite but mandatory for PR2. ## Merge-blocking conditions PR2 must not merge if any of these occur: - KPI, category, budget actual, drilldown, or transaction effect differs by **more than CHF 0.01**. - Any section applies a different filter scope. - Future periods are rendered as observed zero. - Zero values render as non-zero data bars. - Budget line is absent or uses a different scale. - Pagination loses, duplicates, or reorders rows across page boundaries. - A stale or filter-mismatched cursor is accepted. - Browser flow fails at 1440, 820, or 390, has horizontal overflow, or logs console/API errors. - Full backend/frontend/security/build gates fail. - Browser evidence was produced against non-synthetic or stale server data.