Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt. Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder. Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass: Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden. Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202 beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf seiner Frist jedes Mal ab. InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist. - ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten, Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb. - GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie haengt. - Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten, nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz. - EnsureDeadAsync vor jedem Start. Weiteres aus dem Review: - .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/. InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt. - Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein. - Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus. - [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand abstellen konnte, solange E-M5 nicht fertig ist. - Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht. - Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und der Port kommt aus manager.json statt fest aus der Zeile. Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535). 806 Tests gruen, 0 Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
470 lines
17 KiB
C#
470 lines
17 KiB
C#
using ClawdDotNet.Core.Instances;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Manager.Tests;
|
|
|
|
/// <summary>
|
|
/// Die Aufsicht über eine Instanz (#528, Stufe 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>Der schwierigere Fall steht darunter: ein Prozess, der <em>lebt</em> und nicht
|
|
/// mehr arbeitet. Den sieht keine Prozessliste — nur der Socket.</para>
|
|
/// </summary>
|
|
public sealed class InstanceSupervisorTests : IDisposable
|
|
{
|
|
private readonly string _dir = Path.Combine(
|
|
Path.GetTempPath(), "clawd-tests", "sup-" + Guid.NewGuid().ToString("N")[..8]);
|
|
|
|
private readonly FakeLauncher _launcher = new();
|
|
private readonly FakeChannel _kanal = new();
|
|
private readonly List<SupervisionEvent> _ereignisse = [];
|
|
|
|
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.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
private static readonly SupervisionOptions Options = new(
|
|
ProbeInterval: TimeSpan.FromMilliseconds(50),
|
|
StartTimeout: TimeSpan.FromMilliseconds(600),
|
|
SilentChecksBeforeAlarm: 3,
|
|
StopGrace: TimeSpan.FromMilliseconds(300),
|
|
SilentKillGrace: TimeSpan.FromMilliseconds(300),
|
|
MaxDrainWait: TimeSpan.FromSeconds(2));
|
|
|
|
public InstanceSupervisorTests()
|
|
{
|
|
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.
|
|
_kanal.Launcher = _launcher;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); }
|
|
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
|
}
|
|
|
|
private InstanceSupervisor Aufsicht()
|
|
{
|
|
var supervisor = new InstanceSupervisor(
|
|
_dir, _launcher, _kanal, NullLogger.Instance, Options);
|
|
|
|
supervisor.Observed += _ereignisse.Add;
|
|
return supervisor;
|
|
}
|
|
|
|
private IEnumerable<InstanceEventKind> Arten => _ereignisse.Select(e => e.Kind);
|
|
|
|
// ─── Starten ───
|
|
|
|
[Fact]
|
|
public async Task Ein_Start_gilt_erst_wenn_die_Instanz_antwortet()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
(await aufsicht.StartAsync()).ShouldBeTrue();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Running);
|
|
aufsicht.InstanceName.ShouldBe("Test");
|
|
Arten.ShouldBe([InstanceEventKind.Started]);
|
|
|
|
var state = InstanceStateStore.Read(_dir).ShouldNotBeNull();
|
|
state.InstanceId.ShouldBe("i-1");
|
|
state.ProcessId.ShouldBe(_launcher.Last!.Id);
|
|
state.ManagerProcessId.ShouldBe(Environment.ProcessId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// „Der Prozess läuft" ist nicht dasselbe wie „die Instanz läuft".
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Ein_Prozess_der_sich_nie_meldet_gilt_nicht_als_gestartet()
|
|
{
|
|
_kanal.Answer = null; // laeuft, sagt aber nichts
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
(await aufsicht.StartAsync()).ShouldBeFalse();
|
|
|
|
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.
|
|
_launcher.Last!.WasKilled.ShouldBeTrue();
|
|
InstanceStateStore.Read(_dir).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ein_Prozess_der_gar_nicht_startet_meldet_das()
|
|
{
|
|
_launcher.FailStartWith = new InvalidOperationException("Programmdatei fehlt");
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
(await aufsicht.StartAsync()).ShouldBeFalse();
|
|
|
|
Arten.ShouldBe([InstanceEventKind.StartFailed]);
|
|
_ereignisse[0].Message.ShouldContain("Programmdatei fehlt");
|
|
}
|
|
|
|
// ─── Der Fall, um den es geht ───
|
|
|
|
[Fact]
|
|
public async Task Ein_abgeschossener_Prozess_wird_bemerkt_und_gemeldet()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
// Jemand schiesst die Instanz ab. Niemand hat darum gebeten.
|
|
_launcher.Last!.Exit(code: 137);
|
|
_kanal.Answer = null;
|
|
|
|
await aufsicht.CheckAsync();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Crashed);
|
|
|
|
var absturz = _ereignisse.Single(e => e.Kind == InstanceEventKind.Crashed);
|
|
absturz.ExitCode.ShouldBe(137);
|
|
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.
|
|
InstanceStateStore.Read(_dir).ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Der schwierigere Fall: Der Prozess lebt und arbeitet nicht mehr.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Ein_lebender_aber_stummer_Prozess_faellt_auf()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
_kanal.Answer = null; // haengt - der Prozess lebt weiter
|
|
|
|
// Nicht beim ersten Aussetzer: Der waere Alltag, und ein Alarm bei jedem
|
|
// Aussetzer ist nach einer Woche der Alarm, den niemand mehr liest.
|
|
await aufsicht.CheckAsync();
|
|
await aufsicht.CheckAsync();
|
|
Arten.ShouldNotContain(InstanceEventKind.Unresponsive);
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Running);
|
|
|
|
await aufsicht.CheckAsync();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Silent);
|
|
var alarm = _ereignisse.Single(e => e.Kind == InstanceEventKind.Unresponsive);
|
|
alarm.WorthReporting.ShouldBeTrue();
|
|
_launcher.Last!.HasExited.ShouldBeFalse("Der Prozess laeuft ja - das ist der Punkt.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ein_stummer_Prozess_wird_einmal_gemeldet_und_nicht_bei_jedem_Durchgang()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
_kanal.Answer = null;
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
await aufsicht.CheckAsync();
|
|
|
|
_ereignisse.Count(e => e.Kind == InstanceEventKind.Unresponsive).ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wer_wieder_antwortet_wird_wieder_als_laufend_gefuehrt()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
_kanal.Answer = null;
|
|
for (var i = 0; i < 3; i++) await aufsicht.CheckAsync();
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Silent);
|
|
|
|
_kanal.Answer = Test;
|
|
await aufsicht.CheckAsync();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Running);
|
|
Arten.ShouldContain(InstanceEventKind.Recovered);
|
|
}
|
|
|
|
// ─── Anhalten ───
|
|
|
|
[Fact]
|
|
public async Task Ein_erbetenes_Ende_ist_kein_Absturz()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
// Die Instanz endet auf die Bitte hin - so wie im Betrieb.
|
|
_launcher.Last!.Exit(code: 0);
|
|
_kanal.Answer = null;
|
|
|
|
(await aufsicht.StopAsync()).ShouldBeTrue();
|
|
|
|
_kanal.StopRequests.ShouldBe(1, "Erst bitten, nicht schiessen.");
|
|
_launcher.Last.WasKilled.ShouldBeFalse();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Stopped);
|
|
Arten.ShouldNotContain(InstanceEventKind.Crashed);
|
|
_ereignisse.Last().Kind.ShouldBe(InstanceEventKind.StoppedOrderly);
|
|
_ereignisse.Last().WorthReporting.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wer_nicht_hoert_wird_nach_der_Frist_beendet()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
// Antwortet weiter und endet nicht - die Bitte verhallt.
|
|
_kanal.StopRequestArrives = false;
|
|
|
|
(await aufsicht.StopAsync()).ShouldBeTrue();
|
|
|
|
_kanal.StopRequests.ShouldBe(1);
|
|
_launcher.Last!.WasKilled.ShouldBeTrue();
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Stopped);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Schweigen ist kein Todesnachweis, solange ein Prozesshandle das Gegenteil sagt.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Ein_stummer_aber_lebender_Prozess_gilt_nicht_als_beendet()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
var prozess = _launcher.Last.ShouldNotBeNull();
|
|
|
|
// Sie haengt: Der Socket schweigt, der Prozess lebt.
|
|
_kanal.Verstummen(_dir);
|
|
|
|
(await aufsicht.StopAsync()).ShouldBeTrue();
|
|
|
|
prozess.WasKilled.ShouldBeTrue(
|
|
"Wer schweigt und lebt, muss beendet werden - nicht fuer beendet erklaert.");
|
|
|
|
prozess.HasExited.ShouldBeTrue();
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Stopped);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wer aufräumt, wird nicht abgeschossen — die Frist wird nachgezogen.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Wer_aufraeumt_bekommt_die_Frist_nachgezogen()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.StartAsync();
|
|
|
|
var prozess = _launcher.Last.ShouldNotBeNull();
|
|
|
|
// Sie antwortet und meldet: Ich raeume auf. Laenger als StopGrace (300ms), aber
|
|
// kuerzer als MaxDrainWait (2s).
|
|
_kanal.Answer = Test with { Phase = ShutdownPhase.Draining, OpenWork = 1 };
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
await Task.Delay(700);
|
|
_kanal.Answer = null;
|
|
prozess.Exit(0);
|
|
});
|
|
|
|
(await aufsicht.StopAsync()).ShouldBeTrue();
|
|
|
|
prozess.WasKilled.ShouldBeFalse("Wer aufraeumt, wird nicht abgeschossen.");
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Stopped);
|
|
}
|
|
|
|
/// <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.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Was_nicht_zu_fassen_ist_wird_nicht_als_beendet_gemeldet()
|
|
{
|
|
_kanal.Answer = Test;
|
|
_kanal.StopRequestArrives = false;
|
|
|
|
// Sie lief schon, bevor dieser Manager kam - und ohne Zustandsdatei ist ihr
|
|
// Prozess fuer ihn nicht zu greifen.
|
|
_launcher.Fremdstart(_dir, 4711);
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
await aufsicht.ReattachAsync(); // uebernommen, aber ohne Prozess
|
|
aufsicht.ProcessId.ShouldBeNull();
|
|
|
|
(await aufsicht.StopAsync()).ShouldBeFalse();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Running);
|
|
Arten.ShouldNotContain(InstanceEventKind.StoppedOrderly);
|
|
}
|
|
|
|
// ─── Wiederfinden (E-M6) ───
|
|
|
|
[Fact]
|
|
public async Task Eine_laufende_Instanz_wird_nach_einem_Managerneustart_uebernommen()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
// Erster Manager startet sie.
|
|
await using (var erster = Aufsicht())
|
|
await erster.StartAsync();
|
|
|
|
_ereignisse.Clear();
|
|
|
|
// Zweiter Manager, derselbe Rechner, dieselbe laufende Instanz.
|
|
await using var zweiter = Aufsicht();
|
|
(await zweiter.ReattachAsync()).ShouldBeTrue();
|
|
|
|
zweiter.Condition.ShouldBe(InstanceCondition.Running);
|
|
zweiter.InstanceName.ShouldBe("Test");
|
|
zweiter.ProcessId.ShouldBe(_launcher.Last!.Id);
|
|
|
|
// Uebernehmen ist kein Starten - es gibt nichts zu melden.
|
|
_ereignisse.ShouldBeEmpty();
|
|
_launcher.Starts.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Eine wiederverwendete Prozesskennung darf nicht zur Übernahme eines fremden
|
|
/// Programms führen.
|
|
///
|
|
/// <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>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Eine_wiederverwendete_Prozesskennung_wird_nicht_fuer_die_Instanz_gehalten()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using (var erster = Aufsicht())
|
|
await erster.StartAsync();
|
|
|
|
var alte = _launcher.Last!;
|
|
|
|
// Dieselbe Kennung, viel spaeter gestartet: ein anderer Prozess.
|
|
_launcher.Running[alte.Id] = new FakeProcess(alte.Id, alte.StartedAt.AddHours(3));
|
|
|
|
await using var zweiter = Aufsicht();
|
|
(await zweiter.ReattachAsync()).ShouldBeTrue("Der Socket antwortet ja.");
|
|
|
|
zweiter.ProcessId.ShouldBeNull(
|
|
"Der fremde Prozess darf nicht als der eigene uebernommen werden.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Eine_Zustandsdatei_ohne_laufende_Instanz_wird_weggeraeumt()
|
|
{
|
|
_kanal.Answer = Test;
|
|
|
|
await using (var erster = Aufsicht())
|
|
await erster.StartAsync();
|
|
|
|
InstanceStateStore.Read(_dir).ShouldNotBeNull();
|
|
|
|
// Der Manager war weg, die Instanz ist inzwischen auch weg.
|
|
_kanal.Answer = null;
|
|
|
|
await using var zweiter = Aufsicht();
|
|
(await zweiter.ReattachAsync()).ShouldBeFalse();
|
|
|
|
zweiter.Condition.ShouldBe(InstanceCondition.Stopped);
|
|
InstanceStateStore.Read(_dir).ShouldBeNull(
|
|
"Eine Datei, die auf niemanden zeigt, laesst es aussehen, als liefe etwas.");
|
|
}
|
|
|
|
[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.
|
|
_kanal.Answer = Test;
|
|
_launcher.Fremdstart(_dir, 4711);
|
|
|
|
await using var aufsicht = Aufsicht();
|
|
(await aufsicht.ReattachAsync()).ShouldBeTrue();
|
|
|
|
aufsicht.Condition.ShouldBe(InstanceCondition.Running);
|
|
aufsicht.ProcessId.ShouldBeNull("Es gibt keinen Prozess, den dieser Manager kennt.");
|
|
InstanceStateStore.Read(_dir).ShouldNotBeNull();
|
|
}
|
|
|
|
// ─── Was gemeldet wird und was nicht ───
|
|
|
|
/// <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.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(InstanceEventKind.Crashed, true)]
|
|
[InlineData(InstanceEventKind.Unresponsive, true)]
|
|
[InlineData(InstanceEventKind.StartFailed, true)]
|
|
[InlineData(InstanceEventKind.Started, false)]
|
|
[InlineData(InstanceEventKind.StoppedOrderly, false)]
|
|
[InlineData(InstanceEventKind.Recovered, false)]
|
|
public void Gemeldet_wird_nur_was_niemand_bestellt_hat(InstanceEventKind kind, bool erwartet)
|
|
{
|
|
new SupervisionEvent(kind, _dir, "Test", 1, null, "", DateTimeOffset.UtcNow)
|
|
.WorthReporting.ShouldBe(erwartet);
|
|
}
|
|
}
|