Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Engine/AbbruchKontextTests.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +02:00

93 lines
3.6 KiB
C#

using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Tests.Infrastructure;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// What an abort in the middle of a turn does to the conversation context.
///
/// The context is a shared list, changed in place. If a run aborts
/// between the tool call and its answer, an assistant message
/// with tool_calls remains that no answer follows — a message sequence the
/// interface does not accept.
/// </summary>
public class AbbruchKontextTests
{
[Fact]
public async Task Ein_Abbruch_waehrend_des_Werkzeugs_hinterlaesst_keinen_unbeantworteten_Aufruf()
{
var fixture = new EngineFixture().WithTool(
FakeTool.Slow(TimeSpan.FromSeconds(5), name: "LangsamTool"));
var agent = fixture.AddAgent("a1", "LangsamTool");
fixture.Client
.RespondsWithToolCall("LangsamTool")
.AlwaysRespondsWithText("Fertig.");
var lauf = fixture.Engine.ChatAsync(agent, "Los", "inst", CancellationToken.None);
// Wait until the call is surely in place and the tool is running.
await Task.Delay(300);
fixture.Engine.AbortChat("a1");
var result = await lauf;
Assert.Equal(AgentRunStatus.Cancelled, result.Status);
var kontext = fixture.Engine.GetChatContext("a1");
var offeneAufrufe = kontext
.Where(m => m.Role == "assistant" && m.ToolCalls is { Count: > 0 })
.SelectMany(m => m.ToolCalls!)
.Select(tc => tc.Id)
.Where(id => !kontext.Any(m => m.Role == "tool" && m.ToolCallId == id))
.ToList();
Assert.True(offeneAufrufe.Count == 0,
$"Unbeantwortete Werkzeugaufrufe im Kontext: {string.Join(", ", offeneAufrufe)}. " +
"Eine solche Folge lehnt die Schnittstelle mit HTTP 400 ab — der Agent ist " +
"danach dauerhaft blockiert.");
// The substitute answer also says what happened. An empty answer would be formally
// valid, but would leave the model puzzling why its call yielded nothing.
var ersatz = kontext.Last(m => m.Role == "tool");
Assert.Contains("abgebrochen", ersatz.Content, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Nach_einem_Abbruch_laesst_sich_der_Agent_wieder_ansprechen()
{
// The actual point: not the form of the context counts, but whether the
// agent still works afterwards. A second chat has to run through.
var fixture = new EngineFixture().WithTool(
FakeTool.Slow(TimeSpan.FromSeconds(5), name: "LangsamTool"));
var agent = fixture.AddAgent("a1", "LangsamTool");
fixture.Client
.RespondsWithToolCall("LangsamTool")
.AlwaysRespondsWithText("Zweite Antwort.");
var erster = fixture.Engine.ChatAsync(agent, "Los", "inst", CancellationToken.None);
await Task.Delay(300);
fixture.Engine.AbortChat("a1");
await erster;
var zweiter = await fixture.Engine.ChatAsync(agent, "Und jetzt?", "inst", CancellationToken.None);
Assert.Equal(AgentRunStatus.Completed, zweiter.Status);
Assert.Equal("Zweite Antwort.", zweiter.FinalMessage);
// And the sequence that was sent in doing so was completely answered.
var gesendet = fixture.Client.ReceivedRequests[^1].Messages;
var offen = gesendet
.Where(m => m.Role == "assistant" && m.ToolCalls is { Count: > 0 })
.SelectMany(m => m.ToolCalls!)
.Select(tc => tc.Id)
.Where(id => !gesendet.Any(m => m.Role == "tool" && m.ToolCallId == id))
.ToList();
Assert.Empty(offen);
}
}