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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
da4ceac7d5
commit
837a634a22
@@ -5,17 +5,17 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Manager.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Die Aufsicht über eine Instanz (#528, Stufe 3).
|
||||
/// The supervision of an instance (#528, stage 3).
|
||||
///
|
||||
/// <para><b>Der Anlass.</b> Am 2026-09-08 hat sich eine Instanz aufgehängt und wurde von
|
||||
/// Windows beendet. Zu diesem Vorgang steht keine einzige Zeile in den Instanzprotokollen,
|
||||
/// kein Bugtracker-Eintrag, kein Watchdog-Alarm — weil Meldeweg und Watchdog in genau dem
|
||||
/// Prozess lebten, über den sie hätten berichten sollen (#529). Die Abnahmebedingung für
|
||||
/// diese Stufe lautet deshalb: <b>eine Instanz abschießen, und der Manager merkt es und
|
||||
/// meldet es.</b></para>
|
||||
/// <para><b>The occasion.</b> On 2026-09-08 an instance hung and was ended by
|
||||
/// Windows. Not a single line about this event is in the instance logs,
|
||||
/// no bug tracker entry, no watchdog alarm — because reporting path and watchdog lived in exactly the
|
||||
/// process they should have reported on (#529). The acceptance condition for
|
||||
/// this stage is therefore: <b>kill an instance, and the manager notices it and
|
||||
/// reports it.</b></para>
|
||||
///
|
||||
/// <para>Der schwierigere Fall steht darunter: ein Prozess, der <em>lebt</em> und nicht
|
||||
/// mehr arbeitet. Den sieht keine Prozessliste — nur der Socket.</para>
|
||||
/// <para>The harder case stands below: a process that is <em>alive</em> and no longer
|
||||
/// works. No process list sees it — only the socket.</para>
|
||||
/// </summary>
|
||||
public sealed class InstanceSupervisorTests : IDisposable
|
||||
{
|
||||
@@ -29,11 +29,11 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
private static readonly InstanceIdentity Test = new("i-1", "Test", "0.1.2");
|
||||
|
||||
/// <summary>
|
||||
/// Kurze Fristen: Der Test soll die Aufsicht prüfen, nicht die Geduld.
|
||||
/// Short deadlines: the test is supposed to check supervision, not patience.
|
||||
///
|
||||
/// <para>Knapp über dem Abfragetakt von 250 ms, damit die Warteschleifen je einen
|
||||
/// Durchgang schaffen — es geht um die Entscheidung am Ende der Frist, nicht darum,
|
||||
/// wie oft dazwischen gefragt wurde.</para>
|
||||
/// <para>Just above the probe interval of 250 ms, so the waiting loops manage one
|
||||
/// pass each — this is about the decision at the end of the deadline, not about
|
||||
/// how often there was asking in between.</para>
|
||||
/// </summary>
|
||||
private static readonly SupervisionOptions Options = new(
|
||||
ProbeInterval: TimeSpan.FromMilliseconds(50),
|
||||
@@ -47,16 +47,16 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
{
|
||||
Directory.CreateDirectory(_dir);
|
||||
|
||||
// Der Socket antwortet nur, wenn wirklich ein Prozess laeuft - genau wie im
|
||||
// Betrieb. Eine vorab hinterlegte Antwort hiesse sonst "laeuft schon", und die
|
||||
// Aufsicht wuerde uebernehmen statt zu starten.
|
||||
// The socket only answers if a process is really running - exactly as in
|
||||
// operation. An answer stored in advance would otherwise mean "already running", and
|
||||
// supervision would take over instead of starting.
|
||||
_kanal.Launcher = _launcher;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); }
|
||||
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
||||
catch { /* A leftover test folder does not turn a run red. */ }
|
||||
}
|
||||
|
||||
private InstanceSupervisor Aufsicht(SupervisionOptions? options = null)
|
||||
@@ -70,7 +70,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
|
||||
private IEnumerable<InstanceEventKind> Arten => _ereignisse.Select(e => e.Kind);
|
||||
|
||||
// ─── Starten ───
|
||||
// ─── Starting ───
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_Start_gilt_erst_wenn_die_Instanz_antwortet()
|
||||
@@ -91,16 +91,16 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// „Der Prozess läuft" ist nicht dasselbe wie „die Instanz läuft".
|
||||
/// "The process is running" is not the same as "the instance is running".
|
||||
///
|
||||
/// <para>Zwischen beidem liegen Datenbank, Agenten und Kestrel. Wer nur auf
|
||||
/// <c>Process.Start</c> wartet, hält eine Instanz für gestartet, die gleich an einer
|
||||
/// kaputten Konfiguration scheitert.</para>
|
||||
/// <para>Between the two lie database, agents and Kestrel. Whoever only waits for
|
||||
/// <c>Process.Start</c> considers an instance started that is about to fail on a
|
||||
/// broken configuration.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Ein_Prozess_der_sich_nie_meldet_gilt_nicht_als_gestartet()
|
||||
{
|
||||
_kanal.Answer = null; // laeuft, sagt aber nichts
|
||||
_kanal.Answer = null; // runs, but says nothing
|
||||
|
||||
await using var aufsicht = Aufsicht();
|
||||
(await aufsicht.StartAsync()).ShouldBeFalse();
|
||||
@@ -108,8 +108,8 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
aufsicht.Condition.ShouldBe(InstanceCondition.Stopped);
|
||||
Arten.ShouldBe([InstanceEventKind.StartFailed]);
|
||||
|
||||
// Nicht stehen lassen: Ein Prozess, der laeuft und schweigt, haelt den Socket
|
||||
// besetzt und blockiert damit den naechsten Versuch.
|
||||
// Do not leave it standing: a process that runs and stays silent keeps the socket
|
||||
// occupied and thus blocks the next attempt.
|
||||
_launcher.Last!.WasKilled.ShouldBeTrue();
|
||||
InstanceStateStore.Read(_dir).ShouldBeNull();
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
_ereignisse[0].Message.ShouldContain("Programmdatei fehlt");
|
||||
}
|
||||
|
||||
// ─── Der Fall, um den es geht ───
|
||||
// ─── The case this is about ───
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_abgeschossener_Prozess_wird_bemerkt_und_gemeldet()
|
||||
@@ -136,7 +136,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
await using var aufsicht = Aufsicht();
|
||||
await aufsicht.StartAsync();
|
||||
|
||||
// Jemand schiesst die Instanz ab. Niemand hat darum gebeten.
|
||||
// Someone kills the instance. Nobody asked for it.
|
||||
_launcher.Last!.Exit(code: 137);
|
||||
_kanal.Answer = null;
|
||||
|
||||
@@ -149,16 +149,16 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
absturz.InstanceName.ShouldBe("Test");
|
||||
absturz.WorthReporting.ShouldBeTrue("Das ist die Meldung, die es 2026-09-08 nicht gab.");
|
||||
|
||||
// Und die Zustandsdatei zeigt nicht mehr auf einen Prozess, den es nicht gibt.
|
||||
// And the state file no longer points to a process that does not exist.
|
||||
InstanceStateStore.Read(_dir).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der schwierigere Fall: Der Prozess lebt und arbeitet nicht mehr.
|
||||
/// The harder case: the process lives and no longer works.
|
||||
///
|
||||
/// <para>Genau das war der Zustand am 2026-09-08 — die Anwendung stand einwandfrei in
|
||||
/// der Prozessliste, bis Windows sie als „reagiert nicht mehr" beendet hat. Eine
|
||||
/// Aufsicht, die nur Prozesse zählt, sieht davon nichts.</para>
|
||||
/// <para>Exactly that was the state on 2026-09-08 — the application stood flawlessly in
|
||||
/// the process list until Windows ended it as "not responding". A
|
||||
/// supervision that only counts processes sees nothing of it.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Ein_lebender_aber_stummer_Prozess_faellt_auf()
|
||||
@@ -168,10 +168,10 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
await using var aufsicht = Aufsicht();
|
||||
await aufsicht.StartAsync();
|
||||
|
||||
_kanal.Answer = null; // haengt - der Prozess lebt weiter
|
||||
_kanal.Answer = null; // hangs - the process lives on
|
||||
|
||||
// Nicht beim ersten Aussetzer: Der waere Alltag, und ein Alarm bei jedem
|
||||
// Aussetzer ist nach einer Woche der Alarm, den niemand mehr liest.
|
||||
// Not at the first hiccup: that would be everyday life, and an alarm at every
|
||||
// hiccup is after a week the alarm nobody reads any more.
|
||||
await aufsicht.CheckAsync();
|
||||
await aufsicht.CheckAsync();
|
||||
Arten.ShouldNotContain(InstanceEventKind.Unresponsive);
|
||||
@@ -220,7 +220,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
Arten.ShouldContain(InstanceEventKind.Recovered);
|
||||
}
|
||||
|
||||
// ─── Anhalten ───
|
||||
// ─── Stopping ───
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_erbetenes_Ende_ist_kein_Absturz()
|
||||
@@ -230,7 +230,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
await using var aufsicht = Aufsicht();
|
||||
await aufsicht.StartAsync();
|
||||
|
||||
// Die Instanz endet auf die Bitte hin - so wie im Betrieb.
|
||||
// The instance ends upon the request - as in operation.
|
||||
_launcher.Last!.Exit(code: 0);
|
||||
_kanal.Answer = null;
|
||||
|
||||
@@ -253,7 +253,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
await using var aufsicht = Aufsicht();
|
||||
await aufsicht.StartAsync();
|
||||
|
||||
// Antwortet weiter und endet nicht - die Bitte verhallt.
|
||||
// Keeps answering and does not end - the request goes unheard.
|
||||
_kanal.StopRequestArrives = false;
|
||||
|
||||
(await aufsicht.StopAsync()).ShouldBeTrue();
|
||||
@@ -264,14 +264,14 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schweigen ist kein Todesnachweis, solange ein Prozesshandle das Gegenteil sagt.
|
||||
/// Silence is no proof of death as long as a process handle says the opposite.
|
||||
///
|
||||
/// <para><b>Hier lag der gefährlichste Fehlschluss.</b> Die Warteschleife brach ab,
|
||||
/// sobald der Socket nicht mehr antwortete, meldete „beendet" und gab <c>true</c>
|
||||
/// zurück — ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket
|
||||
/// aber von vornherein still: Der Aufruf meldete sofort Erfolg, der Prozess lief
|
||||
/// weiter, und der Neustart setzte einen zweiten auf dieselbe <c>state.db</c>. Genau
|
||||
/// der Schaden, gegen den dieses Vorhaben gebaut ist.</para>
|
||||
/// <para><b>Here lay the most dangerous fallacy.</b> The waiting loop broke off
|
||||
/// as soon as the socket no longer answered, reported "ended" and returned <c>true</c>
|
||||
/// — without touching the process. With a silent instance, however, the socket
|
||||
/// is quiet from the start: the call reported success immediately, the process kept
|
||||
/// running, and the restart put a second one on the same <c>state.db</c>. Exactly
|
||||
/// the damage this project is built against.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Ein_stummer_aber_lebender_Prozess_gilt_nicht_als_beendet()
|
||||
@@ -283,7 +283,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
|
||||
var prozess = _launcher.Last.ShouldNotBeNull();
|
||||
|
||||
// Sie haengt: Der Socket schweigt, der Prozess lebt.
|
||||
// It hangs: the socket is silent, the process lives.
|
||||
_kanal.Verstummen(_dir);
|
||||
|
||||
(await aufsicht.StopAsync()).ShouldBeTrue();
|
||||
@@ -296,28 +296,28 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wer aufräumt, wird nicht abgeschossen — die Frist wird nachgezogen.
|
||||
/// Whoever is cleaning up is not killed — the deadline is extended.
|
||||
///
|
||||
/// <para>Die Instanz gibt sich selbst eine Frist zum Auslaufen. Wäre die des Managers
|
||||
/// kürzer, schösse er mitten in das Aufräumen hinein, um das er gerade gebeten
|
||||
/// hat.</para>
|
||||
/// <para>The instance gives itself a deadline for running out. If the manager's were
|
||||
/// shorter, it would shoot into the middle of the cleanup it has just asked
|
||||
/// for.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Wer_aufraeumt_bekommt_die_Frist_nachgezogen()
|
||||
{
|
||||
_kanal.Answer = Test;
|
||||
|
||||
// #722: Die Aufsicht fragt alle 250 ms und schiebt die Frist nur bei "raeume auf" weiter.
|
||||
// Mit 300 ms Frist kam unter Parallellast die naechste Frage zu spaet, und der Test sah
|
||||
// einen Abschuss, wo keiner hingehoert. Eine Sekunde Frist laesst dem Takt Luft; die
|
||||
// Instanz raeumt trotzdem deutlich laenger auf, als die Frist allein erlaubt.
|
||||
// #722: Supervision asks every 250 ms and only extends the deadline on "cleaning up".
|
||||
// With a 300 ms deadline the next question came too late under parallel load, and the test saw
|
||||
// a kill where none belongs. A one-second deadline leaves the tick room; the
|
||||
// instance still cleans up considerably longer than the deadline alone allows.
|
||||
await using var aufsicht = Aufsicht(Options with { StopGrace = TimeSpan.FromSeconds(1), MaxDrainWait = TimeSpan.FromSeconds(20) });
|
||||
await aufsicht.StartAsync();
|
||||
|
||||
var prozess = _launcher.Last.ShouldNotBeNull();
|
||||
|
||||
// Sie antwortet und meldet: Ich raeume auf. Laenger als StopGrace (1 s), aber
|
||||
// kuerzer als MaxDrainWait.
|
||||
// It answers and reports: I am cleaning up. Longer than StopGrace (1 s), but
|
||||
// shorter than MaxDrainWait.
|
||||
_kanal.Answer = Test with { Phase = ShutdownPhase.Draining, OpenWork = 1 };
|
||||
|
||||
_kanal.OnStopRequest = _ => Task.Run(async () =>
|
||||
@@ -334,11 +334,11 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eine Instanz, die der Manager nicht selbst gestartet hat und die nicht hört, kann
|
||||
/// er auch nicht beenden — und das muss er sagen, statt Erfolg zu melden.
|
||||
/// An instance that the manager did not start itself and that does not listen, it
|
||||
/// cannot end either — and it has to say so instead of reporting success.
|
||||
///
|
||||
/// <para>Der Fall entsteht nach einem Managerneustart, wenn die Prozesskennung nicht
|
||||
/// mehr zu greifen ist (siehe <see cref="Eine_wiederverwendete_Prozesskennung_wird_nicht_fuer_die_Instanz_gehalten"/>).</para>
|
||||
/// <para>The case arises after a manager restart when the process ID can no longer
|
||||
/// be grabbed (see <see cref="Eine_wiederverwendete_Prozesskennung_wird_nicht_fuer_die_Instanz_gehalten"/>).</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Was_nicht_zu_fassen_ist_wird_nicht_als_beendet_gemeldet()
|
||||
@@ -346,12 +346,12 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
_kanal.Answer = Test;
|
||||
_kanal.StopRequestArrives = false;
|
||||
|
||||
// Sie lief schon, bevor dieser Manager kam - und ohne Zustandsdatei ist ihr
|
||||
// Prozess fuer ihn nicht zu greifen.
|
||||
// It was already running before this manager came - and without a state file its
|
||||
// process cannot be grabbed by it.
|
||||
_launcher.Fremdstart(_dir, 4711);
|
||||
|
||||
await using var aufsicht = Aufsicht();
|
||||
await aufsicht.ReattachAsync(); // uebernommen, aber ohne Prozess
|
||||
await aufsicht.ReattachAsync(); // taken over, but without a process
|
||||
aufsicht.ProcessId.ShouldBeNull();
|
||||
|
||||
(await aufsicht.StopAsync()).ShouldBeFalse();
|
||||
@@ -360,20 +360,20 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
Arten.ShouldNotContain(InstanceEventKind.StoppedOrderly);
|
||||
}
|
||||
|
||||
// ─── Wiederfinden (E-M6) ───
|
||||
// ─── Rediscovering (E-M6) ───
|
||||
|
||||
[Fact]
|
||||
public async Task Eine_laufende_Instanz_wird_nach_einem_Managerneustart_uebernommen()
|
||||
{
|
||||
_kanal.Answer = Test;
|
||||
|
||||
// Erster Manager startet sie.
|
||||
// The first manager starts it.
|
||||
await using (var erster = Aufsicht())
|
||||
await erster.StartAsync();
|
||||
|
||||
_ereignisse.Clear();
|
||||
|
||||
// Zweiter Manager, derselbe Rechner, dieselbe laufende Instanz.
|
||||
// Second manager, the same machine, the same running instance.
|
||||
await using var zweiter = Aufsicht();
|
||||
(await zweiter.ReattachAsync()).ShouldBeTrue();
|
||||
|
||||
@@ -381,18 +381,18 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
zweiter.InstanceName.ShouldBe("Test");
|
||||
zweiter.ProcessId.ShouldBe(_launcher.Last!.Id);
|
||||
|
||||
// Uebernehmen ist kein Starten - es gibt nichts zu melden.
|
||||
// Taking over is not starting - there is nothing to report.
|
||||
_ereignisse.ShouldBeEmpty();
|
||||
_launcher.Starts.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eine wiederverwendete Prozesskennung darf nicht zur Übernahme eines fremden
|
||||
/// Programms führen.
|
||||
/// A reused process ID must not lead to taking over a foreign
|
||||
/// program.
|
||||
///
|
||||
/// <para>Nach einem Neustart der Maschine gehört 4711 mit einiger Wahrscheinlichkeit
|
||||
/// etwas ganz anderem. Es dann zu beaufsichtigen — und beim Anhalten zu beenden —
|
||||
/// wäre deutlich schlimmer, als die Instanz gar nicht wiederzufinden.</para>
|
||||
/// <para>After a restart of the machine, 4711 belongs with some probability
|
||||
/// to something entirely different. Supervising it then — and ending it when stopping —
|
||||
/// would be considerably worse than not finding the instance at all.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Eine_wiederverwendete_Prozesskennung_wird_nicht_fuer_die_Instanz_gehalten()
|
||||
@@ -404,7 +404,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
|
||||
var alte = _launcher.Last!;
|
||||
|
||||
// Dieselbe Kennung, viel spaeter gestartet: ein anderer Prozess.
|
||||
// The same ID, started much later: a different process.
|
||||
_launcher.Running[alte.Id] = new FakeProcess(alte.Id, alte.StartedAt.AddHours(3));
|
||||
|
||||
await using var zweiter = Aufsicht();
|
||||
@@ -424,7 +424,7 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
|
||||
InstanceStateStore.Read(_dir).ShouldNotBeNull();
|
||||
|
||||
// Der Manager war weg, die Instanz ist inzwischen auch weg.
|
||||
// The manager was gone, the instance is gone by now too.
|
||||
_kanal.Answer = null;
|
||||
|
||||
await using var zweiter = Aufsicht();
|
||||
@@ -438,8 +438,8 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Ohne_Zustandsdatei_wird_eine_antwortende_Instanz_trotzdem_uebernommen()
|
||||
{
|
||||
// Der Fall nach einem Absturz des Managers, bevor er schreiben konnte - oder
|
||||
// nachdem jemand die Datei geloescht hat. Der Socket antwortet, also laeuft sie.
|
||||
// The case after a crash of the manager before it could write - or
|
||||
// after someone deleted the file. The socket answers, so it is running.
|
||||
_kanal.Answer = Test;
|
||||
_launcher.Fremdstart(_dir, 4711);
|
||||
|
||||
@@ -451,12 +451,12 @@ public sealed class InstanceSupervisorTests : IDisposable
|
||||
InstanceStateStore.Read(_dir).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// ─── Was gemeldet wird und was nicht ───
|
||||
// ─── What is reported and what is not ───
|
||||
|
||||
/// <summary>
|
||||
/// Der Manager meldet, was aus seiner Sicht relevant ist — und nur das (Konzept,
|
||||
/// Abschnitt 4). Was die Instanz betrifft, bleibt in der Instanz und geht mit ihrem
|
||||
/// eigenen Sub-Token raus.
|
||||
/// The manager reports what is relevant from its point of view — and only that (concept,
|
||||
/// section 4). What concerns the instance stays in the instance and goes out with its
|
||||
/// own sub-token.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(InstanceEventKind.Crashed, true)]
|
||||
|
||||
Reference in New Issue
Block a user