256 lines
12 KiB
Go
256 lines
12 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|