Organisationsvorlagen: ein ganzes Haus als importierbare Datei (#691)
- Core/Organisation/OrgTemplate: format, export, parse, structural checks. Export empties every credential (nested, name variants) and names it; a template that carries one is refused. - OrgAdministration: ExportTemplate, PreviewTemplate (problems, warnings, every text in full), ImportTemplateAsync - only with confirmation, units before agents, own workspace per unit, doctrines, model and budget factor overridable, templateOrigin on every unit and agent. - Endpoints GET /organisation/template, POST .../preview, POST .../import; templates card in the organisation view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
11465f3d74
commit
706b5cfd9e
@@ -330,6 +330,82 @@ public sealed class OrgAdministrationTests : IDisposable
|
||||
Unit("marketing").Votes.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ═══════════════════ templates (#691) ═══════════════════
|
||||
|
||||
private static Core.Organisation.OrgTemplate Shop() => new()
|
||||
{
|
||||
Name = "Shop", Version = "2",
|
||||
Units =
|
||||
[
|
||||
new UnitConfig { Id = "shop", Name = "Shop", LeaderAgentId = "shop-lead", ToolCeiling = ["Taskboard", "WebFetch"] },
|
||||
new UnitConfig { Id = "returns", Name = "Returns", ParentId = "shop" }
|
||||
],
|
||||
Agents =
|
||||
[
|
||||
new Core.Organisation.TemplateAgent { AgentId = "shop-lead", DisplayName = "Shop Lead", UnitId = "shop", Model = "m/large",
|
||||
Identity = "You run the shop.", Soul = "Calm.", Budget = new AgentBudget { DailyCostUsd = 4 },
|
||||
Tools = new() { ["Taskboard"] = new System.Text.Json.Nodes.JsonObject() } },
|
||||
new Core.Organisation.TemplateAgent { AgentId = "clerk", DisplayName = "Clerk", UnitId = "returns", Model = "m/small",
|
||||
Tools = new() { ["WebFetch"] = new System.Text.Json.Nodes.JsonObject { ["maxKb"] = 64 } } }
|
||||
],
|
||||
Credentials = [new("clerk", "WebFetch", "apiKey")]
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task A_template_is_imported_only_after_confirmation_with_its_origin_and_overrides()
|
||||
{
|
||||
(await Org.ImportTemplateAsync(Shop(), null, confirmed: false, "shop.json", "", default)).Message.ShouldContain("Confirm");
|
||||
_test.Host.Instance.Units.ShouldBeEmpty("nothing is written before the confirmation");
|
||||
|
||||
var imported = await Org.ImportTemplateAsync(Shop(), new TemplateOverrides(Model: "m/cheap", BudgetFactor: 0.5m), true, "shop.json", "trial", default);
|
||||
|
||||
imported.Ok.ShouldBeTrue(imported.Message);
|
||||
imported.Message.ShouldContain("1 credential");
|
||||
Unit("returns").ParentId.ShouldBe("shop");
|
||||
Unit("shop").TemplateOrigin!.ShouldStartWith("Shop v2");
|
||||
_test.Host.Instance.Workspaces.ShouldContain(w => w.Id == "returns", "every unit gets its own workspace");
|
||||
var lead = Find("shop-lead");
|
||||
lead.Identity.ShouldBe("You run the shop.");
|
||||
lead.Model.ShouldBe("m/cheap");
|
||||
lead.Budget.DailyCostUsd.ShouldBe(2);
|
||||
lead.TemplateOrigin!.ShouldContain("shop.json");
|
||||
Org.Current().LeaderOf("shop")!.AgentId.ShouldBe("shop-lead");
|
||||
Find("clerk").Tools.Keys.ShouldBe(["WebFetch"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_template_that_clashes_or_breaks_a_ceiling_is_refused_in_the_preview()
|
||||
{
|
||||
BuildMarketing();
|
||||
var shop = Shop();
|
||||
shop.Agents.Add(new Core.Organisation.TemplateAgent { AgentId = "ads", DisplayName = "Ads 2", UnitId = "shop" });
|
||||
shop.Agents[0].Tools["Database"] = new System.Text.Json.Nodes.JsonObject();
|
||||
shop.Units.Add(new UnitConfig { Id = "marketing", Name = "Marketing again" });
|
||||
|
||||
var preview = Org.PreviewTemplate(shop);
|
||||
|
||||
preview.CanImport.ShouldBeFalse();
|
||||
preview.Problems.ShouldContain(p => p.Contains("agent 'ads'"));
|
||||
preview.Problems.ShouldContain(p => p.Contains("unit 'marketing'"));
|
||||
preview.Problems.ShouldContain(p => p.Contains("Database, beyond the tool ceiling"));
|
||||
preview.Warnings.ShouldContain(p => p.Contains("become prompts"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_export_of_an_instance_is_a_template_another_can_preview()
|
||||
{
|
||||
BuildMarketing();
|
||||
Find("ads").Tools["WebFetch"]["token"] = "secret-value";
|
||||
|
||||
var exported = Org.ExportTemplate("Marketing");
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(exported, Core.Organisation.OrgTemplates.Json);
|
||||
|
||||
json.ShouldNotContain("secret-value");
|
||||
exported.Credentials.ShouldContain(c => c.AgentId == "ads" && c.Field == "token");
|
||||
Core.Organisation.OrgTemplates.Problems(Core.Organisation.OrgTemplates.Parse(json)).ShouldBeEmpty();
|
||||
Org.PreviewTemplate(exported).Problems.ShouldContain(p => p.Contains("already has"), "into the same instance it clashes");
|
||||
}
|
||||
|
||||
// ═══════════════════ the top (#690) ═══════════════════
|
||||
|
||||
private void MakeDecider(string id)
|
||||
|
||||
Reference in New Issue
Block a user