Files
ClawdDotNet/tests/ClawdDotNet.Web.Tests/ShutdownTests.cs
T
RichardandClaude Opus 5.5 837a634a22 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>
2026-10-04 12:45:03 +02:00

163 lines
6.1 KiB
C#

using System.Diagnostics;
using ClawdDotNet.App;
using ClawdDotNet.App.Services;
using ClawdDotNet.App.Settings;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Instances;
using ClawdDotNet.Core.Tools;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Web.Tests;
/// <summary>
/// The web UI must be able to shut down quickly — even with open event streams.
///
/// <para><b>The occasion.</b> An event stream never ends by itself. During an orderly
/// shutdown the browser does not hang up, so <c>RequestAborted</c> does not fire,
/// and Kestrel waits patiently for the request to finish. Only after the deadline
/// of <c>StopAsync</c> was it cut off: five seconds in which the application stood still.
/// It became visible when switching instances — between "switch" and the restart there was this
/// pause every time, as soon as a browser window was open anywhere.</para>
///
/// <para>The streams now additionally hang on <c>ApplicationStopping</c>. This test
/// records the promise so it does not get silently lost in the next rework.</para>
/// </summary>
public sealed class ShutdownTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(), "clawd-tests", "web-" + Guid.NewGuid().ToString("N")[..8]);
private const string Token = "ein-hinreichend-langes-testtoken";
/// <summary>
/// Generously sized: measured against the deadline of StopAsync (five
/// seconds). If the test stays below two, the promise is kept without a
/// slow build server turning it red.
/// </summary>
private static readonly TimeSpan Grenze = TimeSpan.FromSeconds(2);
private AppHost NeuerHost(int port)
{
var instancesDir = Path.Combine(_root, "Instances");
var instancePath = Path.Combine(instancesDir, "Test");
Directory.CreateDirectory(instancePath);
return new AppHost
{
Settings = new SettingsManager(_root),
Directories = new InstanceDirectoryManager(instancesDir),
Instance = new InstanceConfig
{
InstanceName = "Test",
WebUiEnabled = true,
WebUiBinding = "loopback",
WebUiToken = Token,
WebServerPort = port
},
InstancePath = instancePath,
LogDirectory = Path.Combine(_root, "Logs"),
LoggerFactory = NullLoggerFactory.Instance,
Tools = new ToolRegistry()
};
}
/// <summary>
/// A free port from the operating system. A fixed number would let two test runs
/// fail side by side — and those are exactly the errors you search for the longest.
/// </summary>
private static int FreierPort()
{
using var probe = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
probe.Start();
var port = ((System.Net.IPEndPoint)probe.LocalEndpoint).Port;
probe.Stop();
return port;
}
[Fact]
public async Task Beenden_wartet_nicht_auf_einen_offenen_Ereignisstrom()
{
var port = FreierPort();
await using var host = NeuerHost(port);
var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance);
web.ShouldNotBeNull();
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
// Really open the stream and wait for the first line: only then does
// Kestrel hold the request, and only then does the test check what it wants to check.
using var antwort = await client.GetAsync(
$"http://127.0.0.1:{port}/events?token={Token}",
HttpCompletionOption.ResponseHeadersRead);
antwort.EnsureSuccessStatusCode();
await using var strom = await antwort.Content.ReadAsStreamAsync();
using var leser = new StreamReader(strom);
(await leser.ReadLineAsync()).ShouldBe(": bereit");
var uhr = Stopwatch.StartNew();
await web!.DisposeAsync();
uhr.Stop();
uhr.Elapsed.ShouldBeLessThan(Grenze,
$"Das Beenden dauerte {uhr.Elapsed.TotalSeconds:0.0}s — der Ereignisstrom "
+ "hat das Herunterfahren nicht bemerkt.");
}
[Fact]
public async Task Beenden_geht_auch_ohne_offene_Verbindung_zuegig()
{
var port = FreierPort();
await using var host = NeuerHost(port);
var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance);
web.ShouldNotBeNull();
var uhr = Stopwatch.StartNew();
await web!.DisposeAsync();
uhr.Stop();
uhr.Elapsed.ShouldBeLessThan(Grenze);
}
/// <summary>
/// A taken port costs the web UI, not the instance.
///
/// <para>Previously <c>TryStart</c> returned <c>null</c> here, and with that the
/// entire HTTP surface was gone. Since the socket belongs to it, that is the wrong
/// order of priority: the instance manager will reach the instance through it, and losing it because
/// of a taken port would mean making the instance unreachable because
/// a second one is already running — i.e. exactly in the case the manager
/// exists for.</para>
/// </summary>
[Fact]
public async Task Ein_belegter_Port_kostet_nur_die_Weboberflaeche()
{
var port = FreierPort();
using var blockierer = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, port);
blockierer.Start();
await using var host = NeuerHost(port);
await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance);
web.ShouldNotBeNull();
web!.Url.ShouldBeNull("Die Weboberfläche konnte nicht binden — das gehört gesagt.");
// The socket holds anyway.
using var client = InstanceEndpoint.CreateClient(host.InstancePath);
(await client.GetAsync("/api/info")).EnsureSuccessStatusCode();
}
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); }
catch { /* A leftover test folder does not turn a run red. */ }
}
}