Neues Projekt ClawdDotNet.Licensing. LicenseGate, LicenseWatch, LicenseInfo und ILicensePrompt sind aus ClawdDotNet.App dorthin gezogen, weil zwei Prozesse dieselbe Lizenzpruefung brauchen und keiner von beiden den anderen kennen darf. In Core konnte es nicht landen - dann zoege jedes der sechzehn Werkzeugprojekte das Deploymentcenter-SDK mit. Dabei ist die Kopplung an SettingsManager verschwunden. LicenseGate kennt jetzt nur ILicenseStore: Serveradresse lesen, Schluessel lesen und schreiben, speichern. Die Anwendung fuehrt ihn in Settings.json, der Manager in manager.json. Der Torwaechter prueft Schluessel; Dateien verwaltet er nicht. Auch die Anwendungsversion kommt als Parameter statt aus einem globalen Zugriff - diese Schicht weiss nicht, welche Anwendung sie benutzt. ManagerCore ist die Klammer: eigene Einstellungen, Lizenz, Register, Meldeweg. Drei Regeln: - Ohne gueltige Lizenz startet er keine Instanz. Laufende werden trotzdem in die Liste genommen - man soll sie sehen und anhalten koennen. - Ein Widerruf haelt alle an. Der einzige Fall, in dem der Manager das von sich aus tut. Ein Serverausfall gehoert ausdruecklich nicht dazu; das SDK trennt Urteil und gescheiterten Versuch, und nur das Urteil kommt hier an. Sonst naehme ein Netzproblem saemtliche Installationen mit. - Ohne hinterlegte Lizenzpruefung sperrt er nichts, sonst gaebe es eine Henne-Ei-Sperre, bevor jemand einen Schluessel eintragen kann. Sub-Token je Instanz: Der Manager haelt das Master-Token und tauscht es beim ANLEGEN einer Instanz gegen ein eigenes (watchdog:ping, bugtracker:report, einzeln widerrufbar). Beim Anlegen und nicht beim ersten Start, weil die Instanzdatei dann zweifelsfrei niemandem gehoert - sie spaeter zu schreiben, waehrend der Instanzprozess laeuft, hiesse zwei Schreiber auf einer Datei. Fuer Instanzen, die es vor dem Manager schon gab, wird es vor dem Start nachgeholt. Danach fragt er ueber whoami nach, ob das Token an ein Projekt gebunden ist (SDK 2.8.1). Ein fehlgeschlagener Tokenbezug haelt die Instanz nicht auf. NOCH NICHT VOLLSTAENDIG: Die Instanzen pruefen die Lizenz weiterhin selbst. Das laesst sich erst abschalten, wenn eine Instanz feststellen kann, ob ein Manager da ist - vorher entstuende eine Fassung ganz ohne Durchsetzung. Und ein Testfehler, der zweimal derselbe war: Die Socket-Antwort war hinterlegt, BEVOR der Manager hochfuhr. Damit sagt man "diese Instanz laeuft bereits", der Manager uebernimmt sie, der Starter wird nie angefasst - und der Test prueft etwas anderes, als er behauptet. Es gibt jetzt eine Hilfsmethode, die das ausschliesst. Build ohne Warnungen, 774 Tests gruen, 8 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
7.6 KiB
C#
220 lines
7.6 KiB
C#
using ClawdDotNet.Core.Instances;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Manager.Tests;
|
|
|
|
/// <summary>Zählt mit, wem ein Token besorgt wurde.</summary>
|
|
internal sealed class FakeTokenSource : IInstanceTokenSource
|
|
{
|
|
public List<string> Fuer { get; } = [];
|
|
|
|
public bool Erfolg { get; set; } = true;
|
|
|
|
public Task<bool> EnsureTokenAsync(string instanceDirectory, CancellationToken ct = default)
|
|
{
|
|
Fuer.Add(Path.GetFullPath(instanceDirectory));
|
|
return Task.FromResult(Erfolg);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Der Instanzmanager als Ganzes (#528, Stufe 3).
|
|
///
|
|
/// <para><b>Die Lizenz wird hier ohne Lizenzserver geprüft</b>, und das ist Absicht: Die
|
|
/// Zusage lautet nicht „der Torwächter funktioniert" — das ist die Aufgabe des SDK —,
|
|
/// sondern „ein Urteil führt zum Anhalten aller Instanzen, ein Ausfall nicht". Genau
|
|
/// dieser Weg wird hier gegangen, ausgelöst über
|
|
/// <see cref="ManagerCore.HandleLicenseRevokedAsync"/>.</para>
|
|
/// </summary>
|
|
public sealed class ManagerCoreTests : IAsyncDisposable
|
|
{
|
|
private readonly string _root = Path.Combine(
|
|
Path.GetTempPath(), "clawd-tests", "core-" + Guid.NewGuid().ToString("N")[..8]);
|
|
|
|
private readonly FakeLauncher _launcher = new();
|
|
private readonly FakeChannel _kanal = new();
|
|
private readonly FakeTokenSource _tokens = new();
|
|
private readonly InstanceDirectoryManager _verzeichnisse;
|
|
|
|
private static readonly SupervisionOptions Options = new(
|
|
ProbeInterval: TimeSpan.FromMilliseconds(50),
|
|
StartTimeout: TimeSpan.FromMilliseconds(600),
|
|
SilentChecksBeforeAlarm: 3,
|
|
StopGrace: TimeSpan.FromMilliseconds(300));
|
|
|
|
public ManagerCoreTests()
|
|
{
|
|
Directory.CreateDirectory(_root);
|
|
_verzeichnisse = new InstanceDirectoryManager(Path.Combine(_root, "Instances"));
|
|
}
|
|
|
|
private ManagerCore Manager()
|
|
{
|
|
var register = new InstanceRegistry(
|
|
_verzeichnisse, _launcher, _kanal, NullLoggerFactory.Instance, Options);
|
|
|
|
return new ManagerCore(
|
|
new ManagerSettingsStore(_root), register, _tokens, NullLoggerFactory.Instance);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await Task.CompletedTask;
|
|
try { Directory.Delete(_root, recursive: true); }
|
|
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lässt die Instanz ab jetzt antworten — und prüft, dass sie danach wirklich
|
|
/// gestartet und nicht bloß übernommen wurde.
|
|
///
|
|
/// <para><b>Der Unterschied ist keine Feinheit.</b> Wer die Antwort schon vor dem
|
|
/// Hochfahren des Managers hinterlegt, sagt damit „diese Instanz läuft bereits" — der
|
|
/// Manager übernimmt sie dann (E-M6), der Starter wird nie angefasst, und der Test
|
|
/// prüft etwas anderes, als er behauptet. Genau das ist mir hier zweimal
|
|
/// passiert.</para>
|
|
/// </summary>
|
|
private async Task<StartRefusal> StarteUndPruefe(ManagerCore manager, string pfad, string name)
|
|
{
|
|
var vorher = _launcher.Starts;
|
|
|
|
_kanal.AnswerFor[Path.GetFullPath(pfad)] = new InstanceIdentity("i-1", name, "0.1.2");
|
|
|
|
var ergebnis = await manager.StartInstanceAsync(pfad);
|
|
|
|
_launcher.Starts.ShouldBe(vorher + 1, "Gestartet, nicht uebernommen.");
|
|
return ergebnis;
|
|
}
|
|
|
|
// ─── Hochfahren ───
|
|
|
|
[Fact]
|
|
public async Task Ohne_Lizenzpruefung_arbeitet_der_Manager()
|
|
{
|
|
// Kein Torwaechter heisst keine Sperre - sonst gaebe es eine Henne-Ei-Sperre,
|
|
// bevor ueberhaupt jemand einen Schluessel eintragen kann.
|
|
await using var manager = Manager();
|
|
|
|
(await manager.StartAsync()).ShouldBeTrue();
|
|
manager.LicenseHolds.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Beim_Hochfahren_wird_nichts_gestartet()
|
|
{
|
|
_verzeichnisse.CreateInstance("Alpha");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
|
|
manager.Instances.Instances.Count.ShouldBe(1);
|
|
_launcher.Starts.ShouldBe(0);
|
|
}
|
|
|
|
// ─── Token ───
|
|
|
|
[Fact]
|
|
public async Task Eine_neue_Instanz_bekommt_gleich_ihr_eigenes_Token()
|
|
{
|
|
// Beim Anlegen und nicht beim ersten Start: Dann gehoert die Instanzdatei
|
|
// zweifelsfrei niemandem.
|
|
await using var manager = Manager();
|
|
|
|
var pfad = await manager.CreateInstanceAsync("Frisch");
|
|
|
|
_tokens.Fuer.ShouldBe([Path.GetFullPath(pfad)]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Vor_dem_Start_wird_das_Token_nachgeholt()
|
|
{
|
|
// Der Fall einer Instanz, die es vor dem Manager schon gab - oder bei der das
|
|
// Deploymentcenter beim Anlegen nicht erreichbar war.
|
|
var pfad = _verzeichnisse.CreateInstance("Alt");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
|
|
(await StarteUndPruefe(manager, pfad, "Alt")).ShouldBe(StartRefusal.None);
|
|
|
|
_tokens.Fuer.ShouldContain(Path.GetFullPath(pfad));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ein_fehlgeschlagener_Tokenbezug_verhindert_den_Start_nicht()
|
|
{
|
|
// Monitoring, das nur bei perfekter Rechtelage laeuft, ist genau dann still,
|
|
// wenn man es braucht.
|
|
_tokens.Erfolg = false;
|
|
|
|
var pfad = _verzeichnisse.CreateInstance("Alt");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
|
|
(await StarteUndPruefe(manager, pfad, "Alt")).ShouldBe(StartRefusal.None);
|
|
}
|
|
|
|
// ─── Das Urteil ───
|
|
|
|
/// <summary>
|
|
/// Der einzige Fall, in dem der Manager von sich aus Instanzen anhält.
|
|
///
|
|
/// <para>Ein Serverausfall gehört ausdrücklich nicht dazu — das SDK trennt Urteil und
|
|
/// gescheiterten Versuch, und nur das Urteil kommt hier an. Sonst nähme ein
|
|
/// Netzproblem sämtliche Installationen gleichzeitig mit.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Ein_Lizenzwiderruf_haelt_alle_laufenden_Instanzen_an()
|
|
{
|
|
var pfad = _verzeichnisse.CreateInstance("Alpha");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
await StarteUndPruefe(manager, pfad, "Alpha");
|
|
|
|
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
|
|
|
|
// Die Instanz endet auf die Bitte hin.
|
|
_launcher.Last!.Exit(0);
|
|
_kanal.AnswerFor[Path.GetFullPath(pfad)] = null;
|
|
|
|
await manager.HandleLicenseRevokedAsync("Widerrufen durch den Hersteller");
|
|
|
|
manager.LicenseHolds.ShouldBeFalse();
|
|
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Stopped);
|
|
_kanal.StopRequests.ShouldBe(1, "Erst bitten, nicht schiessen.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Nach_einem_Widerruf_startet_keine_Instanz_mehr()
|
|
{
|
|
var pfad = _verzeichnisse.CreateInstance("Alpha");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
|
|
await manager.HandleLicenseRevokedAsync("Widerrufen");
|
|
|
|
(await manager.StartInstanceAsync(pfad)).ShouldBe(StartRefusal.NoLicense);
|
|
|
|
_launcher.Starts.ShouldBe(0);
|
|
_tokens.Fuer.ShouldBeEmpty("Ohne Lizenz wird auch kein Token verbraucht.");
|
|
}
|
|
|
|
// ─── Wenn der Start scheitert ───
|
|
|
|
[Fact]
|
|
public async Task Eine_Instanz_die_sich_nicht_meldet_gilt_als_nicht_gestartet()
|
|
{
|
|
var pfad = _verzeichnisse.CreateInstance("Stumm");
|
|
|
|
await using var manager = Manager();
|
|
await manager.StartAsync();
|
|
|
|
// Der Prozess laeuft, die Instanz meldet sich nie.
|
|
(await manager.StartInstanceAsync(pfad)).ShouldBe(StartRefusal.DidNotStart);
|
|
}
|
|
}
|