#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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4e41fc12d7
commit
d4b7f34200
@@ -0,0 +1,154 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user