using ClawdDotNet.Core.Activity; using ClawdDotNet.Core.Tests.Infrastructure; using ClawdDotNet.Core.Tools; using Shouldly; namespace ClawdDotNet.Core.Tests.Activity; /// /// #711: Who is working right now and who reached whom — the live side of the team overview. /// public sealed class AgentActivityTests { private readonly FakeTimeProvider _clock = new(new DateTimeOffset(2027, 3, 10, 12, 0, 0, TimeSpan.Zero)); [Fact] public void A_run_is_active_until_its_handle_is_disposed() { var activity = new AgentActivity(_clock); using (var run = activity.Begin("r1", "worker", "task", "t-1", null)) { var active = activity.ActiveRuns().ShouldHaveSingleItem(); active.AgentId.ShouldBe("worker"); active.TaskId.ShouldBe("t-1"); active.CalledBy.ShouldBeNull(); run.Complete("Completed"); } activity.ActiveRuns().ShouldBeEmpty(); } [Fact] public void Start_and_end_are_announced_with_the_status() { var activity = new AgentActivity(_clock); var log = new List(); activity.RunStarted += r => log.Add($"start {r.AgentId}"); activity.RunFinished += (r, status) => log.Add($"end {r.AgentId} {status}"); using (var run = activity.Begin("r1", "worker", "chat", null, null)) run.Complete("LoopLimitExceeded"); using (activity.Begin("r2", "worker", "chat", null, null)) { } log.ShouldBe(["start worker", "end worker LoopLimitExceeded", "start worker", "end worker Aborted"]); } [Fact] public void A_delegated_run_records_who_called_whom() { var activity = new AgentActivity(_clock); AgentLink? seen = null; activity.LinkObserved += l => seen = l; var chain = DelegationChain.Root("lead").Append("buyer"); using (activity.Begin("r1", "buyer", "agent_comm", null, chain)) { } seen.ShouldNotBeNull(); seen.From.ShouldBe("lead"); seen.To.ShouldBe("buyer"); seen.Kind.ShouldBe("delegation"); activity.RecentLinks(TimeSpan.FromMinutes(1)).ShouldHaveSingleItem().From.ShouldBe("lead"); } [Fact] public void Old_links_fall_out_of_the_recent_window() { var activity = new AgentActivity(_clock); using (activity.Begin("r1", "buyer", "agent_comm", null, DelegationChain.Root("lead").Append("buyer"))) { } _clock.Advance(TimeSpan.FromMinutes(11)); activity.RecentLinks(TimeSpan.FromMinutes(10)).ShouldBeEmpty(); } [Fact] public void The_memory_of_links_is_bounded() { var activity = new AgentActivity(_clock); var chain = DelegationChain.Root("lead").Append("buyer"); for (var i = 0; i < AgentActivity.RecentLinkCapacity + 50; i++) using (activity.Begin($"r{i}", "buyer", "agent_comm", null, chain)) { } activity.RecentLinks(TimeSpan.FromDays(1)).Count.ShouldBe(AgentActivity.RecentLinkCapacity); } [Fact] public void A_subscriber_that_throws_does_not_break_the_run() { var activity = new AgentActivity(_clock); activity.RunStarted += _ => throw new InvalidOperationException("broken page"); activity.RunFinished += (_, _) => throw new InvalidOperationException("broken page"); Should.NotThrow(() => { using var run = activity.Begin("r1", "worker", "chat", null, null); run.Complete("Completed"); }); activity.ActiveRuns().ShouldBeEmpty(); } // ─── through the engine ─── [Fact] public async Task The_engine_announces_a_delegated_run_with_its_caller() { var fixture = new EngineFixture(); fixture.AddAgent("lead"); fixture.AddAgent("buyer"); fixture.Client.RespondsWithText("Erledigt."); var started = new List(); var finished = new List(); fixture.Engine.Activity.RunStarted += started.Add; fixture.Engine.Activity.RunFinished += (_, status) => finished.Add(status); var result = await fixture.Engine.SendMessageAsync("lead", "buyer", "Bitte pruefen", "inst", null, default); result.Delivered.ShouldBeTrue(result.Error); var run = started.ShouldHaveSingleItem(); run.AgentId.ShouldBe("buyer"); run.CalledBy.ShouldBe("lead"); finished.ShouldBe(["Completed"]); fixture.Engine.Activity.ActiveRuns().ShouldBeEmpty(); fixture.Engine.Activity.RecentLinks(TimeSpan.FromMinutes(1)).ShouldHaveSingleItem().To.ShouldBe("buyer"); } [Fact] public async Task A_fresh_run_is_visible_too() { // Before #711 only runs through ChatAsync counted as "running"; RunAsync (@new) did not. var fixture = new EngineFixture(); var agent = fixture.AddAgent("worker"); fixture.Client.RespondsWithText("Fertig."); var started = new List(); fixture.Engine.Activity.RunStarted += started.Add; await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None); started.ShouldHaveSingleItem().AgentId.ShouldBe("worker"); } }