Files
ClawdDotNet/tests/ClawdDotNet.Manager.Tests/ReportingTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
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>
2026-10-04 13:18:43 +02:00

163 lines
6.1 KiB
C#

using System.Net;
using System.Text;
using ClawdDotNet.Core.Deploymentcenter;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Manager.Tests;
/// <summary>Captures the last request and answers with a fixed response, without network access.</summary>
internal sealed class CapturingHandler(
HttpStatusCode status = HttpStatusCode.OK,
string responseBody = """{"status":"success","item_id":1,"is_new":true,"occurrence_count":1}""")
: HttpMessageHandler
{
public string? LastBody { get; private set; }
public int Calls { get; private set; }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Calls++;
if (request.Content is not null)
LastBody = await request.Content.ReadAsStringAsync(cancellationToken);
return new HttpResponseMessage(status)
{
Content = new StringContent(responseBody, Encoding.UTF8, "application/json")
};
}
}
/// <summary>
/// The report on the instance's behalf (#528, concept section 4).
///
/// <para>On 2026-09-08 an instance hung and was ended by Windows:
/// no log line, no error message, no bug tracker entry. Inherent in the design —
/// reporting path and watchdog lived in the process they were supposed to report on. The
/// manager therefore reports in their place, and <b>only</b> what is relevant from its point of view.</para>
/// </summary>
public sealed class SupervisionReporterTests
{
private readonly CapturingHandler _handler = new();
private SupervisionReporter Reporter(Func<string, IReadOnlyList<string>>? excerpt = null)
{
var api = new DeploymentcenterApi("https://dc.example", "t", new HttpClient(_handler));
var bugtracker = new BugtrackerClient(api, "clawddotnet", "production", "0.1.2");
return new SupervisionReporter(bugtracker, NullLogger.Instance, excerpt ?? (_ => []));
}
private static SupervisionEvent Event(
InstanceEventKind kind, int? exitCode = null, string dir = @"C:\Instanzen\Instance-Alpha")
=> new(kind, dir, "Alpha", 4711, exitCode, "Instanz Alpha ist unerwartet beendet worden.",
DateTimeOffset.Parse("2026-09-08T17:27:45Z"));
[Fact]
public async Task A_crash_goes_out()
{
(await Reporter().ReportAsync(Event(InstanceEventKind.Crashed, exitCode: 137)))
.ShouldBeTrue();
_handler.Calls.ShouldBe(1);
var body = _handler.LastBody.ShouldNotBeNull();
body.ShouldContain("unerwartet beendet");
body.ShouldContain("4711");
body.ShouldContain("137");
body.ShouldContain("instanzmanager");
body.ShouldContain("clawddotnet"); // project binding
body.ShouldContain("0.1.2"); // build_version
}
/// <summary>
/// What concerns the instance stays in the instance — it reports it itself, with its
/// own sub-token. The manager only reports what nobody ordered.
/// </summary>
[Theory]
[InlineData(InstanceEventKind.Started)]
[InlineData(InstanceEventKind.StoppedOrderly)]
[InlineData(InstanceEventKind.Recovered)]
public async Task Routine_is_not_reported(InstanceEventKind kind)
{
(await Reporter().ReportAsync(Event(kind))).ShouldBeFalse();
_handler.Calls.ShouldBe(0);
}
/// <summary>
/// If the same instance crashes repeatedly, the server should count up instead of filling the
/// bug tracker with twins — repeated crashes are one finding and
/// not a dozen identical ones.
/// </summary>
[Fact]
public async Task The_same_instance_and_kind_give_the_same_key()
{
var reporter = Reporter();
await reporter.ReportAsync(Event(InstanceEventKind.Crashed));
var firstOne = KeyFrom(_handler.LastBody!);
await reporter.ReportAsync(Event(InstanceEventKind.Crashed));
KeyFrom(_handler.LastBody!).ShouldBe(firstOne);
// A different kind is a different finding.
await reporter.ReportAsync(Event(InstanceEventKind.Unresponsive));
KeyFrom(_handler.LastBody!).ShouldNotBe(firstOne);
// And a different instance all the more.
await reporter.ReportAsync(
Event(InstanceEventKind.Crashed, dir: @"C:\Instanzen\Instance-Beta"));
KeyFrom(_handler.LastBody!).ShouldNotBe(firstOne);
}
[Fact]
public async Task The_log_excerpt_comes_along()
{
await Reporter(_ => ["Startup.log: [17:26:50] [INF] Application is shutting down"])
.ReportAsync(Event(InstanceEventKind.Crashed));
_handler.LastBody.ShouldNotBeNull().ShouldContain("Application is shutting down");
}
/// <summary>
/// An empty excerpt is the most meaningful line of the whole report: a process
/// that was killed before its write buffer was flushed leaves nothing.
/// Exactly that was the state from #529.
/// </summary>
[Fact]
public async Task An_empty_log_excerpt_is_stated_explicitly()
{
await Reporter(_ => []).ReportAsync(Event(InstanceEventKind.Crashed));
_handler.LastBody.ShouldNotBeNull().ShouldContain("KEINE");
}
/// <summary>
/// A reporting path that throws itself would be the worst of all worlds: the
/// original finding would get lost, and the supervision would die of its
/// own report.
/// </summary>
[Fact]
public async Task A_server_error_does_not_take_supervision_down()
{
var handler = new CapturingHandler(HttpStatusCode.InternalServerError,
"""{"status":"error","error":{"code":"boom","message":"kaputt"}}""");
var api = new DeploymentcenterApi("https://dc.example", "t", new HttpClient(handler));
var reporter = new SupervisionReporter(
new BugtrackerClient(api, "clawddotnet", "production", "0.1.2"),
NullLogger.Instance, _ => []);
(await reporter.ReportAsync(Event(InstanceEventKind.Crashed))).ShouldBeFalse();
}
private static string KeyFrom(string body)
{
using var doc = System.Text.Json.JsonDocument.Parse(body);
return doc.RootElement.GetProperty("client_ref").GetString()!;
}
}