Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/Netclaw.Actors.Tests/Sessions/ApprovalRehydrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ await sessionManager.Ask<SessionJoined>(new JoinSession(rejoinProbe)
// Idle timeout with the recovered approval still pending: the session
// passivates (approval state is journaled) instead of deferring forever.
var escapedId = Uri.EscapeDataString(sessionId.Value);
// The resolve budget bounds the session spawn and recovery under a
// starved CI scheduler. It does not measure correctness.
var child = await Sys.ActorSelection($"/user/session-manager/{escapedId}")
.ResolveOne(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
.ResolveOne(TimeSpan.FromSeconds(15), TestContext.Current.CancellationToken);
Watch(child);
child.Tell(new LeaveSession(rejoinProbe) { SessionId = sessionId });
child.Tell(ReceiveTimeout.Instance);
Expand Down Expand Up @@ -1516,8 +1518,10 @@ await sessionManager.Ask<SessionJoined>(new JoinSession(subscriber)
await subscriber.ExpectMsgAsync<SessionJoined>(cancellationToken: TestContext.Current.CancellationToken);

var escapedId = Uri.EscapeDataString(sessionId.Value);
// The resolve budget bounds the session spawn and recovery under a
// starved CI scheduler. It does not measure correctness.
var child = await Sys.ActorSelection($"/user/session-manager/{escapedId}")
.ResolveOne(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
.ResolveOne(TimeSpan.FromSeconds(15), TestContext.Current.CancellationToken);
Watch(child);

// Drop the subscriber and force the idle timeout so the session enters
Expand Down Expand Up @@ -1550,8 +1554,13 @@ await sessionManager.Ask<SessionJoined>(new JoinSession(subscriber)
private async Task ColdRespawnAsync(SessionId sessionId)
{
var escapedId = Uri.EscapeDataString(sessionId.Value);
// The resolve waits for the session child to finish its spawn and its
// Akka.Persistence recovery. The budget bounds that multi-hop startup
// under a starved CI scheduler. It does not measure correctness. About
// twenty cold-respawn tests call this helper, so a short budget makes
// the whole group flake at once.
var child = await Sys.ActorSelection($"/user/session-manager/{escapedId}")
.ResolveOne(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
.ResolveOne(TimeSpan.FromSeconds(15), TestContext.Current.CancellationToken);
Watch(child);
Sys.Stop(child);
await ExpectTerminatedAsync(child, cancellationToken: TestContext.Current.CancellationToken);
Expand Down
18 changes: 14 additions & 4 deletions src/Netclaw.Actors.Tests/Sessions/ErrorCorrelationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------
using Akka.Actor;
using Akka.Configuration;
using Akka.Hosting;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -23,6 +24,15 @@ namespace Netclaw.Actors.Tests.Sessions;
/// </summary>
public sealed class ErrorCorrelationTests(ITestOutputHelper output) : LlmSessionTestBase(output)
{
// LlmSessionTestBase seals ConfigureAkka, so the raise goes through the
// Config property seam instead. The stock single-expect-default is 3
// seconds. That value measures scheduler load on a starved CI runner. It
// does not measure correctness. Production allows 30 seconds for a
// comparable ack-after-work handshake — see
// ProactiveSendFormatting.ProactiveThreadAckTimeout.
protected override Config? Config =>
ConfigurationFactory.ParseString("akka.test.single-expect-default = 15s");

private readonly FailingChatClient _chatClient = new();

protected override void ConfigureSessionServices(IServiceCollection services)
Expand Down Expand Up @@ -67,7 +77,7 @@ await sessionManager.Ask<CommandAck>(new SendUserMessage
Content = "trigger error"
}, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);

var error = await subscriber.ExpectMsgAsync<ErrorOutput>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var error = await subscriber.ExpectMsgAsync<ErrorOutput>(cancellationToken: TestContext.Current.CancellationToken);

Assert.Equal(ErrorCategory.ProviderFailure, error.Category);
Assert.NotEqual(Guid.Empty, error.CorrelationId);
Expand Down Expand Up @@ -95,7 +105,7 @@ await sessionManager.Ask<CommandAck>(new SendUserMessage
Content = "first"
}, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);

var firstError = await subscriber.ExpectMsgAsync<ErrorOutput>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var firstError = await subscriber.ExpectMsgAsync<ErrorOutput>(cancellationToken: TestContext.Current.CancellationToken);
await subscriber.ExpectMsgAsync<TurnCompleted>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);

await sessionManager.Ask<CommandAck>(new SendUserMessage
Expand All @@ -104,7 +114,7 @@ await sessionManager.Ask<CommandAck>(new SendUserMessage
Content = "second"
}, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);

var secondError = await subscriber.ExpectMsgAsync<ErrorOutput>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var secondError = await subscriber.ExpectMsgAsync<ErrorOutput>(cancellationToken: TestContext.Current.CancellationToken);
await subscriber.ExpectMsgAsync<TurnCompleted>(TimeSpan.FromSeconds(3), cancellationToken: TestContext.Current.CancellationToken);

Assert.NotEqual(firstError.CorrelationId, secondError.CorrelationId);
Expand Down Expand Up @@ -134,7 +144,7 @@ await sessionManager.Ask<CommandAck>(new SendUserMessage
Content = "trigger timeout"
}, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);

var error = await subscriber.ExpectMsgAsync<ErrorOutput>(TimeSpan.FromSeconds(5), cancellationToken: TestContext.Current.CancellationToken);
var error = await subscriber.ExpectMsgAsync<ErrorOutput>(cancellationToken: TestContext.Current.CancellationToken);

Assert.Equal(ErrorCategory.Timeout, error.Category);
Assert.NotEqual(Guid.Empty, error.CorrelationId);
Expand Down
2 changes: 1 addition & 1 deletion src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,7 @@ public async Task Approve_once_does_not_leak_between_subagent_tool_calls()
Task = "Run the same approval-gated tool twice",
Timeout = TimeSpan.FromSeconds(5)
},
TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
ApprovalAskTimeout, TestContext.Current.CancellationToken);

Assert.True(result.Success);
Assert.Equal(2, approvalBridge.RequestCount);
Expand Down
11 changes: 9 additions & 2 deletions src/Netclaw.Actors.Tests/Tools/ShellApprovalHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ await approvalService.RecordApprovalCandidatesAsync(

if (persistentSeeds.Count > 0)
{
await approvalActor.GracefulStop(TimeSpan.FromSeconds(5));
// The stop waits for a persistence flush and the actor teardown.
// The budget bounds a multi-hop shutdown under a starved CI
// scheduler. It does not measure correctness. Every shell-approval
// test goes through this shared harness, so a short budget makes a
// whole suite flake at once.
await approvalActor.GracefulStop(TimeSpan.FromSeconds(15));
approvalActor = CreateApprovalActor(actorSystem, store);
approvalService = CreateApprovalService(approvalActor);
}
Expand Down Expand Up @@ -295,7 +300,9 @@ public void CreateProjectFileSymlink(string linkPath, string targetPath)

public async ValueTask DisposeAsync()
{
await _approvalActor.GracefulStop(TimeSpan.FromSeconds(5));
// Same reason as the seed-phase stop above: the budget bounds a
// multi-hop teardown under a starved CI scheduler, not correctness.
await _approvalActor.GracefulStop(TimeSpan.FromSeconds(15));
if (Directory.Exists(_rootDirectory))
Directory.Delete(_rootDirectory, recursive: true);
}
Expand Down
Loading