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
@@ -14,14 +14,14 @@ namespace ClawdDotNet.Tools.Tests.FileRW;
/// at the step limit, and the same agent repeatedly fetched files in the wrong workspace,
/// because the error message did not say where they lie.
/// </summary>
public sealed class SammelLesenTests : IDisposable
public sealed class BatchReadTests : IDisposable
{
private readonly string _basis;
private readonly string _personal;
private readonly string _shared;
private readonly FileRWTool _tool = new();
public SammelLesenTests()
public BatchReadTests()
{
_basis = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
_personal = Path.Combine(_basis, "Workspace");
@@ -48,23 +48,23 @@ public sealed class SammelLesenTests : IDisposable
private Task<ToolResult> Run(string json, string sharedLevel = "ReadWrite")
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(sharedLevel), CancellationToken.None);
private void Stundendateien(int anzahl, string tag = "2026-09-20")
private void HourlyFiles(int count, string tag = "2026-09-20")
{
for (var stunde = 0; stunde < anzahl; stunde++)
for (var hour = 0; hour < count; hour++)
{
File.WriteAllText(
Path.Combine(_shared, "wetter", "stunden", $"berlin_{tag}_{stunde:00}-00.txt"),
$"{tag} {stunde:00}:00 Temperatur {10 + stunde} C");
Path.Combine(_shared, "wetter", "stunden", $"berlin_{tag}_{hour:00}-00.txt"),
$"{tag} {hour:00}:00 Temperatur {10 + hour} C");
}
}
// ═══════════════════ #663: one call instead of 24 ═══════════════════
[Fact]
public async Task Ein_Verzeichnis_mit_Muster_kommt_in_einem_Aufruf()
public async Task A_directory_with_pattern_comes_in_one_call()
{
Stundendateien(24);
Stundendateien(3, "2026-09-19");
HourlyFiles(24);
HourlyFiles(3, "2026-09-19");
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden","pattern":"berlin_2026-09-20_*.txt"}""");
@@ -76,23 +76,23 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Die_Dateien_kommen_benannt_und_in_fester_Reihenfolge()
public async Task The_files_come_named_and_in_fixed_order()
{
Stundendateien(3);
HourlyFiles(3);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
var reihenfolge = new[] { "00-00.txt", "01-00.txt", "02-00.txt" }
var sequence = new[] { "00-00.txt", "01-00.txt", "02-00.txt" }
.Select(n => result.Content.IndexOf(n, StringComparison.Ordinal)).ToList();
reihenfolge.ShouldAllBe(i => i > 0);
reihenfolge.ShouldBe(reihenfolge.OrderBy(i => i).ToList(), "sortiert, damit der Agent den Tag zusammensetzen kann");
sequence.ShouldAllBe(i => i > 0);
sequence.ShouldBe(sequence.OrderBy(i => i).ToList(), "sortiert, damit der Agent den Tag zusammensetzen kann");
result.Content.ShouldContain("--- shared:/wetter/stunden/berlin_2026-09-20_01-00.txt ---");
}
[Fact]
public async Task Eine_ausdrueckliche_Liste_wird_gelesen_und_Fehlendes_benannt()
public async Task An_explicit_list_is_read_and_missing_ones_named()
{
Stundendateien(2);
HourlyFiles(2);
var result = await Run("""
{"action":"read_many","workspace":"shared","paths":[
@@ -106,20 +106,20 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Zu_viele_Dateien_werden_gedeckelt_und_der_Rest_genannt()
public async Task Too_many_files_are_capped_and_the_rest_named()
{
Stundendateien(60);
HourlyFiles(60);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain($"{FileRWTool.MaxDateienJeAufruf} of 60 file(s) read");
result.Content.ShouldContain($"{FileRWTool.MaxFilesPerCall} of 60 file(s) read");
result.Content.ShouldContain("Skipped reading");
result.Content.ShouldContain("fetch with an additional call");
}
[Fact]
public async Task Ein_leeres_Verzeichnis_meldet_sich_als_Fehler()
public async Task An_empty_directory_reports_as_an_error()
{
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
@@ -128,9 +128,9 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Ohne_Leserecht_am_gemeinsamen_Bereich_kommt_nichts_heraus()
public async Task Without_read_right_on_the_shared_area_nothing_comes_out()
{
Stundendateien(2);
HourlyFiles(2);
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""", sharedLevel: "Denied");
@@ -139,7 +139,7 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Der_Ausbruch_aus_dem_Workspace_wird_auch_hier_abgewiesen()
public async Task Escaping_the_workspace_is_rejected_here_too()
{
var result = await Run("""{"action":"read_many","workspace":"shared","paths":["../../geheim.txt"]}""");
@@ -148,9 +148,9 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Eine_verbotene_Dateiendung_wird_uebergangen_statt_den_Lauf_zu_kippen()
public async Task A_forbidden_file_extension_is_skipped_instead_of_toppling_the_run()
{
Stundendateien(2);
HourlyFiles(2);
File.WriteAllText(Path.Combine(_shared, "wetter", "stunden", "skript.exe"), "MZ");
var result = await Run("""{"action":"read_many","workspace":"shared","path":"wetter/stunden"}""");
@@ -163,9 +163,9 @@ public sealed class SammelLesenTests : IDisposable
// ═══════════════════ #662: hint about the other workspace ═══════════════════
[Fact]
public async Task Liegt_die_Datei_im_anderen_Workspace_sagt_die_Meldung_das()
public async Task If_the_file_is_in_the_other_workspace_the_message_says_so()
{
Stundendateien(1);
HourlyFiles(1);
var result = await Run("""{"action":"read","path":"wetter/stunden/berlin_2026-09-20_00-00.txt"}""");
@@ -175,7 +175,7 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Auch_ein_Verzeichnis_bekommt_den_Hinweis()
public async Task A_directory_gets_the_hint_too()
{
var result = await Run("""{"action":"list","path":"wetter/stunden"}""");
@@ -184,10 +184,10 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Ohne_Recht_am_anderen_Workspace_gibt_es_keinen_Hinweis()
public async Task Without_right_on_the_other_workspace_there_is_no_hint()
{
// A hint about something the agent may not do anyway sends it into nothing.
Stundendateien(1);
HourlyFiles(1);
var result = await Run("""{"action":"read","path":"wetter/stunden/berlin_2026-09-20_00-00.txt"}""", sharedLevel: "Denied");
@@ -196,7 +196,7 @@ public sealed class SammelLesenTests : IDisposable
}
[Fact]
public async Task Gibt_es_den_Pfad_nirgends_bleibt_die_Meldung_schlicht()
public async Task If_the_path_exists_nowhere_the_message_stays_plain()
{
var result = await Run("""{"action":"read","workspace":"shared","path":"wetter/stunden/fehlt.txt"}""");
@@ -14,14 +14,14 @@ namespace ClawdDotNet.Tools.Tests.FileRW;
/// file system — finding two names in that is search work, and a cheap model
/// does it wrong.
/// </summary>
public sealed class VerzeichnisListeTests : IDisposable
public sealed class DirectoryListTests : IDisposable
{
private readonly string _basis;
private readonly string _personal;
private readonly string _shared;
private readonly FileRWTool _tool = new();
public VerzeichnisListeTests()
public DirectoryListTests()
{
_basis = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
_personal = Path.Combine(_basis, "Workspace");
@@ -49,18 +49,18 @@ public sealed class VerzeichnisListeTests : IDisposable
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
/// <summary>Hourly files, deliberately written in mixed order.</summary>
private void Stundendateien(int anzahl, string tag = "2026-09-21")
private void HourlyFiles(int count, string tag = "2026-09-21")
{
var verzeichnis = Path.Combine(_shared, "wetter", "stunden");
foreach (var stunde in Enumerable.Range(0, anzahl).OrderBy(_ => Guid.NewGuid()))
foreach (var hour in Enumerable.Range(0, count).OrderBy(_ => Guid.NewGuid()))
{
var pfad = Path.Combine(verzeichnis, $"berlin_{tag}_{stunde:00}-00.txt");
File.WriteAllText(pfad, $"{tag} {stunde:00}:00 Temperatur {10 + stunde} C");
File.SetLastWriteTime(pfad, DateTime.Parse($"{tag}T{stunde:00}:05:00"));
var path = Path.Combine(verzeichnis, $"berlin_{tag}_{hour:00}-00.txt");
File.WriteAllText(path, $"{tag} {hour:00}:00 Temperatur {10 + hour} C");
File.SetLastWriteTime(path, DateTime.Parse($"{tag}T{hour:00}:05:00"));
}
}
private static List<string> Namen(string inhalt) => inhalt
private static List<string> Names(string content) => content
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(z => z.Trim())
.Where(z => z.StartsWith("20", StringComparison.Ordinal))
@@ -70,90 +70,90 @@ public sealed class VerzeichnisListeTests : IDisposable
// ═══════════════════ Order ═══════════════════
[Fact]
public async Task Die_juengste_Datei_steht_oben()
public async Task The_newest_file_is_on_top()
{
Stundendateien(6);
HourlyFiles(6);
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("newest first");
Namen(result.Content).First().ShouldBe("berlin_2026-09-21_05-00.txt");
Namen(result.Content).Last().ShouldBe("berlin_2026-09-21_00-00.txt");
Names(result.Content).First().ShouldBe("berlin_2026-09-21_05-00.txt");
Names(result.Content).Last().ShouldBe("berlin_2026-09-21_00-00.txt");
}
[Fact]
public async Task Nach_Namen_sortiert_laeuft_aufsteigend()
public async Task Sorted_by_name_runs_ascending()
{
Stundendateien(4);
HourlyFiles(4);
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","sort":"name"}""");
Namen(result.Content).ShouldBe([
Names(result.Content).ShouldBe([
"berlin_2026-09-21_00-00.txt", "berlin_2026-09-21_01-00.txt",
"berlin_2026-09-21_02-00.txt", "berlin_2026-09-21_03-00.txt"
]);
}
[Fact]
public async Task Die_Richtung_laesst_sich_umkehren()
public async Task The_direction_can_be_reversed()
{
Stundendateien(4);
HourlyFiles(4);
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","sort":"modified","order":"asc"}""");
result.Content.ShouldContain("oldest first");
Namen(result.Content).First().ShouldBe("berlin_2026-09-21_00-00.txt");
Names(result.Content).First().ShouldBe("berlin_2026-09-21_00-00.txt");
}
// ═══════════════════ Cap and total count ═══════════════════
[Fact]
public async Task Der_Kopf_nennt_immer_die_Gesamtzahl()
public async Task The_header_always_names_the_total()
{
Stundendateien(24);
Stundendateien(24, "2026-09-20");
Stundendateien(24, "2026-09-19");
HourlyFiles(24);
HourlyFiles(24, "2026-09-20");
HourlyFiles(24, "2026-09-19");
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
result.Content.ShouldContain($"{FileRWTool.StandardEintraege} of 72 entries");
result.Content.ShouldContain($"{FileRWTool.DefaultEntries} of 72 entries");
result.Content.ShouldContain("22 more not shown");
result.Content.ShouldContain("'pattern'", Case.Sensitive, "der Agent soll wissen, wie er gezielt fragt");
Namen(result.Content).Count.ShouldBe(FileRWTool.StandardEintraege);
Names(result.Content).Count.ShouldBe(FileRWTool.DefaultEntries);
}
[Fact]
public async Task Ein_eigenes_Limit_wird_beachtet_und_gedeckelt()
public async Task A_custom_limit_is_respected_and_capped()
{
Stundendateien(10);
HourlyFiles(10);
var wenig = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":3}""");
Namen(wenig.Content).Count.ShouldBe(3);
wenig.Content.ShouldContain("3 of 10 entries");
var little = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":3}""");
Names(little.Content).Count.ShouldBe(3);
little.Content.ShouldContain("3 of 10 entries");
var zuviel = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":99999}""");
Namen(zuviel.Content).Count.ShouldBe(10, "mehr als da ist gibt es nicht");
var tooMuch = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","limit":99999}""");
Names(tooMuch.Content).Count.ShouldBe(10, "mehr als da ist gibt es nicht");
}
// ═══════════════════ Asking specifically ═══════════════════
[Fact]
public async Task Ein_Muster_beantwortet_die_Frage_nach_einer_bestimmten_Datei()
public async Task A_pattern_answers_the_question_for_a_specific_file()
{
Stundendateien(24);
Stundendateien(24, "2026-09-20");
HourlyFiles(24);
HourlyFiles(24, "2026-09-20");
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden","pattern":"berlin_2026-09-21_1?-00.txt"}""");
result.Success.ShouldBeTrue(result.ErrorMessage);
result.Content.ShouldContain("10 of 10 entries");
result.Content.ShouldContain("Pattern:");
Namen(result.Content).ShouldAllBe(n => n.StartsWith("berlin_2026-09-21_1"));
Names(result.Content).ShouldAllBe(n => n.StartsWith("berlin_2026-09-21_1"));
}
[Fact]
public async Task Ein_leeres_Verzeichnis_ist_kein_Fehler_sondern_eine_Null()
public async Task An_empty_directory_is_not_an_error_but_a_zero()
{
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
@@ -164,9 +164,9 @@ public sealed class VerzeichnisListeTests : IDisposable
// ═══════════════════ Form ═══════════════════
[Fact]
public async Task Jeder_Eintrag_steht_auf_genau_einer_Zeile()
public async Task Every_entry_is_on_exactly_one_line()
{
Stundendateien(3);
HourlyFiles(3);
Directory.CreateDirectory(Path.Combine(_shared, "wetter", "stunden", "alt"));
var result = await Run("""{"action":"list","workspace":"shared","path":"wetter/stunden"}""");
@@ -174,7 +174,7 @@ public sealed class VerzeichnisListeTests : IDisposable
result.Content.ShouldNotContain("\"Name\":", Case.Sensitive, "kein eingeruecktes JSON mehr");
result.Content.ShouldContain("alt/", Case.Sensitive);
result.Content.ShouldContain("<DIR>", Case.Sensitive);
Namen(result.Content).Count.ShouldBe(4);
Names(result.Content).Count.ShouldBe(4);
}
// ═══════════════════ The pattern is not a second path ═══════════════════
@@ -182,11 +182,11 @@ public sealed class VerzeichnisListeTests : IDisposable
[Theory]
[InlineData("list")]
[InlineData("read_many")]
public async Task Ein_Muster_darf_nicht_aus_dem_Verzeichnis_herausfuehren(string action)
public async Task A_pattern_must_not_lead_out_of_the_directory(string action)
{
// Directory.GetFiles evaluates separators in the pattern: "../*" lists the
// parent directory without the path check ever being asked.
Stundendateien(2);
HourlyFiles(2);
File.WriteAllText(Path.Combine(_shared, "wetter", "geheim.txt"), "nicht fuer den Agenten");
var result = await Run($$"""{"action":"{{action}}","workspace":"shared","path":"wetter/stunden","pattern":"../*"}""");
@@ -200,12 +200,12 @@ public sealed class VerzeichnisListeTests : IDisposable
[InlineData("unter/*.txt")]
[InlineData("..\\*")]
[InlineData("*/../*")]
public async Task Auch_die_anderen_Pfadschreibweisen_werden_abgewiesen(string muster)
public async Task The_other_path_spellings_are_rejected_too(string pattern)
{
Stundendateien(2);
HourlyFiles(2);
var input = JsonSerializer.SerializeToElement(
new { action = "list", workspace = "shared", path = "wetter/stunden", pattern = muster });
new { action = "list", workspace = "shared", path = "wetter/stunden", pattern = pattern });
var result = await _tool.ExecuteAsync(input, Context(), CancellationToken.None);
result.Success.ShouldBeFalse();
@@ -13,12 +13,12 @@ namespace ClawdDotNet.Tools.Tests.FileRW;
/// The file system is not abstracted here either — this is precisely about what is on
/// disk after a call was interrupted.
/// </summary>
public sealed class NachschauTests : IDisposable
public sealed class FollowUpCheckTests : IDisposable
{
private readonly string _root;
private readonly FileRWTool _tool = new();
public NachschauTests()
public FollowUpCheckTests()
{
_root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace");
Directory.CreateDirectory(_root);
@@ -47,7 +47,7 @@ public sealed class NachschauTests : IDisposable
// ═══════════════════ #553: writing atomically ═══════════════════
[Fact]
public async Task Schreiben_hinterlaesst_keine_Nebendateien()
public async Task Writing_leaves_no_side_files()
{
var result = await Run("""{"action":"write","path":"a.txt","content":"Inhalt"}""");
result.Success.ShouldBeTrue();
@@ -58,7 +58,7 @@ public sealed class NachschauTests : IDisposable
}
[Fact]
public async Task Ueberschreiben_ersetzt_den_Inhalt_vollstaendig()
public async Task Overwriting_replaces_the_content_completely()
{
await Run("""{"action":"write","path":"a.txt","content":"ein sehr langer alter Inhalt"}""");
await Run("""{"action":"write","path":"a.txt","content":"kurz"}""");
@@ -70,70 +70,70 @@ public sealed class NachschauTests : IDisposable
// ═══════════════════ #555: follow-up check for write ═══════════════════
[Fact]
public async Task Fehlende_Datei_heisst_nichts_geschehen()
public async Task Missing_file_means_nothing_happened()
{
var befund = await Probe("""{"action":"write","path":"nie.txt","content":"x"}""");
var finding = await Probe("""{"action":"write","path":"nie.txt","content":"x"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
[Fact]
public async Task Der_beabsichtigte_Inhalt_heisst_durchgekommen()
public async Task The_intended_content_means_through()
{
await Run("""{"action":"write","path":"a.txt","content":"fertig"}""");
var befund = await Probe("""{"action":"write","path":"a.txt","content":"fertig"}""");
var finding = await Probe("""{"action":"write","path":"a.txt","content":"fertig"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Complete);
finding.Outcome.ShouldBe(ProbeOutcome.Complete);
}
[Fact]
public async Task Der_alte_Inhalt_heisst_nicht_durchgekommen()
public async Task The_old_content_means_not_through()
{
await Run("""{"action":"write","path":"a.txt","content":"alt"}""");
// This is what it would look like if the crash came before the replacement.
var befund = await Probe("""{"action":"write","path":"a.txt","content":"neu"}""");
var finding = await Probe("""{"action":"write","path":"a.txt","content":"neu"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
// ═══════════════════ #555: follow-up check for append ═══════════════════
[Fact]
public async Task Vollstaendig_angehaengter_Text_wird_erkannt()
public async Task Fully_appended_text_is_recognised()
{
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
await Run("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
var befund = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
var finding = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Complete);
finding.Outcome.ShouldBe(ProbeOutcome.Complete);
}
[Fact]
public async Task Ein_halb_angehaengter_Text_wird_als_Riss_erkannt()
public async Task A_half_appended_text_is_recognised_as_a_tear()
{
// Appending is not atomic — exactly this state can really arise.
// Here it is produced by hand, because a real tear cannot be reproduced.
var pfad = Path.Combine(_root, "log.txt");
await File.WriteAllTextAsync(pfad, "Zeile 1\nZeile 2 ist nur halb ang");
var path = Path.Combine(_root, "log.txt");
await File.WriteAllTextAsync(path, "Zeile 1\nZeile 2 ist nur halb ang");
var befund = await Probe(
var finding = await Probe(
"""{"action":"append","path":"log.txt","content":"Zeile 2 ist nur halb angekommen\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Partial);
befund.Evidence.ShouldContain("of");
finding.Outcome.ShouldBe(ProbeOutcome.Partial);
finding.Evidence.ShouldContain("of");
}
[Fact]
public async Task Fehlt_der_Text_ganz_gilt_der_Aufruf_als_nicht_geschehen()
public async Task If_the_text_is_missing_entirely_the_call_counts_as_not_happened()
{
await Run("""{"action":"write","path":"log.txt","content":"Zeile 1\n"}""");
var befund = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
var finding = await Probe("""{"action":"append","path":"log.txt","content":"Zeile 2\n"}""");
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
finding.Outcome.ShouldBe(ProbeOutcome.NotStarted);
}
// ═══════════════════ The limits of the follow-up check ═══════════════════
@@ -142,21 +142,21 @@ public sealed class NachschauTests : IDisposable
[InlineData("read")]
[InlineData("delete")]
[InlineData("list")]
public async Task Fuer_andere_Aktionen_wird_nichts_behauptet(string aktion)
public async Task Nothing_is_claimed_for_other_actions(string action)
{
var befund = await Probe($$"""{"action":"{{aktion}}","path":"a.txt"}""");
var finding = await Probe($$"""{"action":"{{action}}","path":"a.txt"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Unknown);
finding.Outcome.ShouldBe(ProbeOutcome.Unknown);
}
[Fact]
public async Task Ein_unbrauchbarer_Pfad_ergibt_Unbekannt_statt_einer_Ausnahme()
public async Task An_unusable_path_gives_unknown_instead_of_an_exception()
{
// The follow-up check must never throw: a failure of the follow-up check is a statement
// about the follow-up check, not about the world.
var befund = await Probe("""{"action":"write","path":"../../ausserhalb.txt","content":"x"}""");
var finding = await Probe("""{"action":"write","path":"../../ausserhalb.txt","content":"x"}""");
befund.Outcome.ShouldBe(ProbeOutcome.Unknown);
finding.Outcome.ShouldBe(ProbeOutcome.Unknown);
}
}
@@ -57,7 +57,7 @@ public sealed class ReversibleDeleteTests : IDisposable
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), context ?? Context(), CancellationToken.None);
private string? KeyOf(object input, AgentToolContext? context = null)
=> _tool.StagingAktion(JsonSerializer.SerializeToElement(input), context ?? Context());
=> _tool.StagingAction(JsonSerializer.SerializeToElement(input), context ?? Context());
private void Put(string relative, string content = "x")
{
@@ -45,7 +45,7 @@ public sealed class WorkspacePathTests : IDisposable
[InlineData("unterordner/../../../ausserhalb.txt")]
[InlineData("./../../evil.txt")]
[InlineData("..")]
public void Relative_Ausbrueche_werden_abgelehnt(string path)
public void Relative_escapes_are_rejected(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
@@ -68,7 +68,7 @@ public sealed class WorkspacePathTests : IDisposable
[InlineData(@"\\server\share\evil.txt")]
[InlineData("//server/share/evil.txt")]
[InlineData(@"C:\temp\datei.txt")]
public void Absolute_Pfade_und_UNC_Freigaben_werden_abgelehnt(string path)
public void Absolute_paths_and_UNC_shares_are_rejected(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
@@ -82,7 +82,7 @@ public sealed class WorkspacePathTests : IDisposable
[InlineData("/etc/passwd")]
[InlineData("/root/.ssh/id_rsa")]
[InlineData("../../../../etc/shadow")]
public void Unix_Systempfade_werden_abgelehnt(string path)
public void Unix_system_paths_are_rejected(string path)
{
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
}
@@ -90,7 +90,7 @@ public sealed class WorkspacePathTests : IDisposable
[Theory]
[InlineData("datei.txt:versteckt")]
[InlineData("datei.txt:$DATA")]
public void Alternate_Data_Streams_werden_abgelehnt(string path)
public void Alternate_data_streams_are_rejected(string path)
{
// An ADS would otherwise bypass the extension check: "x.txt:evil.exe".
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
@@ -100,7 +100,7 @@ public sealed class WorkspacePathTests : IDisposable
/// The actual core of S6: a neighboring directory with the same prefix.
/// </summary>
[Fact]
public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_gilt_als_ausserhalb()
public void A_sibling_directory_with_the_same_prefix_counts_as_outside()
{
var backup = _root + "-Backup";
@@ -108,7 +108,7 @@ public sealed class WorkspacePathTests : IDisposable
}
[Fact]
public void Auch_bei_Rootangabe_mit_Trenner_bleibt_das_Nachbarverzeichnis_aussen()
public void Even_with_root_given_with_separator_the_sibling_directory_stays_outside()
{
var rootWithSeparator = _root + Path.DirectorySeparatorChar;
@@ -126,7 +126,7 @@ public sealed class WorkspacePathTests : IDisposable
[InlineData("a/b/c/tief.json")]
[InlineData("./bericht.md")]
[InlineData("unterordner/../bericht.md")]
public void Pfade_innerhalb_des_Workspace_werden_aufgeloest(string path)
public void Paths_inside_the_workspace_are_resolved(string path)
{
var resolved = Resolve(path);
@@ -138,7 +138,7 @@ public sealed class WorkspacePathTests : IDisposable
[InlineData("")]
[InlineData(" ")]
[InlineData(".")]
public void Leere_Angaben_ergeben_das_Wurzelverzeichnis(string? path)
public void Empty_values_give_the_root_directory(string? path)
{
var resolved = Resolve(path);
@@ -147,13 +147,13 @@ public sealed class WorkspacePathTests : IDisposable
}
[Fact]
public void Das_Wurzelverzeichnis_selbst_gilt_als_innerhalb()
public void The_root_directory_itself_counts_as_inside()
{
WorkspacePath.IsInside(_root, _root).ShouldBeTrue();
}
[Fact]
public void Ein_Unterverzeichnis_mit_aehnlichem_Namen_bleibt_innerhalb()
public void A_subdirectory_with_a_similar_name_stays_inside()
{
// Counter-check to the prefix rule: within the root everything is allowed.
var inner = Path.Combine(_root, "Workspace-Backup", "x.txt");