Files
RichardandClaude Opus 5.5 8552755bb6 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>
2026-10-02 13:30:20 +02:00

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();
}
}