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
@@ -230,7 +230,7 @@ public sealed class BudgetMissTests : IDisposable
public async Task A_shutdown_is_not_a_budget_miss()
{
await SeedOneTime("t-report");
var dispatcher = new BudgetDispatcher { Result = TaskLauf.NichtGelaufen };
var dispatcher = new BudgetDispatcher { Result = DispatchOutcome.NichtGelaufen };
await Scanner(dispatcher).ScanOnceAsync(default);
var task = await Get("t-report");
@@ -279,15 +279,15 @@ public sealed class BudgetMissTests : IDisposable
private sealed class BudgetDispatcher : ITaskDispatcher
{
public bool HasBudget { get; set; } = true;
public TaskLauf? Result { get; set; }
public DispatchOutcome? Result { get; set; }
public int Calls { get; private set; }
public List<string> Started { get; } = [];
public Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
public Task<DispatchOutcome> DispatchAsync(TaskItem task, CancellationToken ct)
{
Calls++;
Started.Add(task.Id);
return Task.FromResult(Result ?? (HasBudget ? TaskLauf.Erledigt : TaskLauf.KeinBudget));
return Task.FromResult(Result ?? (HasBudget ? DispatchOutcome.Erledigt : DispatchOutcome.KeinBudget));
}
}
}
@@ -44,7 +44,7 @@ public sealed class CoordinationMigrationTests : IDisposable
=> File.WriteAllText(Path.Combine(_coordinationDir, name), content);
[Fact]
public async Task Nur_task_Dateien_werden_uebernommen()
public async Task Only_task_files_are_adopted()
{
Write("task_video.md", "# Task: Video-Zusammenfassung\n## Status: ASSIGNED\nAnalysiere das Video.");
Write("status_senior.md", "# Senior Developer - Status\n## Status: ONLINE");
@@ -64,7 +64,7 @@ public sealed class CoordinationMigrationTests : IDisposable
}
[Fact]
public async Task Die_uebernommene_Datei_wandert_nach_migrated_und_bleibt_erhalten()
public async Task The_adopted_file_moves_to_migrated_and_is_kept()
{
Write("task_a.md", "# Task: Etwas\nInhalt.");
@@ -75,7 +75,7 @@ public sealed class CoordinationMigrationTests : IDisposable
}
[Fact]
public async Task Nicht_Aufgaben_bleiben_unangetastet()
public async Task Non_tasks_stay_untouched()
{
Write("status_x.md", "# Status\nONLINE");
Write("report.json", "{}");
@@ -88,7 +88,7 @@ public sealed class CoordinationMigrationTests : IDisposable
}
[Fact]
public async Task Die_Migration_ist_idempotent()
public async Task The_migration_is_idempotent()
{
Write("task_a.md", "# Task: Einmalig\nInhalt.");
@@ -99,7 +99,7 @@ public sealed class CoordinationMigrationTests : IDisposable
}
[Fact]
public async Task Ohne_Task_Ueberschrift_kommt_der_Titel_aus_dem_Dateinamen()
public async Task Without_task_heading_the_title_comes_from_the_file_name()
{
Write("task_video_analysis_progress.md", "## Status: IN PROGRESS\nOhne echte Überschrift.");
@@ -110,7 +110,7 @@ public sealed class CoordinationMigrationTests : IDisposable
}
[Fact]
public async Task Ohne_coordination_Verzeichnis_passiert_nichts()
public async Task Without_coordination_directory_nothing_happens()
{
Directory.Delete(_coordinationDir, recursive: true);
(await _migration.RunAsync(default)).ShouldBe(0);
@@ -182,9 +182,9 @@ public sealed class MeetingTests : IDisposable
var dispatcher = new EngineTaskDispatcher(_fixture.Engine, () => _fixture.Agents, "test-instance",
_fixture.Registry, _fixture.StateStore, TestLogging.Factory);
var lauf = await dispatcher.DispatchAsync(Meeting("logistics"), default);
var run = await dispatcher.DispatchAsync(Meeting("logistics"), default);
lauf.ShouldBe(TaskLauf.Erledigt);
run.ShouldBe(DispatchOutcome.Erledigt);
_fixture.Client.ReceivedRequests.Count.ShouldBe(2);
}
@@ -31,11 +31,11 @@ public sealed class RunSetStatusTests : IDisposable
/// <summary>A run that sets its own task to a status, as the taskboard tool would.</summary>
private sealed class SettingDispatcher(SqliteTaskRepository repo, TaskItemStatus? set) : ITaskDispatcher
{
public async Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
public async Task<DispatchOutcome> DispatchAsync(TaskItem task, CancellationToken ct)
{
if (set is { } status)
await repo.SetStatusAsync(task.Id, status, DateTime.UtcNow, ct);
return TaskLauf.Erledigt;
return DispatchOutcome.Erledigt;
}
}
@@ -244,7 +244,7 @@ public sealed class ScannerCapacityTests : IDisposable
public List<string> Started { get; } = [];
public int MaxConcurrent { get; private set; }
public async Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
public async Task<DispatchOutcome> DispatchAsync(TaskItem task, CancellationToken ct)
{
TaskCompletionSource? gate = null;
lock (_lock)
@@ -266,7 +266,7 @@ public sealed class ScannerCapacityTests : IDisposable
else if (holdFor is { } delay)
await Task.Delay(delay, ct);
return TaskLauf.Erledigt;
return DispatchOutcome.Erledigt;
}
finally
{
@@ -14,7 +14,7 @@ public sealed class TaskAssigneeTests
[InlineData("@new", TaskAssigneeKind.New)]
[InlineData("@new:crawler", TaskAssigneeKind.New)]
[InlineData("@crawler", TaskAssigneeKind.Agent)]
public void Die_Art_wird_richtig_erkannt(string assignee, TaskAssigneeKind expected)
public void The_kind_is_recognised_correctly(string assignee, TaskAssigneeKind expected)
=> TaskAssignee.KindOf(assignee).ShouldBe(expected);
[Theory]
@@ -22,6 +22,6 @@ public sealed class TaskAssigneeTests
[InlineData("@new:crawler", "crawler")]
[InlineData("@new", "")] // fresh run without a named agent
[InlineData("@human", "")]
public void Die_Agent_Id_wird_richtig_herausgezogen(string assignee, string expected)
public void The_agent_id_is_extracted_correctly(string assignee, string expected)
=> TaskAssignee.AgentId(assignee).ShouldBe(expected);
}
@@ -12,7 +12,7 @@ namespace ClawdDotNet.Core.Tests.Tasks;
public sealed class TaskFrontmatterTests
{
[Fact]
public void Ein_vollstaendiges_Frontmatter_wird_gelesen()
public void A_complete_frontmatter_is_read()
{
var text = """
---
@@ -58,7 +58,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Serialisieren_und_wieder_Einlesen_ergibt_dieselbe_Definition()
public void Serialising_and_reading_back_gives_the_same_definition()
{
var original = new TaskItem
{
@@ -96,7 +96,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Ein_Assignee_mit_At_ueberlebt_den_Rundlauf()
public void An_assignee_with_at_survives_the_round_trip()
{
// In YAML a leading '@' is reserved — without quoting it would get lost.
var task = new TaskItem { Id = "t-1", Title = "x", Assignee = "@new" };
@@ -109,7 +109,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Ohne_when_ist_es_eine_einmalige_Aufgabe()
public void Without_when_it_is_a_one_off_task()
{
var text = """
---
@@ -125,7 +125,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Fehlende_Felder_fallen_auf_sichere_Standards_zurueck()
public void Missing_fields_fall_back_to_safe_defaults()
{
var text = """
---
@@ -145,7 +145,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Eine_unbekannte_Terminart_wird_verworfen_statt_geraten()
public void An_unknown_schedule_kind_is_discarded_instead_of_guessed()
{
var text = """
---
@@ -163,14 +163,14 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Fehlt_der_Frontmatter_Block_meldet_der_Parser_das()
public void If_the_frontmatter_block_is_missing_the_parser_reports_it()
{
TaskFrontmatter.TryParse("Nur Text, kein Frontmatter.", out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Fact]
public void Ein_nicht_geschlossener_Block_wird_gemeldet()
public void An_unclosed_block_is_reported()
{
var text = "---\nid: t-1\ntitle: x\n";
TaskFrontmatter.TryParse(text, out _, out var error).ShouldBeFalse();
@@ -178,7 +178,7 @@ public sealed class TaskFrontmatterTests
}
[Fact]
public void Windows_Zeilenenden_stoeren_nicht()
public void Windows_line_endings_do_no_harm()
{
var text = "---\r\nid: t-1\r\ntitle: Mit CRLF\r\n---\r\nRumpf\r\n";
TaskFrontmatter.TryParse(text, out var task, out var error).ShouldBeTrue(error);
@@ -56,7 +56,7 @@ public sealed class TaskRepositoryTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Eine_Aufgabe_wird_angelegt_und_wiedergefunden()
public async Task A_task_is_created_and_found_again()
{
await Seed("t-1", title: "Recherche");
@@ -68,7 +68,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Der_Import_ist_idempotent_und_erzeugt_keine_Dublette()
public async Task The_import_is_idempotent_and_creates_no_duplicate()
{
await Seed("t-1", title: "Erste Fassung");
await Seed("t-1", title: "Zweite Fassung");
@@ -78,7 +78,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Ein_Re_Import_laesst_den_Ausfuehrungszustand_unangetastet()
public async Task A_re_import_leaves_the_execution_state_untouched()
{
// The crux of the split of truth: the file is the truth about the
// definition, the DB about the execution state. A renewed import must not reset a
@@ -97,7 +97,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Die_Liste_filtert_nach_Status_und_blendet_Archiviertes_aus()
public async Task The_list_filters_by_status_and_hides_archived()
{
await Seed("t-1", TaskItemStatus.Todo);
await Seed("t-2", TaskItemStatus.Done);
@@ -109,7 +109,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Die_Liste_ordnet_nach_Prioritaet()
public async Task The_list_sorts_by_priority()
{
await Seed("t-low", priority: 1, title: "nebensächlich");
await Seed("t-high", priority: 5, title: "dringend");
@@ -122,7 +122,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Die_Liste_filtert_nach_Assignee_und_Freitext()
public async Task The_list_filters_by_assignee_and_free_text()
{
await Seed("t-1", assignee: "@crawler", title: "NVDA Zahlen");
await Seed("t-2", assignee: "@analyst", title: "TSLA Bericht");
@@ -136,7 +136,7 @@ public sealed class TaskRepositoryTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Ein_Termin_laesst_sich_genau_einmal_beanspruchen()
public async Task An_occurrence_can_be_claimed_exactly_once()
{
await Seed("t-1");
@@ -146,7 +146,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Gleichzeitige_Claims_auf_denselben_Termin_ergeben_genau_einen_Gewinner()
public async Task Concurrent_claims_on_the_same_occurrence_give_exactly_one_winner()
{
// The core invariant. Repeated several times, because a race occurs sporadically.
for (var round = 0; round < 25; round++)
@@ -164,7 +164,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Ein_zweiter_Tick_auf_denselben_Termin_loest_keinen_zweiten_Lauf_aus()
public async Task A_second_tick_on_the_same_occurrence_triggers_no_second_run()
{
await Seed("t-1");
@@ -178,7 +178,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Ein_gescheiterter_Lauf_wird_nicht_automatisch_wiederholt()
public async Task A_failed_run_is_not_retried_automatically()
{
// No retry storm: the marker is already set at claim time, not only on success.
await Seed("t-1");
@@ -192,7 +192,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Der_naechste_planmaessige_Termin_laesst_sich_wieder_beanspruchen()
public async Task The_next_scheduled_occurrence_can_be_claimed_again()
{
await Seed("t-1");
(await _repo.TryClaimAsync("t-1", Occ(T0), "tok", Now, NoStaleClaims, default)).ShouldBeTrue();
@@ -209,7 +209,7 @@ public sealed class TaskRepositoryTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Eine_blockierte_Aufgabe_laesst_sich_nicht_beanspruchen()
public async Task A_blocked_task_cannot_be_claimed()
{
await Seed("t-1", TaskItemStatus.Blocked, blockedBy: ["t-0"]);
@@ -218,7 +218,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Wartende_Aufgaben_lassen_sich_ueber_ihren_Blocker_finden()
public async Task Waiting_tasks_can_be_found_via_their_blocker()
{
await Seed("t-a", blockedBy: ["t-blocker"]);
await Seed("t-b", blockedBy: ["t-blocker", "t-anderer"]);
@@ -235,7 +235,7 @@ public sealed class TaskRepositoryTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Ein_Lauf_ohne_gueltigen_Claim_kann_nichts_ueberschreiben()
public async Task A_run_without_a_valid_claim_cannot_overwrite_anything()
{
await Seed("t-1");
(await _repo.TryClaimAsync("t-1", Occ(T0), "echt", Now, NoStaleClaims, default)).ShouldBeTrue();
@@ -249,7 +249,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Verwaiste_Claims_werden_beim_Start_zurueckgesetzt()
public async Task Orphaned_claims_are_reset_on_start()
{
await Seed("t-stuck");
// Claim with a point in time that lies far back already (crashed run).
@@ -266,7 +266,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Ein_frischer_Claim_wird_bei_der_Reconciliation_nicht_angetastet()
public async Task A_fresh_claim_is_not_touched_by_reconciliation()
{
await Seed("t-live");
(await _repo.TryClaimAsync("t-live", Occ(T0), "tok", Now, NoStaleClaims, default)).ShouldBeTrue();
@@ -278,7 +278,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Der_Status_laesst_sich_direkt_setzen()
public async Task The_status_can_be_set_directly()
{
await Seed("t-1");
(await _repo.SetStatusAsync("t-1", TaskItemStatus.Canceled, Now, default)).ShouldBeTrue();
@@ -290,7 +290,7 @@ public sealed class TaskRepositoryTests : IDisposable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Viele_Aufgaben_lassen_sich_gleichzeitig_importieren()
public async Task Many_tasks_can_be_imported_concurrently()
{
var imports = Enumerable.Range(0, 60).Select(i => Seed($"t-{i}"));
await Task.WhenAll(imports);
@@ -299,7 +299,7 @@ public sealed class TaskRepositoryTests : IDisposable
}
[Fact]
public async Task Aufgaben_ueberdauern_das_Schliessen_der_Datenbank()
public async Task Tasks_outlast_closing_the_database()
{
await Seed("t-1", title: "muss einen Neustart überleben");
@@ -55,7 +55,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── Basic flow ───
[Fact]
public async Task Eine_faellige_Aufgabe_wird_ausgefuehrt_und_abgeschlossen()
public async Task A_due_task_is_executed_and_completed()
{
await SeedDue("t-1");
var dispatcher = new FakeDispatcher();
@@ -68,7 +68,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Mit_require_approval_landet_das_Ergebnis_im_Review()
public async Task With_require_approval_the_result_ends_up_in_review()
{
await SeedDue("t-1", requireApproval: true);
@@ -78,7 +78,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Ein_gescheiterter_Lauf_geht_zurueck_auf_todo_und_wird_nicht_wiederholt()
public async Task A_failed_run_goes_back_to_todo_and_is_not_retried()
{
await SeedDue("t-1");
var dispatcher = new FakeDispatcher(succeed: false);
@@ -95,7 +95,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── Invariant 2: no dispatch with an open blocker / to humans ───
[Fact]
public async Task Eine_blockierte_Aufgabe_wird_nicht_angestossen()
public async Task A_blocked_task_is_not_triggered()
{
await SeedDue("t-1", status: TaskItemStatus.Blocked, blockedBy: ["t-0"]);
var dispatcher = new FakeDispatcher();
@@ -105,7 +105,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Eine_Aufgabe_fuer_einen_Menschen_wird_nicht_angestossen()
public async Task A_task_for_a_human_is_not_triggered()
{
await SeedDue("t-1", assignee: "@human");
var dispatcher = new FakeDispatcher();
@@ -118,7 +118,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── Invariant 3: double tick = one run ───
[Fact]
public async Task Zwei_gleichzeitige_Takte_stossen_eine_Aufgabe_nur_einmal_an()
public async Task Two_concurrent_ticks_trigger_a_task_only_once()
{
for (var round = 0; round < 20; round++)
{
@@ -138,7 +138,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── Recurring tasks (replaces the legacy scheduler) ───
[Fact]
public async Task Ein_wiederkehrender_Task_bleibt_nach_dem_Feuern_auf_todo()
public async Task A_recurring_task_stays_on_todo_after_firing()
{
_clock.SetUtcNow(new DateTimeOffset(2027, 1, 1, 9, 0, 0, TimeSpan.Zero));
await _repo.UpsertAsync(new TaskItem
@@ -154,7 +154,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Ein_wiederkehrender_Task_feuert_an_jedem_Termin_erneut()
public async Task A_recurring_task_fires_again_at_every_occurrence()
{
await _repo.UpsertAsync(new TaskItem
{
@@ -174,7 +174,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Ein_backlog_Task_wird_nicht_ausgefuehrt()
public async Task A_backlog_task_is_not_executed()
{
await SeedDue("t-1", status: TaskItemStatus.Backlog);
var dispatcher = new FakeDispatcher();
@@ -186,7 +186,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── C1 hook: market calendar ───
[Fact]
public async Task Bei_geschlossenem_Markt_bleibt_ein_marktabhaengiger_Termin_liegen()
public async Task With_the_market_closed_a_market_dependent_schedule_waits()
{
await SeedDue("t-1", onlyWhenMarketOpen: true);
var dispatcher = new FakeDispatcher();
@@ -201,7 +201,7 @@ public sealed class TaskScannerTests : IDisposable
// ─── Auto-dispatch: blocker done → waiting ones released ───
[Fact]
public async Task Wird_der_letzte_Blocker_fertig_gibt_das_die_wartende_Aufgabe_frei()
public async Task When_the_last_blocker_is_done_the_waiting_task_is_released()
{
await SeedDue("t-blocker");
await SeedDue("t-wartend", status: TaskItemStatus.Blocked, blockedBy: ["t-blocker"]);
@@ -214,7 +214,7 @@ public sealed class TaskScannerTests : IDisposable
}
[Fact]
public async Task Solange_ein_Blocker_offen_ist_bleibt_die_wartende_Aufgabe_blockiert()
public async Task While_a_blocker_is_open_the_waiting_task_stays_blocked()
{
await SeedDue("t-b1"); // finishes in this tick
await SeedDue("t-b2", assignee: "@human"); // a human — stays open
@@ -229,10 +229,10 @@ public sealed class TaskScannerTests : IDisposable
// ─── #643: tasks that did not run ───
[Fact]
public async Task Eine_einmalige_Aufgabe_ohne_Budget_bleibt_faellig_statt_liegenzubleiben()
public async Task A_one_off_task_without_budget_stays_due_instead_of_being_dropped()
{
await _repo.UpsertAsync(new TaskItem { Id = "t-einmal", Title = "einmal", Assignee = "@new:a", Status = TaskItemStatus.Todo }, default);
var dispatcher = new FakeDispatcher { Ergebnis = TaskLauf.NichtGelaufen };
var dispatcher = new FakeDispatcher { Ergebnis = DispatchOutcome.NichtGelaufen };
var scanner = Scanner(dispatcher);
(await scanner.ScanOnceAsync(default)).ShouldBe(1);
@@ -243,14 +243,14 @@ public sealed class TaskScannerTests : IDisposable
(await scanner.ScanOnceAsync(default)).ShouldBe(0, "Wiedervorlage erst nach der Pause, nicht im Minutentakt");
_clock.Advance(TaskScanner.Wiedervorlage + TimeSpan.FromMinutes(1));
dispatcher.Ergebnis = TaskLauf.Erledigt;
_clock.Advance(TaskScanner.RetryDelay + TimeSpan.FromMinutes(1));
dispatcher.Ergebnis = DispatchOutcome.Erledigt;
(await scanner.ScanOnceAsync(default)).ShouldBe(1);
(await _repo.GetAsync("t-einmal", default))!.Status.ShouldBe(TaskItemStatus.Done);
}
[Fact]
public async Task Eine_gescheiterte_einmalige_Aufgabe_bleibt_verbraucht()
public async Task A_failed_one_off_task_stays_used_up()
{
await _repo.UpsertAsync(new TaskItem { Id = "t-kaputt", Title = "kaputt", Assignee = "@new:a", Status = TaskItemStatus.Todo }, default);
var scanner = Scanner(new FakeDispatcher(succeed: false));
@@ -267,13 +267,13 @@ public sealed class TaskScannerTests : IDisposable
private readonly Lock _lock = new();
public List<string> Dispatched { get; } = [];
public TaskLauf? Ergebnis { get; set; }
public DispatchOutcome? Ergebnis { get; set; }
public Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
public Task<DispatchOutcome> DispatchAsync(TaskItem task, CancellationToken ct)
{
lock (_lock)
Dispatched.Add(task.Id);
return Task.FromResult(Ergebnis ?? (succeed ? TaskLauf.Erledigt : TaskLauf.Gescheitert));
return Task.FromResult(Ergebnis ?? (succeed ? DispatchOutcome.Erledigt : DispatchOutcome.Gescheitert));
}
}
@@ -16,7 +16,7 @@ public sealed class TaskScheduleTests
// ─── #597: invalid cron expression ───
[Fact]
public void Ungueltiger_Cron_Ausdruck_wird_benannt_statt_verschwiegen()
public void Invalid_cron_expression_is_named_instead_of_hidden()
{
var task = new TaskItem
{
@@ -29,7 +29,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Nachrichten_Digest_aus_dem_Testbetrieb_ist_um_13_Uhr_faellig()
public void News_digest_from_test_operation_is_due_at_13_o_clock()
{
var task = new TaskItem
{
@@ -43,7 +43,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Andere_Terminarten_haben_keinen_Cron_Fehler()
public void Other_schedule_kinds_have_no_cron_error()
=> TaskSchedule.InvalidCron(new TaskItem
{
Id = "t-1", When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "kaputt" }
@@ -52,7 +52,7 @@ public sealed class TaskScheduleTests
// ─── One-time, without a schedule ───
[Fact]
public void Ohne_Termin_ist_die_Aufgabe_faellig_bis_sie_lief()
public void Without_schedule_the_task_is_due_until_it_ran()
{
var task = new TaskItem { Id = "t-1", CreatedAt = Utc(2026, 7, 31, 6, 0) };
@@ -63,7 +63,7 @@ public sealed class TaskScheduleTests
// ─── at: one-time point in time ───
[Fact]
public void Ein_at_Termin_ist_erst_ab_dem_Zeitpunkt_faellig()
public void An_at_schedule_is_only_due_from_its_time()
{
var task = new TaskItem
{
@@ -77,7 +77,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Ein_at_Termin_feuert_genau_einmal()
public void An_at_schedule_fires_exactly_once()
{
var task = new TaskItem
{
@@ -90,7 +90,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Ein_at_Termin_mit_Z_wird_als_UTC_gedeutet()
public void An_at_schedule_with_Z_is_interpreted_as_UTC()
{
var task = new TaskItem
{
@@ -105,7 +105,7 @@ public sealed class TaskScheduleTests
// ─── every: interval ───
[Fact]
public void Ein_Intervall_wird_erst_nach_Ablauf_faellig()
public void An_interval_only_becomes_due_after_it_elapses()
{
var task = new TaskItem
{
@@ -119,7 +119,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Nach_dem_Marker_zaehlt_das_Intervall_neu()
public void After_the_marker_the_interval_counts_anew()
{
var task = new TaskItem
{
@@ -135,7 +135,7 @@ public sealed class TaskScheduleTests
// ─── cron: with time zone (the core of B7) ───
[Fact]
public void Ein_Cron_Termin_wird_in_seiner_Zeitzone_gedeutet_Sommer()
public void A_cron_schedule_is_interpreted_in_its_time_zone_summer()
{
var task = new TaskItem
{
@@ -150,7 +150,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Derselbe_Cron_liegt_im_Winter_auf_einer_anderen_UTC_Stunde()
public void The_same_cron_lands_on_a_different_UTC_hour_in_winter()
{
var task = new TaskItem
{
@@ -165,7 +165,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Vor_der_geplanten_Zeit_ist_der_Cron_nicht_faellig()
public void Before_the_planned_time_the_cron_is_not_due()
{
var task = new TaskItem
{
@@ -179,7 +179,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Eine_frische_Cron_Aufgabe_holt_keinen_Termin_von_vor_ihrer_Anlage_nach()
public void A_fresh_cron_task_does_not_catch_up_an_occurrence_from_before_its_creation()
{
var task = new TaskItem
{
@@ -193,7 +193,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Derselbe_Cron_Termin_feuert_nicht_zweimal()
public void The_same_cron_occurrence_does_not_fire_twice()
{
var task = new TaskItem
{
@@ -210,7 +210,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Ein_kaputter_Cron_Ausdruck_kippt_den_Scanner_nicht()
public void A_broken_cron_expression_does_not_topple_the_scanner()
{
var task = new TaskItem
{
@@ -228,7 +228,7 @@ public sealed class TaskScheduleTests
/// being noticed anywhere. Now it does not fire at all — and the scanner reports once why.
/// </summary>
[Fact]
public void Eine_unbekannte_Zeitzone_laesst_den_Termin_nicht_faellig_werden()
public void An_unknown_time_zone_does_not_make_the_schedule_due()
{
var task = new TaskItem
{
@@ -249,7 +249,7 @@ public sealed class TaskScheduleTests
[Theory]
[InlineData("Europe/Berlin")]
[InlineData("W. Europe Standard Time")]
public void Beide_Schreibweisen_derselben_Zone_ergeben_dieselbe_Feuerzeit(string tz)
public void Both_spellings_of_the_same_zone_give_the_same_fire_time(string tz)
{
var task = new TaskItem
{
@@ -266,7 +266,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Keine_Zeitzonenangabe_bedeutet_weiterhin_UTC()
public void No_time_zone_still_means_UTC()
{
var task = new TaskItem
{
@@ -289,18 +289,18 @@ public sealed class TaskScheduleTests
// twice.
// ═══════════════════════════════════════════════════════════
private static TaskItem TaeglichUmZwei(DateTime angelegt) => new()
private static TaskItem DailyAtTwo(DateTime created) => new()
{
Id = "t-dst",
CreatedAt = angelegt,
CreatedAt = created,
When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "0 2 * * *", TimeZone = "Europe/Berlin" }
};
/// <summary>R5 — the case #235 is about.</summary>
[Fact]
public void Zeitumstellung_Maerz_wird_nicht_uebersprungen()
public void DST_change_in_March_is_not_skipped()
{
var task = TaeglichUmZwei(Utc(2026, 3, 28, 0, 0));
var task = DailyAtTwo(Utc(2026, 3, 28, 0, 0));
// "02:00" does not exist on 29 March. The occurrence still happens, namely
// at the first point in time after it: 03:00 CEST = 01:00 UTC.
@@ -310,39 +310,39 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Der_verschobene_Termin_feuert_trotzdem_nur_einmal()
public void The_shifted_occurrence_still_fires_only_once()
{
var task = TaeglichUmZwei(Utc(2026, 3, 28, 0, 0));
var task = DailyAtTwo(Utc(2026, 3, 28, 0, 0));
var ersteFaelligkeit = TaskSchedule.DueOccurrence(task, Utc(2026, 3, 29, 2, 0));
ersteFaelligkeit.ShouldNotBeNull();
var firstDue = TaskSchedule.DueOccurrence(task, Utc(2026, 3, 29, 2, 0));
firstDue.ShouldNotBeNull();
TaskSchedule.DueOccurrence(task with { LastOccurrence = ersteFaelligkeit }, Utc(2026, 3, 29, 2, 30))
TaskSchedule.DueOccurrence(task with { LastOccurrence = firstDue }, Utc(2026, 3, 29, 2, 30))
.ShouldBeNull();
}
/// <summary>R4 — the other direction.</summary>
[Fact]
public void Zeitumstellung_Oktober_feuert_genau_einmal()
public void DST_change_in_October_fires_exactly_once()
{
var task = TaeglichUmZwei(Utc(2026, 10, 24, 0, 0));
var task = DailyAtTwo(Utc(2026, 10, 24, 0, 0));
// 02:00 occurs twice on 25 October: in summer time (00:00 UTC) and in
// standard time (01:00 UTC). Both passes result in the same occurrence key —
// the marker catches the second.
var ersterDurchlauf = TaskSchedule.DueOccurrence(task, Utc(2026, 10, 25, 0, 30));
ersterDurchlauf.ShouldNotBeNull();
var firstPass = TaskSchedule.DueOccurrence(task, Utc(2026, 10, 25, 0, 30));
firstPass.ShouldNotBeNull();
var zweiterDurchlauf = TaskSchedule.DueOccurrence(
task with { LastOccurrence = ersterDurchlauf }, Utc(2026, 10, 25, 1, 30));
zweiterDurchlauf.ShouldBeNull();
var secondPass = TaskSchedule.DueOccurrence(
task with { LastOccurrence = firstPass }, Utc(2026, 10, 25, 1, 30));
secondPass.ShouldBeNull();
}
[Fact]
public void An_gewoehnlichen_Tagen_bleibt_es_bei_zwei_Uhr_Ortszeit()
public void On_ordinary_days_it_stays_at_two_o_clock_local_time()
{
// The counter-check: the shift may only apply in the gap.
var task = TaeglichUmZwei(Utc(2026, 3, 21, 0, 0));
var task = DailyAtTwo(Utc(2026, 3, 21, 0, 0));
// 02:00 CET = 01:00 UTC.
TaskSchedule.DueOccurrence(task, Utc(2026, 3, 22, 2, 0))
@@ -350,7 +350,7 @@ public sealed class TaskScheduleTests
}
[Fact]
public void Auch_ein_einmaliger_Termin_in_der_Luecke_faellt_nicht_aus()
public void A_one_off_schedule_in_the_gap_is_not_skipped_either()
{
// The same handling on the at path — the same rule and the same gap applied there.
var task = new TaskItem
@@ -7,7 +7,7 @@ namespace ClawdDotNet.Core.Tests.Tasks;
/// Checking schedules, next run and runs per day (#594, #595, #597) as well as the hint
/// about growing context (#598).
/// </summary>
public sealed class TaskboardAusbauTests
public sealed class TaskboardExtensionTests
{
private static DateTime Utc(int y, int mo, int d, int h, int mi) => new(y, mo, d, h, mi, 0, DateTimeKind.Utc);
@@ -17,15 +17,15 @@ public sealed class TaskboardAusbauTests
// ─── #597 checking schedules ───
[Fact]
public void Gueltiger_Termin_mit_Bereich_und_Schritt_besteht_die_Pruefung()
public void Valid_schedule_with_range_and_step_passes_the_check()
=> TaskSchedule.ValidateWhen(Cron("0 7-22/3 * * *")).ShouldBeNull();
[Fact]
public void Kaputter_Cron_Ausdruck_wird_benannt()
public void Broken_cron_expression_is_named()
=> TaskSchedule.ValidateWhen(Cron("0 7-22/x * * *")).ShouldNotBeNull().ShouldContain("'x' ist keine Zahl");
[Fact]
public void Unbekannte_Zeitzone_wird_benannt()
public void Unknown_time_zone_is_named()
=> TaskSchedule.ValidateWhen(Cron("0 9 * * *", "Phantasie/Ort")).ShouldNotBeNull().ShouldContain("Phantasie/Ort");
[Theory]
@@ -33,14 +33,14 @@ public sealed class TaskboardAusbauTests
[InlineData(TaskWhenKind.Every, "halbstündlich", false)]
[InlineData(TaskWhenKind.At, "2026-09-15T08:00", true)]
[InlineData(TaskWhenKind.At, "morgen früh", false)]
public void Intervall_und_Zeitpunkt_werden_geprueft(TaskWhenKind kind, string value, bool gueltig)
public void Interval_and_time_are_checked(TaskWhenKind kind, string value, bool valid)
=> (TaskSchedule.ValidateWhen(new TaskWhen { Kind = kind, Value = value, TimeZone = "Europe/Berlin" }) is null)
.ShouldBe(gueltig);
.ShouldBe(valid);
// ─── #594 next run ───
[Fact]
public void Naechster_Lauf_eines_Cron_Termins_in_Berliner_Zeit()
public void Next_run_of_a_cron_schedule_in_Berlin_time()
{
var task = new TaskItem { Id = "t-7e0bbe", When = Cron("0 7-22/3 * * *") };
@@ -49,7 +49,7 @@ public sealed class TaskboardAusbauTests
}
[Fact]
public void Naechster_Lauf_eines_Intervalls_zaehlt_ab_dem_letzten_Lauf()
public void Next_run_of_an_interval_counts_from_the_last_run()
{
var task = new TaskItem
{
@@ -62,47 +62,47 @@ public sealed class TaskboardAusbauTests
}
[Fact]
public void Einmalige_Aufgabe_steht_an_bis_sie_lief()
public void One_off_task_is_pending_until_it_ran()
{
var jetzt = Utc(2026, 9, 14, 10, 0);
TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1" }, jetzt).ShouldBe(jetzt);
TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1", LastOccurrence = "x" }, jetzt).ShouldBeNull();
var now = Utc(2026, 9, 14, 10, 0);
TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1" }, now).ShouldBe(now);
TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1", LastOccurrence = "x" }, now).ShouldBeNull();
}
// ─── #595 runs per day ───
[Fact]
public void Laeufe_je_Tag_aus_Cron_und_Intervall()
public void Runs_per_day_from_cron_and_interval()
{
var jetzt = Utc(2026, 9, 14, 10, 0);
var now = Utc(2026, 9, 14, 10, 0);
TaskSchedule.RunsPerDay(new TaskItem { When = Cron("0 7-22/3 * * *") }, jetzt).ShouldBe(6);
TaskSchedule.RunsPerDay(new TaskItem { When = Cron("5 * * * *") }, jetzt).ShouldBe(24);
TaskSchedule.RunsPerDay(new TaskItem { When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "30m" } }, jetzt).ShouldBe(48);
TaskSchedule.RunsPerDay(new TaskItem(), jetzt).ShouldBe(0);
TaskSchedule.RunsPerDay(new TaskItem { When = Cron("0 7-22/3 * * *") }, now).ShouldBe(6);
TaskSchedule.RunsPerDay(new TaskItem { When = Cron("5 * * * *") }, now).ShouldBe(24);
TaskSchedule.RunsPerDay(new TaskItem { When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "30m" } }, now).ShouldBe(48);
TaskSchedule.RunsPerDay(new TaskItem(), now).ShouldBe(0);
}
// ─── #598 hint ───
[Fact]
public void Wiederkehrend_im_Chatverlauf_bekommt_den_Hinweis()
=> TaskHinweise.WachsenderKontext(new TaskItem { Assignee = "@nachrichten", When = Cron("0 7-22/3 * * *") })
public void Recurring_in_chat_history_gets_the_hint()
=> TaskHints.GrowingContext(new TaskItem { Assignee = "@nachrichten", When = Cron("0 7-22/3 * * *") })
.ShouldNotBeNull().ShouldContain("@new:nachrichten");
[Theory]
[InlineData("@new:nachrichten", true)]
[InlineData("@nachrichten", false)]
[InlineData("@human", true)]
public void Kein_Hinweis_fuer_frische_Laeufe_einmalige_und_menschliche_Aufgaben(string assignee, bool wiederkehrend)
=> TaskHinweise.WachsenderKontext(new TaskItem
public void No_hint_for_fresh_runs_one_off_and_human_tasks(string assignee, bool recurring)
=> TaskHints.GrowingContext(new TaskItem
{
Assignee = assignee,
When = wiederkehrend ? Cron("5 * * * *") : null
When = recurring ? Cron("5 * * * *") : null
}).ShouldBeNull();
[Fact]
public void Kein_Hinweis_fuer_Werkzeugjobs()
=> TaskHinweise.WachsenderKontext(new TaskItem
public void No_hint_for_tool_jobs()
=> TaskHints.GrowingContext(new TaskItem
{
Assignee = "@telegram", Type = TaskItemType.ToolJob, When = Cron("* * * * *")
}).ShouldBeNull();
@@ -37,7 +37,7 @@ public sealed class TaskboardServiceTests : IDisposable
=> new() { Title = title, Assignee = assignee, Body = "Tu etwas." };
[Fact]
public async Task Anlegen_erzeugt_Datei_und_DB_Zeile_in_einem_Zug()
public async Task Creating_writes_file_and_DB_row_in_one_go()
{
var created = await _board.CreateAsync(Def("NVDA recherchieren"), default);
@@ -58,7 +58,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Der_Dateiname_ist_beschreibend()
public async Task The_file_name_is_descriptive()
{
var created = await _board.CreateAsync(Def("NVDA Earnings recherchieren"), default);
created.FileName.ShouldStartWith("nvda-earnings-recherchieren-");
@@ -66,7 +66,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Aendern_schreibt_Datei_und_DB_fort()
public async Task Changing_updates_file_and_DB()
{
var created = await _board.CreateAsync(Def("Alt"), default);
@@ -85,7 +85,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Ein_Kommentar_landet_im_Rumpf()
public async Task A_comment_ends_up_in_the_body()
{
var created = await _board.CreateAsync(Def(), default);
@@ -99,7 +99,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Import_spiegelt_vorhandene_Dateien_in_die_DB()
public async Task Import_mirrors_existing_files_into_the_DB()
{
Directory.CreateDirectory(_tasksDir);
File.WriteAllText(Path.Combine(_tasksDir, "eins.md"),
@@ -115,7 +115,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Eine_von_Hand_angelegte_Datei_ohne_Id_bekommt_eine_und_behaelt_sie()
public async Task A_hand_made_file_without_id_gets_one_and_keeps_it()
{
Directory.CreateDirectory(_tasksDir);
var path = Path.Combine(_tasksDir, "handarbeit.md");
@@ -132,7 +132,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Eine_verschwundene_Datei_wird_archiviert_statt_geloescht()
public async Task A_vanished_file_is_archived_instead_of_deleted()
{
var created = await _board.CreateAsync(Def("Vergänglich"), default);
File.Delete(Path.Combine(_tasksDir, created.FileName));
@@ -146,7 +146,7 @@ public sealed class TaskboardServiceTests : IDisposable
}
[Fact]
public async Task Ein_Re_Import_setzt_einen_laufenden_Status_nicht_zurueck()
public async Task A_re_import_does_not_reset_a_running_status()
{
// The core rule of the split of truth, checked via the service.
var created = await _board.CreateAsync(Def(), default);
@@ -30,7 +30,7 @@ public sealed class ToolJobDispatchTests
fixture.Registry, fixture.StateStore, NullLoggerFactory.Instance);
[Fact]
public async Task Ein_Poll_ohne_Fund_weckt_den_Agenten_nicht()
public async Task A_poll_without_a_hit_does_not_wake_the_agent()
{
var tool = new FakeJobTool(ToolJobResult.NoAction("nichts Neues"));
var fixture = new EngineFixture().WithTool(tool);
@@ -38,13 +38,13 @@ public sealed class ToolJobDispatchTests
var ok = await Dispatcher(fixture).DispatchAsync(PollTask(), default);
ok.ShouldBe(TaskLauf.Erledigt, "ein Poll ohne Fund ist kein Fehler");
ok.ShouldBe(DispatchOutcome.Erledigt, "ein Poll ohne Fund ist kein Fehler");
tool.Ticks.ShouldBe(1);
fixture.Client.ReceivedRequests.ShouldBeEmpty("kein Agent-Lauf, wenn nichts zu wecken war");
}
[Fact]
public async Task Ein_Poll_mit_Fund_weckt_den_Agenten()
public async Task A_poll_with_a_hit_wakes_the_agent()
{
var tool = new FakeJobTool(ToolJobResult.Wake("Neue Nachricht eingetroffen"));
var fixture = new EngineFixture().WithTool(tool);
@@ -53,18 +53,18 @@ public sealed class ToolJobDispatchTests
var ok = await Dispatcher(fixture).DispatchAsync(PollTask(), default);
ok.ShouldBe(TaskLauf.Erledigt);
ok.ShouldBe(DispatchOutcome.Erledigt);
tool.Ticks.ShouldBe(1);
fixture.Client.ReceivedRequests.ShouldNotBeEmpty("der Agent wurde geweckt");
}
[Fact]
public async Task Ein_unbekanntes_Poll_Tool_meldet_einen_Fehler()
public async Task An_unknown_poll_tool_reports_an_error()
{
var fixture = new EngineFixture(); // PollTool not registered
fixture.AddAgent("agent-a");
(await Dispatcher(fixture).DispatchAsync(PollTask(), default)).ShouldBe(TaskLauf.Gescheitert);
(await Dispatcher(fixture).DispatchAsync(PollTask(), default)).ShouldBe(DispatchOutcome.Gescheitert);
}
/// <summary>A tool that is at the same time a poll provider — counts ticks, delivers a fixed result.</summary>