using ClawdDotNet.App.Services; using ClawdDotNet.Core.Config; using Shouldly; namespace ClawdDotNet.App.Tests.Services; /// /// Die Regeln, die bis zum 2026-09-06 in den Avalonia-Ansichtsmodellen standen. /// /// Dass sie dort standen, war der Grund für diese Tests: Ein Ansichtsmodell hat /// keine Testabdeckung — das sagt der Oberflächen-Leitfaden ausdrücklich —, und mit der /// zweiten Oberfläche wären dieselben Regeln ein zweites Mal geschrieben worden. Jetzt /// gibt es sie einmal, und ab hier ist geprüft, dass beide Oberflächen dasselbe /// bekommen. /// public sealed class AgentAdministrationTests { [Fact] public void Eine_doppelte_Agenten_ID_wird_abgelehnt() { using var host = new TestHost(TestHost.Agent("scout")); var ergebnis = host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell")); ergebnis.Ok.ShouldBeFalse(); ergebnis.Message.ShouldContain("existiert bereits"); host.Host.Instance.Agents.Count.ShouldBe(1); } [Fact] public void Die_Pruefung_ignoriert_Gross_und_Kleinschreibung() { // Zwei Agenten, die sich nur darin unterscheiden, waeren auf der Zielplattform // zwei Ordner und fuer jeden Menschen derselbe Agent. using var host = new TestHost(TestHost.Agent("scout")); host.Host.AgentAdmin.Add(new NewAgent("SCOUT", "Scout", "modell")).Ok.ShouldBeFalse(); } [Fact] public void Ohne_ID_wird_kein_Agent_angelegt() { using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent(" ", "Scout", "modell")).Ok.ShouldBeFalse(); host.Host.Instance.Agents.ShouldBeEmpty(); } [Fact] public void Ein_angelegter_Agent_ueberlebt_den_Neustart() { // Der Kern des Befunds: LoadInstanceConfig leert die Agentenliste und baut sie // aus den Ordnern Agents/Agent-* wieder auf. Ein Agent, der nur in // InstanceSettings.json stand, war nach dem naechsten Start weg — genau das tat // die Oberflaeche bis zum 2026-09-06. using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell", "Späher")).Ok.ShouldBeTrue(); var nachNeustart = host.Host.Directories.LoadInstanceConfig(host.Host.InstancePath); nachNeustart.Agents.ShouldContain(a => a.AgentId == "scout" && a.DisplayName == "Scout"); } [Fact] public void Aenderungen_an_einem_Agenten_ueberleben_den_Neustart() { // Dieselbe Ursache aus der anderen Richtung: Bis zum 2026-09-06 rief niemand im // laufenden Betrieb SaveAgentSettings. Modell, Prompt und Budget standen nur in // InstanceSettings.json — es sah aus wie Speichern und war keines. 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 nachNeustart = host.Host.Directories .LoadInstanceConfig(host.Host.InstancePath).Agents .Single(a => a.AgentId == "scout"); nachNeustart.Model.ShouldBe("neues-modell"); nachNeustart.Budget.DailyTokens.ShouldBe(12_345); nachNeustart.Identity.ShouldBe("# Identity: geändert"); } [Fact] public void Ein_Werkzeug_bleibt_ueber_den_Neustart_zugelassen() { 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 Jede_Aenderung_meldet_sich() { // Ohne diese Meldung erschiene ein in der Weboberflaeche angelegter Agent im // laufenden Fenster nicht — der ganze Grund, warum es das Ereignis gibt. 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 Ein_entfernter_Agent_kommt_nach_dem_Neustart_nicht_wieder() { // Die Gegenrichtung desselben Befunds: Nur aus der Liste zu streichen genuegte // nicht — der Ordner blieb liegen, und beim naechsten Start stand der Agent // wieder da. 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 Ein_entfernter_Agent_behaelt_seine_Arbeit() { // Umbenannt, nicht geloescht. Im Arbeitsordner steht seine Arbeit, und ein Klick // in einer Liste ist keine Entscheidung, sie wegzuwerfen. using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "modell")); var arbeitsdatei = Path.Combine( host.Host.AgentAdmin.Find("scout")!.AgentDir, "Workspace", "ergebnis.txt"); File.WriteAllText(arbeitsdatei, "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 Ein_Werkzeug_zulassen_legt_eine_leere_Konfiguration_an() { 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(); } [Fact] public void Ein_Werkzeug_wegnehmen_entfernt_auch_seine_Einstellungen() { using var host = new TestHost(TestHost.Agent("scout")); host.Host.AgentAdmin.SetToolSettings( "scout", "Mail", new Dictionary { ["host"] = "mail.example" }); host.Host.AgentAdmin.SetToolEnabled("scout", "Mail", enabled: false); host.Host.AgentAdmin.Find("scout")!.Tools.ShouldNotContainKey("Mail"); } [Fact] public void Einstellungen_zu_schreiben_heisst_das_Werkzeug_zu_wollen() { using var host = new TestHost(TestHost.Agent("scout")); host.Host.AgentAdmin.SetToolSettings( "scout", "Mail", new Dictionary { ["host"] = "mail.example" }); host.Host.AgentAdmin.Find("scout")!.Tools.ShouldContainKey("Mail"); } [Fact] public void Ein_unbekannter_Agent_liefert_eine_Antwort_und_keine_Ausnahme() { 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 Ein_unlesbarer_Zeitplan_wird_abgelehnt() { using var host = new TestHost(TestHost.Agent("scout")); var ergebnis = host.Host.JobAdmin.Add(new JobDefinition( "scout", JobKind.AgentWakeup, Cron: "jeden zweiten Dienstag")); ergebnis.Ok.ShouldBeFalse(); ergebnis.Message.ShouldContain("nicht lesbar"); host.Host.AgentAdmin.Find("scout")!.Scheduler.ShouldBeNull(); } [Fact] public void Ein_leerer_Zeitplan_ist_kein_Fehler() { // Leer heisst „nie" und ist eine gueltige Angabe. Nur unlesbar ist ein Fehler. using var host = new TestHost(TestHost.Agent("scout")); host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup)).Ok.ShouldBeTrue(); } [Fact] public void Ein_Wakeup_laesst_sich_nicht_abschalten() { // SchedulerConfig hat keinen Schalter, und einen hier zu erfinden hiesse, dass // der Scanner ihn erst einmal lesen muesste. Sonst liefe ein „abgeschalteter" // Wakeup munter weiter. using var host = new TestHost(TestHost.Agent("scout")); host.Host.JobAdmin.Add(new JobDefinition("scout", JobKind.AgentWakeup, Cron: "0 * * * *")); var ergebnis = host.Host.JobAdmin.SetEnabled( "scout", JobAdministration.WakeupId("scout"), enabled: false); ergebnis.Ok.ShouldBeFalse(); ergebnis.Message.ShouldContain("Nur Tool-Jobs"); } [Fact] public void Ein_Tool_Job_laesst_sich_abschalten_ohne_ihn_zu_verlieren() { 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 danach = host.Host.JobAdmin.List().Single(); danach.Enabled.ShouldBeFalse(); danach.ToolName.ShouldBe("Mail"); } [Fact] public void Ohne_Scanner_gilt_ein_Wakeup_als_nicht_aktiv() { // Er laeuft, solange der Scanner laeuft — der ist der einzige periodische // Treiber. Ohne ihn steht er, ohne dass jemand ihn abgeschaltet haette. 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 Der_naechste_Lauf_wird_aus_dem_Zeitplan_gerechnet() { var (next, valid) = JobAdministration.NextRun("0 * * * *"); valid.ShouldBeTrue(); next.ShouldNotBeNull(); next!.Value.ShouldBeGreaterThan(DateTime.UtcNow); } [Fact] public void Ein_unlesbarer_Zeitplan_ist_von_einem_fehlenden_zu_unterscheiden() { JobAdministration.NextRun("").ShouldBe((null, true)); JobAdministration.NextRun("kaputt").ShouldBe((null, false)); } [Fact] public void Ein_entfernter_Wakeup_ist_weg_und_der_Agent_bleibt() { 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 Die_eingebauten_Dienste_kommen_genau_einmal_dazu() { using var host = new TestHost(); host.Host.ServiceAdmin.EnsureBuiltIns(); var nachDemErsten = host.Host.Instance.Services.Count; host.Host.ServiceAdmin.EnsureBuiltIns(); nachDemErsten.ShouldBe(BuiltInServices.CreateDefaults().Count); host.Host.Instance.Services.Count.ShouldBe(nachDemErsten); } [Fact] public void Ein_abgeschalteter_eingebauter_Dienst_bleibt_abgeschaltet() { using var host = new TestHost(); host.Host.ServiceAdmin.EnsureBuiltIns(); var dienst = host.Host.Instance.Services.First(s => s.BuiltIn); host.Host.ServiceAdmin.SetEnabled(dienst.ServiceId, enabled: false); host.Host.ServiceAdmin.EnsureBuiltIns(); host.Host.ServiceAdmin.Find(dienst.ServiceId)!.Enabled.ShouldBeFalse(); } [Fact] public void Ein_Dienst_ohne_Namen_wird_abgelehnt() { using var host = new TestHost(); host.Host.ServiceAdmin.Add(new NewService(" ", "HTTP", 8080)).Ok.ShouldBeFalse(); host.Host.Instance.Services.ShouldBeEmpty(); } }