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>
163 lines
6.2 KiB
C#
163 lines
6.2 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 Melder(Func<string, IReadOnlyList<string>>? auszug = 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, auszug ?? (_ => []));
|
|
}
|
|
|
|
private static SupervisionEvent Ereignis(
|
|
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 Ein_Absturz_geht_raus()
|
|
{
|
|
(await Melder().ReportAsync(Ereignis(InstanceEventKind.Crashed, exitCode: 137)))
|
|
.ShouldBeTrue();
|
|
|
|
_handler.Calls.ShouldBe(1);
|
|
|
|
var rumpf = _handler.LastBody.ShouldNotBeNull();
|
|
rumpf.ShouldContain("unerwartet beendet");
|
|
rumpf.ShouldContain("4711");
|
|
rumpf.ShouldContain("137");
|
|
rumpf.ShouldContain("instanzmanager");
|
|
rumpf.ShouldContain("clawddotnet"); // project binding
|
|
rumpf.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 Alltag_wird_nicht_gemeldet(InstanceEventKind kind)
|
|
{
|
|
(await Melder().ReportAsync(Ereignis(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 Dieselbe_Instanz_und_dieselbe_Art_ergeben_denselben_Schluessel()
|
|
{
|
|
var melder = Melder();
|
|
|
|
await melder.ReportAsync(Ereignis(InstanceEventKind.Crashed));
|
|
var erster = SchluesselAus(_handler.LastBody!);
|
|
|
|
await melder.ReportAsync(Ereignis(InstanceEventKind.Crashed));
|
|
SchluesselAus(_handler.LastBody!).ShouldBe(erster);
|
|
|
|
// A different kind is a different finding.
|
|
await melder.ReportAsync(Ereignis(InstanceEventKind.Unresponsive));
|
|
SchluesselAus(_handler.LastBody!).ShouldNotBe(erster);
|
|
|
|
// And a different instance all the more.
|
|
await melder.ReportAsync(
|
|
Ereignis(InstanceEventKind.Crashed, dir: @"C:\Instanzen\Instance-Beta"));
|
|
SchluesselAus(_handler.LastBody!).ShouldNotBe(erster);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Der_Protokollauszug_kommt_mit()
|
|
{
|
|
await Melder(_ => ["Startup.log: [17:26:50] [INF] Application is shutting down"])
|
|
.ReportAsync(Ereignis(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 Ein_leerer_Protokollauszug_wird_ausdruecklich_gesagt()
|
|
{
|
|
await Melder(_ => []).ReportAsync(Ereignis(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 Ein_Serverfehler_reisst_die_Aufsicht_nicht_mit()
|
|
{
|
|
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 melder = new SupervisionReporter(
|
|
new BugtrackerClient(api, "clawddotnet", "production", "0.1.2"),
|
|
NullLogger.Instance, _ => []);
|
|
|
|
(await melder.ReportAsync(Ereignis(InstanceEventKind.Crashed))).ShouldBeFalse();
|
|
}
|
|
|
|
private static string SchluesselAus(string body)
|
|
{
|
|
using var doc = System.Text.Json.JsonDocument.Parse(body);
|
|
return doc.RootElement.GetProperty("client_ref").GetString()!;
|
|
}
|
|
}
|