Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
da4ceac7d5
commit
837a634a22
@@ -9,8 +9,8 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Vorpruefung (#579), Freigabeschluessel (#609) und Rechte (#641) des Database-Werkzeugs -
|
||||
/// alles ohne Datenbankverbindung pruefbar, weil es nur Konfiguration und Statement ansieht.
|
||||
/// Pre-check (#579), approval key (#609) and permissions (#641) of the Database tool -
|
||||
/// all checkable without a database connection, because it only looks at configuration and statement.
|
||||
/// </summary>
|
||||
public sealed class DatabaseVorpruefungTests
|
||||
{
|
||||
@@ -48,8 +48,8 @@ public sealed class DatabaseVorpruefungTests
|
||||
[InlineData("DESCRIBE messwerte", "ReadOnly")]
|
||||
public void Die_Vorpruefung_urteilt_nicht_mehr_ueber_die_Operation(string sql, string level)
|
||||
{
|
||||
// #641: Ob ein INSERT eines lesenden Agenten laeuft, vorgelegt oder verboten wird,
|
||||
// entscheidet die Freigabestelle - nicht mehr eine stille Ablehnung im Werkzeug.
|
||||
// #641: Whether an INSERT of a reading agent runs, is presented or is forbidden
|
||||
// is decided by the approval point - no longer by a silent refusal in the tool.
|
||||
Tool.Preflight(Sql(sql), Kontext(level, "messwerte")).ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ public sealed class DatabaseVorpruefungTests
|
||||
[Fact]
|
||||
public async Task Ohne_Freigabestelle_bleibt_die_Zugriffsstufe_eine_harte_Grenze()
|
||||
{
|
||||
// Kein Gate, keine Pruefung: dann muss das Werkzeug selbst verweigern, bevor es
|
||||
// ueberhaupt eine Verbindung aufbaut.
|
||||
// No gate, no check: then the tool itself has to refuse before it
|
||||
// even establishes a connection.
|
||||
var ergebnis = await Tool.ExecuteAsync(
|
||||
Sql("INSERT INTO messwerte (ort) VALUES ('Berlin')"), Kontext("ReadOnly", "messwerte"), default);
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.Database;
|
||||
|
||||
/// <summary>
|
||||
/// S1 aus der Bestandsaufnahme: Die Tabellen-Whitelist suchte den erlaubten Namen als
|
||||
/// Teilzeichenkette irgendwo im Statement — auch in einem Kommentar. Bei
|
||||
/// allowedTables ["prices"] genügte deshalb "DELETE FROM users -- prices", um eine
|
||||
/// beliebige andere Tabelle zu löschen.
|
||||
/// S1 from the inventory: the table whitelist looked for the allowed name as a
|
||||
/// substring anywhere in the statement — including in a comment. With
|
||||
/// allowedTables ["prices"] therefore "DELETE FROM users -- prices" was enough to delete
|
||||
/// any other table.
|
||||
///
|
||||
/// Die Gegenproben sind genauso wichtig wie die Angriffe: Ein zu strenger Filter, der
|
||||
/// legitime Abfragen blockiert, wäre im Betrieb genauso unbrauchbar.
|
||||
/// The counter-checks are just as important as the attacks: a filter that is too strict and
|
||||
/// blocks legitimate queries would be just as useless in operation.
|
||||
/// </summary>
|
||||
public sealed class SqlGuardTests
|
||||
{
|
||||
@@ -19,10 +19,10 @@ public sealed class SqlGuardTests
|
||||
private static SqlInspection Inspect(string sql) => SqlGuard.Inspect(sql, Allowed);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Angriffe — alle müssen abgelehnt werden
|
||||
// Attacks — all must be rejected
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// ─── #610: wiederholbares Anlegen und Strukturabfragen ───
|
||||
// ─── #610: repeatable creation and structure queries ───
|
||||
|
||||
[Theory]
|
||||
[InlineData("CREATE TABLE IF NOT EXISTS prices (id INT)", SqlOperation.Schema)]
|
||||
@@ -86,7 +86,7 @@ public sealed class SqlGuardTests
|
||||
[InlineData("INSERT INTO users SELECT * FROM prices")]
|
||||
public void Ein_einziger_unerlaubter_Join_reicht_zur_Ablehnung(string sql)
|
||||
{
|
||||
// Entscheidend: JEDE referenzierte Tabelle muss erlaubt sein, nicht irgendeine.
|
||||
// Crucial: EVERY referenced table must be allowed, not just any one.
|
||||
var result = Inspect(sql);
|
||||
|
||||
result.IsValid.ShouldBeFalse();
|
||||
@@ -109,7 +109,7 @@ public sealed class SqlGuardTests
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Gegenproben — legitime Abfragen müssen durchgehen
|
||||
// Counter-checks — legitimate queries must get through
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
@@ -132,8 +132,8 @@ public sealed class SqlGuardTests
|
||||
[InlineData("SELECT * FROM prices WHERE aktion = 'drop'")]
|
||||
public void Harmlose_Schluesselwoerter_in_Werten_gelten_nicht_als_Schreibzugriff(string sql)
|
||||
{
|
||||
// Der alte Substring-Test stufte diese Abfragen fälschlich als Schreib- bzw.
|
||||
// Strukturzugriff ein und blockierte sie für ReadOnly-Agenten.
|
||||
// The old substring test wrongly classified these queries as write or
|
||||
// structure access and blocked them for read-only agents.
|
||||
var result = Inspect(sql);
|
||||
|
||||
result.IsValid.ShouldBeTrue(result.Error);
|
||||
|
||||
@@ -4,9 +4,9 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.DirectAPI;
|
||||
|
||||
/// <summary>
|
||||
/// S3 aus der Bestandsaufnahme: Die abgerufene URL ging als "source" an das Modell —
|
||||
/// inklusive apikey= im Query-String. Der Schlüssel landete damit im Kontext, wurde
|
||||
/// bei jedem Folgeschritt erneut gesendet und in ChatContext.json geschrieben.
|
||||
/// S3 from the inventory: the fetched URL went to the model as "source" —
|
||||
/// including apikey= in the query string. The key thus ended up in the context, was
|
||||
/// sent again at every following step and written into ChatContext.json.
|
||||
/// </summary>
|
||||
public sealed class UrlSanitizerTests
|
||||
{
|
||||
@@ -87,7 +87,7 @@ public sealed class UrlSanitizerTests
|
||||
[Fact]
|
||||
public void Ein_Schluessel_als_Teil_eines_anderen_Namens_wird_nicht_faelschlich_maskiert()
|
||||
{
|
||||
// "monkey" enthält "key", ist aber kein sensibler Parameter.
|
||||
// "monkey" contains "key", but is not a sensitive parameter.
|
||||
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?monkey=banane");
|
||||
|
||||
sanitized.ShouldBe("https://x.de/a?monkey=banane");
|
||||
|
||||
@@ -8,10 +8,10 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.FileRW;
|
||||
|
||||
/// <summary>
|
||||
/// Die Nachschau nach einem Absturz (#555) und das atomare Schreiben (#553).
|
||||
/// The follow-up check after a crash (#555) and atomic writing (#553).
|
||||
///
|
||||
/// Das Dateisystem wird auch hier nicht abstrahiert — es geht gerade darum, was auf der
|
||||
/// Platte steht, nachdem ein Aufruf unterbrochen wurde.
|
||||
/// 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
|
||||
{
|
||||
@@ -27,7 +27,7 @@ public sealed class NachschauTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
@@ -44,7 +44,7 @@ public sealed class NachschauTests : IDisposable
|
||||
private Task<ToolResult> Run(string json)
|
||||
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
// ═══════════════════ #553: atomar schreiben ═══════════════════
|
||||
// ═══════════════════ #553: writing atomically ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Schreiben_hinterlaesst_keine_Nebendateien()
|
||||
@@ -52,7 +52,7 @@ public sealed class NachschauTests : IDisposable
|
||||
var result = await Run("""{"action":"write","path":"a.txt","content":"Inhalt"}""");
|
||||
result.Success.ShouldBeTrue();
|
||||
|
||||
// Das atomare Schreiben geht über eine Nebendatei — die muss danach weg sein.
|
||||
// Atomic writing goes via a side file — it must be gone afterwards.
|
||||
Directory.GetFiles(_root).ShouldHaveSingleItem();
|
||||
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("Inhalt");
|
||||
}
|
||||
@@ -63,11 +63,11 @@ public sealed class NachschauTests : IDisposable
|
||||
await Run("""{"action":"write","path":"a.txt","content":"ein sehr langer alter Inhalt"}""");
|
||||
await Run("""{"action":"write","path":"a.txt","content":"kurz"}""");
|
||||
|
||||
// Beim Ersetzen darf kein Rest der alten, längeren Fassung stehenbleiben.
|
||||
// When replacing, no remnant of the old, longer version may remain.
|
||||
(await File.ReadAllTextAsync(Path.Combine(_root, "a.txt"))).ShouldBe("kurz");
|
||||
}
|
||||
|
||||
// ═══════════════════ #555: Nachschau bei write ═══════════════════
|
||||
// ═══════════════════ #555: follow-up check for write ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Fehlende_Datei_heisst_nichts_geschehen()
|
||||
@@ -92,13 +92,13 @@ public sealed class NachschauTests : IDisposable
|
||||
{
|
||||
await Run("""{"action":"write","path":"a.txt","content":"alt"}""");
|
||||
|
||||
// So sähe es aus, wenn der Absturz vor dem Ersetzen kam.
|
||||
// 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"}""");
|
||||
|
||||
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
|
||||
}
|
||||
|
||||
// ═══════════════════ #555: Nachschau bei append ═══════════════════
|
||||
// ═══════════════════ #555: follow-up check for append ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Vollstaendig_angehaengter_Text_wird_erkannt()
|
||||
@@ -114,8 +114,8 @@ public sealed class NachschauTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Ein_halb_angehaengter_Text_wird_als_Riss_erkannt()
|
||||
{
|
||||
// Anhängen ist nicht atomar — genau dieser Zustand kann wirklich entstehen.
|
||||
// Hier wird er von Hand hergestellt, weil ein echter Riss nicht reproduzierbar ist.
|
||||
// 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");
|
||||
|
||||
@@ -136,7 +136,7 @@ public sealed class NachschauTests : IDisposable
|
||||
befund.Outcome.ShouldBe(ProbeOutcome.NotStarted);
|
||||
}
|
||||
|
||||
// ═══════════════════ Die Grenzen der Nachschau ═══════════════════
|
||||
// ═══════════════════ The limits of the follow-up check ═══════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData("read")]
|
||||
@@ -152,8 +152,8 @@ public sealed class NachschauTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Ein_unbrauchbarer_Pfad_ergibt_Unbekannt_statt_einer_Ausnahme()
|
||||
{
|
||||
// Die Nachschau darf nie werfen: Ein Fehlschlag der Nachschau ist eine Aussage
|
||||
// über die Nachschau, nicht über die Welt.
|
||||
// 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"}""");
|
||||
|
||||
befund.Outcome.ShouldBe(ProbeOutcome.Unknown);
|
||||
|
||||
@@ -8,11 +8,11 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.FileRW;
|
||||
|
||||
/// <summary>
|
||||
/// Sammel-Lesen (#663) und der Hinweis auf den anderen Workspace (#662).
|
||||
/// Batch reading (#663) and the hint about the other workspace (#662).
|
||||
///
|
||||
/// Anlass aus dem Testbetrieb: Der Tagesabschluss las 24 Stundendateien einzeln und starb
|
||||
/// an der Schrittgrenze, und derselbe Agent rief Dateien wiederholt im falschen Workspace
|
||||
/// ab, weil die Fehlermeldung nicht sagte, wo sie liegen.
|
||||
/// Occasion from test operation: the daily close read 24 hourly files individually and died
|
||||
/// 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
|
||||
{
|
||||
@@ -33,7 +33,7 @@ public sealed class SammelLesenTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_basis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context(string sharedLevel = "ReadWrite") => new(
|
||||
@@ -58,7 +58,7 @@ public sealed class SammelLesenTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════ #663: ein Aufruf statt 24 ═══════════════════
|
||||
// ═══════════════════ #663: one call instead of 24 ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_Verzeichnis_mit_Muster_kommt_in_einem_Aufruf()
|
||||
@@ -160,7 +160,7 @@ public sealed class SammelLesenTests : IDisposable
|
||||
result.Content.ShouldNotContain("skript.exe");
|
||||
}
|
||||
|
||||
// ═══════════════════ #662: Hinweis auf den anderen Workspace ═══════════════════
|
||||
// ═══════════════════ #662: hint about the other workspace ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Liegt_die_Datei_im_anderen_Workspace_sagt_die_Meldung_das()
|
||||
@@ -186,7 +186,7 @@ public sealed class SammelLesenTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Ohne_Recht_am_anderen_Workspace_gibt_es_keinen_Hinweis()
|
||||
{
|
||||
// Ein Hinweis auf etwas, das der Agent ohnehin nicht darf, schickt ihn ins Leere.
|
||||
// A hint about something the agent may not do anyway sends it into nothing.
|
||||
Stundendateien(1);
|
||||
|
||||
var result = await Run("""{"action":"read","path":"wetter/stunden/berlin_2026-09-20_00-00.txt"}""", sharedLevel: "Denied");
|
||||
|
||||
@@ -7,12 +7,12 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.FileRW;
|
||||
|
||||
/// <summary>
|
||||
/// Sortiertes, gedeckeltes Auflisten (#673).
|
||||
/// Sorted, capped listing (#673).
|
||||
///
|
||||
/// Anlass aus dem Testbetrieb: Die Betriebskontrolle meldete zwei Stundendateien als
|
||||
/// fehlend, die es gab. Das Verzeichnis lieferte 105 Eintraege in der Reihenfolge des
|
||||
/// Dateisystems — darin zwei Namen zu finden ist Sucharbeit, und ein guenstiges Modell
|
||||
/// erledigt sie falsch.
|
||||
/// Occasion from test operation: the operations check reported two hourly files as
|
||||
/// missing that existed. The directory delivered 105 entries in the order of the
|
||||
/// file system — finding two names in that is search work, and a cheap model
|
||||
/// does it wrong.
|
||||
/// </summary>
|
||||
public sealed class VerzeichnisListeTests : IDisposable
|
||||
{
|
||||
@@ -33,7 +33,7 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_basis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
@@ -48,7 +48,7 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
private Task<ToolResult> Run(string json)
|
||||
=> _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, Context(), CancellationToken.None);
|
||||
|
||||
/// <summary>Stundendateien, absichtlich in gemischter Reihenfolge geschrieben.</summary>
|
||||
/// <summary>Hourly files, deliberately written in mixed order.</summary>
|
||||
private void Stundendateien(int anzahl, string tag = "2026-09-21")
|
||||
{
|
||||
var verzeichnis = Path.Combine(_shared, "wetter", "stunden");
|
||||
@@ -67,7 +67,7 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
.Select(z => z.Split(" ", StringSplitOptions.RemoveEmptyEntries).Last())
|
||||
.ToList();
|
||||
|
||||
// ═══════════════════ Reihenfolge ═══════════════════
|
||||
// ═══════════════════ Order ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Die_juengste_Datei_steht_oben()
|
||||
@@ -106,7 +106,7 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
Namen(result.Content).First().ShouldBe("berlin_2026-09-21_00-00.txt");
|
||||
}
|
||||
|
||||
// ═══════════════════ Deckel und Gesamtzahl ═══════════════════
|
||||
// ═══════════════════ Cap and total count ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Der_Kopf_nennt_immer_die_Gesamtzahl()
|
||||
@@ -136,7 +136,7 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
Namen(zuviel.Content).Count.ShouldBe(10, "mehr als da ist gibt es nicht");
|
||||
}
|
||||
|
||||
// ═══════════════════ Gezielt fragen ═══════════════════
|
||||
// ═══════════════════ Asking specifically ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public async Task Ein_Muster_beantwortet_die_Frage_nach_einer_bestimmten_Datei()
|
||||
@@ -177,15 +177,15 @@ public sealed class VerzeichnisListeTests : IDisposable
|
||||
Namen(result.Content).Count.ShouldBe(4);
|
||||
}
|
||||
|
||||
// ═══════════════════ Das Muster ist kein zweiter Pfad ═══════════════════
|
||||
// ═══════════════════ The pattern is not a second path ═══════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData("list")]
|
||||
[InlineData("read_many")]
|
||||
public async Task Ein_Muster_darf_nicht_aus_dem_Verzeichnis_herausfuehren(string action)
|
||||
{
|
||||
// Directory.GetFiles wertet Trennzeichen im Muster aus: "../*" listet das
|
||||
// Elternverzeichnis, ohne dass die Pfadpruefung je gefragt wurde.
|
||||
// Directory.GetFiles evaluates separators in the pattern: "../*" lists the
|
||||
// parent directory without the path check ever being asked.
|
||||
Stundendateien(2);
|
||||
File.WriteAllText(Path.Combine(_shared, "wetter", "geheim.txt"), "nicht fuer den Agenten");
|
||||
|
||||
|
||||
@@ -4,18 +4,18 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.FileRW;
|
||||
|
||||
/// <summary>
|
||||
/// S6 aus der Bestandsaufnahme: Die Pfadprüfung verglich nur Zeichenketten-Präfixe.
|
||||
/// Ohne abschließenden Verzeichnistrenner erlaubte ein Root "…\Workspace" damit auch
|
||||
/// S6 from the inventory: the path check only compared string prefixes.
|
||||
/// Without a trailing directory separator a root "…\Workspace" thus also allowed
|
||||
/// "…\Workspace-Backup\…".
|
||||
///
|
||||
/// Das Dateisystem wird hier bewusst NICHT abstrahiert — die Tests sollen die echte
|
||||
/// Pfadsemantik der jeweiligen Plattform prüfen (.., UNC, Alternate Data Streams,
|
||||
/// abschließende Punkte, unter Linux zusätzlich Groß-/Kleinschreibung und
|
||||
/// Verknüpfungen). Eine Abstraktion würde genau die Fehlerklasse verstecken, um die es
|
||||
/// geht.
|
||||
/// The file system is deliberately NOT abstracted here — the tests are supposed to check the real
|
||||
/// path semantics of the respective platform (.., UNC, alternate data streams,
|
||||
/// trailing dots, under Linux additionally upper/lower case and
|
||||
/// links). An abstraction would hide exactly the class of errors this is
|
||||
/// about.
|
||||
///
|
||||
/// Die Verzeichnisgrenze selbst prüft <c>PathBoundaryTests</c> in Core.Tests; hier geht
|
||||
/// es um das, was <see cref="WorkspacePath.Resolve"/> darüber hinaus abweist.
|
||||
/// The directory boundary itself is checked by <c>PathBoundaryTests</c> in Core.Tests; here it
|
||||
/// is about what <see cref="WorkspacePath.Resolve"/> rejects beyond that.
|
||||
/// </summary>
|
||||
public sealed class WorkspacePathTests : IDisposable
|
||||
{
|
||||
@@ -30,13 +30,13 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(Path.GetDirectoryName(_root)!, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private string Resolve(string? relative) => WorkspacePath.Resolve(_root, relative, "personal");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Ausbruchsversuche
|
||||
// Breakout attempts
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
@@ -51,17 +51,17 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die Windows-Formen müssen auch unter Linux abgewiesen werden — dort greift
|
||||
/// aber eine andere Regel: <c>Path.IsPathRooted(@"C:\temp\x")</c> ist unter Linux
|
||||
/// <c>false</c>, weil das schlicht ein Dateiname mit Doppelpunkt ist. Gefangen
|
||||
/// werden diese Fälle von der Zeichenregel: Doppelpunkt und Backslash sind im
|
||||
/// Pfad unzulässig, getrennt wird mit <c>/</c>. Das Ergebnis ist auf beiden
|
||||
/// Plattformen dasselbe, der Weg dorthin nicht.
|
||||
/// The Windows forms must be rejected under Linux too — but there a different
|
||||
/// rule applies: <c>Path.IsPathRooted(@"C:\temp\x")</c> is <c>false</c> under Linux,
|
||||
/// because that is simply a file name with a colon. These cases are caught
|
||||
/// by the character rule: colon and backslash are not permitted in the
|
||||
/// path, separation is done with <c>/</c>. The result is the same on both
|
||||
/// platforms, the way there is not.
|
||||
///
|
||||
/// Dieselbe Regel fängt unter Linux auch <c>@"..\..\evil.txt"</c> aus dem Test
|
||||
/// darüber. Ein Ausbruch war das dort nie — der Backslash trennt unter Linux
|
||||
/// nichts, die Datei wäre im Workspace gelandet —, aber der Test erwartete eine
|
||||
/// Abweisung und bekam keine.
|
||||
/// The same rule also catches <c>@"..\..\evil.txt"</c> from the test
|
||||
/// above under Linux. That was never a breakout there — the backslash separates
|
||||
/// nothing under Linux, the file would have landed in the workspace —, but the test expected a
|
||||
/// rejection and got none.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(@"C:\Windows\System32\config\SAM")]
|
||||
@@ -74,9 +74,9 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die Linux-Entsprechungen. Unter Windows sind das gewöhnliche relative Pfade,
|
||||
/// die entweder ins Leere zeigen oder von der <c>..</c>-Regel gefangen werden —
|
||||
/// deshalb laufen sie dort mit, statt übersprungen zu werden.
|
||||
/// The Linux equivalents. Under Windows these are ordinary relative paths
|
||||
/// that either point nowhere or are caught by the <c>..</c> rule —
|
||||
/// that's why they run along there instead of being skipped.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("/etc/passwd")]
|
||||
@@ -92,12 +92,12 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
[InlineData("datei.txt:$DATA")]
|
||||
public void Alternate_Data_Streams_werden_abgelehnt(string path)
|
||||
{
|
||||
// Ein ADS umgeht sonst die Endungsprüfung: "x.txt:evil.exe".
|
||||
// An ADS would otherwise bypass the extension check: "x.txt:evil.exe".
|
||||
Should.Throw<UnauthorizedAccessException>(() => Resolve(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der eigentliche Kern von S6: ein Nachbarverzeichnis mit gleichem Präfix.
|
||||
/// The actual core of S6: a neighboring directory with the same prefix.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_gilt_als_ausserhalb()
|
||||
@@ -117,7 +117,7 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Gegenproben — normale Nutzung muss funktionieren
|
||||
// Counter-checks — normal use must work
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
@@ -155,7 +155,7 @@ public sealed class WorkspacePathTests : IDisposable
|
||||
[Fact]
|
||||
public void Ein_Unterverzeichnis_mit_aehnlichem_Namen_bleibt_innerhalb()
|
||||
{
|
||||
// Gegenprobe zur Präfix-Regel: Innerhalb des Roots ist alles erlaubt.
|
||||
// Counter-check to the prefix rule: within the root everything is allowed.
|
||||
var inner = Path.Combine(_root, "Workspace-Backup", "x.txt");
|
||||
|
||||
WorkspacePath.IsInside(inner, _root).ShouldBeTrue();
|
||||
|
||||
@@ -10,9 +10,9 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// #607: Der Nachrichten-Agent merkte sich bei jedem Lauf die ganze, wachsende Linkliste ohne
|
||||
/// Schluessel. Sobald der neueste Eintrag allein ueber 8.000 Zeichen lag, lieferte recall keinen
|
||||
/// einzigen Inhalt mehr - der Agent haette jede Meldung erneut fuer neu gehalten.
|
||||
/// #607: The news agent remembered the whole, growing link list without a key at every run.
|
||||
/// As soon as the newest entry alone was above 8,000 characters, recall delivered not a
|
||||
/// single content any more - the agent would have considered every news item new again.
|
||||
/// </summary>
|
||||
public sealed class MemoryToolTests : IDisposable
|
||||
{
|
||||
@@ -30,7 +30,7 @@ public sealed class MemoryToolTests : IDisposable
|
||||
{
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
|
||||
try { Directory.Delete(_dir, recursive: true); }
|
||||
catch { /* Aufraeumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private Task<ToolResult> Aufruf(object eingabe) => _tool.ExecuteAsync(
|
||||
@@ -45,7 +45,7 @@ public sealed class MemoryToolTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Ein_einzelner_uebergrosser_Eintrag_wird_gekuerzt_statt_weggelassen()
|
||||
{
|
||||
var liste = Linkliste(250); // ~11.500 Zeichen
|
||||
var liste = Linkliste(250); // ~11,500 characters
|
||||
(await Aufruf(new { action = "remember", subject = "gemeldet", content = liste })).Success.ShouldBeTrue();
|
||||
|
||||
var abruf = await Aufruf(new { action = "recall", query = "gemeldet" });
|
||||
|
||||
@@ -4,17 +4,17 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.SocialMedia;
|
||||
|
||||
/// <summary>
|
||||
/// S2 aus der Bestandsaufnahme: Die Kanal-/Video-Angabe wurde ungeprüft in eine
|
||||
/// Argument-Zeichenkette für yt-dlp interpoliert. UseShellExecute=false verhindert
|
||||
/// Shell-Metazeichen, nicht aber Options-Injection — yt-dlp kennt --exec, das beliebige
|
||||
/// Befehle ausführt.
|
||||
/// S2 from the inventory: the channel/video statement was interpolated unchecked into an
|
||||
/// argument string for yt-dlp. UseShellExecute=false prevents
|
||||
/// shell metacharacters, but not option injection — yt-dlp knows --exec, which executes arbitrary
|
||||
/// commands.
|
||||
///
|
||||
/// Die Angriffsfälle bleiben hier dauerhaft als Testfälle dokumentiert.
|
||||
/// The attack cases stay permanently documented here as test cases.
|
||||
/// </summary>
|
||||
public sealed class YouTubeUrlTests
|
||||
{
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Angriffe
|
||||
// Attacks
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
@@ -44,9 +44,9 @@ public sealed class YouTubeUrlTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("kanal name")] // Leerzeichen
|
||||
[InlineData("kanal\"name")] // Anführungszeichen
|
||||
[InlineData("kanal\nname")] // Zeilenumbruch
|
||||
[InlineData("kanal name")] // space
|
||||
[InlineData("kanal\"name")] // quotation marks
|
||||
[InlineData("kanal\nname")] // line break
|
||||
[InlineData("kanal;name")]
|
||||
[InlineData("kanal&name")]
|
||||
[InlineData("../../etc/passwd")]
|
||||
@@ -67,7 +67,7 @@ public sealed class YouTubeUrlTests
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Gegenproben — legitime Eingaben müssen weiter funktionieren
|
||||
// Counter-checks — legitimate inputs must keep working
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
@@ -93,14 +93,14 @@ public sealed class YouTubeUrlTests
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Argumentbildung
|
||||
// Building the arguments
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Die_URL_steht_hinter_dem_Optionsende_Trenner()
|
||||
{
|
||||
// "--" beendet die Optionsliste. Selbst wenn ein Wert wie eine Option aussähe,
|
||||
// würde yt-dlp ihn danach als Adresse behandeln.
|
||||
// "--" ends the option list. Even if a value looked like an option,
|
||||
// yt-dlp would treat it as an address after that.
|
||||
var args = YouTubeUrl.BuildLatestVideoIdArgs("https://www.youtube.com/@kanal/videos");
|
||||
|
||||
var separator = args.IndexOf("--");
|
||||
@@ -112,8 +112,8 @@ public sealed class YouTubeUrlTests
|
||||
[Fact]
|
||||
public void Jedes_Argument_ist_ein_eigener_Eintrag()
|
||||
{
|
||||
// Entscheidend: Kein Eintrag darf mehrere Argumente enthalten — sonst könnten
|
||||
// sie beim Start wieder aufgetrennt werden.
|
||||
// Crucial: no entry may contain several arguments — otherwise they could
|
||||
// be split apart again at startup.
|
||||
var args = YouTubeUrl.BuildAudioDownloadArgs(
|
||||
@"C:\Pfad mit Leerzeichen\datei.mp3",
|
||||
"https://www.youtube.com/watch?v=abc",
|
||||
|
||||
@@ -11,11 +11,11 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.Taskboard;
|
||||
|
||||
/// <summary>
|
||||
/// task_find und die Dublettenbremse (#674).
|
||||
/// task_find and the duplicate brake (#674).
|
||||
///
|
||||
/// Anlass aus dem Testbetrieb: Die Betriebskontrolle laeuft alle drei Stunden und legte
|
||||
/// bei jedem Lauf erneut an, was beim vorigen schon offen war — ueber dreissig Aufgaben
|
||||
/// an @human, darunter sechsmal derselbe Kursbefund.
|
||||
/// Occasion from test operation: the operations check runs every three hours and created
|
||||
/// again at every run what was already open from the previous one — over thirty tasks
|
||||
/// for @human, among them the same price finding six times.
|
||||
/// </summary>
|
||||
public sealed class DublettenTests : IDisposable
|
||||
{
|
||||
@@ -37,7 +37,7 @@ public sealed class DublettenTests : IDisposable
|
||||
{
|
||||
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_verzeichnis);
|
||||
try { Directory.Delete(_verzeichnis, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Context() => new(
|
||||
@@ -92,7 +92,7 @@ public sealed class DublettenTests : IDisposable
|
||||
result.ErrorMessage.ShouldNotBeNull().ShouldContain("'title' is required");
|
||||
}
|
||||
|
||||
// ─── die Bremse beim Anlegen ───
|
||||
// ─── the brake when creating ───
|
||||
|
||||
[Fact]
|
||||
public async Task Eine_zweite_gleichlautende_Aufgabe_wird_nicht_angelegt()
|
||||
@@ -146,8 +146,8 @@ public sealed class DublettenTests : IDisposable
|
||||
[Fact]
|
||||
public async Task Die_Dublette_wird_auch_hinter_500_anderen_Aufgaben_gefunden()
|
||||
{
|
||||
// #716: Die Suche las ListAsync mit Limit 500, sortiert nach Prioritaet. Eine alte
|
||||
// Aufgabe mit niedriger Prioritaet fiel damit aus dem Blick, sobald das Brett wuchs.
|
||||
// #716: The search read ListAsync with a limit of 500, sorted by priority. An old
|
||||
// task with low priority thus fell out of sight as soon as the board grew.
|
||||
var alt = await _board.CreateAsync(new TaskItem
|
||||
{
|
||||
Title = "Betriebsluecke: Eur Kurse nicht aktuell", Assignee = "@human", Priority = 1
|
||||
|
||||
@@ -9,8 +9,8 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.Vorpruefung;
|
||||
|
||||
/// <summary>
|
||||
/// Die Vorpruefung der Werkzeuge (#579) und die Haertung des AgentEditors (#205): Was das
|
||||
/// Werkzeug ohnehin ablehnen wuerde, soll gar nicht erst in die Freigabeliste kommen.
|
||||
/// The pre-check of the tools (#579) and the hardening of the AgentEditor (#205): what the
|
||||
/// tool would refuse anyway should not even get into the approval list.
|
||||
/// </summary>
|
||||
public sealed class VorpruefungTests : IDisposable
|
||||
{
|
||||
@@ -36,7 +36,7 @@ public sealed class VorpruefungTests : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* Aufraeumen ist Nebensache */ }
|
||||
catch { /* Cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentToolContext Kontext(string agentId, Dictionary<string, object?>? config = null) => new(
|
||||
|
||||
@@ -5,8 +5,8 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
||||
|
||||
/// <summary>
|
||||
/// Stufe 1 von WebFetch (#602): Hauptinhalt als kompaktes Markdown statt der ganzen Seite als
|
||||
/// Fliesstext. Die Beispielseiten sind selbst geschrieben und bilden typische Aufbauten nach.
|
||||
/// Stage 1 of WebFetch (#602): main content as compact Markdown instead of the whole page as
|
||||
/// running text. The sample pages are self-written and reproduce typical structures.
|
||||
/// </summary>
|
||||
public sealed class HtmlExtraktorTests
|
||||
{
|
||||
@@ -208,14 +208,14 @@ public sealed class HtmlExtraktorTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Was bei welcher Antwort beim Modell ankommt (#602).</summary>
|
||||
/// <summary>What arrives at the model for which response (#602).</summary>
|
||||
public sealed class WebInhaltTests
|
||||
{
|
||||
private static readonly Uri Quelle = new("https://api.beispiel.de/v1/wetter");
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json")]
|
||||
[InlineData("text/html")] // Server, die JSON als HTML ausliefern, gibt es
|
||||
[InlineData("text/html")] // servers that deliver JSON as HTML do exist
|
||||
[InlineData(null)]
|
||||
public void Json_bleibt_unveraendert(string? mediaType)
|
||||
{
|
||||
@@ -231,9 +231,9 @@ public sealed class WebInhaltTests
|
||||
[Fact]
|
||||
public void Zu_wenig_Hauptinhalt_faellt_mit_Hinweis_auf_den_Volltext_zurueck()
|
||||
{
|
||||
// Uebersichtsseite: Der Inhalt ist eine Linkliste in der Navigation, im Body steht nur ein
|
||||
// Platzhalter, den JavaScript fuellen wuerde. Die Extraktion entfernt die Navigation zu Recht
|
||||
// und behaelt fast nichts - dann ist der Volltext die bessere Antwort.
|
||||
// Overview page: the content is a link list in the navigation, the body only holds a
|
||||
// placeholder that JavaScript would fill. The extraction rightly removes the navigation
|
||||
// and keeps almost nothing - then the full text is the better answer.
|
||||
var html = "<html><body><nav><ul>"
|
||||
+ string.Concat(Enumerable.Range(1, 60).Select(i => $"<li><a href=\"/rubrik/{i}\">Rubrik {i}</a></li>"))
|
||||
+ "</ul></nav><div id=\"app\">Lade …</div></body></html>";
|
||||
|
||||
@@ -5,9 +5,9 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
||||
|
||||
/// <summary>
|
||||
/// #632: Die DWD-Unwetterwarnungen (JSONP, ganz Deutschland) wurden abschnittsweise
|
||||
/// vollstaendig gelesen - 160.000 Tokens fuer eine einzige Stadt. Mit filter kommt nur,
|
||||
/// was passt.
|
||||
/// #632: The DWD severe weather warnings (JSONP, all of Germany) were read completely,
|
||||
/// section by section - 160,000 tokens for a single city. With filter only what
|
||||
/// matches comes.
|
||||
/// </summary>
|
||||
public sealed class JsonFilterTests
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
||||
|
||||
/// <summary>
|
||||
/// #580: Die Pruefung der URL sieht nur den Namen. localtest.me steht auf keiner Sperrliste
|
||||
/// und loest auf 127.0.0.1 auf - im Pruefdurchgang oeffnete WebFetch damit eine Verbindung
|
||||
/// auf Loopback. Geprueft wird deshalb die aufgeloeste Adresse, ohne echtes Netz.
|
||||
/// #580: Checking the URL only sees the name. localtest.me is on no blocklist
|
||||
/// and resolves to 127.0.0.1 - in the test run WebFetch thus opened a connection
|
||||
/// to loopback. That's why the resolved address is checked, without a real network.
|
||||
/// </summary>
|
||||
public sealed class UrlGuardAufloesungTests
|
||||
{
|
||||
@@ -33,7 +33,7 @@ public sealed class UrlGuardAufloesungTests
|
||||
[Fact]
|
||||
public async Task Ein_Name_mit_oeffentlicher_und_interner_Adresse_wird_gesperrt()
|
||||
{
|
||||
// Genau das Muster, mit dem man eine Stichprobenpruefung ueberlistet.
|
||||
// Exactly the pattern with which you outwit a sampling check.
|
||||
await Should.ThrowAsync<HttpRequestException>(
|
||||
() => UrlGuard.ResolveAllowedAsync("gemischt.example", Aufloesung("93.184.215.14", "127.0.0.1"), default));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ using Shouldly;
|
||||
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
||||
|
||||
/// <summary>
|
||||
/// S5 aus der Bestandsaufnahme: Die Whitelist wurde nur auf die Ausgangs-URL angewendet,
|
||||
/// HttpClient folgte Weiterleitungen aber selbst. Eine erlaubte Domain konnte damit ins
|
||||
/// lokale Netz weiterleiten — Router, NAS, der eigene Git-Server, Cloud-Metadatendienste.
|
||||
/// S5 from the inventory: the whitelist was only applied to the initial URL,
|
||||
/// but HttpClient followed redirects itself. An allowed domain could thus redirect into the
|
||||
/// local network — router, NAS, the own Git server, cloud metadata services.
|
||||
/// </summary>
|
||||
public sealed class UrlGuardTests
|
||||
{
|
||||
@@ -16,11 +16,11 @@ public sealed class UrlGuardTests
|
||||
=> UrlGuard.TryValidate(url, Allowed, out _, out error);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Netzsperren
|
||||
// Network blocks
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://169.254.169.254/latest/meta-data/")] // Cloud-Metadaten
|
||||
[InlineData("http://169.254.169.254/latest/meta-data/")] // cloud metadata
|
||||
[InlineData("http://127.0.0.1:8080/")]
|
||||
[InlineData("http://localhost:8418/")]
|
||||
[InlineData("http://192.168.178.10:8418/Richard/ClawdDotNet.git")]
|
||||
@@ -40,8 +40,8 @@ public sealed class UrlGuardTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("172.15.0.1")] // knapp unterhalb von 172.16/12
|
||||
[InlineData("172.32.0.1")] // knapp oberhalb
|
||||
[InlineData("172.15.0.1")] // just below 172.16/12
|
||||
[InlineData("172.32.0.1")] // just above
|
||||
[InlineData("11.0.0.1")]
|
||||
[InlineData("8.8.8.8")]
|
||||
public void Oeffentliche_Adressen_gelten_nicht_als_privat(string host)
|
||||
@@ -94,8 +94,8 @@ public sealed class UrlGuardTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die alte Fassung entfernte "www." per Ersetzen über die ganze Zeichenkette:
|
||||
/// aus "mywww.example.com" wurde "myexample.com".
|
||||
/// The old version removed "www." via a replace across the whole string:
|
||||
/// "mywww.example.com" became "myexample.com".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Die_www_Behandlung_verstuemmelt_keine_anderen_Hostnamen()
|
||||
@@ -107,7 +107,7 @@ public sealed class UrlGuardTests
|
||||
[Fact]
|
||||
public void Ein_abschliessender_Punkt_im_Host_umgeht_die_Whitelist_nicht()
|
||||
{
|
||||
// "example.com." ist DNS-technisch derselbe Host.
|
||||
// "example.com." is the same host in DNS terms.
|
||||
UrlGuard.IsDomainAllowed("evil.com.", Allowed).ShouldBeFalse();
|
||||
UrlGuard.IsDomainAllowed("example.com.", Allowed).ShouldBeTrue();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user