Manual Test Plan — Projection & subscription actions
Covers the write surface: pause, restart, rebuild, rewind. Long-running rebuild scenarios are in the dedicated long-running rebuilds plan; this one stays at the second-or-two scale.
A-1 — Pause a running projection
| Field | Value |
|---|---|
| Setup | dotnet run --project src/BffHost (Full scenario). Wait for TripService and TripPublisher to be Running. Confirm TripProjection shows Updated action and an advancing sequence on the Projections page. |
| Action | On the Projection Detail page, click Pause. Confirm the operator confirmation modal. |
| Expected observation | Within 1–2 seconds the projection's state badge changes to Paused, the agent status pill turns amber, the Pause Reason field populates (operator-driven if the extended-progression-tracking columns are enabled). The sequence freezes at the value it had when paused. The Pause button is replaced by Restart. |
| How to verify | UI: el-tag:has-text("Paused") on the projection's row. The agent_status column in mt_event_progression shows Paused. API: service.shardStates[<shardName>].action === 'Paused'. SQL: SELECT name, agent_status, pause_reason FROM trips.mt_event_progression WHERE name = 'Trip:All'; |
A-2 — Restart a paused projection
| Field | Value |
|---|---|
| Setup | Continue from A-1 with TripProjection paused. |
| Action | Click Restart. |
| Expected observation | State badge transitions Paused → Updated within ~1 second. Sequence resumes advancing from where it paused — no replay. Agent status returns to Running. Sparkline shows a flat segment during the pause then resumes the climb. |
| How to verify | UI: state badge change. API: service.shardStates[<shardName>].action === 'Updated'. SQL: agent_status column flips back to Running. The first post-restart sequence value should equal the last pre-pause value (no gap, no replay). |
A-3 — Rebuild a small projection from scratch
| Field | Value |
|---|---|
| Setup | Full scenario. Confirm DistanceProjection (smallest of the Trip projections) shows Updated and an established sequence ≥ ~50 from prior publisher activity. |
| Action | Click Rebuild on the Distance projection's detail page. Confirm the (heavy) operator confirmation modal. |
| Expected observation | Projection's state badge changes through Rebuilding → Updated. Sequence drops to 0 then climbs back to HWM as the rebuild replays the event stream. For a small projection this completes in 1–5 seconds; the sparkline shows the entire run. The Pause / Restart buttons are disabled during rebuild. |
| How to verify | UI: state badge transitions visible in the State Transitions Timeline card. SQL: SELECT name, last_seq_id, rebuild_threshold FROM trips.mt_event_progression WHERE name = 'Distance:All'; — last_seq_id resets to 0 then climbs. API: service.shardStates[<shardName>].action cycles through the rebuild states. |
A-4 — Rewind a subscription to a specific sequence
| Field | Value |
|---|---|
| Setup | Full scenario with publisher idle (or paused) so the HWM is stable. Pick a projection with > 100 events advanced (TripProjection typically). |
| Action | On the Projection Detail page, open the Rewind Subscription modal. Pick mode To Sequence, enter a target sequence ≤ current sequence (e.g. current - 50). Submit. |
| Expected observation | Projection sequence drops to the target value and starts re-advancing. State briefly shows Stopped during the rewind, then resumes Updated. Events between the target sequence and the previous high-water mark are re-applied — visible side effects depend on the projection (idempotent projections converge; non-idempotent projections see duplicates, which the rewind action explicitly warns about). |
| How to verify | API: service.shardStates[<shardName>].sequence drops to the requested value then climbs. The Rewind Result card on the page shows Success=true with the target sequence echoed. The pre-existing integration test Tests.Integration.subscription_rewind.rewind_subscription_to_sequence_via_critterwatch exercises the same flow against DistanceProjection. |
A-5 — Rewind to a timestamp
| Field | Value |
|---|---|
| Setup | Same as A-4. Note the current wall-clock time before the action. |
| Action | Rewind modal → To Timestamp → pick a timestamp 30 seconds ago. Submit. |
| Expected observation | The daemon resolves the timestamp into a sequence floor (via IEventDatabase.FindEventStoreFloorAtTimeAsync), then rewinds to that floor. The Rewind Result card surfaces both the requested timestamp and the resolved sequence floor. |
| How to verify | The resolved sequence in the result must be ≤ the sequence the projection was at 30s ago. The API echoes both the timestamp and the resolved floor. SQL: the projection's last_seq_id after rewind matches the resolved floor. |
A-6 — Per-tenant projection action (phase 3a)
| Field | Value |
|---|---|
| Setup | Switch BffHost to CRITTERWATCH_SCENARIO=tenancy to load the MTTrip family. Wait for MTTripService and MTTripPublisher to be Running. Open the Projection Detail page for a tenant-aware projection (MTTripProjection). |
| Action | In the Pause / Restart / Rebuild modal, scope the action to a specific tenant id (e.g. tenant1). Submit. |
| Expected observation | Only the tenant-scoped shard (e.g. MTTrip:All:tenant1) transitions; other tenants' shards stay Updated. The tenant-scoped agent URI is event-subscription://MTTripService/MTTrip:All/tenant1 (matches the JasperFx ShardName tenant-grammar). |
| How to verify | UI: the per-tenant view selector on the Projections page shows the tenant-scoped shard with a Paused badge while other tenants under the same projection name keep their Updated badge. SQL: SELECT name, last_seq_id, agent_status FROM mttrips.mt_event_progression WHERE name LIKE 'MTTrip:%'; — only the row for the targeted tenant has agent_status = Paused. |
A-7 — Pause-then-rebuild lock-out (negative case)
| Field | Value |
|---|---|
| Setup | Full scenario. Pause TripProjection via A-1. |
| Action | Try to click Rebuild on the paused projection. |
| Expected observation | The Rebuild button is disabled with a tooltip explaining "Restart the projection before rebuilding". Or — if the button is clickable — the rebuild fails with a clear error message and no state change. The point of the test is the lockout exists at one layer or the other. |
| How to verify | UI: data-testid="rebuild-button" has disabled attribute, or clicking surfaces an el-message-box with an error. The RebuildProjection integration test in Tests.Integration.projection_commands covers the happy path; the lockout is the operator-facing affordance on top of that. |
A-8 — Cross-scenario admin command on an ancillary store (PS#2 regression gate)
| Field | Value |
|---|---|
| Setup | Full scenario or multi-store scenario. Wait for MultiStoreHost to be Running with three event stores surfaced. |
| Action | Pause the IncidentsByCategory projection (lives on the IIncidentsStore ancillary). |
| Expected observation | Only that ancillary projection pauses. The main Trips projection (Itinerary) and the other ancillary's projection (TelehealthComposite) continue advancing. |
| How to verify | UI: only the IncidentsByCategory:All row shows Paused. SQL across schemas: SELECT name, agent_status FROM multistore_incidents.mt_event_progression; — paused row visible. SELECT name, agent_status FROM multistore_trips.mt_event_progression; — all rows Running. SELECT name, agent_status FROM multistore_telehealth.mt_event_progression; — all rows Running. The PS#2 PR-D integration test in Tests.Integration.multi_store_admin_commands automates this flow. |
Cross-reference
- Pre-existing automated coverage:
Tests.Integration.projection_commands,Tests.Integration.subscription_rewind,Tests.Integration.multi_store_admin_commands(PS#2 PR-D). - The per-tenant action surface depends on JasperFx Phase 3a + #209 — both landed; see CW7 for the per-tenant rewind handler fix.
