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
@@ -10,11 +10,11 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Backup;
/// <summary>
/// Ein ungeprüftes Wiederherstellen ist kein Backup, sondern eine Vermutung.
/// Deshalb liegt der Schwerpunkt hier auf dem vollständigen Rundlauf.
/// An unchecked restore is not a backup, but a guess.
/// That's why the focus here is on the complete round trip.
///
/// Läuft in <see cref="SecretKeyCollection"/>, weil das Umschreiben der Geheimnisse
/// beim Sichern denselben statischen Schlüssel benutzt wie die Geheimnistests.
/// Runs in <see cref="SecretKeyCollection"/>, because rewriting the secrets
/// when backing up uses the same static key as the secret tests.
/// </summary>
[Collection(SecretKeyCollection.Name)]
public sealed class BackupServiceTests : IDisposable
@@ -37,14 +37,14 @@ public sealed class BackupServiceTests : IDisposable
{
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
try { Directory.Delete(_root, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
catch { /* Cleaning up is secondary */ }
}
// ─── Aufbau einer realistischen Instanz ───
// ─── Building a realistic instance ───
private async Task BuildInstanceAsync()
{
// Instanzkonfiguration — Zugangsdaten liegen DPAPI-geschützt wie im Betrieb.
// Instance configuration — credentials are DPAPI-protected as in operation.
var instanceSettings = $$"""
{
"instanceId": "test-01",
@@ -81,7 +81,7 @@ public sealed class BackupServiceTests : IDisposable
AtomicFile.WriteAllText(Path.Combine(agentDir, "Workspace", "bericht.md"), "# Bericht\nInhalt.");
AtomicFile.WriteAllText(Path.Combine(agentDir, "Logs", "lauf.log"), "viele Zeilen Protokoll");
// Datenbank mit einer Erinnerung — der wertvollste Teil.
// Database with a memory — the most valuable part.
var storage = new SqliteStorage(Path.Combine(_instanceDir, "state.db"));
var memory = new SqliteMemoryRepository(storage);
await memory.RememberAsync(new MemoryEntry
@@ -94,7 +94,7 @@ public sealed class BackupServiceTests : IDisposable
CreatedBy = "analyst"
}, default);
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
// Close connections so VACUUM INTO meets a quiet file.
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_instanceDir);
}
@@ -102,7 +102,7 @@ public sealed class BackupServiceTests : IDisposable
private string RestoreDir => Path.Combine(_root, "wiederhergestellt");
// ═══════════════════════════════════════════════════════════
// Der Rundlauf
// The round trip
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -118,17 +118,17 @@ public sealed class BackupServiceTests : IDisposable
result.Written.ShouldNotBeEmpty();
// Persönlichkeit
// Personality
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Identity.md"))
.ShouldContain("Der Analyst");
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Soul.md"))
.ShouldContain("Gründlich");
// Arbeitsstand
// Work state
File.ReadAllText(Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "Workspace", "bericht.md"))
.ShouldContain("Inhalt");
// Datenbank samt Gedächtnis
// Database including memory
File.Exists(Path.Combine(RestoreDir, "state.db")).ShouldBeTrue();
var memory = new SqliteMemoryRepository(new SqliteStorage(Path.Combine(RestoreDir, "state.db")));
@@ -142,8 +142,8 @@ public sealed class BackupServiceTests : IDisposable
[Fact]
public async Task Zugangsdaten_sind_nach_dem_Wiederherstellen_wieder_nutzbar()
{
// Der eigentliche Zweck der Passphrase: Ein Backup wird gebraucht, wenn der
// Rechner defekt ist — DPAPI-geschützte Werte wären dann unlesbar.
// The actual purpose of the passphrase: a backup is needed when the
// machine is broken — DPAPI-protected values would then be unreadable.
await BuildInstanceAsync();
await _service.CreateAsync(_instanceDir, ZipPath,
@@ -160,17 +160,17 @@ public sealed class BackupServiceTests : IDisposable
}
/// <summary>
/// Der Umzug auf einen anderen Rechner — der Migrationspfad aus #190.
/// The move to another machine — the migration path from #190.
///
/// <para>Die Tests darüber sichern und stellen im selben Lauf wieder her und
/// benutzen dabei denselben Schlüssel. Sie können damit gar nicht zeigen, worauf es
/// beim Umzug ankommt: dass die Werte auch dann lesbar sind, wenn der Schlüssel der
/// Quellmaschine <b>nicht</b> mitkommt. Genau das ist der Fall Windows → Linux —
/// die bestehenden Werte sind DPAPI-verschlüsselt, und
/// <c>SecretProtector.UnprotectV1</c> wirft dafür auf Linux bewusst.</para>
/// <para>The tests above back up and restore in the same run and
/// use the same key in doing so. They therefore cannot show what matters
/// when moving: that the values are readable even if the key of the
/// source machine does <b>not</b> come along. Exactly that is the case Windows → Linux —
/// the existing values are DPAPI-encrypted, and
/// <c>SecretProtector.UnprotectV1</c> deliberately throws for them on Linux.</para>
///
/// <para>Hier wechselt der Schlüssel deshalb mitten im Test. Ohne den Wechsel wäre
/// der Test grün, ohne etwas zu belegen.</para>
/// <para>Here the key therefore changes in the middle of the test. Without the change
/// the test would be green without proving anything.</para>
/// </summary>
[Fact]
public async Task Der_Umzug_auf_einen_anderen_Rechner_traegt()
@@ -179,15 +179,15 @@ public sealed class BackupServiceTests : IDisposable
try
{
// Maschine 1: Werte liegen unter dem hiesigen Schlüssel.
// Machine 1: values lie under the local key.
await BuildInstanceAsync();
var schluesselDerQuelle = File.ReadAllBytes(SecretKeyStore.KeyFilePath);
await _service.CreateAsync(_instanceDir, ZipPath,
new BackupOptions { Secrets = SecretMode.Passphrase, Passphrase = "umzug" });
// Maschine 2: anderer Rechner, anderer Schlüssel. Der Schlüssel reist nicht
// mit — er liegt bewusst außerhalb des Instanzverzeichnisses.
// Machine 2: different machine, different key. The key does not travel
// along — it deliberately lies outside the instance directory.
SecretKeyStore.UseDirectory(zielMaschine);
await _service.RestoreAsync(ZipPath, RestoreDir, new RestoreOptions { Passphrase = "umzug" });
@@ -201,7 +201,7 @@ public sealed class BackupServiceTests : IDisposable
var stored = settings.RootElement.GetProperty("openRouterApiKey").GetString();
SecretProtector.Unprotect(stored).ShouldBe(ApiKeyPlain);
// Auch die verschachtelten Werte, nicht nur der offensichtliche oben.
// The nested values too, not only the obvious one at the top.
var agent = JsonDocument.Parse(File.ReadAllText(
Path.Combine(RestoreDir, "Agents", "Agent-Analyst", "AgentSettings.json")));
@@ -212,22 +212,22 @@ public sealed class BackupServiceTests : IDisposable
}
finally
{
// Ein eigenes Verzeichnis für den nächsten Test der Sammlung: Das der
// Quellmaschine steht hier nicht mehr zur Verfügung, und der echte
// Benutzerschlüssel darf es erst recht nicht sein.
// A directory of its own for the next test of the collection: that of the
// source machine is no longer available here, and the real
// user key must certainly not be it.
SecretKeyStore.UseDirectory(
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* secondary */ }
}
}
[Fact]
public async Task Ohne_die_Passphrase_ist_der_Umzug_verloren()
{
// Die Kehrseite, und der Grund, warum die Passphrase beim Sichern angeboten
// werden muss: Ohne sie ist ein Backup auf der Zielmaschine wertlos, was die
// Zugangsdaten angeht. Das ist kein Fehler, sondern der Preis dafür, dass der
// Schlüssel nicht mitreist — aber es muss auffallen, statt still zu geschehen.
// The flip side, and the reason why the passphrase has to be offered when backing
// up: without it a backup on the target machine is worthless as far as the
// credentials are concerned. That is not an error, but the price for the
// key not travelling along — but it has to be noticed instead of happening silently.
var zielMaschine = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
try
@@ -246,7 +246,7 @@ public sealed class BackupServiceTests : IDisposable
{
SecretKeyStore.UseDirectory(
Path.Combine(Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* Nebensache */ }
try { Directory.Delete(zielMaschine, recursive: true); } catch { /* secondary */ }
}
}
@@ -271,8 +271,8 @@ public sealed class BackupServiceTests : IDisposable
[Fact]
public async Task Unbekannte_Felder_ueberstehen_den_Rundlauf()
{
// Eine Sicherung darf nichts wegwerfen, nur weil eine Programmfassung ein
// Feld nicht kennt.
// A backup must not throw anything away just because a program version does not know
// a field.
await BuildInstanceAsync();
await _service.CreateAsync(_instanceDir, ZipPath,
@@ -284,7 +284,7 @@ public sealed class BackupServiceTests : IDisposable
}
// ═══════════════════════════════════════════════════════════
// Zugangsdaten weglassen
// Leaving out credentials
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -295,7 +295,7 @@ public sealed class BackupServiceTests : IDisposable
await _service.CreateAsync(_instanceDir, ZipPath,
new BackupOptions { Secrets = SecretMode.Exclude });
// Das gesamte Archiv im Klartext durchsuchen.
// Search the entire archive in plain text.
var inhalt = ReadWholeArchive(ZipPath);
inhalt.ShouldNotContain(ApiKeyPlain);
@@ -318,7 +318,7 @@ public sealed class BackupServiceTests : IDisposable
}
// ═══════════════════════════════════════════════════════════
// Manifest und Fehlerfälle
// Manifest and error cases
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -369,7 +369,7 @@ public sealed class BackupServiceTests : IDisposable
await BuildInstanceAsync();
await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());
// Eine Datei im Archiv nachträglich verändern.
// Change a file in the archive afterwards.
using (var archive = ZipFile.Open(ZipPath, ZipArchiveMode.Update))
{
var entry = archive.Entries.First(e => e.FullName.EndsWith("Identity.md"));
@@ -400,7 +400,7 @@ public sealed class BackupServiceTests : IDisposable
}
// ═══════════════════════════════════════════════════════════
// Vorschau und Überschreiben
// Preview and overwriting
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -449,7 +449,7 @@ public sealed class BackupServiceTests : IDisposable
}
// ═══════════════════════════════════════════════════════════
// Umfang
// Scope
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -477,7 +477,7 @@ public sealed class BackupServiceTests : IDisposable
[Fact]
public async Task Die_WAL_Begleitdateien_landen_nicht_im_Archiv()
{
// VACUUM INTO arbeitet sie bereits ein — mitzusichern wäre irreführend.
// VACUUM INTO already incorporates them — backing them up too would be misleading.
await BuildInstanceAsync();
await _service.CreateAsync(_instanceDir, ZipPath, new BackupOptions());
@@ -505,7 +505,7 @@ public sealed class BackupServiceTests : IDisposable
_instanceDir, ZipPath, new BackupOptions { Secrets = SecretMode.Passphrase }));
}
// ─── Helfer ───
// ─── Helpers ───
private static string ReadWholeArchive(string zipPath)
{
@@ -10,15 +10,15 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Backup;
/// <summary>
/// Die Sicherung, verschlüsselt mit dem Lizenzschlüssel.
/// The backup, encrypted with the license key.
///
/// <para><b>Worum es geht.</b> Eine Sicherung soll einen Maschinenwechsel überstehen,
/// ohne dass eine Passphrase gespeichert oder eingetippt werden muss. Der Lizenzschlüssel
/// ist ohnehin da. Und ein Archiv auf einem FTP-Server oder einem USB-Stick soll niemandem
/// nützen, der es nur findet.</para>
/// <para><b>What this is about.</b> A backup should survive a change of machine
/// without a passphrase having to be stored or typed in. The license key
/// is there anyway. And an archive on an FTP server or a USB stick should be of no use
/// to anyone who merely finds it.</para>
///
/// <para>Läuft in <see cref="SecretKeyCollection"/>: Der Maschinenwechsel wird über den
/// statischen Schlüsselspeicher nachgestellt.</para>
/// <para>Runs in <see cref="SecretKeyCollection"/>: the change of machine is reproduced via the
/// static key store.</para>
/// </summary>
[Collection(SecretKeyCollection.Name)]
public sealed class LicenseBackupTests : IDisposable
@@ -42,7 +42,7 @@ public sealed class LicenseBackupTests : IDisposable
SecretKeyStore.UseDirectory(null);
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
try { Directory.Delete(_root, recursive: true); }
catch { /* Aufräumen ist Nebensache */ }
catch { /* Cleaning up is secondary */ }
}
private string Quelle(byte[] inhalt)
@@ -52,11 +52,11 @@ public sealed class LicenseBackupTests : IDisposable
return pfad;
}
// ─── Der Behälter ───
// ─── The container ───
/// <summary>
/// Mehr als ein Stück: Die Stückelung ist der Teil, an dem ein Fehler Daten
/// verschluckt, ohne dass es auffällt.
/// More than one chunk: the chunking is the part where an error swallows data
/// without it being noticed.
/// </summary>
[Fact]
public void Ein_Archiv_ueber_mehrere_Stuecke_kommt_unveraendert_zurueck()
@@ -79,8 +79,8 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// Ein anderer Schlüssel wird am Kopf erkannt — bevor irgendetwas entschlüsselt und
/// vor allem bevor eine Instanz dafür angehalten wird.
/// A different key is recognized at the header — before anything is decrypted and
/// above all before an instance is stopped for it.
/// </summary>
[Fact]
public void Ein_fremder_Schluessel_wird_erkannt_bevor_etwas_geschrieben_wird()
@@ -97,8 +97,8 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// Im Wiederherstellungsdialog tippt ein Mensch einen fremden Schlüssel ab. Ein
/// Leerzeichen oder ein kleiner Buchstabe dürfen dort nicht über „verloren" entscheiden.
/// In the restore dialog a human types in a foreign key. A
/// space or a lowercase letter must not decide about "lost" there.
/// </summary>
[Fact]
public void Schreibweise_und_Leerzeichen_des_Schluessels_spielen_keine_Rolle()
@@ -119,8 +119,8 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// Fehlt hinten ein ganzes Stück, muss das auffallen. Ohne Endmarke hielte man den Rest
/// für vollständig und bekäme ein ZIP, das beim Auspacken irgendwo abbricht.
/// If a whole chunk is missing at the end, that must be noticed. Without the end marker you would consider the rest
/// complete and get a ZIP that aborts somewhere when unpacking.
/// </summary>
[Fact]
public void Ein_fehlendes_letztes_Stueck_faellt_auf()
@@ -130,7 +130,7 @@ public sealed class LicenseBackupTests : IDisposable
LicenseBackupCipher.Encrypt(Quelle(inhalt), manifest, Datei("a.clawdbak"), Lizenz);
// Kopf 8+1+16, Manifest 12+4+16+len, erstes Stueck 12+4+16+1 MiB - danach kappen.
// Header 8+1+16, manifest 12+4+16+len, first chunk 12+4+16+1 MiB - cut after that.
var bisErstesStueck = 25 + (32 + Encoding.UTF8.GetByteCount(manifest)) + (32 + 1024 * 1024);
var voll = File.ReadAllBytes(Datei("a.clawdbak"));
File.WriteAllBytes(Datei("a.clawdbak"), voll[..bisErstesStueck]);
@@ -153,7 +153,7 @@ public sealed class LicenseBackupTests : IDisposable
LicenseBackupCipher.DecryptTo(Datei("a.clawdbak"), Datei("zurueck.bin"), Lizenz));
}
// ─── Die Sicherung als Ganzes ───
// ─── The backup as a whole ───
private string Instanz()
{
@@ -185,9 +185,9 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// Der Fall, um den es geht: Sichern auf Maschine A, zurückspielen auf Maschine B, und
/// der API-Schlüssel ist danach lesbar — ohne Passphrase, allein mit dem
/// Lizenzschlüssel.
/// The case this is about: back up on machine A, restore on machine B, and
/// the API key is readable afterwards — without a passphrase, solely with the
/// license key.
/// </summary>
[Fact]
public async Task Eine_Sicherung_ueberlebt_den_Maschinenwechsel()
@@ -207,8 +207,8 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// In der Datei steht nichts im Klartext — weder der Schlüssel noch, dass es überhaupt
/// einen gibt.
/// Nothing is in plain text in the file — neither the key nor that there is
/// one at all.
/// </summary>
[Fact]
public async Task Die_Sicherung_verraet_keinen_Klartext()
@@ -224,7 +224,7 @@ public sealed class LicenseBackupTests : IDisposable
}
/// <summary>
/// Mit fremdem Schlüssel wird nichts geschrieben — nicht halb, nicht teilweise.
/// With a foreign key nothing is written — not half, not partially.
/// </summary>
[Fact]
public async Task Mit_fremdem_Schluessel_bleibt_das_Ziel_unberuehrt()