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
@@ -0,0 +1,295 @@
using System.Text.Json;
using ClawdDotNet.Tools.WebFetch;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.WebFetch;
/// <summary>
/// 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 HtmlExtractorTests
{
private static readonly Uri Basis = new("https://www.beispiel.de/politik/artikel-123.html");
private const string ArticlePage = """
<!DOCTYPE html>
<html lang="de">
<head>
<title>Stadtrat beschliesst Radweg | Beispielzeitung</title>
<meta property="og:title" content="Stadtrat beschliesst Radweg">
<meta property="article:published_time" content="2026-09-14T08:30:00+02:00">
<link rel="canonical" href="/politik/radweg-123.html">
<style>.x { color: red }</style>
<script>window.tracking = "GEHEIMER-SKRIPTTEXT";</script>
</head>
<body>
<header class="seitenkopf"><a href="/">Startseite</a> <a href="/sport">Sport</a></header>
<nav><ul><li><a href="/politik">Politik</a></li><li><a href="/wirtschaft">Wirtschaft</a></li></ul></nav>
<div id="cookie-consent">Wir verwenden Cookies. Alle akzeptieren?</div>
<main>
<article>
<header><h1>Stadtrat beschliesst Radweg</h1><p>Von Erika Beispiel</p></header>
<p>Der Stadtrat hat am Montag mit großer Mehrheit den Bau eines Radwegs entlang der Hauptstraße beschlossen.
Die Arbeiten sollen im <strong>Frühjahr</strong> beginnen.</p>
<h2>Kosten und Zeitplan</h2>
<p>Laut <a href="/dokumente/beschluss.pdf">Beschlussvorlage</a> kostet das Vorhaben rund 2,4 Millionen Euro.</p>
<ul>
<li>Planung bis März</li>
<li>Bau von April bis Oktober</li>
</ul>
<table>
<tr><th>Abschnitt</th><th>Länge</th></tr>
<tr><td>Nord</td><td>1,2 km</td></tr>
<tr><td>Süd</td><td>0,8 km</td></tr>
</table>
<pre>Beschluss 2026/117</pre>
</article>
<aside class="related">Das könnte Sie auch interessieren: Zehn Tipps fürs Radfahren</aside>
</main>
<footer>Impressum · Datenschutz · Kontakt</footer>
</body>
</html>
""";
[Fact]
public void Article_returns_main_content_without_navigation_cookie_notice_and_scripts()
{
var content = HtmlExtractor.Extract(ArticlePage, Basis);
content.Bereich.ShouldBe("article");
content.Markdown.ShouldContain("Der Stadtrat hat am Montag");
content.Markdown.ShouldNotContain("Wirtschaft");
content.Markdown.ShouldNotContain("Cookies");
content.Markdown.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
content.Markdown.ShouldNotContain("Impressum");
content.Markdown.ShouldNotContain("Zehn Tipps");
}
[Fact]
public void Structure_is_kept_as_markdown()
{
var md = HtmlExtractor.Extract(ArticlePage, Basis).Markdown;
md.ShouldContain("# Stadtrat beschliesst Radweg");
md.ShouldContain("## Kosten und Zeitplan");
md.ShouldContain("**Frühjahr**");
md.ShouldContain("- Planung bis März");
md.ShouldContain("| Abschnitt | Länge |");
md.ShouldContain("| --- | --- |");
md.ShouldContain("| Nord | 1,2 km |");
md.ShouldContain("```\nBeschluss 2026/117\n```");
}
[Fact]
public void The_article_header_is_content_not_page_frame()
=> HtmlExtractor.Extract(ArticlePage, Basis).Markdown.ShouldContain("Von Erika Beispiel");
[Fact]
public void With_links_relative_targets_become_absolute()
=> HtmlExtractor.Extract(ArticlePage, Basis, withLinks: true).Markdown
.ShouldContain("[Beschlussvorlage](https://www.beispiel.de/dokumente/beschluss.pdf)");
[Fact]
public void Without_asking_for_links_only_the_link_text_stays()
{
var md = HtmlExtractor.Extract(ArticlePage, Basis).Markdown;
md.ShouldContain("Laut Beschlussvorlage kostet");
md.ShouldNotContain("https://www.beispiel.de/dokumente");
}
[Fact]
public void Overview_page_with_many_teaser_articles_takes_the_whole_main_area()
{
var teaser = string.Concat(Enumerable.Range(1, 6).Select(i =>
$"<article><h2>Meldung {i}</h2><p>Kurzfassung der Meldung {i} mit so viel Text, dass die Kachel fuer sich allein "
+ "die Mindestlaenge fuer einen Bereich erreicht und damit als Kandidat in Frage kaeme, obwohl sie nur einer von vielen ist.</p></article>"));
var html = $"<html><body><nav>Menue</nav><main>{teaser}</main></body></html>";
var content = HtmlExtractor.Extract(html, Basis);
content.Bereich.ShouldBe("main");
content.Markdown.ShouldContain("## Meldung 1");
content.Markdown.ShouldContain("## Meldung 6");
}
[Fact]
public void Metadata_is_included()
{
var content = HtmlExtractor.Extract(ArticlePage, Basis);
content.Titel.ShouldBe("Stadtrat beschliesst Radweg");
content.Sprache.ShouldBe("de");
content.KanonischeUrl.ShouldBe("https://www.beispiel.de/politik/radweg-123.html");
content.Veroeffentlicht.ShouldBe(new DateTimeOffset(2026, 9, 14, 8, 30, 0, TimeSpan.FromHours(2)));
}
[Fact]
public void The_markdown_text_is_much_shorter_than_the_page()
=> HtmlExtractor.Extract(ArticlePage, Basis).Markdown.Length.ShouldBeLessThan(ArticlePage.Length / 2);
[Fact]
public void A_selector_fetches_only_that_part()
{
var content = HtmlExtractor.Extract(ArticlePage, Basis, "table");
content.Bereich.ShouldBe("selector");
content.Markdown.ShouldStartWith("| Abschnitt | Länge |");
content.Markdown.ShouldNotContain("Stadtrat");
content.Hinweis.ShouldBeNull();
}
[Fact]
public void A_selector_without_matches_falls_back_to_the_main_content_with_a_hint()
{
var content = HtmlExtractor.Extract(ArticlePage, Basis, "#gibt-es-nicht");
content.Bereich.ShouldBe("article");
content.Hinweis.ShouldNotBeNull().ShouldContain("matched nothing");
}
[Fact]
public void An_invalid_selector_does_not_crash_the_tool()
{
var content = HtmlExtractor.Extract(ArticlePage, Basis, "div[[[");
content.Markdown.ShouldContain("Der Stadtrat hat am Montag");
content.Hinweis.ShouldNotBeNull().ShouldContain("invalid");
}
[Fact]
public void Without_article_and_main_text_density_finds_the_content_block()
{
const string html = """
<html><body>
<div class="menue"><a href="/a">Alpha</a> <a href="/b">Beta</a> <a href="/c">Gamma</a> <a href="/d">Delta</a></div>
<div class="inhalt">
<p>Die Messstation im Stadtpark meldete gestern die höchste Temperatur des Monats. Fachleute erwarten,
dass die warme Phase noch einige Tage anhält und erst am Wochenende kühlere Luft ankommt.</p>
<p>Für Allergiker bleibt die Belastung durch Gräserpollen hoch. Die Behörde empfiehlt, Fenster in den
Morgenstunden geschlossen zu halten und Wäsche nicht im Freien zu trocknen.</p>
</div>
<div class="linkliste"><a href="/1">Mehr Wetter</a> <a href="/2">Pollenkalender</a> <a href="/3">Archiv</a></div>
</body></html>
""";
var content = HtmlExtractor.Extract(html, Basis);
content.Bereich.ShouldBe("hauptinhalt");
content.Markdown.ShouldContain("Messstation im Stadtpark");
content.Markdown.ShouldNotContain("Pollenkalender");
content.Markdown.ShouldNotContain("Gamma");
}
[Fact]
public void Nested_lists_keep_their_indent()
{
const string html = """
<html><body><main>
<p>Einleitung mit genug Text, damit der Bereich als Hauptinhalt zaehlt und nicht als leer uebergangen wird. Noch ein Satz zur Sicherheit, damit es sicher reicht.</p>
<ol><li>Erster Schritt<ul><li>Unterpunkt A</li><li>Unterpunkt B</li></ul></li><li>Zweiter Schritt</li></ol>
</main></body></html>
""";
var md = HtmlExtractor.Extract(html, Basis).Markdown;
md.ShouldContain("1. Erster Schritt\n - Unterpunkt A\n - Unterpunkt B\n2. Zweiter Schritt");
}
[Fact]
public void Full_text_contains_everything_visible_except_scripts()
{
var full = HtmlExtractor.FullText(ArticlePage);
full.ShouldContain("Wirtschaft");
full.ShouldContain("Der Stadtrat hat am Montag");
full.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
}
}
/// <summary>What arrives at the model for which response (#602).</summary>
public sealed class WebContentTests
{
private static readonly Uri Source = new("https://api.beispiel.de/v1/wetter");
[Theory]
[InlineData("application/json")]
[InlineData("text/html")] // servers that deliver JSON as HTML do exist
[InlineData(null)]
public void Json_stays_unchanged(string? mediaType)
{
const string json = """{"current":{"time":"2026-09-15T12:00","temperature_2m":18.4}}""";
var content = WebContent.Prepare(json, mediaType, Source, FetchMode.Auto, null);
content.Art.ShouldBe("json");
content.Bereich.ShouldBe("roh");
content.Text.ShouldBe(json);
}
[Fact]
public void Too_little_main_content_falls_back_to_the_full_text_with_a_hint()
{
// 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>";
var content = WebContent.Prepare(html, "text/html", Source, FetchMode.Auto, null);
content.Bereich.ShouldBe("vollstaendig");
content.Hinweis.ShouldNotBeNull().ShouldContain("JavaScript");
}
[Fact]
public void Mode_full_returns_the_whole_text()
{
const string html = "<html><body><nav>Menue</nav><article><p>Inhalt</p></article></body></html>";
var content = WebContent.Prepare(html, "text/html", Source, FetchMode.Voll, null);
content.Bereich.ShouldBe("vollstaendig");
content.Text.ShouldContain("Menue");
}
[Fact]
public void Long_content_comes_in_sections_that_together_form_the_whole_text()
{
var text = string.Join("\n\n", Enumerable.Range(1, 400).Select(i => $"Absatz {i}: " + new string('x', 60)));
var parts = new List<string>();
int? offset = 0;
while (offset is not null)
{
var content = WebContent.Prepare(text, "text/plain", Source, FetchMode.Auto, null, offset.Value, 5_000);
parts.Add(content.Text);
if (content.NaechsterOffset is not null)
content.Hinweis.ShouldNotBeNull().ShouldContain($"offset={content.NaechsterOffset}");
offset = content.NaechsterOffset;
}
parts.Count.ShouldBeGreaterThan(1);
string.Concat(parts).ShouldBe(text);
parts[0].ShouldEndWith("\n\n", customMessage: "an einer Absatzgrenze geschnitten");
}
[Fact]
public void Umlauts_are_not_escaped_in_the_answer()
{
var json = JsonSerializer.Serialize(new { text = "Grüße aus Köln – 18 °C" }, WebContent.JsonOptionen);
json.ShouldContain("Grüße aus Köln – 18 °C");
json.ShouldNotContain("\\u00", Case.Sensitive);
}
[Theory]
[InlineData("application/rss+xml", "<rss/>", "xml")]
[InlineData("text/csv", "a,b", "text")]
[InlineData(null, "<html><body>x</body></html>", "html")]
[InlineData(null, "<?xml version=\"1.0\"?><feed/>", "xml")]
public void The_kind_of_content_is_recognised(string? mediaType, string body, string expected)
=> WebContent.KindOf(mediaType, body).ShouldBe(expected);
}