UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4
Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag. Oberflaeche - Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml (eigene ControlThemes statt Fluent umzufaerben). - Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite statt eigenem Fenster, Texteditor-Fenster. - ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt. Rocket.Chat - Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird nur, wenn wirklich etwas anliegt. - send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist. - Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei Annahmen waren falsch und sind korrigiert. Deploymentcenter - DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json) erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke. - AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using ClawdDotNet.Core.Deploymentcenter.Watchdog;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Deploymentcenter;
|
||||
|
||||
public sealed class WatchdogHeartbeatServiceTests
|
||||
{
|
||||
/// <summary>Merkt sich, was gesendet wurde, ohne Netzwerk.</summary>
|
||||
private sealed class RecordingClient : IWatchdogClient
|
||||
{
|
||||
public List<(string Status, string? Message)> Heartbeats { get; } = [];
|
||||
public List<(string Kind, string? Message)> Events { get; } = [];
|
||||
|
||||
public Task<WatchdogPingResult> SendHeartbeatAsync(
|
||||
InstanceHealth health, int intervalSeconds, CancellationToken ct)
|
||||
{
|
||||
Heartbeats.Add((health.Status, health.Message));
|
||||
return Task.FromResult(new WatchdogPingResult("up", []));
|
||||
}
|
||||
|
||||
public Task SendEventAsync(
|
||||
string kind, string severity, string? message, object? meta, CancellationToken ct)
|
||||
{
|
||||
Events.Add((kind, message));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedHealth(InstanceHealth health) : IInstanceHealthProvider
|
||||
{
|
||||
public Task<InstanceHealth> GetAsync(CancellationToken ct) => Task.FromResult(health);
|
||||
}
|
||||
|
||||
private static WatchdogHeartbeatService Build(RecordingClient client) =>
|
||||
new(client, new FixedHealth(InstanceHealth.Ok("läuft")), 3600, NullLogger.Instance);
|
||||
|
||||
[Fact]
|
||||
public async Task Regulaeres_Ende_meldet_stopped()
|
||||
{
|
||||
var client = new RecordingClient();
|
||||
var service = Build(client);
|
||||
|
||||
service.Start();
|
||||
await service.DisposeAsync();
|
||||
|
||||
client.Heartbeats[^1].Status.ShouldBe(WatchdogStatus.Stopped);
|
||||
client.Events[^1].Kind.ShouldBe(WatchdogEventKind.StoppedGraceful);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Beim Update ist die Instanz gleich weg, kommt aber wieder. <c>stopped</c> wäre
|
||||
/// die falsche Auskunft: Der Monitor bliebe liegen, bis jemand ihn anfasst.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Angekuendigte_Wartung_meldet_maintenance()
|
||||
{
|
||||
var client = new RecordingClient();
|
||||
var service = Build(client);
|
||||
|
||||
service.Start();
|
||||
service.AnnounceMaintenance("Update auf 0.1.1 wird eingespielt.");
|
||||
await service.DisposeAsync();
|
||||
|
||||
var last = client.Heartbeats[^1];
|
||||
last.Status.ShouldBe(WatchdogStatus.Maintenance);
|
||||
last.Message.ShouldContain("0.1.1");
|
||||
|
||||
client.Events[^1].Kind.ShouldBe(WatchdogEventKind.MaintenanceStart);
|
||||
}
|
||||
|
||||
/// <summary>Der erste Beat geht sofort raus, nicht erst nach einem vollen Intervall.</summary>
|
||||
[Fact]
|
||||
public async Task Erster_Heartbeat_kommt_sofort()
|
||||
{
|
||||
var client = new RecordingClient();
|
||||
var service = Build(client);
|
||||
|
||||
service.Start();
|
||||
await service.DisposeAsync();
|
||||
|
||||
// Start-Ereignis, mindestens ein Beat, dann die Abmeldung.
|
||||
client.Events[0].Kind.ShouldBe(WatchdogEventKind.Started);
|
||||
client.Heartbeats.Count.ShouldBeGreaterThanOrEqualTo(2);
|
||||
client.Heartbeats[0].Status.ShouldBe(WatchdogStatus.Ok);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,14 @@ public sealed class SecretProtectorTests : IDisposable
|
||||
[Fact]
|
||||
public void Klartext_aus_alten_Konfigurationen_wird_unveraendert_gelesen()
|
||||
{
|
||||
// Bestehende Installationen müssen ohne Zutun weiterlaufen.
|
||||
// Zwei Gründe, nicht nur einer:
|
||||
//
|
||||
// 1. Bestehende Installationen müssen ohne Zutun weiterlaufen.
|
||||
// 2. Die Erstinstallation über den update-agent schreibt Lizenzschlüssel und
|
||||
// Deploymentcenter-Token als Klartext in die Settings.json — der Installer
|
||||
// kennt unsere Verschlüsselung nicht. Beim ersten Speichern werden sie
|
||||
// übernommen und verschlüsselt. Wer diesen Zweig entfernt, weil er nach
|
||||
// Altlast aussieht, sperrt jede frisch installierte Instanz aus.
|
||||
SecretProtector.Unprotect("sk-or-alter-klartext").ShouldBe("sk-or-alter-klartext");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user