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:
co-authored by
Claude Opus 5.5
parent
837a634a22
commit
77062facc9
@@ -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
|
||||
|
||||
+25
-25
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user