using ClawdDotNet.App.Services; using ClawdDotNet.Core.Config; using Shouldly; namespace ClawdDotNet.App.Tests.Services; /// /// #764: Editing an agent from the WebUI — what the desktop agent page did directly on the /// configuration objects, now one checked call. /// public sealed class AgentUpdateTests { [Fact] public void Changes_survive_a_restart_including_identity_and_soul() { // Identity and Soul live in their own files; written only into AgentSettings.json they // would be overwritten by the next load. using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "old-model")).Ok.ShouldBeTrue(); var result = host.Host.AgentAdmin.Update("scout", new AgentUpdate( DisplayName: "Scout II", Model: "new-model", SystemPrompt: "Be brief.", Identity: "# Identity: changed", Soul: "# Soul: changed", PromptCaching: "OFF", MaxSteps: 42, DailyTokens: 12_345, DailyCostUsd: 1.5m, WhenExhausted: "catchupOnce")); result.Ok.ShouldBeTrue(result.Message); var reloaded = host.Host.Directories .LoadInstanceConfig(host.Host.InstancePath).Agents .Single(a => a.AgentId == "scout"); reloaded.DisplayName.ShouldBe("Scout II"); reloaded.Model.ShouldBe("new-model"); reloaded.SystemPrompt.ShouldBe("Be brief."); reloaded.Identity.ShouldBe("# Identity: changed"); reloaded.Soul.ShouldBe("# Soul: changed"); reloaded.PromptCaching.ShouldBe("off"); reloaded.LoopGuard.MaxSteps.ShouldBe(42); reloaded.Budget.DailyTokens.ShouldBe(12_345); reloaded.Budget.DailyCostUsd.ShouldBe(1.5m); reloaded.Budget.WhenExhausted.ShouldBe("catchUpOnce"); } [Fact] public void Fields_left_out_keep_their_value() { using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "model", "Watches the market")).Ok.ShouldBeTrue(); host.Host.AgentAdmin.Update("scout", new AgentUpdate(Identity: "keep me")).Ok.ShouldBeTrue(); host.Host.AgentAdmin.Update("scout", new AgentUpdate(Model: "other")).Ok.ShouldBeTrue(); var agent = host.Host.AgentAdmin.Find("scout")!; agent.Model.ShouldBe("other"); agent.Description.ShouldBe("Watches the market"); agent.Identity.ShouldBe("keep me"); } [Theory] [InlineData("display")] [InlineData("caching")] [InlineData("exhausted")] [InlineData("steps")] [InlineData("budget")] public void One_bad_value_changes_nothing(string bad) { using var host = new TestHost(TestHost.Agent("scout", "Scout")); var update = bad switch { "display" => new AgentUpdate(Model: "new-model", DisplayName: " "), "caching" => new AgentUpdate(Model: "new-model", PromptCaching: "sometimes"), "exhausted" => new AgentUpdate(Model: "new-model", WhenExhausted: "panic"), "steps" => new AgentUpdate(Model: "new-model", MaxSteps: 0), _ => new AgentUpdate(Model: "new-model", DailyCostUsd: -1m) }; host.Host.AgentAdmin.Update("scout", update).Ok.ShouldBeFalse(); host.Host.AgentAdmin.Find("scout")!.Model.ShouldBe("anthropic/claude-sonnet-4-5"); host.Changes.ShouldBeEmpty(); } [Fact] public void An_unknown_agent_is_refused() { using var host = new TestHost(); host.Host.AgentAdmin.Update("ghost", new AgentUpdate(Model: "x")).Ok.ShouldBeFalse(); } [Fact] public void A_change_is_announced() { // Without the event the other views (and a second browser) would keep the old values. using var host = new TestHost(TestHost.Agent("scout")); host.Host.AgentAdmin.Update("scout", new AgentUpdate(Model: "x")).Ok.ShouldBeTrue(); host.Changes.ShouldHaveSingleItem().Kind.ShouldBe(InstanceChangeKind.Agents); } }