using System.Text.Json; using System.Text.Json.Nodes; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Organisation; using Shouldly; namespace ClawdDotNet.Core.Tests.Organisation; /// /// #691: A whole house as a file — never with credentials, and a broken file is refused before /// it touches an instance. /// public sealed class OrgTemplateTests { private static InstanceConfig House() { var instance = new InstanceConfig(); instance.Units.Add(new UnitConfig { Id = "newsroom", Name = "Newsroom", LeaderAgentId = "editor", Goal = "Publish daily." }); instance.Units.Add(new UnitConfig { Id = "desk", Name = "Desk", ParentId = "newsroom" }); instance.Agents.Add(new AgentConfig { AgentId = "editor", DisplayName = "Editor", UnitId = "newsroom", Model = "m/large", Identity = "You lead the newsroom.", Soul = "Careful.", Budget = new AgentBudget { DailyCostUsd = 2 }, Tools = new() { ["Mail"] = new() { ["host"] = "mail.example.org", ["password"] = "hunter2", ["smtpPassword"] = "x" }, ["DirectAPI"] = new() { ["providers"] = JsonSerializer.Deserialize("""{"weather":{"url":"https://w.example","apiKey":"k-123"}}""") } } }); instance.Agents.Add(new AgentConfig { AgentId = "writer", DisplayName = "Writer", UnitId = "desk", Model = "m/small" }); return instance; } private static OrgTemplate Export() => OrgTemplates.Export(House(), "Newsroom", "A small newsroom.", unit => unit == "newsroom" ? "Every claim has a source." : null, DateTime.UtcNow); [Fact] public void Export_empties_every_credential_and_names_it_instead() { var template = Export(); var json = JsonSerializer.Serialize(template, OrgTemplates.Json); json.ShouldNotContain("hunter2"); json.ShouldNotContain("k-123"); json.ShouldContain("mail.example.org", customMessage: "what is not secret stays"); template.Credentials.Select(c => $"{c.AgentId}:{c.Tool}:{c.Field}").ShouldBe( ["editor:Mail:password", "editor:Mail:smtpPassword", "editor:DirectAPI:providers.weather.apiKey"], ignoreOrder: true); } [Fact] public void Export_carries_the_structure_texts_and_doctrines() { var template = OrgTemplates.Parse(JsonSerializer.Serialize(Export(), OrgTemplates.Json)); template.Units.Select(u => u.Id).ShouldBe(["newsroom", "desk"]); template.Agents.Single(a => a.AgentId == "editor").Identity.ShouldBe("You lead the newsroom."); template.Doctrines["newsroom"].ShouldBe("Every claim has a source."); OrgTemplates.Problems(template).ShouldBeEmpty(); } [Fact] public void A_file_that_is_no_template_is_refused() { Should.Throw(() => OrgTemplates.Parse("""{"units":[]}""")); Should.Throw(() => OrgTemplates.Parse("""{"format":"clawddotnet-org-template","formatVersion":99}""")); Should.Throw(() => OrgTemplates.Parse("not json")); } [Fact] public void A_broken_structure_is_named_before_anything_is_written() { var template = Export(); template.Units.Add(new UnitConfig { Id = "a", Name = "A", ParentId = "b" }); template.Units.Add(new UnitConfig { Id = "b", Name = "B", ParentId = "a" }); template.Units.Add(new UnitConfig { Id = "orphan", Name = "Orphan", ParentId = "nowhere" }); template.Units[1].LeaderAgentId = "editor"; template.Agents.Add(new TemplateAgent { AgentId = "writer", DisplayName = "Writer 2" }); template.Agents[0].Tools["Mail"]["password"] = "smuggled"; var problems = OrgTemplates.Problems(template); problems.ShouldContain(p => p.Contains("circle")); problems.ShouldContain(p => p.Contains("'nowhere'")); problems.ShouldContain(p => p.Contains("led by 'editor'")); problems.ShouldContain(p => p.Contains("'writer' appears twice")); problems.ShouldContain(p => p.Contains("never carries one")); } [Fact] public void A_nested_secret_is_found_under_any_variant_of_its_name() { var node = JsonNode.Parse("""{"a":{"clientSecret":"s","list":[{"accessToken":"t"}],"name":"keep"}}""")!; OrgTemplates.StripSecrets(node, "").ShouldBe(["a.clientSecret", "a.list[0].accessToken"]); node.ToJsonString().ShouldContain("keep"); node.ToJsonString().ShouldNotContain("\"s\""); } }