PR 2467 Testing Handover¶
Context handover for continuing testing work on PR #2467 in a fresh session.
Current state¶
- Branch:
feat/signalr-active-reviewer-tracking - Head:
59a9e7c72(all pushed to origin) - All existing tests green: 801 PM core, 455 API, 216 PM endpoint, 1377 web
What was done in this session (8 commits)¶
-
7705bd958— Server-side fix: removed!IsSuspendedfilter fromExtractionInfo.SessionTalliesso suspended sessions hold their slot during the 2h grace window (Design A). Flipped 3 tests, added 2 new (domain + integration). -
9fa4dbd17— Client UX: disconnection countdown banner with 3 states (hidden first 10 min, countdown, expired).DisconnectionBannerComponent,StudyPresenceStoredisconnection tracking, wired viastudy-presence.init.ts. -
e63b3890a— Timing fix: banner shows after 10 min of disconnection (not last 10 min). Improved messaging with Comfort-Explain-Act pattern. -
62c3f39cf— Atomic save guard:SaveWithCapacityGuardAsynconStudyRepositoryusesFindOneAndReplacewith capacity filter. Wired intoReviewController.SubmitSession, returns 409 on rejection. 2 integration tests (real Mongo). -
5ef1fd7da— Rejoin failure handling:rejoinFailed$subject onSignalRService, store setssessionRemovedon failure, existing surplus/expired UX takes over. -
03a29dcbf— Copy centralisation:session-messages.tswith all 9 reviewer-facing messages using "review spot" domain term. All components updated to reference constants. Lifecycle timestamps added toActiveReviewSession(JoinedAtUtc, StartedAnnotatingAtUtc) andAnnotationSession(CreatedAtUtc, CompletedAtUtc). -
59a9e7c72— Timestamp carry-forward:ExpiredReviewSessionnow receives JoinedAtUtc and StartedAnnotatingAtUtc from the active session when it's removed by cleanup consumers.
Testing gaps to fill (the work for the next session)¶
1. Lifecycle timestamp unit tests (Easy, ~30 min)¶
Add to ActiveReviewSessionTests.cs:
ActiveReviewSession_ShouldSetJoinedAtUtc_OnConstruction— verifyJoinedAtUtcis set to approximatelyDateTime.UtcNowin the constructor.MarkDirty_ShouldSetStartedAnnotatingAtUtc_OnFirstCall— verifyStartedAnnotatingAtUtcis set on firstMarkDirty()call.MarkDirty_ShouldNotOverwriteStartedAnnotatingAtUtc_OnSubsequentCalls— verify the timestamp is only set once (uses??=pattern).
Add to a new or existing AnnotationSessionTests file:
AnnotationSession_ShouldSetCreatedAtUtc_OnConstruction— verifyCreatedAtUtcis set.UpdateStatus_ToCompleted_ShouldSetCompletedAtUtc— verifyCompletedAtUtcis set whenUpdateStatus(Completed)is called.UpdateStatus_ToCompleted_ShouldNotOverwriteCompletedAtUtc— verify the timestamp is only set once.
2. ExpiredReviewSession timestamp carry-forward test (Easy, ~15 min)¶
Add to ExpiredReviewSessionTests.cs:
CreateFromActiveSession_ShouldCarryForwardJoinedAtUtc— create anActiveReviewSession, set JoinedAtUtc, callCreateFromActiveSessionAsync, verify the expired record has the correctJoinedAtUtc.CreateFromActiveSession_ShouldCarryForwardStartedAnnotatingAtUtc— same for the form-dirty timestamp.
File: src/libs/project-management/SyRF.ProjectManagement.Mongo.Data/Repositories/ExpiredReviewSessionRepository.cs
Test file: src/services/api/SyRF.API.Endpoint.Tests/SignalR/ExpiredReviewSessionTests.cs
3. RemoveSuspendedSessionConsumer integration test (Medium, ~45 min)¶
The consumer at src/services/project-management/SyRF.ProjectManagement.Endpoint/Consumers/RemoveSuspendedSessionConsumer.cs
loads the study, checks IsSuspended, removes the session, creates an
ExpiredReviewSession, and saves. After the tally fix, the save should
recompute tallies correctly (suspended session removed → tally drops).
Add to SessionTimeoutConsumerTests.cs:
RemoveSuspendedSession_ShouldRemoveSessionAndCreateExpiredRecord— basic flow.RemoveSuspendedSession_ShouldSkipIfReconnected— verify the consumer is a no-op whenIsSuspendedis false (reviewer reconnected before the consumer fired).RemoveSuspendedSession_ShouldUpdateStoredTally— after removal, the storedTotalAllocatedSessionCountshould decrease.
4. ReviewController save guard rejection test (Medium, ~30 min)¶
Add to ReviewControllerTests.cs:
SubmitSession_ShouldReturn409_WhenCapacityGuardRejects— mockSaveWithCapacityGuardAsyncto return false, verify 409 response with the correct message body.SubmitSession_ShouldLogError_WhenCapacityGuardRejects— verify the logger is called at Error level (for Sentry capture).
The controller already has a CreateController helper and mock setup
for IPmUnitOfWork.
5. E2E tests (Hard, separate session with infra)¶
These need the full containerized E2E infrastructure running.
Disconnection scenario (adapt annotation-capacity.spec.ts pattern):
- Reviewer A opens study, starts typing, connection drops (simulate via network interception in Playwright).
- Verify disconnection banner appears after 10 min (use clock mocking).
- Verify form locks after 2h (clock mocking).
- Reconnect, verify auto-rejoin.
Save guard rejection (new test):
- Reviewer A opens study with capacity=1, starts typing.
- Reviewer B is granted the slot while A is suspended (directly manipulate DB to simulate the race).
- A's connection comes back, A tries to save → 409.
- Verify the rejection is surfaced to the user.
These E2E tests are complex and require infra setup. Consider labelling the
PR with run:e2e-full after the unit/integration gaps are filled.
Key files to look at¶
Domain model (timestamps)¶
src/libs/project-management/SyRF.ProjectManagement.Core/Model/StudyAggregate/ActiveReviewSession.cssrc/libs/project-management/SyRF.ProjectManagement.Core/Model/StudyAggregate/AnnotationSession.cssrc/libs/project-management/SyRF.ProjectManagement.Core/Model/ExpiredReviewSession.cs
Capacity guard¶
src/libs/project-management/SyRF.ProjectManagement.Mongo.Data/Repositories/StudyRepository.cs—SaveWithCapacityGuardAsyncsrc/services/api/SyRF.API.Endpoint/Controllers/ReviewController.cs— 409 path
Consumers¶
src/services/project-management/SyRF.ProjectManagement.Endpoint/Consumers/RemoveSuspendedSessionConsumer.cssrc/services/project-management/SyRF.ProjectManagement.Endpoint.Tests/SessionTimeoutConsumerTests.cs
Client-side¶
src/services/web/src/app/study-presence/session-messages.ts— centralised copysrc/services/web/src/app/study-presence/study-presence.store.ts— disconnection statesrc/services/web/src/app/shared/disconnection-banner/disconnection-banner.component.ts
Copy review document¶
docs/planning/session-copy-review.md— full before/after for all 9 messages
Design decisions made in this session¶
-
Design A (hold slot during grace) — suspended sessions count toward capacity. Consistent with idle session treatment. The original
!IsSuspendedfilter was the lone inconsistency. -
"Review spot" as the reviewer-facing term for
ActiveReviewSession. -
No dirty-form timeout — once the form is dirty, the idle timer is cancelled and nothing replaces it. A reviewer holding a spot with one typed character is a known gap, flagged in
session-copy-review.md. -
Timestamps are additive — new fields on existing entities, no changes to existing logic. Carried forward to
ExpiredReviewSession. -
Copy follows Comfort > Explain > Act — no em dashes in displayed text, no false "saved work is safe" claims, save-regularly advice on #9.