package main import ( "encoding/json" "strings" "testing" "time" ) // A section registered for the mesh before the controller's terminal took that over, or set at the terminal, is // replaced or removed by a proposal the operator approves (novox/hq ADR 0274, issue 340 note 14). const registeredOld = "# What this mesh is\n\n## Its purpose\n\nThe old words.\n" const registeredNew = "# What this mesh is\n\n## Its purpose\n\nThe new words.\n" // register places the old text the way the node keeps it: in the registered items (mesh scope) or in the settings. func register(t *testing.T, p Paths, where string) { t.Helper() switch where { case "registered": raw, _ := json.Marshal(map[string]Item{"mesh.instructions.what-this-mesh-is": {Kind: KindInstructions, Name: "what-this-mesh-is", Scope: ScopeMesh, Files: one("what-this-mesh-is", registeredOld), By: "g14"}}) writeFile(t, p.config(), string(raw)) case "terminal": raw, _ := json.Marshal(map[string]any{"instructions": map[string]string{"what-this-mesh-is": registeredOld}}) writeFile(t, p.Settings, string(raw)) } } func TestARegisteredSectionIsReplacedOrRemovedOnTheOperatorsApproval(t *testing.T) { for _, where := range []string{"registered", "terminal"} { t.Run(where, func(t *testing.T) { p, _ := node(t, "g14") other, _ := node(t, "ace") register(t, p, where) register(t, other, where) store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() propose := func(in ProposeInput, at time.Time) string { t.Helper() in.Scope, in.Name = ScopeMesh, "what-this-mesh-is" out, err := Propose(p, in, store, r, at) if err != nil { t.Fatal(err) } return out["proposed"].(string) } // The same text is no change; a new section with such headings is still refused. if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "what-this-mesh-is", Text: registeredOld}, store, r, t0); err == nil { t.Error("the registered text was proposed over itself") } if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "fresh", Text: registeredNew}, store, r, t0); err == nil { t.Error("a new section may hold the mesh's own headings") } id := propose(ProposeInput{Text: registeredNew}, t0) a := r.published[id] if a.Headline != "Replace Claude instructions 'what-this-mesh-is'?" || !strings.Contains(a.Explanation, "in place of the instructions 'what-this-mesh-is' registered before") { t.Errorf("the ask does not say it replaces the registered section: %q / %q", a.Headline, a.Explanation) } if !strings.Contains(a.Details, "It replaces the registered instructions of this name, whose text has the fingerprint "+ fingerprintOf(textDigest(registeredOld))) || !strings.Contains(a.Details, "The new words.") { t.Errorf("the ask does not show the text and what it replaces: %q", a.Details) } if got := renderedInstructions(t, p); !strings.Contains(got, "The old words.") || strings.Contains(got, "The new words.") { t.Fatal("a proposal changed what renders") } // Declined, nothing changes. r.close(r.chose(id, OptionDecline, t0.Add(time.Hour))) if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(time.Hour)); changed { t.Error("a decline changed what renders") } // Approved, the registered section is replaced in place, on every node that proves the warrant. id = propose(ProposeInput{Text: registeredNew}, t0.Add(2*time.Hour)) r.close(r.chose(id, OptionApprove, t0.Add(3*time.Hour))) for _, n := range []Paths{p, other} { if changed, _, err := Reconcile(n, store, r, nil, t0.Add(3*time.Hour)); err != nil || !changed { t.Fatalf("%s: the approval changed nothing (%v)", n.Node, err) } got := renderedInstructions(t, n) if strings.Contains(got, "The old words.") || strings.Count(got, "The new words.") != 1 || strings.Count(got, "### what-this-mesh-is") != 1 { t.Fatalf("%s: the section was not replaced once, in place:\n%s", n.Node, got) } if strings.Contains(got, "## Instructions the operator approved") { t.Errorf("%s: a replacement is listed apart from the section it replaces", n.Node) } } // claude_code_registrations and claude_code_proposals show it. reg, err := Registrations(p, &bus{kept: map[string]map[string]any{}}, memConfig{}) if err != nil { t.Fatal(err) } raw, _ := json.Marshal(reg) if !strings.Contains(string(raw), "registered text replaced by an approved proposal") { t.Errorf("claude_code_registrations does not show the replacement: %s", raw) } list, _ := ListProposals(p, store, r, "", t0.Add(4*time.Hour)) raw, _ = json.Marshal(list) if !strings.Contains(string(raw), "replaces the registered section whose text") { t.Errorf("claude_code_proposals does not show the replacement: %s", raw) } // A removal of the registered section is asked the same way and leaves nothing. rid := propose(ProposeInput{Remove: true}, t0.Add(5*time.Hour)) if got := r.published[rid]; got.Headline != "Remove Claude instructions 'what-this-mesh-is'?" { t.Errorf("removal ask: %q", got.Headline) } r.close(r.chose(rid, OptionApprove, t0.Add(6*time.Hour))) if changed, _, _ := Reconcile(p, store, r, nil, t0.Add(6*time.Hour)); !changed { t.Fatal("the approved removal changed nothing") } if got := renderedInstructions(t, p); strings.Contains(got, "what-this-mesh-is") { t.Errorf("the removal left the section:\n%s", got) } }) } } func TestARegisteredSectionIsRemovedByAProposalToo(t *testing.T) { p, _ := node(t, "g14") register(t, p, "registered") store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "what-this-mesh-is", Remove: true}, store, r, t0) if err != nil { t.Fatal(err) } id := out["proposed"].(string) if !strings.Contains(r.published[id].Explanation, "registered") || !strings.Contains(r.published[id].Details, "registered text it removes") { t.Errorf("the ask does not say it removes a registered section: %+v", r.published[id]) } r.close(r.chose(id, OptionApprove, t0.Add(time.Hour))) if _, _, err := Reconcile(p, store, r, nil, t0.Add(time.Hour)); err != nil { t.Fatal(err) } if got := renderedInstructions(t, p); strings.Contains(got, "The old words") { t.Errorf("the registered section still renders:\n%s", got) } // Nothing registered and nothing approved: still refused. if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "nothing-here", Remove: true}, store, r, t0); err == nil { t.Error("a section nobody holds was removed") } } func TestAnApprovalIsBoundToTheRegisteredTextItReplaces(t *testing.T) { prop := Proposal{ID: "instr-0123456789abcdef", Action: ActionReplace, Scope: ScopeMesh, Name: "x", Text: "t\n", Digest: textDigest("t\n"), From: "g14", At: t0} plain := prop.Act(OptionApprove) if _, ok := plain["over"]; ok { t.Error("a proposal over nothing binds an over: its older warrants would no longer hold") } prop.Over = textDigest("old\n") if prop.Act(OptionApprove)["over"] != prop.Over { t.Error("the act does not bind the registered text it replaces") } prop.Over = "not a digest" if ProposalProblem(prop) == "" { t.Error("a proposal over a text with no digest is one this module makes") } } func TestANodeSectionRegisteredForThisNodeIsReplacedOnlyThere(t *testing.T) { p, _ := node(t, "g14") raw, _ := json.Marshal(map[string]Item{"node.g14.instructions.local": {Kind: KindInstructions, Name: "local", Scope: ScopeNode, Files: one("local", "## Local\n\nold\n"), By: "g14"}}) writeFile(t, p.config(), string(raw)) store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() out, err := Propose(p, ProposeInput{Scope: ScopeNode, Nodes: []string{"g14"}, Name: "local", Text: "## Local\n\nnew\n"}, store, r, t0) if err != nil { t.Fatal(err) } id := out["proposed"].(string) r.close(r.chose(id, OptionApprove, t0.Add(time.Hour))) if _, _, err := Reconcile(p, store, r, nil, t0.Add(time.Hour)); err != nil { t.Fatal(err) } got := renderedInstructions(t, p) if strings.Contains(got, "\nold\n") || !strings.Contains(got, "\nnew\n") || strings.Count(got, "### local") != 1 { t.Errorf("the node's section was not replaced in place:\n%s", got) } } // The cases ADR 0642 cites, by name. func TestRegisteredSectionEdgeCases(t *testing.T) { const name = "what-this-mesh-is" t.Run("TestANameRegisteredWithTwoTextsIsRefusedUntilOneIsUnregistered", func(t *testing.T) { p, _ := node(t, "g14") register(t, p, "registered") raw, _ := json.Marshal(map[string]any{"instructions": map[string]string{name: registeredNew}}) writeFile(t, p.Settings, string(raw)) store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() in := ProposeInput{Scope: ScopeMesh, Name: name, Text: "A third text.\n"} if _, err := Propose(p, in, store, r, t0); err == nil || !strings.Contains(err.Error(), "different texts") { t.Fatalf("a name registered with two texts was proposed over: %v", err) } if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: name, Remove: true}, store, r, t0); err == nil { t.Error("a name registered with two texts was removed") } writeFile(t, p.config(), "{}") // one of the two is unregistered if _, err := Propose(p, in, store, r, t0); err != nil { t.Errorf("refused after one was unregistered: %v", err) } }) t.Run("TestRemovingAReplacementLeavesNeitherText", func(t *testing.T) { p, _ := node(t, "g14") register(t, p, "registered") store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() approve := func(in ProposeInput, at time.Time) string { in.Scope, in.Name = ScopeMesh, name out, err := Propose(p, in, store, r, at) if err != nil { t.Fatal(err) } id := out["proposed"].(string) r.close(r.chose(id, OptionApprove, at.Add(time.Minute))) if _, _, err := Reconcile(p, store, r, nil, at.Add(time.Minute)); err != nil { t.Fatal(err) } return id } approve(ProposeInput{Text: registeredNew}, t0) rid := approve(ProposeInput{Remove: true}, t0.Add(time.Hour)) if got := renderedInstructions(t, p); strings.Contains(got, "The old words.") || strings.Contains(got, "The new words.") { t.Errorf("a text is left:\n%s", got) } // The ask names the registered text's fingerprint, not the replacement's. d := r.published[rid].Details if !strings.Contains(d, "The registered text it removes has the fingerprint "+fingerprintOf(textDigest(registeredOld))) || strings.Contains(d, fingerprintOf(textDigest(registeredNew))) { t.Errorf("the removal names the wrong fingerprint: %q", d) } }) t.Run("TestAChangedRegisteredTextOutlivesItsApproval", func(t *testing.T) { p, _ := node(t, "g14") register(t, p, "registered") store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter() out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: name, Text: registeredNew}, store, r, t0) if err != nil { t.Fatal(err) } id := out["proposed"].(string) r.close(r.chose(id, OptionApprove, t0.Add(time.Hour))) if _, _, err := Reconcile(p, store, r, nil, t0.Add(time.Hour)); err != nil { t.Fatal(err) } // The registered text changes after the approval: the approval was bound to the old text, so it is not // taken for the new one, which renders as registered. raw, _ := json.Marshal(map[string]Item{"mesh.instructions." + name: {Kind: KindInstructions, Name: name, Scope: ScopeMesh, Files: one(name, "A later registered text.\n"), By: "g14"}}) writeFile(t, p.config(), string(raw)) if got := renderedInstructions(t, p); !strings.Contains(got, "A later registered text.") { t.Errorf("the changed registered text was swallowed by the old approval:\n%s", got) } }) }