using System.Text.Json; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Instances; using ClawdDotNet.Licensing; using Deploymentcenter.Client; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Manager.Tests; /// /// #764: The license key entered in the web interface — the only way in for a manager that /// runs as a service and never asks (). /// /// The license server is replaced by given answers. The promise checked here is not /// "the server judges correctly" — that is the SDK's job — but "only a confirmed key is /// stored, and a confirmed key lifts the lock without a restart". /// public sealed class LicenseKeyEntryTests : IAsyncDisposable { private readonly string _root = Path.Combine( Path.GetTempPath(), "clawd-tests", "lic-" + Guid.NewGuid().ToString("N")[..8]); private readonly FakeLauncher _launcher = new(); private readonly FakeChannel _channel = new(); private readonly InstanceDirectoryManager _directories; /// What the "server" says, per key. private readonly Dictionary _answers = new(); 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 LicenseKeyEntryTests() { Directory.CreateDirectory(_root); _directories = new InstanceDirectoryManager(Path.Combine(_root, "Instances")); _channel.Launcher = _launcher; _answers["GOOD-KEY"] = new LicenseValidationResult { IsValid = true, Status = "valid" }; _answers["REVOKED-KEY"] = new LicenseValidationResult { IsValid = false, Status = "revoked" }; _answers["OFFLINE-KEY"] = new LicenseValidationResult { IsValid = false, Status = "server_unavailable", IsTransient = true }; } 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 ManagerCore Manager() { var store = new ManagerSettingsStore(_root); store.Load(); var gate = new LicenseGate( new ManagerLicenseStore(store), NullLogger.Instance, new NonInteractiveLicensePrompt(_ => { }), "0.0.0-test", (key, _) => Task.FromResult(_answers.TryGetValue(key, out var answer) ? answer : new LicenseValidationResult { IsValid = false, Status = "not_found" })); var registry = new InstanceRegistry( _directories, _launcher, _channel, NullLoggerFactory.Instance, Options); return new ManagerCore(store, registry, new FakeTokenSource(), NullLoggerFactory.Instance, license: gate); } private string StoredKey() { var store = new ManagerSettingsStore(_root); return store.Load().LicenseKey; } private void AutostartInstance(string name) { var path = _directories.CreateInstance(name); 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-" + name, name, "0.1.2"); } [Fact] public async Task A_confirmed_key_is_stored_and_lifts_the_lock_at_once() { AutostartInstance("Alpha"); await using var manager = Manager(); (await manager.StartAsync()).ShouldBeFalse(); // no key yet: locked _launcher.Starts.ShouldBe(0); var result = await manager.ApplyLicenseKeyAsync(" GOOD-KEY "); result.Applied.ShouldBeTrue(result.Message); manager.LicenseHolds.ShouldBeTrue(); StoredKey().ShouldBe("GOOD-KEY"); _launcher.Starts.ShouldBe(1); // the autostart instance came up } [Fact] public async Task A_refused_key_leaves_the_current_one_in_place() { await using (var first = Manager()) (await first.ApplyLicenseKeyAsync("GOOD-KEY")).Applied.ShouldBeTrue(); await using var manager = Manager(); var result = await manager.ApplyLicenseKeyAsync("REVOKED-KEY"); result.Applied.ShouldBeFalse(); result.Transient.ShouldBeFalse(); StoredKey().ShouldBe("GOOD-KEY"); } [Fact] public async Task An_unreachable_server_stores_nothing() { await using var manager = Manager(); await manager.StartAsync(); var result = await manager.ApplyLicenseKeyAsync("OFFLINE-KEY"); result.Applied.ShouldBeFalse(); result.Transient.ShouldBeTrue(); manager.LicenseHolds.ShouldBeFalse(); StoredKey().ShouldBeEmpty(); } [Fact] public async Task An_empty_key_is_not_sent_anywhere() { await using var manager = Manager(); (await manager.ApplyLicenseKeyAsync(" ")).Applied.ShouldBeFalse(); } [Fact] public async Task Without_a_license_check_nothing_is_stored() { var registry = new InstanceRegistry( _directories, _launcher, _channel, NullLoggerFactory.Instance, Options); await using var manager = new ManagerCore( new ManagerSettingsStore(_root), registry, new FakeTokenSource(), NullLoggerFactory.Instance); (await manager.ApplyLicenseKeyAsync("GOOD-KEY")).Applied.ShouldBeFalse(); StoredKey().ShouldBeEmpty(); } }