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>
This commit is contained in:
Richard
2026-10-04 12:45:03 +02:00
co-authored by Claude Opus 5.5
parent da4ceac7d5
commit 837a634a22
311 changed files with 7238 additions and 7252 deletions
@@ -4,19 +4,19 @@ using Shouldly;
namespace ClawdDotNet.App.Tests;
/// <summary>
/// Wohin eine Instanz protokolliert und sichert — und dass zwei Instanzen dabei nicht
/// im selben Ordner landen (#528, Stufe 1).
/// Where an instance logs to and backs up to — and that two instances do not end up
/// in the same folder (#528, stage 1).
///
/// <para><b>Der Anlass.</b> Beide Vorgaben waren anwendungsweit. In <c>Startup.log</c>
/// standen deshalb abwechselnd Zeilen zweier Instanzen, ohne Kennzeichnung, welche
/// gerade spricht; und weil die Aufbewahrung nach Präfix in genau einem Ordner
/// aufräumt, warf die laufende Instanz die Archive der anderen mit weg. Solange das so
/// ist, ist jede Fehlersuche im Mehrinstanzbetrieb Ratearbeit.</para>
/// <para><b>The occasion.</b> Both defaults were application-wide. <c>Startup.log</c>
/// therefore contained alternating lines of two instances, without marking which one
/// was speaking; and because retention cleans up by prefix in exactly one folder,
/// the running instance threw away the other's archives along with its own. As long as that is
/// the case, every troubleshooting session in multi-instance operation is guesswork.</para>
///
/// <para><b>Was aus #375 bleibt.</b> Ein relativer Wert zählt gegen das
/// Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses. Ein
/// systemd-Dienst ohne <c>WorkingDirectory=</c> startet in <c>/</c>, und <c>"./Logs"</c>
/// zeigte dann auf <c>/Logs</c> — für ein Dienstkonto nicht anlegbar.</para>
/// <para><b>What remains from #375.</b> A relative value is resolved against the
/// instance directory, not against the working directory of the process. A
/// systemd service without <c>WorkingDirectory=</c> starts in <c>/</c>, and <c>"./Logs"</c>
/// then pointed to <c>/Logs</c> — not creatable for a service account.</para>
/// </summary>
public sealed class LogDirectoryResolutionTests
{
@@ -32,13 +32,13 @@ public sealed class LogDirectoryResolutionTests
WorkingDirectory = workingDirectory ?? InstancePath
};
// ─── Vorgaben ───
// ─── Defaults ───
[Fact]
public void Die_Vorbelegungen_sind_leer()
{
// Der Kern: Leer heisst „nimm den Ordner dieser Instanz". Steht hier ein Wert,
// traegt ihn jede neue Instanz mit sich herum.
// The core: empty means "take the folder of this instance". If a value stands here,
// every new instance carries it around.
new InstanceConfig().LogDirectory.ShouldBe("");
new InstanceConfig().BackupDirectory.ShouldBe("");
}
@@ -58,8 +58,8 @@ public sealed class LogDirectoryResolutionTests
}
/// <summary>
/// Das eigentliche Versprechen von Stufe 1. Alles andere in dieser Datei prüft die
/// Mechanik; dieser Test prüft die Zusage.
/// The actual promise of stage 1. Everything else in this file checks the
/// mechanics; this test checks the promise.
/// </summary>
[Fact]
public void Zwei_Instanzen_ohne_Angabe_teilen_sich_nichts()
@@ -77,7 +77,7 @@ public sealed class LogDirectoryResolutionTests
backupA.ShouldNotBe(backupB);
}
// ─── Eingetragene Werte ───
// ─── Entered values ───
[Fact]
public void Ein_absoluter_Wert_geht_vor()
@@ -95,7 +95,7 @@ public sealed class LogDirectoryResolutionTests
[Fact]
public void Ein_relativer_Wert_zaehlt_gegen_das_Instanzverzeichnis()
{
// Nicht gegen das Arbeitsverzeichnis des Prozesses — das war der Fehler aus #375.
// Not against the working directory of the process — that was the error from #375.
AppHost.ResolveLogDirectory(Instance(logDirectory: "./Protokolle"), InstancePath)
.ShouldBe(Path.Combine(InstancePath, "Protokolle"));
}
@@ -112,8 +112,8 @@ public sealed class LogDirectoryResolutionTests
[InlineData("Logs")]
public void Die_alte_Vorbelegung_zeigt_auf_dieselbe_Stelle_wie_keine_Angabe(string alt)
{
// Deshalb darf LoadInstanceConfig sie zu „leer" machen, ohne dass sich etwas
// verschiebt.
// That's why LoadInstanceConfig may turn them into "empty" without anything
// shifting.
AppHost.ResolveLogDirectory(Instance(logDirectory: alt), InstancePath)
.ShouldBe(AppHost.ResolveLogDirectory(Instance(), InstancePath));
}
@@ -125,13 +125,13 @@ public sealed class LogDirectoryResolutionTests
.ShouldBe(Path.Combine(InstancePath, "Logs"));
}
// ─── Bezugspunkt ───
// ─── Reference point ───
[Fact]
public void Ein_leeres_WorkingDirectory_faellt_auf_den_Instanzpfad_zurueck()
{
// Das Feld steht seit CreateInstance absolut in der Datei, kann aber von Hand
// geleert worden sein. Dann trägt der Ort, aus dem geladen wurde.
// Since CreateInstance the field is in the file as an absolute path, but may have been
// emptied by hand. Then the place it was loaded from carries the day.
AppHost.ResolveLogDirectory(
Instance(logDirectory: "Logs", workingDirectory: ""), InstancePath)
.ShouldBe(Path.Combine(InstancePath, "Logs"));
@@ -140,8 +140,8 @@ public sealed class LogDirectoryResolutionTests
[Fact]
public void Ein_relatives_WorkingDirectory_faellt_ebenfalls_auf_den_Instanzpfad_zurueck()
{
// "./data/" ist die alte Vorbelegung des Feldes und wäre wieder gegen das
// Arbeitsverzeichnis des Prozesses aufgelöst worden.
// "./data/" is the old default of the field and would again have been resolved against the
// working directory of the process.
AppHost.ResolveLogDirectory(
Instance(logDirectory: "Logs", workingDirectory: "./data/"), InstancePath)
.ShouldBe(Path.Combine(InstancePath, "Logs"));
@@ -5,13 +5,13 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Die Regeln, die bis zum 2026-09-06 in den Avalonia-Ansichtsmodellen standen.
/// The rules that until 2026-09-06 lived in the Avalonia view models.
///
/// <para>Dass sie dort standen, war der Grund für diese Tests: Ein Ansichtsmodell hat
/// keine Testabdeckung — das sagt der Oberflächen-Leitfaden ausdrücklich —, und mit der
/// zweiten Oberfläche wären dieselben Regeln ein zweites Mal geschrieben worden. Jetzt
/// gibt es sie einmal, und ab hier ist geprüft, dass beide Oberflächen dasselbe
/// bekommen.</para>
/// <para>That they lived there was the reason for these tests: a view model has
/// no test coverage — the UI guide says so explicitly —, and with the
/// second user interface the same rules would have been written a second time. Now
/// they exist once, and from here on it is checked that both user interfaces get the same
/// thing.</para>
/// </summary>
public sealed class AgentAdministrationTests
{
@@ -30,8 +30,8 @@ public sealed class AgentAdministrationTests
[Fact]
public void Die_Pruefung_ignoriert_Gross_und_Kleinschreibung()
{
// Zwei Agenten, die sich nur darin unterscheiden, waeren auf der Zielplattform
// zwei Ordner und fuer jeden Menschen derselbe Agent.
// Two agents differing only in that would be two folders on the target platform
// and the same agent to every human.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.Add(new NewAgent("SCOUT", "Scout", "modell")).Ok.ShouldBeFalse();
@@ -49,10 +49,10 @@ public sealed class AgentAdministrationTests
[Fact]
public void Ein_angelegter_Agent_ueberlebt_den_Neustart()
{
// Der Kern des Befunds: LoadInstanceConfig leert die Agentenliste und baut sie
// aus den Ordnern Agents/Agent-* wieder auf. Ein Agent, der nur in
// InstanceSettings.json stand, war nach dem naechsten Start weg — genau das tat
// die Oberflaeche bis zum 2026-09-06.
// The core of the finding: LoadInstanceConfig clears the agent list and rebuilds it
// from the folders Agents/Agent-*. An agent that only stood in
// InstanceSettings.json was gone after the next start — exactly what
// the user interface did until 2026-09-06.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell", "Späher")).Ok.ShouldBeTrue();
@@ -65,9 +65,9 @@ public sealed class AgentAdministrationTests
[Fact]
public void Aenderungen_an_einem_Agenten_ueberleben_den_Neustart()
{
// Dieselbe Ursache aus der anderen Richtung: Bis zum 2026-09-06 rief niemand im
// laufenden Betrieb SaveAgentSettings. Modell, Prompt und Budget standen nur in
// InstanceSettings.json — es sah aus wie Speichern und war keines.
// The same cause from the other direction: until 2026-09-06 nobody called
// SaveAgentSettings during operation. Model, prompt and budget only stood in
// InstanceSettings.json — it looked like saving and wasn't.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "altes-modell"));
@@ -105,8 +105,8 @@ public sealed class AgentAdministrationTests
[Fact]
public void Jede_Aenderung_meldet_sich()
{
// Ohne diese Meldung erschiene ein in der Weboberflaeche angelegter Agent im
// laufenden Fenster nicht — der ganze Grund, warum es das Ereignis gibt.
// Without this notification an agent created in the web UI would not appear in the
// running window — the whole reason the event exists.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
@@ -119,9 +119,9 @@ public sealed class AgentAdministrationTests
[Fact]
public void Ein_entfernter_Agent_kommt_nach_dem_Neustart_nicht_wieder()
{
// Die Gegenrichtung desselben Befunds: Nur aus der Liste zu streichen genuegte
// nicht — der Ordner blieb liegen, und beim naechsten Start stand der Agent
// wieder da.
// The opposite direction of the same finding: merely striking it from the list was not
// enough — the folder stayed behind, and at the next start the agent was
// back.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
@@ -135,8 +135,8 @@ public sealed class AgentAdministrationTests
[Fact]
public void Ein_entfernter_Agent_behaelt_seine_Arbeit()
{
// Umbenannt, nicht geloescht. Im Arbeitsordner steht seine Arbeit, und ein Klick
// in einer Liste ist keine Entscheidung, sie wegzuwerfen.
// Renamed, not deleted. Its work is in the working folder, and a click
// in a list is no decision to throw it away.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
@@ -167,7 +167,7 @@ public sealed class AgentAdministrationTests
host.Host.AgentAdmin.Find("scout")!.Tools["FileRW"].ShouldBeEmpty();
}
// ─── #641: Rechte in der Werkzeugkonfiguration ───
// ─── #641: permissions in the tool configuration ───
[Fact]
public void Rechte_werden_normalisiert_gespeichert_und_ueberleben_den_Neustart()
@@ -188,7 +188,7 @@ public sealed class AgentAdministrationTests
[Fact]
public void Einstellungen_speichern_loescht_die_Rechte_nicht()
{
// Die Einstellungsmasken schreiben ein frisches Woerterbuch ohne "erlaubt".
// The settings forms write a fresh dictionary without "erlaubt".
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolEnabled("scout", "Database", enabled: true);
host.Host.AgentAdmin.SetToolRights("scout", "Database", ["select"], []);
@@ -278,7 +278,7 @@ public sealed class JobAdministrationTests
[Fact]
public void Ein_leerer_Zeitplan_ist_kein_Fehler()
{
// Leer heisst „nie" und ist eine gueltige Angabe. Nur unlesbar ist ein Fehler.
// Empty means "never" and is a valid statement. Only unreadable is an error.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup)).Ok.ShouldBeTrue();
@@ -287,9 +287,9 @@ public sealed class JobAdministrationTests
[Fact]
public void Ein_Wakeup_laesst_sich_nicht_abschalten()
{
// SchedulerConfig hat keinen Schalter, und einen hier zu erfinden hiesse, dass
// der Scanner ihn erst einmal lesen muesste. Sonst liefe ein „abgeschalteter"
// Wakeup munter weiter.
// SchedulerConfig has no switch, and inventing one here would mean that
// the scanner would first have to read it. Otherwise a "disabled"
// wakeup would happily keep running.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
@@ -321,8 +321,8 @@ public sealed class JobAdministrationTests
[Fact]
public void Ohne_Scanner_gilt_ein_Wakeup_als_nicht_aktiv()
{
// Er laeuft, solange der Scanner laeuft — der ist der einzige periodische
// Treiber. Ohne ihn steht er, ohne dass jemand ihn abgeschaltet haette.
// It runs as long as the scanner runs — that is the only periodic
// driver. Without it, it stands still without anyone having disabled it.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
@@ -6,13 +6,13 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Die tägliche Sicherung, seit sie der Instanz gehört (#528, Stufe 1).
/// The daily backup, since it belongs to the instance (#528, stage 1).
///
/// <para>Schalter, Uhrzeit, Zielordner und Aufbewahrung standen in den
/// Anwendungseinstellungen und galten damit für jede Instanz. Zwei Folgen: Wer die
/// Sicherung für eine Instanz einschaltete, schaltete sie für alle ein — und alle
/// Archive lagen in einem Ordner, in dem die Aufbewahrung nach dem Präfix
/// <c>backup_&lt;Instanzname&gt;_</c> aufräumt. Dass ein Präfix keine Grenze ist, zeigt
/// <para>Switch, time, target folder and retention lived in the
/// application settings and thus applied to every instance. Two consequences: whoever
/// enabled the backup for one instance enabled it for all — and all
/// archives lay in one folder in which retention cleans up by the prefix
/// <c>backup_&lt;Instanzname&gt;_</c>. That a prefix is not a boundary is shown by
/// <see cref="Im_gemeinsamen_Ordner_greift_die_Aufbewahrung_ueber_die_Instanz_hinaus"/>.</para>
/// </summary>
public sealed class BackupSchedulerTests : IDisposable
@@ -23,10 +23,10 @@ public sealed class BackupSchedulerTests : IDisposable
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); }
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
catch { /* A leftover test folder does not turn a run red. */ }
}
/// <summary>Ein Instanzverzeichnis mit genug Inhalt, dass sich eine Sicherung lohnt.</summary>
/// <summary>An instance directory with enough content that a backup is worthwhile.</summary>
private string NeueInstanz(string name)
{
var dir = Path.Combine(_root, "Instance-" + name);
@@ -47,8 +47,8 @@ public sealed class BackupSchedulerTests : IDisposable
};
/// <summary>
/// Eine Uhr, die auf einer Zeit steht — der Zeitpunkt entscheidet, ob
/// <c>TickAsync</c> etwas tut.
/// A clock standing at a time — the point in time decides whether
/// <c>TickAsync</c> does anything.
/// </summary>
private sealed class FesteUhr(DateTime jetzt) : TimeProvider
{
@@ -61,7 +61,7 @@ public sealed class BackupSchedulerTests : IDisposable
private static DateTime Nachts(int tag) => new(2026, 9, tag, 4, 0, 0, DateTimeKind.Utc);
// ─── Der Zielordner ───
// ─── The target folder ───
[Fact]
public async Task Ohne_Angabe_landet_die_Sicherung_im_Instanzverzeichnis()
@@ -88,7 +88,7 @@ public sealed class BackupSchedulerTests : IDisposable
Directory.GetFiles(Path.Combine(b, "Backups"), "*.zip").Length.ShouldBe(1);
}
// ─── Der Schalter ───
// ─── The switch ───
[Fact]
public async Task Ausgeschaltet_passiert_nichts()
@@ -125,7 +125,7 @@ public sealed class BackupSchedulerTests : IDisposable
Directory.GetFiles(Path.Combine(dir, "Backups"), "*.zip").Length.ShouldBe(1);
}
// ─── Die Aufbewahrung ───
// ─── Retention ───
[Fact]
public async Task Die_Aufbewahrung_raeumt_die_eigenen_Archive_auf()
@@ -134,7 +134,7 @@ public sealed class BackupSchedulerTests : IDisposable
var ziel = Path.Combine(dir, "Backups");
Directory.CreateDirectory(ziel);
// Drei Archive aus der Vergangenheit, die aelter sind als das gleich entstehende.
// Three archives from the past that are older than the one about to be created.
foreach (var tag in new[] { 1, 2, 3 })
{
var datei = Path.Combine(ziel, $"backup_Alt_2026-08-0{tag}_0300.zip");
@@ -161,14 +161,14 @@ public sealed class BackupSchedulerTests : IDisposable
}
/// <summary>
/// Warum der Zielordner der Instanz gehören muss.
/// Why the target folder has to belong to the instance.
///
/// <para>Die Aufbewahrung sucht nach dem Präfix <c>backup_&lt;Instanzname&gt;_</c>,
/// und ein Präfix ist keine Grenze: <c>backup_Test_2_….zip</c> — das Archiv der
/// Instanz <c>Test_2</c> — fängt mit <c>backup_Test_</c> an. Im gemeinsamen Ordner
/// zählt <c>Test</c> es also zu den eigenen und wirft es weg. Getrennte Ordner
/// nehmen der Namensfrage ihre Wirkung: Der Test hier stellt den alten Zustand
/// eigens wieder her, indem er beiden Instanzen denselben Ordner einträgt.</para>
/// <para>Retention looks for the prefix <c>backup_&lt;Instanzname&gt;_</c>,
/// and a prefix is not a boundary: <c>backup_Test_2_….zip</c> — the archive of the
/// instance <c>Test_2</c> — starts with <c>backup_Test_</c>. In the shared folder
/// <c>Test</c> therefore counts it among its own and throws it away. Separate folders
/// take away the effect of the naming question: the test here deliberately restores the old state
/// by entering the same folder for both instances.</para>
/// </summary>
[Fact]
public async Task Im_gemeinsamen_Ordner_greift_die_Aufbewahrung_ueber_die_Instanz_hinaus()
@@ -187,7 +187,7 @@ public sealed class BackupSchedulerTests : IDisposable
"Der alte Zustand ist damit belegt — im gemeinsamen Ordner räumt eine "
+ "Instanz über ihre eigenen Archive hinaus auf.");
// Und die Gegenprobe: mit getrennten Ordnern kommen sie sich nicht in die Quere.
// And the counter-check: with separate folders they do not get in each other's way.
var eigen = NeueInstanz("Test2");
var eigenesArchiv = Path.Combine(eigen, "Backups", "backup_Test2_2026-08-01_0300.zip");
Directory.CreateDirectory(Path.GetDirectoryName(eigenesArchiv)!);
@@ -7,15 +7,15 @@ using ClawdDotNet.Core.Instances;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Die andere Hälfte von #375: Bestehende Instanzen tragen <c>"logDirectory": "./Logs"</c>
/// in ihrer <c>InstanceSettings.json</c> — <see cref="InstanceDirectoryManager.CreateInstance"/>
/// hat es dort hineingeschrieben. Beim Laden wird daraus „leer".
/// The other half of #375: existing instances carry <c>"logDirectory": "./Logs"</c>
/// in their <c>InstanceSettings.json</c> — <see cref="InstanceDirectoryManager.CreateInstance"/>
/// wrote it there. On loading it becomes "empty".
///
/// <para>Seit #528 (Stufe 1) zeigen beide Schreibweisen ohnehin auf denselben Ort:
/// <c>&lt;Instanz&gt;/Logs</c>. Der leere Wert sagt das, ohne es festzuschreiben — zieht
/// die Instanz um, zieht das Protokoll mit.</para>
/// <para>Since #528 (stage 1) both spellings point to the same place anyway:
/// <c>&lt;Instanz&gt;/Logs</c>. The empty value says so without pinning it down — if
/// the instance moves, the log moves with it.</para>
///
/// Wer bewusst einen anderen Pfad eingetragen hat, behält ihn.
/// Whoever deliberately entered a different path keeps it.
/// </summary>
public sealed class InstanceLogDirectoryMigrationTests : IDisposable
{
@@ -32,10 +32,10 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
public void Dispose()
{
try { Directory.Delete(Path.GetDirectoryName(_instancesDir)!, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
catch { /* Cleaning up is secondary */ }
}
/// <summary>Legt eine Instanz an und setzt logDirectory in der Datei auf den Altwert.</summary>
/// <summary>Creates an instance and sets logDirectory in the file to the legacy value.</summary>
private string InstanceWithLogDirectory(string value)
{
var dir = _manager.CreateInstance("Test");
@@ -71,8 +71,8 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
[Fact]
public void Die_Datei_wird_beim_Laden_nicht_zurueckgeschrieben()
{
// Die Migration greift nur im Speicher. Ein Ladevorgang darf nichts anfassen —
// erst das nächste Speichern lässt den Altwert verschwinden.
// The migration only applies in memory. A load must not touch anything —
// only the next save makes the legacy value disappear.
var dir = InstanceWithLogDirectory("./Logs");
var path = Path.Combine(dir, "InstanceSettings.json");
@@ -94,8 +94,8 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
}
/// <summary>
/// Der eigentliche Zweck von Stufe 1: Zwei frisch angelegte Instanzen protokollieren
/// und sichern nicht in denselben Ordner.
/// The actual purpose of stage 1: two freshly created instances do not log
/// and back up into the same folder.
/// </summary>
[Fact]
public void Zwei_frische_Instanzen_landen_nicht_im_selben_Ordner()
@@ -5,16 +5,16 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Zugangsdaten der Instanz müssen den Weg über die Platte und zurück überstehen.
/// The instance's credentials must survive the way to disk and back.
///
/// <para><c>SaveInstanceConfig</c> verschlüsselte beim Schreiben, <c>LoadInstanceConfig</c>
/// entschlüsselte beim Lesen aber nicht. Was einmal über die Oberfläche gespeichert war,
/// kam als <c>enc:v2:…</c> zurück und wurde als Wert benutzt — der OpenRouter-Schlüssel
/// ging so verschlüsselt an die API.</para>
/// <para><c>SaveInstanceConfig</c> encrypted when writing, but <c>LoadInstanceConfig</c>
/// did not decrypt when reading. Whatever had once been saved via the user interface
/// came back as <c>enc:v2:…</c> and was used as the value — the OpenRouter key
/// thus went to the API encrypted.</para>
///
/// <para>Unentdeckt geblieben ist es, weil <c>ConfigLoader.LoadAsync</c> es richtig macht
/// und im laufenden Programm gar nicht aufgerufen wird: Gebraucht wird von dieser Klasse
/// nur <c>Migrate</c>.</para>
/// <para>It went undiscovered because <c>ConfigLoader.LoadAsync</c> does it right
/// and is not called at all in the running program: only <c>Migrate</c>
/// of that class is needed.</para>
/// </summary>
public sealed class InstanceSecretRoundtripTests
{
@@ -39,8 +39,8 @@ public sealed class InstanceSecretRoundtripTests
[Fact]
public void In_der_Datei_stehen_sie_trotzdem_geschuetzt()
{
// Die Gegenprobe: Entschluesseln beim Laden darf nicht heissen, dass gar nicht
// mehr verschluesselt wird.
// The counter-check: decrypting when loading must not mean that nothing is
// encrypted any more.
using var host = new TestHost();
host.Host.Instance.OpenRouterApiKey = "sk-or-v1-beispielschluessel";
@@ -55,8 +55,8 @@ public sealed class InstanceSecretRoundtripTests
[Fact]
public void Klartext_aus_aelteren_Dateien_bleibt_lesbar()
{
// Entschluesseln muss gefahrlos auf etwas anwendbar sein, das nie verschluesselt
// war — sonst braeche der erste Start nach dem Update jede bestehende Instanz.
// Decrypting must be safely applicable to something that was never encrypted
// — otherwise the first start after the update would break every existing instance.
using var host = new TestHost();
var pfad = Path.Combine(host.Host.InstancePath, "InstanceSettings.json");
@@ -4,14 +4,14 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// #376: Jedes Modul schreibt in seine eigene Datei, und eingesammelt wurde Datei für
/// Datei — also alphabetisch nach Modulname. In der Log-Ansicht standen die Zeilen
/// damit in Blöcken je Modul statt in der Reihenfolge, in der sie entstanden sind.
/// Beobachtet wurde eine Zeile von 14:34:40 über vier Zeilen von 14:34:39.
/// #376: Every module writes into its own file, and collection happened file by
/// file — i.e. alphabetically by module name. In the log view the lines thus stood
/// in blocks per module instead of in the order in which they came into being.
/// Observed was a line from 14:34:40 above four lines from 14:34:39.
///
/// Das Dateisystem wird hier nicht abstrahiert: <see cref="LogTail"/> ist genau der
/// Umgang mit echten, nebenher wachsenden Dateien — eine Abstraktion würde das
/// Verhalten verstecken, um das es geht.
/// The file system is not abstracted here: <see cref="LogTail"/> is exactly the
/// handling of real files that grow alongside — an abstraction would hide the
/// behavior this is about.
/// </summary>
public sealed class LogTailTests : IDisposable
{
@@ -23,8 +23,8 @@ public sealed class LogTailTests : IDisposable
_logDirectory = Path.Combine(
Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Logs");
// LogTail liest ausschließlich das Verzeichnis des heutigen Tages, und zwar
// nach Ortszeit — die Schreibseite legt es genauso an.
// LogTail reads exclusively the directory of today, by
// local time — the writing side creates it the same way.
_today = Path.Combine(_logDirectory, DateTime.Now.ToString("yyyy-MM-dd"));
Directory.CreateDirectory(_today);
}
@@ -32,7 +32,7 @@ public sealed class LogTailTests : IDisposable
public void Dispose()
{
try { Directory.Delete(Path.GetDirectoryName(_logDirectory)!, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
catch { /* Cleaning up is secondary */ }
}
private void Write(string module, params string[] lines)
@@ -41,14 +41,14 @@ public sealed class LogTailTests : IDisposable
private LogTail Tail() => new(_logDirectory);
// ═══════════════════════════════════════════════════════════
// Reihenfolge über Moduldateien hinweg
// Order across module files
// ═══════════════════════════════════════════════════════════
[Fact]
public void Zeilen_aus_mehreren_Modulen_stehen_in_zeitlicher_Reihenfolge()
{
// Genau der beobachtete Fall: "Deploymentcenter" steht alphabetisch vor
// "Startup", seine Zeile ist aber die jüngste von allen.
// Exactly the observed case: "Deploymentcenter" comes alphabetically before
// "Startup", but its line is the most recent of all.
Write("Deploymentcenter", "[14:34:40.246] [INF] kein Token hinterlegt");
Write("Startup",
"[14:34:39.840] [INF] ClawdDotNet startet",
@@ -68,12 +68,12 @@ public sealed class LogTailTests : IDisposable
[Fact]
public void Bei_gleichem_Zeitstempel_bleibt_die_bisherige_Ordnung()
{
// Stabil sortiert: Zwei Zeilen derselben Millisekunde dürfen nicht die Plätze
// tauschen, sonst zerfällt ein zusammenhängender Ablauf innerhalb eines Moduls.
// Sorted stably: two lines of the same millisecond must not swap
// places, otherwise a coherent sequence within a module falls apart.
//
// Über Moduldateien hinweg entscheidet dann der Modulname — "Alpha" vor "Beta".
// Directory.GetFiles liefert unter Linux die Verzeichnisreihenfolge; ohne die
// Sortierung in ReadNew stünde hier von Lauf zu Lauf etwas anderes.
// Across module files the module name then decides — "Alpha" before "Beta".
// Directory.GetFiles returns the directory order under Linux; without the
// sorting in ReadNew something different would stand here from run to run.
Write("Alpha", "[10:00:00.000] [INF] alpha eins", "[10:00:00.000] [INF] alpha zwei");
Write("Beta", "[10:00:00.000] [INF] beta eins");
@@ -89,8 +89,8 @@ public sealed class LogTailTests : IDisposable
[Fact]
public void Ein_Stacktrace_bleibt_bei_seiner_Meldung()
{
// Eine Ausnahme hängt ihre Zeilen ohne eigenen Zeitstempel unter die Meldung.
// Ohne Sonderbehandlung wären sie ohne Schlüssel nach ganz vorn gerutscht.
// An exception hangs its lines without their own timestamp below the message.
// Without special handling they would have slipped to the very front without a key.
Write("Alpha", "[10:00:00.500] [INF] danach");
Write("Engine",
"[10:00:00.100] [ERR] Aufruf fehlgeschlagen",
@@ -110,9 +110,9 @@ public sealed class LogTailTests : IDisposable
[Fact]
public void Ein_Stacktrace_bleibt_auch_ueber_die_Taktgrenze_bei_seiner_Meldung()
{
// Der Takt kann mitten in einem Eintrag enden: Die Meldung ist gelesen, die
// Fortsetzung kommt erst beim nächsten Aufruf. Der Zeitstempel muss dann aus
// dem vorigen Takt weitergetragen werden.
// The tick can end in the middle of an entry: the message has been read, the
// continuation only comes with the next call. The timestamp must then be carried over from
// the previous tick.
var tail = Tail();
Write("Engine", "[10:00:00.100] [ERR] Aufruf fehlgeschlagen");
@@ -132,7 +132,7 @@ public sealed class LogTailTests : IDisposable
[Fact]
public void Eine_Uhrzeit_mitten_in_der_Meldung_ist_kein_Sortierschluessel()
{
// Gesucht wird der Zeitstempel am Zeilenanfang, nicht irgendeine Uhrzeit im Text.
// What is looked for is the timestamp at the start of the line, not any time in the text.
Write("Alpha", "[10:00:00.100] [INF] naechster Lauf um [23:59:59.999]");
Write("Beta", "[10:00:00.200] [INF] danach");
@@ -145,7 +145,7 @@ public sealed class LogTailTests : IDisposable
}
// ═══════════════════════════════════════════════════════════
// Was die Sortierung nicht kaputtmachen darf
// What the sorting must not break
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -174,9 +174,9 @@ public sealed class LogTailTests : IDisposable
[Fact]
public void Eine_weggefilterte_Zeile_traegt_ihren_Zeitstempel_trotzdem_weiter()
{
// Der Stufenfilter darf die Zuordnung der Fortsetzungszeilen nicht verlieren:
// Die Meldung fällt weg, ihr Stacktrace nicht — er hat keine eigene Stufe und
// wird bewusst durchgelassen. Er muss dann an ihrer Stelle stehen.
// The level filter must not lose the assignment of the continuation lines:
// the message drops out, its stack trace does not — it has no level of its own and
// is deliberately let through. It must then stand in the message's place.
Write("Alpha", "[10:00:00.500] [ERR] spaeter Fehler");
Write("Engine",
"[10:00:00.100] [INF] harmlos",
@@ -8,8 +8,8 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Das Taskboard fuer die Oberflaechen (#594) mit den Regeln aus #597 (Termin pruefen) und
/// #598 (Hinweis auf wachsenden Kontext).
/// The taskboard for the user interfaces (#594) with the rules from #597 (check schedule) and
/// #598 (hint about growing context).
/// </summary>
public sealed class TaskAdministrationTests : IDisposable
{
@@ -30,7 +30,7 @@ public sealed class TaskAdministrationTests : IDisposable
{
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
try { Directory.Delete(_dir, recursive: true); }
catch { /* Aufraeumen ist Nebensache */ }
catch { /* Cleaning up is secondary */ }
}
private async Task<TaskItem> Einzige() => (await _board.ListAsync(new TaskQuery(), default)).Single();
@@ -8,13 +8,13 @@ using ClawdDotNet.Core.Instances;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Ein <see cref="AppHost"/> ohne Engine, Scanner und Netz — genug für die
/// Verwaltungsdienste.
/// An <see cref="AppHost"/> without engine, scanner and network — enough for the
/// administration services.
///
/// <para>Möglich ist das, weil <c>AppHost.StartAsync</c> nur <em>ein</em> Weg ist, einen
/// aufzubauen: Die Eigenschaften sind <c>required init</c>, mehr braucht es nicht. Was
/// hier fehlt — <c>Engine</c>, <c>Scanner</c>, <c>Staging</c> — ist auch im Betrieb
/// regelmäßig null, und genau dieses Verhalten soll geprüft werden.</para>
/// <para>This is possible because <c>AppHost.StartAsync</c> is only <em>one</em> way to
/// build one: the properties are <c>required init</c>, nothing more is needed. What
/// is missing here — <c>Engine</c>, <c>Scanner</c>, <c>Staging</c> — is regularly null in operation
/// too, and exactly that behavior is to be tested.</para>
/// </summary>
internal sealed class TestHost : IDisposable
{
@@ -22,7 +22,7 @@ internal sealed class TestHost : IDisposable
public AppHost Host { get; }
/// <summary>Jede Änderung, die über <c>SaveInstance</c> gelaufen ist.</summary>
/// <summary>Every change that went through <c>SaveInstance</c>.</summary>
public List<InstanceChange> Changes { get; } = [];
public TestHost(params AgentConfig[] agents)
@@ -55,7 +55,7 @@ internal sealed class TestHost : IDisposable
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); }
catch { /* Ein liegengebliebener Testordner ist kein Grund, einen Lauf rot zu färben. */ }
catch { /* A leftover test folder is no reason to turn a run red. */ }
}
public static AgentConfig Agent(string agentId, string displayName = "")
@@ -4,17 +4,17 @@ using Shouldly;
namespace ClawdDotNet.App.Tests.Settings;
/// <summary>
/// Was mit den Einstellungen geschieht, die es hier nicht mehr gibt (#528, Stufe 1).
/// What happens to the settings that no longer exist here (#528, stage 1).
///
/// <para><b>Der Anlass.</b> Fünf Schlüssel sind aus <see cref="AppSettings"/> in die
/// Instanz gewandert. System.Text.Json überliest unbekannte Schlüssel wortlos, und
/// <c>Save</c> schreibt sie danach nicht wieder — wer ein eigenes Log- oder
/// Sicherungsverzeichnis eingetragen hatte, verlöre den Wert also, ohne es zu erfahren.
/// Ein Umzug ohne Ausweichlogik ist in Ordnung; einer ohne Ansage nicht.</para>
/// <para><b>The occasion.</b> Five keys moved from <see cref="AppSettings"/> into the
/// instance. System.Text.Json silently skips unknown keys, and
/// <c>Save</c> does not write them again afterwards — whoever had entered their own log or
/// backup directory would therefore lose the value without finding out.
/// A move without fallback logic is fine; one without an announcement is not.</para>
///
/// <para>Bewusst keine Übernahme in die Instanzen: Ein anwendungsweiter Pfad, in alle
/// Instanzen kopiert, wäre wieder derselbe Ordner für alle — also genau der Zustand, den
/// Stufe 1 beseitigt.</para>
/// <para>Deliberately no takeover into the instances: an application-wide path, copied into all
/// instances, would again be the same folder for all — i.e. exactly the state
/// stage 1 eliminates.</para>
/// </summary>
public sealed class SettingsManagerTests : IDisposable
{
@@ -28,7 +28,7 @@ public sealed class SettingsManagerTests : IDisposable
public void Dispose()
{
try { Directory.Delete(_dir, recursive: true); }
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
catch { /* A leftover test folder does not turn a run red. */ }
}
private SettingsManager Geladen(string json)
@@ -62,7 +62,7 @@ public sealed class SettingsManagerTests : IDisposable
[Fact]
public void Was_die_Datei_sonst_enthaelt_wird_weiter_gelesen()
{
// Die Ansage darf nichts daran ändern, dass der Rest ankommt.
// The announcement must not change the fact that the rest arrives.
var manager = Geladen("""
{
"logDirectory": "/var/log/clawd",
@@ -89,7 +89,7 @@ public sealed class SettingsManagerTests : IDisposable
File.ReadAllText(Datei).ShouldNotContain("logDirectory");
// Und der zweite Ladevorgang hat nichts mehr zu melden.
// And the second load has nothing more to report.
var erneut = new SettingsManager(_dir);
erneut.Load();
erneut.AbandonedKeys.ShouldBeEmpty();