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
+31
-31
@@ -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"}""");
|
||||
|
||||
+44
-44
@@ -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();
|
||||
+31
-31
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user