Stufe 1 (#764): Agent bearbeiten, Lizenz und Einstellungen in der WebUI
- 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9ce72abe2
commit
8552755bb6
@@ -0,0 +1,155 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user