Manual Test Plan — Viewing projections & subscriptions
Covers the read-only surface: projections list page, projection detail page, subscription detail page, per-store filtering and drill-in.
V-1 — Projections page renders one row per projection + one HWM row per store
| Field | Value |
|---|---|
| Setup | dotnet run --project src/BffHost (Full scenario, default). Wait for RepairShop, TripService, TripPublisher to appear in the Aspire dashboard as Running. |
| Action | Navigate to the Projections page (/projections) in the SPA. |
| Expected observation | The table renders one HWM row per (service, store) pair, then one row per projection beneath. For TripService you should see exactly one HWM row (single-store) and rows for TripProjection, DayProjection, DistanceProjection. For MultiStoreHost (PS#2 fix) you should see three HWM rows — one for each registered event store — with distinct sequence numbers. |
| How to verify | Each HWM row has name === 'HighWaterMark' and a non-empty store label in the Store column. For multi-store: assert at least 2 rows match el-table__row:has-text("HighWaterMark") and the row sequence numbers differ. Cross-check against GET /api/critterwatch/service-summaries/MultiStoreHost — shardStatesByStore should have ≥ 2 outer keys (one per store URI). The Playwright spec at src/FrontEnd/e2e/projections/multi-store-projections.spec.ts automates this assertion. |
V-2 — Projection detail page surfaces shard state + lag
| Field | Value |
|---|---|
| Setup | Same as V-1 — Full scenario, wait for traffic. Click into the TripProjection row from the Projections page. |
| Action | Wait for the Projection Detail page to mount (URL pattern /projection/<serviceId>/<shardName>). |
| Expected observation | Header shows projection name, lifecycle (Async), shard name (Trip:All). Below: current sequence, owning store's HWM, gap (events behind), state badge (Updated), agent status pill, last-advanced and last-heartbeat timestamps. Sparkline / sequence history chart populates from the rolling shardHistory buffer (last ~100 snapshots). |
| How to verify | Header text matches via .el-card__header:has-text("TripProjection"). The gap value should equal hwm - state.sequence (positive while TripPublisher is generating events; trends toward 0 when the publisher pauses). Cross-check GET /api/critterwatch/service-summaries/TripService → shardStatesByStore[<storeUri>][<shardName>] matches what's rendered. |
V-3 — Projection detail Dead Letter Events section is policy-aware (PS#3)
| Field | Value |
|---|---|
| Setup | Same as V-2, but click into an Incidents.Service projection (Trips publisher doesn't drive Incidents; you may need to drop a manual IncidentLogged via IncidentSamplePublisher first, or use the existing Incidents publisher which fires immediately on boot). |
| Action | Open the Apply Errors card on the Projection Detail page. |
| Expected observation | One of three states based on the monitored service's Options.Projections.Errors.SkipApplyErrors policy: 1. skipApplyErrors === true (default — Trips, TeleHealth, MultiStoreHost-Incidents) — current section: failures route to Wolverine's DLQ, View Related Dead Letters button visible. 2. skipApplyErrors === false (Incidents.Service after #316 lands the stop-on-error config) — el-alert warning Shard halts on apply error, no DLQ button, restart/rebuild guidance pointer. 3. undefined (older monitored service that hasn't bumped to JasperFx.Events 2.9.2+ / Marten 9.7.1+ / Polecat 4.4.1+) — soft-warn copy that doesn't presume DLQ routing. |
| How to verify | data-testid selectors: apply-error-policy-skip, apply-error-policy-stop, or apply-error-policy-unknown. The descriptor on the wire is at service.eventStores[storeIndex].projectionErrors.skipApplyErrors. PS#3 frontend test in src/FrontEnd/src/stores/__tests__/projections-store.test.ts covers the three branches in isolation. |
V-4 — Subscription detail page renders separately from projection detail
| Field | Value |
|---|---|
| Setup | Full scenario. Pick a service that registers an ISubscription (vs IProjection) — currently the Trip family doesn't register subscriptions explicitly; this scenario is partially gated by sample availability. If no subscription is registered, document that the Subscriptions tab on the service detail page is empty rather than missing. |
| Action | Navigate to a subscription's detail page once one exists. |
| Expected observation | Layout mirrors Projection Detail but the action set is constrained — no Rebuild (subscriptions can't be rebuilt the same way, only rewound to a sequence/timestamp). Surface: current sequence, lag against HWM, state, Rewind Subscription action. |
| How to verify | URL pattern /subscription/<serviceId>/<shardName>. The Rewind action's modal is the same component used on the projection detail page for rewinds (see RewindSubscription message). |
| Status | Gated on a sample that registers a subscription (vs a projection) — likely a future-task surface; flag as such. |
V-5 — Per-store filtering / drill-in (PS#2 fix)
| Field | Value |
|---|---|
| Setup | Run BffHost with CRITTERWATCH_SCENARIO=multi-store (the dedicated MultiStoreHost profile — AncillaryOnly mode, no main store). Wait for MultiStoreHost and MultiStorePublisher to appear in the Aspire dashboard. |
| Action | On the Projections page, open the Service filter dropdown and pick MultiStoreHost. Inspect the HWM rows that render. |
| Expected observation | Three HWM rows — one per ancillary store (Trips, Incidents, Telehealth) — each with their own sequence number drawn from ShardStatesByStore[storeUri][HighWaterMark]. Below them, projection rows for Itinerary, IncidentsByCategory, TelehealthComposite. Each projection's Behind column is computed against its own store's HWM — the Itinerary projection (on the Trips store) doesn't appear thousands of events behind just because the TelehealthComposite store's HWM is way ahead. |
| How to verify | UI: three distinct el-table__row:has-text("HighWaterMark") rows when filtered to MultiStoreHost. Each projection row's gap number can be reproduced by hwm_for_its_store - projection_sequence (positive and small under steady-state publisher traffic). API: GET /api/critterwatch/service-summaries/MultiStoreHost → shardStatesByStore keyed by 3 store URIs with HighWaterMark entries each at distinct sequence numbers. |
