Files
ClawdDotNet/tests/ClawdDotNet.App.Tests/Services/AdministrationTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

406 lines
14 KiB
C#

using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Config;
using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// The rules that until 2026-09-06 lived in the Avalonia view models.
///
/// <para>That they lived there was the reason for these tests: a view model has
/// no test coverage — the UI guide says so explicitly —, and with the
/// second user interface the same rules would have been written a second time. Now
/// they exist once, and from here on it is checked that both user interfaces get the same
/// thing.</para>
/// </summary>
public sealed class AgentAdministrationTests
{
[Fact]
public void A_duplicate_agent_ID_is_rejected()
{
using var host = new TestHost(TestHost.Agent("scout"));
var result = host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("existiert bereits");
host.Host.Instance.Agents.Count.ShouldBe(1);
}
[Fact]
public void The_check_ignores_case()
{
// Two agents differing only in that would be two folders on the target platform
// and the same agent to every human.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.Add(new NewAgent("SCOUT", "Scout", "modell")).Ok.ShouldBeFalse();
}
[Fact]
public void Without_ID_no_agent_is_created()
{
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent(" ", "Scout", "modell")).Ok.ShouldBeFalse();
host.Host.Instance.Agents.ShouldBeEmpty();
}
[Fact]
public void A_created_agent_survives_the_restart()
{
// The core of the finding: LoadInstanceConfig clears the agent list and rebuilds it
// from the folders Agents/Agent-*. An agent that only stood in
// InstanceSettings.json was gone after the next start — exactly what
// the user interface did until 2026-09-06.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell", "Späher")).Ok.ShouldBeTrue();
var afterRestart = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
afterRestart.Agents.ShouldContain(a => a.AgentId == "scout" && a.DisplayName == "Scout");
}
[Fact]
public void Changes_to_an_agent_survive_the_restart()
{
// The same cause from the other direction: until 2026-09-06 nobody called
// SaveAgentSettings during operation. Model, prompt and budget only stood in
// InstanceSettings.json — it looked like saving and wasn't.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "altes-modell"));
var agent = host.Host.AgentAdmin.Find("scout")!;
agent.Model = "neues-modell";
agent.Budget.DailyTokens = 12_345;
agent.Identity = "# Identity: geändert";
host.Host.AgentAdmin.Save();
var afterRestart = host.Host.Directories
.LoadInstanceConfig(host.Host.InstancePath).Agents
.Single(a => a.AgentId == "scout");
afterRestart.Model.ShouldBe("neues-modell");
afterRestart.Budget.DailyTokens.ShouldBe(12_345);
afterRestart.Identity.ShouldBe("# Identity: geändert");
}
[Fact]
public void A_tool_stays_allowed_across_the_restart()
{
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
host.Host.AgentAdmin.SetToolEnabled("scout", "FileRW", enabled: true);
host.Host.Directories
.LoadInstanceConfig(host.Host.InstancePath).Agents
.Single(a => a.AgentId == "scout")
.Tools.ShouldContainKey("FileRW");
}
[Fact]
public void Every_change_reports_itself()
{
// Without this notification an agent created in the web UI would not appear in the
// running window — the whole reason the event exists.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
host.Changes.ShouldHaveSingleItem();
host.Changes[0].Kind.ShouldBe(InstanceChangeKind.Agents);
host.Changes[0].Id.ShouldBe("scout");
}
[Fact]
public void A_removed_agent_does_not_return_after_the_restart()
{
// The opposite direction of the same finding: merely striking it from the list was not
// enough — the folder stayed behind, and at the next start the agent was
// back.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
host.Host.AgentAdmin.Remove("scout").Ok.ShouldBeTrue();
host.Host.Directories
.LoadInstanceConfig(host.Host.InstancePath).Agents
.ShouldNotContain(a => a.AgentId == "scout");
}
[Fact]
public void A_removed_agent_keeps_its_work()
{
// Renamed, not deleted. Its work is in the working folder, and a click
// in a list is no decision to throw it away.
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
var workFile = Path.Combine(
host.Host.AgentAdmin.Find("scout")!.AgentDir, "Workspace", "ergebnis.txt");
File.WriteAllText(workFile, "wichtig");
host.Host.AgentAdmin.Remove("scout").Ok.ShouldBeTrue();
var agentsDir = Path.Combine(host.Host.InstancePath, "Agents");
Directory.GetDirectories(agentsDir, "Entfernt-*").ShouldHaveSingleItem();
Directory.GetFiles(agentsDir, "ergebnis.txt", SearchOption.AllDirectories)
.ShouldHaveSingleItem();
}
[Fact]
public void Allowing_a_tool_creates_an_empty_configuration()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolEnabled("scout", "FileRW", enabled: true).Ok.ShouldBeTrue();
host.Host.AgentAdmin.Find("scout")!.Tools.ShouldContainKey("FileRW");
host.Host.AgentAdmin.Find("scout")!.Tools["FileRW"].ShouldBeEmpty();
}
// ─── #641: permissions in the tool configuration ───
[Fact]
public void Rights_are_stored_normalised_and_survive_the_restart()
{
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell"));
host.Host.AgentAdmin.SetToolEnabled("scout", "Database", enabled: true);
host.Host.AgentAdmin.SetToolRights("scout", "Database", ["SELECT", "Insert", "drop"], ["drop"]).Ok.ShouldBeTrue();
var rights = ClawdDotNet.Core.Staging.ToolRights.From(
host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath).Agents
.Single(a => a.AgentId == "scout").Tools["Database"].AsReadOnly());
rights.Erlaubt.ShouldNotBeNull().OrderBy(x => x).ToArray().ShouldBe(["insert", "select"], "verboten schlaegt erlaubt");
rights.IsForbidden("DROP").ShouldBeTrue();
}
[Fact]
public void Saving_settings_does_not_delete_the_rights()
{
// The settings forms write a fresh dictionary without "erlaubt".
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolEnabled("scout", "Database", enabled: true);
host.Host.AgentAdmin.SetToolRights("scout", "Database", ["select"], []);
host.Host.AgentAdmin.SetToolSettings("scout", "Database", new Dictionary<string, object?> { ["type"] = "mysql" });
host.Host.AgentAdmin.Find("scout")!.Tools["Database"].ShouldContainKey("erlaubt");
}
// ─── #660: prompt history retention ───
[Fact]
public void Prompt_history_retention_is_stored_per_agent_and_for_the_instance()
{
using var host = new TestHost();
host.Host.AgentAdmin.Add(new NewAgent("tresor", "Tresor", "modell"));
host.Host.AgentAdmin.SetPromptHistoryRetention("tresor", 0).Ok.ShouldBeTrue();
host.Host.AgentAdmin.SetDefaultPromptHistoryRetention(30).Ok.ShouldBeTrue();
var reloaded = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath);
reloaded.PromptHistory.RetentionDays.ShouldBe(30);
reloaded.Agents.Single(a => a.AgentId == "tresor").PromptHistory.RetentionDays.ShouldBe(0);
host.Host.AgentAdmin.SetPromptHistoryRetention("tresor", null).Ok.ShouldBeTrue();
host.Host.AgentAdmin.Find("tresor")!.PromptHistory.RetentionDays.ShouldBeNull("back to the instance default");
}
[Fact]
public void Rights_only_for_assigned_tools()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolRights("scout", "Database", ["select"], []).Ok.ShouldBeFalse();
}
[Fact]
public void Removing_a_tool_also_removes_its_settings()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolSettings(
"scout", "Mail", new Dictionary<string, object?> { ["host"] = "mail.example" });
host.Host.AgentAdmin.SetToolEnabled("scout", "Mail", enabled: false);
host.Host.AgentAdmin.Find("scout")!.Tools.ShouldNotContainKey("Mail");
}
[Fact]
public void Writing_settings_means_wanting_the_tool()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.AgentAdmin.SetToolSettings(
"scout", "Mail", new Dictionary<string, object?> { ["host"] = "mail.example" });
host.Host.AgentAdmin.Find("scout")!.Tools.ShouldContainKey("Mail");
}
[Fact]
public void An_unknown_agent_returns_an_answer_and_no_exception()
{
using var host = new TestHost();
host.Host.AgentAdmin.Remove("gibtsnicht").Ok.ShouldBeFalse();
host.Host.AgentAdmin.SetToolEnabled("gibtsnicht", "FileRW", true).Ok.ShouldBeFalse();
host.Changes.ShouldBeEmpty();
}
}
public sealed class JobAdministrationTests
{
[Fact]
public void An_unreadable_schedule_is_rejected()
{
using var host = new TestHost(TestHost.Agent("scout"));
var result = host.Host.JobAdmin.Add(new JobDefinition(
"scout", JobKind.AgentWakeup, Cron: "jeden zweiten Dienstag"));
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("nicht lesbar");
host.Host.AgentAdmin.Find("scout")!.Scheduler.ShouldBeNull();
}
[Fact]
public void An_empty_schedule_is_not_an_error()
{
// Empty means "never" and is a valid statement. Only unreadable is an error.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup)).Ok.ShouldBeTrue();
}
[Fact]
public void A_wakeup_cannot_be_switched_off()
{
// SchedulerConfig has no switch, and inventing one here would mean that
// the scanner would first have to read it. Otherwise a "disabled"
// wakeup would happily keep running.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
var result = host.Host.JobAdmin.SetEnabled(
"scout", JobAdministration.WakeupId("scout"), enabled: false);
result.Ok.ShouldBeFalse();
result.Message.ShouldContain("Nur Tool-Jobs");
}
[Fact]
public void A_tool_job_can_be_switched_off_without_losing_it()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition(
"scout", JobKind.ToolJob, ToolName: "Mail", JobTypeId: "PollJob", Cron: "0 * * * *"));
var job = host.Host.JobAdmin.List().Single();
host.Host.JobAdmin.SetEnabled("scout", job.JobId, enabled: false).Ok.ShouldBeTrue();
var after = host.Host.JobAdmin.List().Single();
after.Enabled.ShouldBeFalse();
after.ToolName.ShouldBe("Mail");
}
[Fact]
public void Without_scanner_a_wakeup_counts_as_not_active()
{
// It runs as long as the scanner runs — that is the only periodic
// driver. Without it, it stands still without anyone having disabled it.
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
host.Host.Scanner.ShouldBeNull();
host.Host.JobAdmin.List().Single().Enabled.ShouldBeFalse();
}
[Fact]
public void The_next_run_is_calculated_from_the_schedule()
{
var (next, valid) = JobAdministration.NextRun("0 * * * *");
valid.ShouldBeTrue();
next.ShouldNotBeNull();
next!.Value.ShouldBeGreaterThan(DateTime.UtcNow);
}
[Fact]
public void An_unreadable_schedule_is_distinguishable_from_a_missing_one()
{
JobAdministration.NextRun("").ShouldBe((null, true));
JobAdministration.NextRun("kaputt").ShouldBe((null, false));
}
[Fact]
public void A_removed_wakeup_is_gone_and_the_agent_stays()
{
using var host = new TestHost(TestHost.Agent("scout"));
host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *"));
host.Host.JobAdmin.Remove("scout", JobAdministration.WakeupId("scout"), JobKind.AgentWakeup)
.Ok.ShouldBeTrue();
host.Host.AgentAdmin.Find("scout").ShouldNotBeNull();
host.Host.JobAdmin.List().ShouldBeEmpty();
}
}
public sealed class ServiceAdministrationTests
{
[Fact]
public void The_built_in_services_are_added_exactly_once()
{
using var host = new TestHost();
host.Host.ServiceAdmin.EnsureBuiltIns();
var afterFirst = host.Host.Instance.Services.Count;
host.Host.ServiceAdmin.EnsureBuiltIns();
afterFirst.ShouldBe(BuiltInServices.CreateDefaults().Count);
host.Host.Instance.Services.Count.ShouldBe(afterFirst);
}
[Fact]
public void A_disabled_built_in_service_stays_disabled()
{
using var host = new TestHost();
host.Host.ServiceAdmin.EnsureBuiltIns();
var service = host.Host.Instance.Services.First(s => s.BuiltIn);
host.Host.ServiceAdmin.SetEnabled(service.ServiceId, enabled: false);
host.Host.ServiceAdmin.EnsureBuiltIns();
host.Host.ServiceAdmin.Find(service.ServiceId)!.Enabled.ShouldBeFalse();
}
[Fact]
public void A_service_without_name_is_rejected()
{
using var host = new TestHost();
host.Host.ServiceAdmin.Add(new NewService(" ", "HTTP", 8080)).Ok.ShouldBeFalse();
host.Host.Instance.Services.ShouldBeEmpty();
}
}