using System.Text.Json; using ClawdDotNet.Core.State; using ClawdDotNet.Core.Tasks; using ClawdDotNet.Core.Tests.Infrastructure; using ClawdDotNet.Core.Tools; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Core.Tests.Tasks; /// /// The tool_job dispatch — replaces the ToolJobScheduler. A due poll task ticks an /// and only wakes the agent if the tick reports something. /// public sealed class ToolJobDispatchTests { private static TaskItem PollTask(string agent = "agent-a") => new() { Id = "tj-1", Title = "Poll", Type = TaskItemType.ToolJob, ToolName = "PollTool", JobTypeId = "poll", Assignee = "@" + agent }; private static EngineTaskDispatcher Dispatcher(EngineFixture fixture) => new(fixture.Engine, () => fixture.Agents, "test-instance", fixture.Registry, fixture.StateStore, NullLoggerFactory.Instance); [Fact] public async Task A_poll_without_a_hit_does_not_wake_the_agent() { var tool = new FakeJobTool(ToolJobResult.NoAction("nichts Neues")); var fixture = new EngineFixture().WithTool(tool); fixture.AddAgent("agent-a", "PollTool"); var ok = await Dispatcher(fixture).DispatchAsync(PollTask(), default); ok.ShouldBe(DispatchOutcome.Erledigt, "ein Poll ohne Fund ist kein Fehler"); tool.Ticks.ShouldBe(1); fixture.Client.ReceivedRequests.ShouldBeEmpty("kein Agent-Lauf, wenn nichts zu wecken war"); } [Fact] public async Task A_poll_with_a_hit_wakes_the_agent() { var tool = new FakeJobTool(ToolJobResult.Wake("Neue Nachricht eingetroffen")); var fixture = new EngineFixture().WithTool(tool); fixture.AddAgent("agent-a", "PollTool"); fixture.Client.RespondsWithText("verarbeitet"); var ok = await Dispatcher(fixture).DispatchAsync(PollTask(), default); ok.ShouldBe(DispatchOutcome.Erledigt); tool.Ticks.ShouldBe(1); fixture.Client.ReceivedRequests.ShouldNotBeEmpty("der Agent wurde geweckt"); } [Fact] public async Task An_unknown_poll_tool_reports_an_error() { var fixture = new EngineFixture(); // PollTool not registered fixture.AddAgent("agent-a"); (await Dispatcher(fixture).DispatchAsync(PollTask(), default)).ShouldBe(DispatchOutcome.Gescheitert); } /// A tool that is at the same time a poll provider — counts ticks, delivers a fixed result. private sealed class FakeJobTool(ToolJobResult result) : IAgentTool, IToolJobProvider { public int Ticks { get; private set; } public string Name => "PollTool"; public string Description => "Test-Poll"; public JsonElement InputSchema { get; } = JsonDocument.Parse("""{ "type": "object" }""").RootElement.Clone(); public Task ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct) => Task.FromResult(ToolResult.Ok("ok")); public IReadOnlyList GetJobDefinitions() => []; public Task ExecuteJobAsync( string jobTypeId, IReadOnlyDictionary toolConfig, IStateStore stateStore, ILogger logger, CancellationToken ct, string? agentId = null, string? workspacePath = null) { Ticks++; return Task.FromResult(result); } } }