Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen

Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-04 13:18:43 +02:00
co-authored by Claude Opus 5.5
parent 837a634a22
commit 77062facc9
190 changed files with 4813 additions and 4813 deletions
@@ -16,19 +16,19 @@ namespace ClawdDotNet.App.Tests.Services;
public sealed class AgentAdministrationTests
{
[Fact]
public void Eine_doppelte_Agenten_ID_wird_abgelehnt()
public void A_duplicate_agent_ID_is_rejected()
{
using var host = new TestHost(TestHost.Agent("scout"));
var ergebnis = host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
var result = host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("existiert bereits");
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("existiert bereits");
host.Host.Instance.Agents.Count.ShouldBe(1);
}
[Fact]
public void Die_Pruefung_ignoriert_Gross_und_Kleinschreibung()
public void The_check_ignores_case()
{
// Two agents differing only in that would be two folders on the target platform
// and the same agent to every human.
@@ -38,7 +38,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ohne_ID_wird_kein_Agent_angelegt()
public void Without_ID_no_agent_is_created()
{
using var host = new TestHost();
@@ -47,7 +47,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_angelegter_Agent_ueberlebt_den_Neustart()
public void A_created_agent_survives_the_restart()
{
// The core of the finding: LoadInstanceConfig clears the agent list and rebuilds it
// from the folders Agents/Agent-*. An agent that only stood in
@@ -57,13 +57,13 @@ public sealed class AgentAdministrationTests
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell", "Späher")).Ok.ShouldBeTrue();
var nachNeustart = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
var afterRestart = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
nachNeustart.Agents.ShouldContain(a => a.AgentId == "scout" && a.DisplayName == "Scout");
afterRestart.Agents.ShouldContain(a => a.AgentId == "scout" && a.DisplayName == "Scout");
}
[Fact]
public void Aenderungen_an_einem_Agenten_ueberleben_den_Neustart()
public void Changes_to_an_agent_survive_the_restart()
{
// The same cause from the other direction: until 2026-09-06 nobody called
// SaveAgentSettings during operation. Model, prompt and budget only stood in
@@ -79,17 +79,17 @@ public sealed class AgentAdministrationTests
host.Host.AgentAdmin.Save();
var nachNeustart = host.Host.Directories
var afterRestart = host.Host.Directories
.LoadInstanceConfig(host.Host.InstancePath).Agents
.Single(a => a.AgentId == "scout");
nachNeustart.Model.ShouldBe("neues-modell");
nachNeustart.Budget.DailyTokens.ShouldBe(12_345);
nachNeustart.Identity.ShouldBe("# Identity: geändert");
afterRestart.Model.ShouldBe("neues-modell");
afterRestart.Budget.DailyTokens.ShouldBe(12_345);
afterRestart.Identity.ShouldBe("# Identity: geändert");
}
[Fact]
public void Ein_Werkzeug_bleibt_ueber_den_Neustart_zugelassen()
public void A_tool_stays_allowed_across_the_restart()
{
using var host = new TestHost();
@@ -103,7 +103,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Jede_Aenderung_meldet_sich()
public void Every_change_reports_itself()
{
// Without this notification an agent created in the web UI would not appear in the
// running window — the whole reason the event exists.
@@ -117,7 +117,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_entfernter_Agent_kommt_nach_dem_Neustart_nicht_wieder()
public void A_removed_agent_does_not_return_after_the_restart()
{
// 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
@@ -133,7 +133,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_entfernter_Agent_behaelt_seine_Arbeit()
public void A_removed_agent_keeps_its_work()
{
// Renamed, not deleted. Its work is in the working folder, and a click
// in a list is no decision to throw it away.
@@ -141,10 +141,10 @@ public sealed class AgentAdministrationTests
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
var arbeitsdatei = Path.Combine(
var workFile = Path.Combine(
host.Host.AgentAdmin.Find("scout")!.AgentDir, "Workspace", "ergebnis.txt");
File.WriteAllText(arbeitsdatei, "wichtig");
File.WriteAllText(workFile, "wichtig");
host.Host.AgentAdmin.Remove("scout").Ok.ShouldBeTrue();
@@ -157,7 +157,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_Werkzeug_zulassen_legt_eine_leere_Konfiguration_an()
public void Allowing_a_tool_creates_an_empty_configuration()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -170,7 +170,7 @@ public sealed class AgentAdministrationTests
// ─── #641: permissions in the tool configuration ───
[Fact]
public void Rechte_werden_normalisiert_gespeichert_und_ueberleben_den_Neustart()
public void Rights_are_stored_normalised_and_survive_the_restart()
{
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
@@ -178,15 +178,15 @@ public sealed class AgentAdministrationTests
host.Host.AgentAdmin.SetToolRights("scout", "Database", ["SELECT", "Insert", "drop"], ["drop"]).Ok.ShouldBeTrue();
var rechte = ClawdDotNet.Core.Staging.Werkzeugrechte.Aus(
var rights = ClawdDotNet.Core.Staging.ToolRights.From(
host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath).Agents
.Single(a => a.AgentId == "scout").Tools["Database"].AsReadOnly());
rechte.Erlaubt.ShouldNotBeNull().OrderBy(x => x).ToArray().ShouldBe(["insert", "select"], "verboten schlaegt erlaubt");
rechte.IstVerboten("DROP").ShouldBeTrue();
rights.Erlaubt.ShouldNotBeNull().OrderBy(x => x).ToArray().ShouldBe(["insert", "select"], "verboten schlaegt erlaubt");
rights.IsForbidden("DROP").ShouldBeTrue();
}
[Fact]
public void Einstellungen_speichern_loescht_die_Rechte_nicht()
public void Saving_settings_does_not_delete_the_rights()
{
// The settings forms write a fresh dictionary without "erlaubt".
using var host = new TestHost(TestHost.Agent("scout"));
@@ -218,7 +218,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Rechte_nur_fuer_zugewiesene_Werkzeuge()
public void Rights_only_for_assigned_tools()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -226,7 +226,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_Werkzeug_wegnehmen_entfernt_auch_seine_Einstellungen()
public void Removing_a_tool_also_removes_its_settings()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -239,7 +239,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Einstellungen_zu_schreiben_heisst_das_Werkzeug_zu_wollen()
public void Writing_settings_means_wanting_the_tool()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -250,7 +250,7 @@ public sealed class AgentAdministrationTests
}
[Fact]
public void Ein_unbekannter_Agent_liefert_eine_Antwort_und_keine_Ausnahme()
public void An_unknown_agent_returns_an_answer_and_no_exception()
{
using var host = new TestHost();
@@ -263,20 +263,20 @@ public sealed class AgentAdministrationTests
public sealed class JobAdministrationTests
{
[Fact]
public void Ein_unlesbarer_Zeitplan_wird_abgelehnt()
public void An_unreadable_schedule_is_rejected()
{
using var host = new TestHost(TestHost.Agent("scout"));
var ergebnis = host.Host.JobAdmin.Add(new JobDefinition(
var result = host.Host.JobAdmin.Add(new JobDefinition(
"scout", JobKind.AgentWakeup, Cron: "jeden zweiten Dienstag"));
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("nicht lesbar");
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("nicht lesbar");
host.Host.AgentAdmin.Find("scout")!.Scheduler.ShouldBeNull();
}
[Fact]
public void Ein_leerer_Zeitplan_ist_kein_Fehler()
public void An_empty_schedule_is_not_an_error()
{
// Empty means "never" and is a valid statement. Only unreadable is an error.
using var host = new TestHost(TestHost.Agent("scout"));
@@ -285,7 +285,7 @@ public sealed class JobAdministrationTests
}
[Fact]
public void Ein_Wakeup_laesst_sich_nicht_abschalten()
public void A_wakeup_cannot_be_switched_off()
{
// SchedulerConfig has no switch, and inventing one here would mean that
// the scanner would first have to read it. Otherwise a "disabled"
@@ -294,15 +294,15 @@ public sealed class JobAdministrationTests
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
var ergebnis = host.Host.JobAdmin.SetEnabled(
var result = host.Host.JobAdmin.SetEnabled(
"scout", JobAdministration.WakeupId("scout"), enabled: false);
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("Nur Tool-Jobs");
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("Nur Tool-Jobs");
}
[Fact]
public void Ein_Tool_Job_laesst_sich_abschalten_ohne_ihn_zu_verlieren()
public void A_tool_job_can_be_switched_off_without_losing_it()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -313,13 +313,13 @@ public sealed class JobAdministrationTests
host.Host.JobAdmin.SetEnabled("scout", job.JobId, enabled: false).Ok.ShouldBeTrue();
var danach = host.Host.JobAdmin.List().Single();
danach.Enabled.ShouldBeFalse();
danach.ToolName.ShouldBe("Mail");
var after = host.Host.JobAdmin.List().Single();
after.Enabled.ShouldBeFalse();
after.ToolName.ShouldBe("Mail");
}
[Fact]
public void Ohne_Scanner_gilt_ein_Wakeup_als_nicht_aktiv()
public void Without_scanner_a_wakeup_counts_as_not_active()
{
// It runs as long as the scanner runs — that is the only periodic
// driver. Without it, it stands still without anyone having disabled it.
@@ -332,7 +332,7 @@ public sealed class JobAdministrationTests
}
[Fact]
public void Der_naechste_Lauf_wird_aus_dem_Zeitplan_gerechnet()
public void The_next_run_is_calculated_from_the_schedule()
{
var (next, valid) = JobAdministration.NextRun("0 * * * *");
@@ -342,14 +342,14 @@ public sealed class JobAdministrationTests
}
[Fact]
public void Ein_unlesbarer_Zeitplan_ist_von_einem_fehlenden_zu_unterscheiden()
public void An_unreadable_schedule_is_distinguishable_from_a_missing_one()
{
JobAdministration.NextRun("").ShouldBe((null, true));
JobAdministration.NextRun("kaputt").ShouldBe((null, false));
}
[Fact]
public void Ein_entfernter_Wakeup_ist_weg_und_der_Agent_bleibt()
public void A_removed_wakeup_is_gone_and_the_agent_stays()
{
using var host = new TestHost(TestHost.Agent("scout"));
@@ -366,36 +366,36 @@ public sealed class JobAdministrationTests
public sealed class ServiceAdministrationTests
{
[Fact]
public void Die_eingebauten_Dienste_kommen_genau_einmal_dazu()
public void The_built_in_services_are_added_exactly_once()
{
using var host = new TestHost();
host.Host.ServiceAdmin.EnsureBuiltIns();
var nachDemErsten = host.Host.Instance.Services.Count;
var afterFirst = host.Host.Instance.Services.Count;
host.Host.ServiceAdmin.EnsureBuiltIns();
nachDemErsten.ShouldBe(BuiltInServices.CreateDefaults().Count);
host.Host.Instance.Services.Count.ShouldBe(nachDemErsten);
afterFirst.ShouldBe(BuiltInServices.CreateDefaults().Count);
host.Host.Instance.Services.Count.ShouldBe(afterFirst);
}
[Fact]
public void Ein_abgeschalteter_eingebauter_Dienst_bleibt_abgeschaltet()
public void A_disabled_built_in_service_stays_disabled()
{
using var host = new TestHost();
host.Host.ServiceAdmin.EnsureBuiltIns();
var dienst = host.Host.Instance.Services.First(s => s.BuiltIn);
host.Host.ServiceAdmin.SetEnabled(dienst.ServiceId, enabled: false);
var service = host.Host.Instance.Services.First(s => s.BuiltIn);
host.Host.ServiceAdmin.SetEnabled(service.ServiceId, enabled: false);
host.Host.ServiceAdmin.EnsureBuiltIns();
host.Host.ServiceAdmin.Find(dienst.ServiceId)!.Enabled.ShouldBeFalse();
host.Host.ServiceAdmin.Find(service.ServiceId)!.Enabled.ShouldBeFalse();
}
[Fact]
public void Ein_Dienst_ohne_Namen_wird_abgelehnt()
public void A_service_without_name_is_rejected()
{
using var host = new TestHost();
@@ -13,7 +13,7 @@ namespace ClawdDotNet.App.Tests.Services;
/// 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>
/// <see cref="In_the_shared_folder_retention_reaches_beyond_the_instance"/>.</para>
/// </summary>
public sealed class BackupSchedulerTests : IDisposable
{
@@ -27,7 +27,7 @@ public sealed class BackupSchedulerTests : IDisposable
}
/// <summary>An instance directory with enough content that a backup is worthwhile.</summary>
private string NeueInstanz(string name)
private string NewInstance(string name)
{
var dir = Path.Combine(_root, "Instance-" + name);
Directory.CreateDirectory(dir);
@@ -50,25 +50,25 @@ public sealed class BackupSchedulerTests : IDisposable
/// 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
private sealed class FixedClock(DateTime jetzt) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => new(jetzt, TimeSpan.Zero);
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}
private static BackupScheduler Scheduler(string dir, InstanceConfig config, DateTime jetzt)
=> new(dir, config, NullLogger.Instance, new FesteUhr(jetzt));
private static BackupScheduler Scheduler(string dir, InstanceConfig config, DateTime now)
=> new(dir, config, NullLogger.Instance, new FixedClock(now));
private static DateTime Nachts(int tag) => new(2026, 9, tag, 4, 0, 0, DateTimeKind.Utc);
private static DateTime AtNight(int tag) => new(2026, 9, tag, 4, 0, 0, DateTimeKind.Utc);
// ─── The target folder ───
[Fact]
public async Task Ohne_Angabe_landet_die_Sicherung_im_Instanzverzeichnis()
public async Task Without_value_the_backup_ends_up_in_the_instance_directory()
{
var dir = NeueInstanz("Allein");
var dir = NewInstance("Allein");
await Scheduler(dir, Config("Allein"), Nachts(1)).TickAsync();
await Scheduler(dir, Config("Allein"), AtNight(1)).TickAsync();
var ziel = Path.Combine(dir, "Backups");
Directory.Exists(ziel).ShouldBeTrue();
@@ -76,13 +76,13 @@ public sealed class BackupSchedulerTests : IDisposable
}
[Fact]
public async Task Zwei_Instanzen_sichern_ohne_Angabe_getrennt()
public async Task Two_instances_without_value_back_up_separately()
{
var a = NeueInstanz("Eins");
var b = NeueInstanz("Zwei");
var a = NewInstance("Eins");
var b = NewInstance("Zwei");
await Scheduler(a, Config("Eins"), Nachts(1)).TickAsync();
await Scheduler(b, Config("Zwei"), Nachts(1)).TickAsync();
await Scheduler(a, Config("Eins"), AtNight(1)).TickAsync();
await Scheduler(b, Config("Zwei"), AtNight(1)).TickAsync();
Directory.GetFiles(Path.Combine(a, "Backups"), "*.zip").Length.ShouldBe(1);
Directory.GetFiles(Path.Combine(b, "Backups"), "*.zip").Length.ShouldBe(1);
@@ -91,21 +91,21 @@ public sealed class BackupSchedulerTests : IDisposable
// ─── The switch ───
[Fact]
public async Task Ausgeschaltet_passiert_nichts()
public async Task Switched_off_nothing_happens()
{
var dir = NeueInstanz("Ruhig");
var dir = NewInstance("Ruhig");
var config = Config("Ruhig");
config.AutoBackupEnabled = false;
await Scheduler(dir, config, Nachts(1)).TickAsync();
await Scheduler(dir, config, AtNight(1)).TickAsync();
Directory.Exists(Path.Combine(dir, "Backups")).ShouldBeFalse();
}
[Fact]
public async Task Vor_der_eingestellten_Uhrzeit_passiert_nichts()
public async Task Before_the_configured_time_nothing_happens()
{
var dir = NeueInstanz("Frueh");
var dir = NewInstance("Frueh");
await Scheduler(dir, Config("Frueh"), new DateTime(2026, 9, 1, 2, 0, 0, DateTimeKind.Utc))
.TickAsync();
@@ -114,10 +114,10 @@ public sealed class BackupSchedulerTests : IDisposable
}
[Fact]
public async Task Zweimal_am_selben_Tag_sichert_nur_einmal()
public async Task Twice_on_the_same_day_backs_up_only_once()
{
var dir = NeueInstanz("Einmal");
var scheduler = Scheduler(dir, Config("Einmal"), Nachts(1));
var dir = NewInstance("Einmal");
var scheduler = Scheduler(dir, Config("Einmal"), AtNight(1));
await scheduler.TickAsync();
await scheduler.TickAsync();
@@ -128,9 +128,9 @@ public sealed class BackupSchedulerTests : IDisposable
// ─── Retention ───
[Fact]
public async Task Die_Aufbewahrung_raeumt_die_eigenen_Archive_auf()
public async Task Retention_cleans_up_its_own_archives()
{
var dir = NeueInstanz("Alt");
var dir = NewInstance("Alt");
var ziel = Path.Combine(dir, "Backups");
Directory.CreateDirectory(ziel);
@@ -142,20 +142,20 @@ public sealed class BackupSchedulerTests : IDisposable
File.SetLastWriteTime(datei, new DateTime(2026, 8, tag, 3, 0, 0));
}
await Scheduler(dir, Config("Alt", keep: 2), Nachts(1)).TickAsync();
await Scheduler(dir, Config("Alt", keep: 2), AtNight(1)).TickAsync();
Directory.GetFiles(ziel, "*.zip").Length.ShouldBe(2);
}
[Fact]
public async Task Aufbewahrung_null_behaelt_alles()
public async Task Retention_zero_keeps_everything()
{
var dir = NeueInstanz("Sammler");
var dir = NewInstance("Sammler");
var ziel = Path.Combine(dir, "Backups");
Directory.CreateDirectory(ziel);
File.WriteAllText(Path.Combine(ziel, "backup_Sammler_2026-08-01_0300.zip"), "alt");
await Scheduler(dir, Config("Sammler", keep: 0), Nachts(1)).TickAsync();
await Scheduler(dir, Config("Sammler", keep: 0), AtNight(1)).TickAsync();
Directory.GetFiles(ziel, "*.zip").Length.ShouldBe(2);
}
@@ -171,31 +171,31 @@ public sealed class BackupSchedulerTests : IDisposable
/// by entering the same folder for both instances.</para>
/// </summary>
[Fact]
public async Task Im_gemeinsamen_Ordner_greift_die_Aufbewahrung_ueber_die_Instanz_hinaus()
public async Task In_the_shared_folder_retention_reaches_beyond_the_instance()
{
var gemeinsam = Path.Combine(_root, "Backups");
Directory.CreateDirectory(gemeinsam);
var shared = Path.Combine(_root, "Backups");
Directory.CreateDirectory(shared);
var fremd = Path.Combine(gemeinsam, "backup_Test_2_2026-08-01_0300.zip");
File.WriteAllText(fremd, "gehoert Test_2");
File.SetLastWriteTime(fremd, new DateTime(2026, 8, 1, 3, 0, 0));
var foreign = Path.Combine(shared, "backup_Test_2_2026-08-01_0300.zip");
File.WriteAllText(foreign, "gehoert Test_2");
File.SetLastWriteTime(foreign, new DateTime(2026, 8, 1, 3, 0, 0));
var dir = NeueInstanz("Test");
await Scheduler(dir, Config("Test", gemeinsam, keep: 1), Nachts(1)).TickAsync();
var dir = NewInstance("Test");
await Scheduler(dir, Config("Test", shared, keep: 1), AtNight(1)).TickAsync();
File.Exists(fremd).ShouldBeFalse(
File.Exists(foreign).ShouldBeFalse(
"Der alte Zustand ist damit belegt — im gemeinsamen Ordner räumt eine "
+ "Instanz über ihre eigenen Archive hinaus auf.");
// 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)!);
File.WriteAllText(eigenesArchiv, "gehoert Test2");
var own = NewInstance("Test2");
var ownArchive = Path.Combine(own, "Backups", "backup_Test2_2026-08-01_0300.zip");
Directory.CreateDirectory(Path.GetDirectoryName(ownArchive)!);
File.WriteAllText(ownArchive, "gehoert Test2");
var dir2 = NeueInstanz("Test3");
await Scheduler(dir2, Config("Test3", keep: 1), Nachts(2)).TickAsync();
var dir2 = NewInstance("Test3");
await Scheduler(dir2, Config("Test3", keep: 1), AtNight(2)).TickAsync();
File.Exists(eigenesArchiv).ShouldBeTrue();
File.Exists(ownArchive).ShouldBeTrue();
}
}
@@ -52,24 +52,24 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
[InlineData("./Logs")]
[InlineData(@".\Logs")]
[InlineData("Logs")]
public void Die_alte_Vorbelegung_wird_zu_keiner_Angabe(string alt)
public void The_old_default_becomes_no_value(string old)
{
var dir = InstanceWithLogDirectory(alt);
var dir = InstanceWithLogDirectory(old);
_manager.LoadInstanceConfig(dir).LogDirectory.ShouldBe("");
}
[Fact]
public void Ein_bewusst_gesetzter_Pfad_bleibt_stehen()
public void A_deliberately_set_path_stays()
{
var eigen = Path.Combine(Path.GetTempPath(), "clawd-eigene-logs");
var dir = InstanceWithLogDirectory(eigen);
var own = Path.Combine(Path.GetTempPath(), "clawd-eigene-logs");
var dir = InstanceWithLogDirectory(own);
_manager.LoadInstanceConfig(dir).LogDirectory.ShouldBe(eigen);
_manager.LoadInstanceConfig(dir).LogDirectory.ShouldBe(own);
}
[Fact]
public void Die_Datei_wird_beim_Laden_nicht_zurueckgeschrieben()
public void The_file_is_not_written_back_on_load()
{
// The migration only applies in memory. A load must not touch anything —
// only the next save makes the legacy value disappear.
@@ -82,7 +82,7 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
}
[Fact]
public void Eine_neue_Instanz_bekommt_gar_keinen_Wert()
public void A_new_instance_gets_no_value_at_all()
{
var dir = _manager.CreateInstance("Frisch");
var config = _manager.LoadInstanceConfig(dir);
@@ -98,7 +98,7 @@ public sealed class InstanceLogDirectoryMigrationTests : IDisposable
/// and back up into the same folder.
/// </summary>
[Fact]
public void Zwei_frische_Instanzen_landen_nicht_im_selben_Ordner()
public void Two_fresh_instances_do_not_end_up_in_the_same_folder()
{
var a = _manager.CreateInstance("Eins");
var b = _manager.CreateInstance("Zwei");
@@ -19,7 +19,7 @@ namespace ClawdDotNet.App.Tests.Services;
public sealed class InstanceSecretRoundtripTests
{
[Fact]
public void Zugangsdaten_kommen_im_Klartext_zurueck()
public void Credentials_come_back_in_plaintext()
{
using var host = new TestHost();
@@ -29,15 +29,15 @@ public sealed class InstanceSecretRoundtripTests
host.Host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
var geladen = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
var loaded = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
geladen.OpenRouterApiKey.ShouldBe("sk-or-v1-beispielschluessel");
geladen.WebUiToken.ShouldBe("ein-hinreichend-langes-token");
geladen.Watchdog.AgentToken.ShouldBe("dc-token-beispiel");
loaded.OpenRouterApiKey.ShouldBe("sk-or-v1-beispielschluessel");
loaded.WebUiToken.ShouldBe("ein-hinreichend-langes-token");
loaded.Watchdog.AgentToken.ShouldBe("dc-token-beispiel");
}
[Fact]
public void In_der_Datei_stehen_sie_trotzdem_geschuetzt()
public void In_the_file_they_are_still_protected()
{
// The counter-check: decrypting when loading must not mean that nothing is
// encrypted any more.
@@ -46,22 +46,22 @@ public sealed class InstanceSecretRoundtripTests
host.Host.Instance.OpenRouterApiKey = "sk-or-v1-beispielschluessel";
host.Host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
var rohtext = File.ReadAllText(
var rawText = File.ReadAllText(
Path.Combine(host.Host.InstancePath, "InstanceSettings.json"));
rohtext.ShouldNotContain("sk-or-v1-beispielschluessel");
rawText.ShouldNotContain("sk-or-v1-beispielschluessel");
}
[Fact]
public void Klartext_aus_aelteren_Dateien_bleibt_lesbar()
public void Plaintext_from_older_files_stays_readable()
{
// 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");
var path = Path.Combine(host.Host.InstancePath, "InstanceSettings.json");
File.WriteAllText(pfad, """
File.WriteAllText(path, """
{
"instanceId": "alt",
"instanceName": "Alt",
@@ -70,14 +70,14 @@ public sealed class InstanceSecretRoundtripTests
}
""");
var geladen = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
var loaded = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
geladen.OpenRouterApiKey.ShouldBe("klartext-von-hand-eingetragen");
geladen.WebUiToken.ShouldBe("auch-klartext");
loaded.OpenRouterApiKey.ShouldBe("klartext-von-hand-eingetragen");
loaded.WebUiToken.ShouldBe("auch-klartext");
}
[Fact]
public void Zweimal_speichern_verschluesselt_nicht_zweimal()
public void Saving_twice_does_not_encrypt_twice()
{
using var host = new TestHost();
@@ -45,7 +45,7 @@ public sealed class LogTailTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public void Zeilen_aus_mehreren_Modulen_stehen_in_zeitlicher_Reihenfolge()
public void Lines_from_several_modules_are_in_time_order()
{
// Exactly the observed case: "Deploymentcenter" comes alphabetically before
// "Startup", but its line is the most recent of all.
@@ -66,7 +66,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Bei_gleichem_Zeitstempel_bleibt_die_bisherige_Ordnung()
public void With_equal_timestamps_the_previous_order_stays()
{
// Sorted stably: two lines of the same millisecond must not swap
// places, otherwise a coherent sequence within a module falls apart.
@@ -87,7 +87,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Ein_Stacktrace_bleibt_bei_seiner_Meldung()
public void A_stack_trace_stays_with_its_message()
{
// 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.
@@ -108,7 +108,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Ein_Stacktrace_bleibt_auch_ueber_die_Taktgrenze_bei_seiner_Meldung()
public void A_stack_trace_stays_with_its_message_even_across_the_tick_boundary()
{
// 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
@@ -130,7 +130,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Eine_Uhrzeit_mitten_in_der_Meldung_ist_kein_Sortierschluessel()
public void A_time_in_the_middle_of_the_message_is_not_a_sort_key()
{
// 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]");
@@ -149,7 +149,7 @@ public sealed class LogTailTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public void Nur_das_Neue_kommt_zurueck()
public void Only_the_new_comes_back()
{
var tail = Tail();
@@ -161,7 +161,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Der_Modulfilter_laesst_nur_sein_Modul_durch()
public void The_module_filter_only_lets_its_module_through()
{
Write("Alpha", "[10:00:00.100] [INF] alpha");
Write("Beta", "[10:00:00.200] [INF] beta");
@@ -172,7 +172,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Eine_weggefilterte_Zeile_traegt_ihren_Zeitstempel_trotzdem_weiter()
public void A_filtered_out_line_still_passes_on_its_timestamp()
{
// 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
@@ -191,7 +191,7 @@ public sealed class LogTailTests : IDisposable
}
[Fact]
public void Ohne_Tagesverzeichnis_kommt_eine_leere_Liste()
public void Without_day_directory_an_empty_list_comes_back()
{
Directory.Delete(_today, recursive: true);
@@ -33,88 +33,88 @@ public sealed class TaskAdministrationTests : IDisposable
catch { /* Cleaning up is secondary */ }
}
private async Task<TaskItem> Einzige() => (await _board.ListAsync(new TaskQuery(), default)).Single();
private async Task<TaskItem> Single() => (await _board.ListAsync(new TaskQuery(), default)).Single();
[Fact]
public async Task Ein_Termin_der_nie_feuern_kann_wird_nicht_gespeichert()
public async Task A_schedule_that_can_never_fire_is_not_saved()
{
var ergebnis = await _admin.CreateAsync(new TaskInput(
var result = await _admin.CreateAsync(new TaskInput(
"Nachrichten-Digest", Assignee: "@new:nachrichten",
WhenKind: "cron", WhenValue: "0 7-22/x * * *", TimeZone: "Europe/Berlin"), default);
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("keine Zahl");
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("keine Zahl");
(await _board.ListAsync(new TaskQuery(), default)).ShouldBeEmpty();
}
[Fact]
public async Task Wiederkehrend_im_Chatverlauf_wird_angelegt_aber_mit_Hinweis()
public async Task Recurring_in_chat_history_is_created_but_with_a_hint()
{
var ergebnis = await _admin.CreateAsync(new TaskInput(
var result = await _admin.CreateAsync(new TaskInput(
"Nachrichten-Digest", Assignee: "@nachrichten",
WhenKind: "cron", WhenValue: "0 7-22/3 * * *", TimeZone: "Europe/Berlin"), default);
ergebnis.Ok.ShouldBeTrue();
ergebnis.Message.ShouldContain("@new:nachrichten");
result.Ok.ShouldBeTrue();
result.Message.ShouldContain("@new:nachrichten");
}
[Theory]
[InlineData("@new:gibtsnicht")]
[InlineData("@gibtsnicht")]
[InlineData("")]
public async Task Ohne_gueltige_Zuordnung_entsteht_keine_Aufgabe(string assignee)
public async Task Without_a_valid_assignment_no_task_is_created(string assignee)
=> (await _admin.CreateAsync(new TaskInput("X", Assignee: assignee), default)).Ok.ShouldBeFalse();
[Fact]
public async Task Pausieren_und_Aktivieren_setzen_backlog_und_todo()
public async Task Pausing_and_activating_set_backlog_and_todo()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default);
var id = (await Einzige()).Id;
var id = (await Single()).Id;
(await _admin.SetEnabledAsync(id, false, default)).Ok.ShouldBeTrue();
(await Einzige()).Status.ShouldBe(TaskItemStatus.Backlog);
(await Single()).Status.ShouldBe(TaskItemStatus.Backlog);
(await _admin.SetEnabledAsync(id, true, default)).Ok.ShouldBeTrue();
(await Einzige()).Status.ShouldBe(TaskItemStatus.Todo);
(await Single()).Status.ShouldBe(TaskItemStatus.Todo);
}
[Fact]
public async Task Die_Liste_nennt_den_naechsten_Lauf()
public async Task The_list_names_the_next_run()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *", TimeZone: "Europe/Berlin"), default);
var zeile = (await _admin.ListAsync(50, default))!.Single();
var line = (await _admin.ListAsync(50, default))!.Single();
zeile.NextRunUtc.ShouldNotBeNull();
zeile.Hinweis.ShouldBeNull();
zeile.LastRun.ShouldBeNull();
line.NextRunUtc.ShouldNotBeNull();
line.Hinweis.ShouldBeNull();
line.LastRun.ShouldBeNull();
}
[Fact]
public async Task Bearbeiten_aendert_den_Termin()
public async Task Editing_changes_the_schedule()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default);
var id = (await Einzige()).Id;
var id = (await Single()).Id;
var ergebnis = await _admin.UpdateAsync(id, new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "every", WhenValue: "2h"), default);
var result = await _admin.UpdateAsync(id, new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "every", WhenValue: "2h"), default);
ergebnis.Ok.ShouldBeTrue();
(await Einzige()).When!.Kind.ShouldBe(TaskWhenKind.Every);
result.Ok.ShouldBeTrue();
(await Single()).When!.Kind.ShouldBe(TaskWhenKind.Every);
}
[Fact]
public async Task Sofortlauf_ohne_Scanner_wird_abgelehnt_statt_still_nichts_zu_tun()
public async Task Immediate_run_without_scanner_is_rejected_instead_of_silently_doing_nothing()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst"), default);
var ergebnis = await _admin.RunNowAsync((await Einzige()).Id, default);
var result = await _admin.RunNowAsync((await Single()).Id, default);
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("Scanner");
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("Scanner");
}
[Fact]
public async Task Ohne_Taskboard_liefert_die_Liste_null()
public async Task Without_taskboard_the_list_returns_null()
=> (await new TaskAdministration(null, null, null, null, null, () => _agents, NullLogger.Instance)
.ListAsync(10, default)).ShouldBeNull();
}
@@ -105,8 +105,8 @@ public sealed class ToolSettingsFormTests
var tool = host.Host.AgentAdmin.Find("scout")!.Tools["FTP"];
tool["host"].ShouldBe("ftp.example.org");
tool.ShouldContainKey(Werkzeugrechte.ErlaubtSchluessel);
tool.ShouldContainKey(Werkzeugrechte.VerbotenSchluessel);
tool.ShouldContainKey(ToolRights.AllowedKey);
tool.ShouldContainKey(ToolRights.ForbiddenKey);
}
[Fact]