using ClawdDotNet.App.Services; using ClawdDotNet.Core.Activity; using Shouldly; namespace ClawdDotNet.App.Tests.Services; /// /// #711: The rules that decide what the team overview shows for an agent. Every state comes /// from a recorded fact, never from a model's judgement. /// public sealed class TeamOverviewTests { private static readonly DateTime Now = new(2027, 3, 10, 12, 0, 0, DateTimeKind.Utc); private static ActiveRun RunningFor(TimeSpan duration) => new("r", "worker", "task", null, null, Now - duration); [Fact] public void Nothing_running_and_a_good_last_run_is_idle() => TeamOverview.Classify([], 0, "Completed", TimeSpan.FromMinutes(1), Now).ShouldBe(TeamState.Idle); [Fact] public void An_agent_without_any_run_yet_is_idle() => TeamOverview.Classify([], 0, null, TimeSpan.Zero, Now).ShouldBe(TeamState.Idle); [Fact] public void A_run_in_progress_is_working() => TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(1))], 0, null, TimeSpan.FromMinutes(1), Now) .ShouldBe(TeamState.Working); [Fact] public void A_run_far_beyond_the_usual_duration_is_stuck() // Usually 2 minutes: three times that is 6, running for 7. => TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(7))], 0, null, TimeSpan.FromMinutes(2), Now) .ShouldBe(TeamState.Stuck); [Fact] public void Short_runs_get_a_floor_before_they_count_as_stuck() // Usually 20 seconds: three times is one minute, but the floor is five. => TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(3))], 0, null, TimeSpan.FromSeconds(20), Now) .ShouldBe(TeamState.Working); [Fact] public void Without_history_a_run_is_stuck_only_after_a_long_while() { TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(10))], 0, null, TimeSpan.Zero, Now).ShouldBe(TeamState.Working); TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(16))], 0, null, TimeSpan.Zero, Now).ShouldBe(TeamState.Stuck); } [Fact] public void An_open_approval_outranks_working() // What the human can do something about comes first. => TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(1))], 2, null, TimeSpan.FromMinutes(1), Now) .ShouldBe(TeamState.WaitingForApproval); [Fact] public void Stuck_outranks_an_open_approval() => TeamOverview.Classify([RunningFor(TimeSpan.FromHours(1))], 2, null, TimeSpan.FromMinutes(1), Now) .ShouldBe(TeamState.Stuck); [Theory] [InlineData("Failed")] [InlineData("LoopLimitExceeded")] [InlineData("TargetBusy")] public void A_failed_last_run_is_a_problem(string status) => TeamOverview.Classify([], 0, status, TimeSpan.Zero, Now).ShouldBe(TeamState.Problem); [Fact] public void A_refused_last_run_shows_the_exhausted_budget() => TeamOverview.Classify([], 0, "BudgetExceeded", TimeSpan.Zero, Now).ShouldBe(TeamState.BudgetExhausted); [Fact] public void A_new_run_hides_the_old_problem() // A new run is the attempt to get past it; the problem shows again if it fails. => TeamOverview.Classify([RunningFor(TimeSpan.FromMinutes(1))], 0, "Failed", TimeSpan.FromMinutes(1), Now) .ShouldBe(TeamState.Working); [Fact] public void A_cancelled_run_is_not_a_problem() // Cancelled means a human stopped it — that was a decision, not a fault. => TeamOverview.Classify([], 0, "Cancelled", TimeSpan.Zero, Now).ShouldBe(TeamState.Idle); }