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; /// /// #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. /// 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 _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 RunningManagerAsync(string? license = "CLAWD-TEST-KEY") { var path = _directories.CreateInstance("Alpha"); var file = Path.Combine(path, "InstanceSettings.json"); var config = JsonSerializer.Deserialize(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"); } }