Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
6.7 KiB
C#
148 lines
6.7 KiB
C#
using ClawdDotNet.Core.Engine;
|
|
using ClawdDotNet.Core.Tasks;
|
|
using ClawdDotNet.Core.Tests.Infrastructure;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Engine;
|
|
|
|
/// <summary>
|
|
/// Regression tests for the findings from test operation on the Linux VM (#572).
|
|
/// </summary>
|
|
public sealed class TestOperationRegressionTests
|
|
{
|
|
// ═══════════════════════════════════════════════════════════
|
|
// #578 — aborted runs carry the tokens they paid for
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public async Task RunAsync_at_the_step_limit_reports_the_tokens_paid_so_far()
|
|
{
|
|
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1)));
|
|
var agent = fixture.AddAgent("agent-grenze", "TestTool");
|
|
agent.LoopGuard.MaxSteps = 3;
|
|
|
|
// Every answer is a tool call with 100 prompt and 20 completion tokens.
|
|
fixture.Client.AlwaysRespondsWithToolCall("TestTool");
|
|
|
|
var result = await fixture.Engine.RunAsync(agent, "Arbeite", "test-instance", default);
|
|
|
|
result.Status.ShouldBe(AgentRunStatus.LoopLimitExceeded);
|
|
fixture.Client.CallCount.ShouldBe(3);
|
|
result.PromptTokens.ShouldBe(300, "drei bezahlte Aufrufe - vorher stand hier 0");
|
|
result.CompletionTokens.ShouldBe(60);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ChatAsync_at_the_step_limit_reports_the_tokens_paid_so_far()
|
|
{
|
|
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1)));
|
|
var agent = fixture.AddAgent("agent-chat-grenze", "TestTool");
|
|
agent.LoopGuard.MaxSteps = 2;
|
|
|
|
fixture.Client.AlwaysRespondsWithToolCall("TestTool");
|
|
|
|
var result = await fixture.Engine.ChatAsync(agent, "Arbeite", "test-instance", default);
|
|
|
|
result.Status.ShouldBe(AgentRunStatus.LoopLimitExceeded);
|
|
result.PromptTokens.ShouldBe(200);
|
|
result.CompletionTokens.ShouldBe(40);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_with_a_provider_error_reports_the_tokens_paid_so_far()
|
|
{
|
|
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1)));
|
|
var agent = fixture.AddAgent("agent-fehler", "TestTool");
|
|
|
|
fixture.Client
|
|
.RespondsWithToolCall("TestTool")
|
|
.Throws(new HttpRequestException("502 vom Anbieter"));
|
|
|
|
var result = await fixture.Engine.RunAsync(agent, "Arbeite", "test-instance", default);
|
|
|
|
result.Status.ShouldBe(AgentRunStatus.Failed);
|
|
result.PromptTokens.ShouldBe(100);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// #581 — incomplete argument JSON
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Theory]
|
|
[InlineData("{\"action\": \"write\", \"workspace\": \"personal\", \"path\": \"gross.txt\"", false)]
|
|
[InlineData("{\"action\": \"send_message\", \"targetAgentId\": \"netzpruefer\"", false)]
|
|
[InlineData("{\"action\": \"list\"}", true)]
|
|
public void IsCompleteJson_recognises_truncated_arguments(string arguments, bool expected)
|
|
=> AgentEngine.IsCompleteJson(arguments).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public async Task Truncated_arguments_do_not_reach_the_tool_and_the_model_learns_why()
|
|
{
|
|
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1)));
|
|
var agent = fixture.AddAgent("agent-abgeschnitten", "TestTool");
|
|
|
|
// Exactly the shape from the test bench: the JSON breaks off before the large value.
|
|
fixture.Client
|
|
.RespondsWithToolCall("TestTool", "{\"action\": \"write\", \"path\": \"gross.txt\"")
|
|
.RespondsWithText("verstanden");
|
|
|
|
await fixture.Engine.RunAsync(agent, "Schreibe viel", "test-instance", default);
|
|
|
|
var answerToModel = fixture.Client.ReceivedRequests[1].Messages.Last();
|
|
answerToModel.Role.ShouldBe("tool");
|
|
answerToModel.Content.ShouldNotBeNull();
|
|
answerToModel.Content.ShouldContain("kein vollstaendiges JSON");
|
|
answerToModel.Content.ShouldContain("mehrere kleinere Aufrufe");
|
|
answerToModel.Content.ShouldNotContain("Expected depth");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// #589 — time and schedule in the assignment message
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Time_header_names_task_local_time_UTC_and_schedule()
|
|
{
|
|
var task = new TaskItem
|
|
{
|
|
Id = "t-1",
|
|
Title = "Wetter",
|
|
When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "5 * * * *", TimeZone = "Europe/Berlin" }
|
|
};
|
|
|
|
var text = EngineTaskDispatcher.TimeHeaderFor(task, new DateTimeOffset(2026, 9, 14, 13, 5, 0, TimeSpan.Zero));
|
|
|
|
text.ShouldContain("14.09.2026 15:05 Uhr");
|
|
text.ShouldContain("Europe/Berlin");
|
|
text.ShouldContain("2026-09-14T13:05Z");
|
|
text.ShouldContain("cron \"5 * * * *\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Assignment_message_contains_the_time_header()
|
|
{
|
|
var fixture = new EngineFixture();
|
|
var dispatcher = new EngineTaskDispatcher(
|
|
fixture.Engine, () => fixture.Agents, "test-instance", fixture.Registry, fixture.StateStore,
|
|
NullLoggerFactory.Instance, new FixedTime(new DateTimeOffset(2026, 1, 5, 7, 15, 0, TimeSpan.Zero)));
|
|
|
|
var message = dispatcher.BuildMessage(new TaskItem
|
|
{
|
|
Id = "t-2",
|
|
Title = "Morgenbericht",
|
|
Body = "Schreibe den Bericht.",
|
|
When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "15 7 * * *", TimeZone = "Europe/Berlin" }
|
|
});
|
|
|
|
message.ShouldStartWith("[Aufgabe t-2] Morgenbericht");
|
|
message.ShouldContain("Aktuelle Zeit: 05.01.2026 08:15 Uhr (Europe/Berlin, UTC+01:00)");
|
|
message.ShouldContain("Schreibe den Bericht.");
|
|
}
|
|
|
|
private sealed class FixedTime(DateTimeOffset now) : TimeProvider
|
|
{
|
|
public override DateTimeOffset GetUtcNow() => now;
|
|
}
|
|
}
|