diff --git a/src/Netclaw.Actors.Tests/Channels/Contracts/SessionBindingContractTests.cs b/src/Netclaw.Actors.Tests/Channels/Contracts/SessionBindingContractTests.cs index 5698d0657..a4b3f5e46 100644 --- a/src/Netclaw.Actors.Tests/Channels/Contracts/SessionBindingContractTests.cs +++ b/src/Netclaw.Actors.Tests/Channels/Contracts/SessionBindingContractTests.cs @@ -477,6 +477,14 @@ public async Task Approval_response_sends_feedback() var actor = CreateBindingActor(sid, pipeline, detector); + // Gate on pipeline creation (persistent-actor recovery + init round-trip) + // before polling for rendered output. Under CI CPU starvation the cold + // start alone can exceed the default 3s AwaitAssert budget — the poll loop + // observed only ~2 attempts before the deadline on the Windows runner — so + // a linear await on the real readiness signal removes the race. Matches the + // Reminder_delivery_* and Stashes_messages_during_init siblings. + await pipeline.Created.WaitAsync(ct); + // Wait for approval to be rendered await AwaitAssertAsync(() => { diff --git a/src/Netclaw.Actors.Tests/Sessions/SessionMemoryObserverActorTests.cs b/src/Netclaw.Actors.Tests/Sessions/SessionMemoryObserverActorTests.cs index 83a42f847..fd7e848dd 100644 --- a/src/Netclaw.Actors.Tests/Sessions/SessionMemoryObserverActorTests.cs +++ b/src/Netclaw.Actors.Tests/Sessions/SessionMemoryObserverActorTests.cs @@ -459,6 +459,20 @@ public async Task DistillMemories_only_persists_accepted_proposals_for_future_de var observer = CreateObserver("accepted-only", client: firstClient); var replyProbe = CreateTestProbe("accepted-only-probe"); + // Gate on recovery before timing the distillation round-trip below. This is + // an early persistent actor, so its journal/snapshot plugin cold start + + // recovery otherwise races the fixed 5s ExpectMsg (observed on the Windows + // runner: "session_observer_recovery_complete" logged right at the deadline). + // Persistent actors stash commands until RecoveryCompleted; an empty + // RecordAcceptedDistillationProposals is answered immediately post-recovery + // with no Persist and no state change, so awaiting it is a side-effect-free + // readiness ack. The generous ceiling absorbs cold start without polling; the + // behavioral 5s windows are then measured from a warm actor. + await observer.Ask( + new RecordAcceptedDistillationProposals([]), + TimeSpan.FromSeconds(30), + TestContext.Current.CancellationToken); + observer.Tell(new SendUserMessage { SessionId = new SessionId("test-channel/accepted-only"),