- PUT /api/agents/{id} und /texts: Stammdaten, Grenzen, Budget, Identity/Soul/
Systemprompt (L6/L7); "Jetzt ausfuehren" in der Agentenansicht (L9)
- Lizenzschluessel ueber die Weboberflaeche (L1): erst vom Lizenzserver
bestaetigt, dann gespeichert; hebt die Sperre ohne Neustart auf
- GET/PUT /api/settings: Instanz- und Anwendungseinstellungen inkl.
OpenRouter-Key (L3/L4/L5), Geheimnisse nur schreibend
- GET/PUT /api/verwaltung/settings: Deploymentcenter und Update-Kanal des Managers
- Einstellungsseite zeigt echte Werte statt fester Texte
- NonInteractiveLicensePrompt verwies auf --license-set-key, den es nie gab
- Sprachpakete werden bei jedem Lauf geprueft (LanguagePackTests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.8 KiB
C#
156 lines
5.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// #764: The license key entered in the web interface — the only way in for a manager that
|
|
/// runs as a service and never asks (<see cref="NonInteractiveLicensePrompt"/>).
|
|
///
|
|
/// <para>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".</para>
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>What the "server" says, per key.</summary>
|
|
private readonly Dictionary<string, LicenseValidationResult> _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<InstanceConfig>(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();
|
|
}
|
|
}
|