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");
}
}