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>
241 lines
8.5 KiB
C#
241 lines
8.5 KiB
C#
using ClawdDotNet.Core.Instances;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Manager.Tests;
|
|
|
|
/// <summary>
|
|
/// <c>manager.json</c> and the log excerpt — the two places where the manager
|
|
/// talks to the file system (#528, stage 3).
|
|
/// </summary>
|
|
public sealed class ManagerSettingsTests : IDisposable
|
|
{
|
|
private readonly string _dir = Path.Combine(
|
|
Path.GetTempPath(), "clawd-tests", "mgr-" + Guid.NewGuid().ToString("N")[..8]);
|
|
|
|
public ManagerSettingsTests() => Directory.CreateDirectory(_dir);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_dir, recursive: true); }
|
|
catch { /* A leftover test folder does not turn a run red. */ }
|
|
}
|
|
|
|
[Fact]
|
|
public void Eine_fehlende_Datei_wird_mit_Vorgaben_angelegt()
|
|
{
|
|
var store = new ManagerSettingsStore(_dir);
|
|
var settings = store.Load();
|
|
|
|
File.Exists(store.SettingsPath).ShouldBeTrue("Der Ort soll sichtbar werden.");
|
|
settings.WebUiPort.ShouldBe(7000);
|
|
settings.UpdateChannel.ShouldBe("prod");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Master token and license key are not in plain text in the file — but they are at
|
|
/// runtime, otherwise nobody could work with them.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Geheimnisse_stehen_nicht_im_Klartext_in_der_Datei()
|
|
{
|
|
var store = new ManagerSettingsStore(_dir);
|
|
store.Load();
|
|
|
|
store.Settings.DeploymentcenterMasterToken = "dc_master_geheim_1234567890";
|
|
store.Settings.LicenseKey = "LIZ-1234-5678";
|
|
store.Save();
|
|
|
|
File.ReadAllText(store.SettingsPath).ShouldNotContain("dc_master_geheim_1234567890");
|
|
File.ReadAllText(store.SettingsPath).ShouldNotContain("LIZ-1234-5678");
|
|
|
|
var erneut = new ManagerSettingsStore(_dir).Load();
|
|
erneut.DeploymentcenterMasterToken.ShouldBe("dc_master_geheim_1234567890");
|
|
erneut.LicenseKey.ShouldBe("LIZ-1234-5678");
|
|
}
|
|
|
|
/// <summary>
|
|
/// A file written by hand or by PowerShell often has a byte order mark,
|
|
/// and it must still apply.
|
|
///
|
|
/// <para>In the acceptance run it looked as if it were ignored — the manager worked
|
|
/// with defaults. This test clarifies whether that was due to the file.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void Eine_Datei_mit_Byte_Order_Mark_gilt_auch()
|
|
{
|
|
var pfad = Path.Combine(_dir, "manager.json");
|
|
|
|
// Exactly as PowerShell writes it: with BOM, with double spaces after
|
|
// the colon, and with doubled backslashes in the path.
|
|
var inhalt = """
|
|
{
|
|
"instanceExecutable": "J:\\Programme\\ClawdDotNet.exe",
|
|
"webUiPort": 7009,
|
|
"deploymentcenterUrl": ""
|
|
}
|
|
""";
|
|
|
|
File.WriteAllText(pfad, inhalt, new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
|
File.ReadAllBytes(pfad)[0].ShouldBe((byte)0xEF, "Der Test braucht das BOM.");
|
|
|
|
var store = new ManagerSettingsStore(_dir);
|
|
var settings = store.Load();
|
|
|
|
store.LoadProblem.ShouldBeNull();
|
|
settings.InstanceExecutable.ShouldBe(@"J:\Programme\ClawdDotNet.exe");
|
|
settings.WebUiPort.ShouldBe(7009);
|
|
settings.DeploymentcenterUrl.ShouldBe("");
|
|
}
|
|
|
|
/// <summary>
|
|
/// An unreadable file must not prevent the manager from starting — then
|
|
/// no instance would run at all, and that is the worse state.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Eine_kaputte_Datei_verhindert_den_Start_nicht()
|
|
{
|
|
File.WriteAllText(Path.Combine(_dir, "manager.json"), "{ das ist kein JSON");
|
|
|
|
var store = new ManagerSettingsStore(_dir);
|
|
|
|
store.Load().WebUiPort.ShouldBe(7000);
|
|
|
|
// And the reason is recorded - otherwise the manager silently works with
|
|
// defaults, and nobody can say why its instance directory is wrong.
|
|
store.LoadProblem.ShouldNotBeNullOrWhiteSpace();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Nonsensical numbers can be in a hand-editable file. A
|
|
/// probe interval of 0 would be an endless loop, an alarm after zero failed attempts an
|
|
/// alarm at every hiccup.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Unsinnige_Aufsichtswerte_werden_eingefangen()
|
|
{
|
|
var settings = new ManagerSettings
|
|
{
|
|
ProbeIntervalSeconds = 0,
|
|
StartTimeoutSeconds = -5,
|
|
SilentChecksBeforeAlarm = 0,
|
|
StopGraceSeconds = 99999
|
|
};
|
|
|
|
var options = settings.ToSupervisionOptions();
|
|
|
|
options.ProbeInterval.ShouldBeGreaterThan(TimeSpan.Zero);
|
|
options.StartTimeout.ShouldBeGreaterThan(TimeSpan.Zero);
|
|
options.SilentChecksBeforeAlarm.ShouldBeGreaterThanOrEqualTo(1);
|
|
options.StopGrace.ShouldBeLessThanOrEqualTo(TimeSpan.FromMinutes(10));
|
|
}
|
|
}
|
|
|
|
/// <summary>The log excerpt that only gives a crash report its value.</summary>
|
|
public sealed class InstanceLogExcerptTests : IDisposable
|
|
{
|
|
private readonly string _instanz = Path.Combine(
|
|
Path.GetTempPath(), "clawd-tests", "log-" + Guid.NewGuid().ToString("N")[..8]);
|
|
|
|
public InstanceLogExcerptTests() => Directory.CreateDirectory(_instanz);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_instanz, recursive: true); }
|
|
catch { /* A leftover test folder does not turn a run red. */ }
|
|
}
|
|
|
|
private void Schreibe(string tag, string modul, params string[] zeilen)
|
|
{
|
|
var dir = Path.Combine(_instanz, "Logs", tag);
|
|
Directory.CreateDirectory(dir);
|
|
File.WriteAllLines(Path.Combine(dir, modul), zeilen);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ohne_Protokoll_kommt_nichts_zurueck()
|
|
{
|
|
// Not an error, but a finding: a process that was killed before its
|
|
// write buffer was flushed leaves exactly nothing (#529).
|
|
InstanceLogExcerpt.Read(_instanz).ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modules describe the same period. With a crash you want to know what
|
|
/// happened at the same time — not first everything from one file, then everything from the
|
|
/// next.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Die_Module_werden_nach_Zeit_zusammengefuehrt()
|
|
{
|
|
Schreibe("2026-09-08", "Startup.log",
|
|
"[17:26:48.100] [INF] Instanz Test2 angelegt",
|
|
"[17:26:50.900] [INF] Application is shutting down");
|
|
|
|
Schreibe("2026-09-08", "Web.log",
|
|
"[17:26:49.500] [INF] Weboberfläche beendet");
|
|
|
|
var zeilen = InstanceLogExcerpt.Read(_instanz);
|
|
|
|
zeilen.Count.ShouldBe(3);
|
|
zeilen[0].ShouldContain("17:26:48");
|
|
zeilen[1].ShouldContain("17:26:49");
|
|
zeilen[2].ShouldContain("17:26:50");
|
|
|
|
// Which file a line comes from is part of it.
|
|
zeilen[1].ShouldStartWith("Web.log:");
|
|
}
|
|
|
|
[Fact]
|
|
public void Nur_der_juengste_Tag_zaehlt()
|
|
{
|
|
Schreibe("2026-09-01", "Startup.log", "[10:00:00.000] [INF] uralt");
|
|
Schreibe("2026-09-08", "Startup.log", "[10:00:00.000] [INF] frisch");
|
|
|
|
InstanceLogExcerpt.Read(_instanz).ShouldHaveSingleItem().ShouldContain("frisch");
|
|
}
|
|
|
|
[Fact]
|
|
public void Es_werden_nur_die_letzten_Zeilen_genommen()
|
|
{
|
|
Schreibe("2026-09-08", "Startup.log",
|
|
Enumerable.Range(0, 500).Select(i => $"[10:00:{i % 60:00}.000] [INF] Zeile {i}").ToArray());
|
|
|
|
InstanceLogExcerpt.Read(_instanz, lines: 10).Count.ShouldBeLessThanOrEqualTo(10);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The file may still be written to at exactly this moment — by an instance
|
|
/// that is just ending. Whoever wanted to open it exclusively might get nothing at all.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Eine_offen_beschriebene_Datei_laesst_sich_lesen()
|
|
{
|
|
Schreibe("2026-09-08", "Startup.log", "[10:00:00.000] [INF] erste Zeile");
|
|
|
|
var pfad = Path.Combine(_instanz, "Logs", "2026-09-08", "Startup.log");
|
|
|
|
using var offen = new FileStream(
|
|
pfad, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
|
|
|
InstanceLogExcerpt.Read(_instanz).ShouldNotBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Ein_eigenes_Protokollverzeichnis_wird_beachtet()
|
|
{
|
|
var eigen = Path.Combine(_instanz, "AndereLogs");
|
|
Directory.CreateDirectory(Path.Combine(eigen, "2026-09-08"));
|
|
File.WriteAllLines(
|
|
Path.Combine(eigen, "2026-09-08", "Startup.log"),
|
|
["[10:00:00.000] [INF] woanders"]);
|
|
|
|
var config = new ClawdDotNet.Core.Config.InstanceConfig
|
|
{
|
|
WorkingDirectory = _instanz,
|
|
LogDirectory = "AndereLogs"
|
|
};
|
|
|
|
InstanceLogExcerpt.Read(_instanz, config).ShouldHaveSingleItem().ShouldContain("woanders");
|
|
}
|
|
}
|