Files
ClawdDotNet/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs
T
RichardandClaude Opus 5.5 837a634a22 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>
2026-10-04 12:45:03 +02:00

224 lines
7.8 KiB
C#

using ClawdDotNet.Core.Instances;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Manager.Tests;
/// <summary>Counts for whom a token was obtained.</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>
/// The instance manager as a whole (#528, stage 3).
///
/// <para><b>The license is checked here without a license server</b>, 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
/// <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),
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. */ }
}
/// <summary>
/// Lets the instance answer from now on — and checks that it was really
/// started afterwards and not merely taken over.
///
/// <para><b>The difference is not a subtlety.</b> 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.</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;
}
// ─── 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 ───
/// <summary>
/// The only case in which the manager stops instances on its own initiative.
///
/// <para>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.</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);
// 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);
}
}