using ClawdDotNet.Core.Instances; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Manager.Tests; /// Counts for whom a token was obtained. internal sealed class FakeTokenSource : IInstanceTokenSource { public List Fuer { get; } = []; public bool Erfolg { get; set; } = true; public Task EnsureTokenAsync(string instanceDirectory, CancellationToken ct = default) { Fuer.Add(Path.GetFullPath(instanceDirectory)); return Task.FromResult(Erfolg); } } /// /// The instance manager as a whole (#528, stage 3). /// /// The license is checked here without a license server, and that is intentional: the /// promise is not "the gatekeeper works" — that is the SDK's job —, /// but "a verdict leads to stopping all instances, an outage does not". Exactly /// this path is taken here, triggered via /// . /// 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), SilentKillGrace: TimeSpan.FromMilliseconds(300), MaxDrainWait: TimeSpan.FromSeconds(2)); public ManagerCoreTests() { Directory.CreateDirectory(_root); _verzeichnisse = new InstanceDirectoryManager(Path.Combine(_root, "Instances")); // The socket only answers if a process is really running - exactly as in // operation (see FakeChannel.Launcher). _kanal.Launcher = _launcher; } 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 { /* A leftover test folder does not turn a run red. */ } } /// /// Lets the instance answer from now on — and checks that it was really /// started afterwards and not merely taken over. /// /// The difference is not a subtlety. Whoever stores the answer before /// the manager comes up says with it "this instance is already running" — the /// manager then takes it over (E-M6), the launcher is never touched, and the test /// checks something other than it claims. Exactly that happened to me here twice. /// private async Task 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; } // ─── Starting up ─── [Fact] public async Task Ohne_Lizenzpruefung_arbeitet_der_Manager() { // No gatekeeper means no lock - otherwise there would be a chicken-and-egg lock // before anyone can even enter a key. 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() { // At creation and not at the first start: then the instance file belongs // to nobody without doubt. 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() { // The case of an instance that existed before the manager - or for which the // Deploymentcenter was not reachable at creation. 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 that only runs with perfect permissions is silent exactly // when you need it. _tokens.Erfolg = false; var pfad = _verzeichnisse.CreateInstance("Alt"); await using var manager = Manager(); await manager.StartAsync(); (await StarteUndPruefe(manager, pfad, "Alt")).ShouldBe(StartRefusal.None); } // ─── The verdict ─── /// /// The only case in which the manager stops instances on its own initiative. /// /// A server outage explicitly does not belong here — the SDK separates verdict and /// failed attempt, and only the verdict arrives here. Otherwise a /// network problem would take all installations down at the same time. /// [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); // The instance ends upon the request. var prozess = _launcher.Last!; _kanal.OnStopRequest = _ => { _kanal.AnswerFor[Path.GetFullPath(pfad)] = null; prozess.Exit(0); }; 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."); } // ─── When the start fails ─── [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(); // The process runs, the instance never reports in. (await manager.StartInstanceAsync(pfad)).ShouldBe(StartRefusal.DidNotStart); } }