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:
co-authored by
Claude Opus 5.5
parent
da4ceac7d5
commit
837a634a22
@@ -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_<Instanzname>_</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_<Instanzname>_</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_<Instanzname>_</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_<Instanzname>_</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><Instanz>/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><Instanz>/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();
|
||||
|
||||
Reference in New Issue
Block a user