Files
RichardandClaude Opus 5.5 d4b7f34200 #795: Updates aus der WebUI ueber den Instanzmanager
Das Update-Angebot gab es nur im Desktop-Fenster. Der Manager prueft jetzt
selbst und spielt ein: erst alle Instanzen geordnet anhalten, dann
- ohne systemd: update-agent mit --wait-for-pid und --restart,
- unter systemd: clawddotnet-update.service anstossen (Vorlage unter
  deploy/systemd), damit der agent keinen zweiten Manager neben dem
  Dienst startet (DC SETUP_INTEGRATION_GUIDE 4A).
Fehlt die Unit, wird nichts angehalten; scheitert die Uebergabe, kommen
die Instanzen wieder hoch. Anleitung in InstanceSetupGuide Abschnitt 8.

Nebenbei: Sprachpaket wird vor dem Kopf geladen (rohe Schluessel im Kopf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:15:18 +02:00

155 lines
6.1 KiB
C#

using System.Text.Json;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Instances;
using Deploymentcenter.Client;
using Deploymentcenter.Client.Models;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Manager.Tests;
/// <summary>
/// #795: Updates from the web interface. The promise: instances are stopped before anything is
/// handed over, nothing is stopped for nothing, and a failed hand-over brings them back.
/// </summary>
public sealed class ManagerUpdatesTests : IAsyncDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(), "clawd-tests", "mupd-" + Guid.NewGuid().ToString("N")[..8]);
private readonly FakeLauncher _launcher = new();
private readonly FakeChannel _channel = new();
private readonly InstanceDirectoryManager _directories;
private readonly List<string[]> _systemctlCalls = [];
private static readonly SupervisionOptions Options = new(
ProbeInterval: TimeSpan.FromMilliseconds(50),
StartTimeout: TimeSpan.FromMilliseconds(600),
SilentChecksBeforeAlarm: 3,
StopGrace: TimeSpan.FromMilliseconds(300),
SilentKillGrace: TimeSpan.FromMilliseconds(300),
MaxDrainWait: TimeSpan.FromSeconds(2));
public ManagerUpdatesTests()
{
Directory.CreateDirectory(_root);
_directories = new InstanceDirectoryManager(Path.Combine(_root, "Instances"));
_channel.Launcher = _launcher;
}
public async ValueTask DisposeAsync()
{
await Task.CompletedTask;
try { Directory.Delete(_root, recursive: true); }
catch { /* A leftover test folder does not turn a run red. */ }
}
private async Task<ManagerCore> RunningManagerAsync(string? license = "CLAWD-TEST-KEY")
{
var path = _directories.CreateInstance("Alpha");
var file = Path.Combine(path, "InstanceSettings.json");
var config = JsonSerializer.Deserialize<InstanceConfig>(File.ReadAllText(file))!;
config.AutostartByManager = true;
File.WriteAllText(file, JsonSerializer.Serialize(config));
_channel.AnswerFor[Path.GetFullPath(path)] = new InstanceIdentity("i-alpha", "Alpha", "0.1.18");
var store = new ManagerSettingsStore(_root);
store.Settings.LicenseKey = license ?? "";
var registry = new InstanceRegistry(_directories, _launcher, _channel, NullLoggerFactory.Instance, Options);
var manager = new ManagerCore(store, registry, new FakeTokenSource(), NullLoggerFactory.Instance);
await manager.StartAsync();
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
return manager;
}
private ManagerUpdates Updates(ManagerCore manager, bool systemd, bool unitInstalled = true,
string? agentProblem = null, int startExit = 0)
=> new(manager, "0.1.18", NullLogger.Instance,
check: _ => Task.FromResult(new UpdateCheckResult { UpdateAvailable = true, LatestRelease = new VersionInfo { Version = "0.1.19" } }),
launchAgent: () => agentProblem,
systemctl: args =>
{
_systemctlCalls.Add(args);
return args.Contains("cat") ? (unitInstalled ? 0 : 1, "") : (startExit, startExit == 0 ? "" : "failed");
},
underSystemd: systemd);
[Fact]
public async Task Without_systemd_the_instances_stop_and_the_manager_ends()
{
await using var manager = await RunningManagerAsync();
var updates = Updates(manager, systemd: false);
var ended = false;
updates.ShutdownRequested += () => ended = true;
var result = await updates.InstallAsync();
result.Ok.ShouldBeTrue(result.Message);
ended.ShouldBeTrue();
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Stopped);
}
[Fact]
public async Task Under_systemd_the_update_unit_is_started_without_blocking()
{
await using var manager = await RunningManagerAsync();
var updates = Updates(manager, systemd: true);
var ended = false;
updates.ShutdownRequested += () => ended = true;
(await updates.InstallAsync()).Ok.ShouldBeTrue();
_systemctlCalls.ShouldContain(a => a.SequenceEqual(new[] { "--user", "start", "--no-block", ManagerUpdates.UpdateUnit }));
ended.ShouldBeFalse(); // the unit stops the service; the manager does not end itself
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Stopped);
}
[Fact]
public async Task Without_the_update_unit_nothing_is_stopped()
{
await using var manager = await RunningManagerAsync();
var result = await Updates(manager, systemd: true, unitInstalled: false).InstallAsync();
result.Ok.ShouldBeFalse();
result.Message.ShouldContain(ManagerUpdates.UpdateUnit);
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
}
[Fact]
public async Task A_failed_hand_over_brings_the_instances_back()
{
await using var manager = await RunningManagerAsync();
var updates = Updates(manager, systemd: false, agentProblem: "agent not found");
var ended = false;
updates.ShutdownRequested += () => ended = true;
var result = await updates.InstallAsync();
result.Ok.ShouldBeFalse();
ended.ShouldBeFalse();
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
}
[Fact]
public async Task Without_a_license_key_nothing_happens()
{
await using var manager = await RunningManagerAsync(license: null);
(await Updates(manager, systemd: false).InstallAsync()).Ok.ShouldBeFalse();
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
}
[Fact]
public async Task The_check_reports_the_new_version()
{
await using var manager = await RunningManagerAsync();
var status = await Updates(manager, systemd: false).CheckAsync();
status.Available.ShouldBeTrue();
status.LatestVersion.ShouldBe("0.1.19");
status.InstalledVersion.ShouldBe("0.1.18");
}
}