Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Tasks/ToolJobDispatchTests.cs
RichardandClaude Opus 5 2f0361278b Freigaben buendeln statt Weckflut (#643), Freigabemodell ueber Werkzeugrechte (#641)
#643: Entscheidungen wecken den Agenten nur noch bei Ablehnung oder gescheiterter
Ausfuehrung, gesammelt in einer Aufgabe je Agent (5 min Ruhefenster), frisch per
@new:agent statt im fortgefuehrten Chatverlauf. Eine einmalige Aufgabe, deren Lauf
wegen Budget oder Herunterfahren gar nicht stattfand, bleibt faellig (Wiedervorlage
nach 30 min) statt mit gesetztem Marker liegenzubleiben.

#641: Rechte stehen in der Werkzeugkonfiguration des Agenten (erlaubt/verboten).
Erlaubtes laeuft, Nicht-Erlaubtes wird einzeln vorgelegt statt verweigert, Verbotenes
gar nicht erst. "Gleichartiges kuenftig erlauben" erteilt eine widerrufbare
Dauerfreigabe je Agent/Werkzeug/Operation; nach drei gleichartigen Freigaben schlaegt
die Oberflaeche sie vor. Aenderungen an Agenten bleiben ausnahmslos freigabepflichtig.
Database: Operation ist das SQL-Schluesselwort; die eigene Zugriffspruefung greift nur
noch ohne Freigabestelle (AgentToolContext.FreigabeGeprueft). WebUI: Rechte-Tabelle je
Werkzeug, Dauerfreigaben mit Widerruf.

Version 0.1.11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:37:44 +02:00

94 lines
3.5 KiB
C#

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;
/// <summary>
/// Der tool_job-Dispatch — ersetzt den ToolJobScheduler. Ein fälliger Poll-Task tickt einen
/// <see cref="IToolJobProvider"/> und weckt den Agenten nur, wenn der Tick etwas meldet.
/// </summary>
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 Ein_Poll_ohne_Fund_weckt_den_Agenten_nicht()
{
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(TaskLauf.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 Ein_Poll_mit_Fund_weckt_den_Agenten()
{
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(TaskLauf.Erledigt);
tool.Ticks.ShouldBe(1);
fixture.Client.ReceivedRequests.ShouldNotBeEmpty("der Agent wurde geweckt");
}
[Fact]
public async Task Ein_unbekanntes_Poll_Tool_meldet_einen_Fehler()
{
var fixture = new EngineFixture(); // PollTool nicht registriert
fixture.AddAgent("agent-a");
(await Dispatcher(fixture).DispatchAsync(PollTask(), default)).ShouldBe(TaskLauf.Gescheitert);
}
/// <summary>Ein Tool, das zugleich Poll-Provider ist — zählt Ticks, liefert ein festes Ergebnis.</summary>
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<ToolResult> ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
=> Task.FromResult(ToolResult.Ok("ok"));
public IReadOnlyList<ToolJobDefinition> GetJobDefinitions() => [];
public Task<ToolJobResult> ExecuteJobAsync(
string jobTypeId, IReadOnlyDictionary<string, object?> toolConfig, IStateStore stateStore,
ILogger logger, CancellationToken ct, string? agentId = null, string? workspacePath = null)
{
Ticks++;
return Task.FromResult(result);
}
}
}