569 lines
24 KiB
Go
569 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// standIn is the operator channel's router and a channel that proves who answered, in memory: it takes the asks
|
|
// mesh-config publishes, keeps its own record of each under mesh-config's name, and records the operator's choice
|
|
// as the router's warrant. Nothing of mesh-config's is trusted by it: it digests the ask it took.
|
|
type standIn struct {
|
|
mu sync.Mutex
|
|
subjects []string
|
|
asks map[string]asks.Ask
|
|
records map[string][]byte
|
|
kinds []stdio.KindHeld
|
|
refuse error
|
|
warranted []string
|
|
// refuseWith makes the router refuse every ask it is published, as the router does for an ask no channel can
|
|
// carry: no record, and its word on the decided subject (heard through hear).
|
|
refuseWith string
|
|
hear func(asks.Warrant)
|
|
// noRecord keeps the router from recording an ask it took (an ask it has not taken yet).
|
|
noRecord bool
|
|
}
|
|
|
|
func newStandIn() *standIn {
|
|
return &standIn{asks: map[string]asks.Ask{}, records: map[string][]byte{}, kinds: []stdio.KindHeld{
|
|
{Seat: asks.ChannelSeat, Kind: "telegram", Module: "telegram", Node: "novox",
|
|
Capabilities: []string{"choice", "exact-render", "verified-sender"}}}}
|
|
}
|
|
|
|
func (r *standIn) Publish(subject string, body any, id string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.refuse != nil {
|
|
return r.refuse
|
|
}
|
|
a, ok := body.(asks.Ask)
|
|
if !ok || a.ID != id {
|
|
return errors.New("not an ask")
|
|
}
|
|
r.subjects = append(r.subjects, subject)
|
|
r.asks[id] = a
|
|
if r.refuseWith != "" {
|
|
w := asks.Warrant{Ask: id, Asker: Module, About: a.About, Outcome: asks.OutcomeRefused, Words: r.refuseWith,
|
|
At: time.Now()}
|
|
go r.hear(w)
|
|
return nil
|
|
}
|
|
if !r.noRecord {
|
|
raw, _ := json.Marshal(map[string]any{"state": "open", "ask": a})
|
|
r.records[id] = raw
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *standIn) Record(id string) (*stdio.StateEntry, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
raw, ok := r.records[id]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &stdio.StateEntry{Key: Module + "." + id, Value: raw}, nil
|
|
}
|
|
|
|
func (r *standIn) Kinds() ([]stdio.KindHeld, error) { return r.kinds, nil }
|
|
|
|
func (r *standIn) Warranted(ask string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.warranted = append(r.warranted, ask)
|
|
return nil
|
|
}
|
|
|
|
// choose is the operator choosing an option on a channel that proves who answered: the router's record of the ask
|
|
// it took, closed with its warrant.
|
|
func (r *standIn) choose(t *testing.T, id, option string, tweak func(*asks.Warrant)) asks.Warrant {
|
|
t.Helper()
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
a, ok := r.asks[id]
|
|
if !ok {
|
|
t.Fatalf("the router took no ask %s", id)
|
|
}
|
|
o, ok := a.Option(option)
|
|
if !ok {
|
|
t.Fatalf("the ask %s offers no %s", id, option)
|
|
}
|
|
w := asks.Warrant{Ask: id, Asker: Module, About: a.About, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label,
|
|
Level: o.Level, By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "4242", Verified: "user id verified"},
|
|
Channel: "telegram", Proofs: []string{"P1"}, At: time.Date(2026, 10, 14, 19, 30, 0, 0, time.UTC),
|
|
AskDigest: a.Digest()}
|
|
if tweak != nil {
|
|
tweak(&w)
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"state": "closed", "ask": a, "warrant": w})
|
|
r.records[id] = raw
|
|
return w
|
|
}
|
|
|
|
// aProposingMesh is aMesh with the stand-in channel, its waits short.
|
|
func aProposingMesh(t *testing.T) (*Config, *memStore, *standIn) {
|
|
t.Helper()
|
|
c, m := aMesh(t)
|
|
r := newStandIn()
|
|
r.hear = func(w asks.Warrant) { c.HeardEnd(w) }
|
|
c.Asks, c.RouterWithin, c.RouterEvery = r, 300*time.Millisecond, 5*time.Millisecond
|
|
return c, m, r
|
|
}
|
|
|
|
func propose(t *testing.T, c *Config, call Call) *ProposeAnswer {
|
|
t.Helper()
|
|
call.Action = "propose"
|
|
if call.Caller == "" {
|
|
call.Caller = "g14.node-tools"
|
|
}
|
|
if call.Why == "" {
|
|
call.Why = "the agents need the new rule"
|
|
}
|
|
a, err := c.Propose(call)
|
|
if err != nil {
|
|
t.Fatalf("propose %s: %v", call.Key, err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func storedProposal(t *testing.T, m *memStore, id string) Proposal {
|
|
t.Helper()
|
|
e, _ := m.Get(ProposalsState, id)
|
|
if e == nil {
|
|
t.Fatalf("no proposal %s is kept", id)
|
|
}
|
|
var p Proposal
|
|
if err := json.Unmarshal(e.Value, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func storedValue(t *testing.T, m *memStore, key string) (Value, bool) {
|
|
t.Helper()
|
|
e, _ := m.Get(ValuesState, key)
|
|
if e == nil {
|
|
return Value{}, false
|
|
}
|
|
var v Value
|
|
if err := json.Unmarshal(e.Value, &v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// **A trusted value proposed end to end** (ADR 0348 §8, How it is checked: "A proposal's test"): mesh-config asks
|
|
// under its own name; its ask names the level, the key and the value and says the change takes effect at once;
|
|
// each option binds them; nothing is set until the operator's Approve; on Approve the value is set as a set is,
|
|
// who approved it is kept beside it, and the warrant is recorded in the hand-act log.
|
|
func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
m.events, m.handActs = nil, nil
|
|
value := map[string]any{"permissions": map[string]any{"deny": []any{"Bash(ssh:*)"}}}
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: value})
|
|
|
|
// Nothing is set by a proposal.
|
|
if _, set := storedValue(t, m, "mesh.agent-managed-settings"); set || len(m.events) != 0 || len(m.handActs) != 0 {
|
|
t.Fatalf("a proposal set something: %d events, %d hand-acts", len(m.events), len(m.handActs))
|
|
}
|
|
if !strings.Contains(a.Router, "took the ask") || a.Proposal == "" || len(a.Reaches) != 1 ||
|
|
a.Reaches[0] != "claude-code on g14" {
|
|
t.Errorf("the proposal answered %+v", a)
|
|
}
|
|
// Asked under mesh-config's own name, on the operator channel.
|
|
if len(r.subjects) != 1 || r.subjects[0] != asks.AskSubject("mesh-config") {
|
|
t.Fatalf("asked on %v", r.subjects)
|
|
}
|
|
ask := r.asks[a.Proposal]
|
|
if err := ask.Check(c.now()); err != nil {
|
|
t.Fatalf("the ask: %v", err)
|
|
}
|
|
if err := asks.Glance(ask.Explanation); err != nil {
|
|
t.Errorf("the ask does not read at a glance: %v\n%s", err, ask.Explanation)
|
|
}
|
|
for _, want := range []string{"agent-managed-settings", "mesh config", "takes effect at once", "claude-code",
|
|
"a trusted value"} {
|
|
if !strings.Contains(ask.Explanation, want) {
|
|
t.Errorf("the ask's explanation does not say %q:\n%s", want, ask.Explanation)
|
|
}
|
|
}
|
|
if !strings.Contains(ask.Headline, "agent-managed-settings") || !strings.Contains(ask.Headline, "whole mesh") {
|
|
t.Errorf("the headline is %q", ask.Headline)
|
|
}
|
|
// The value exact where who answers is proven; under the content rule elsewhere.
|
|
exact := `+ agent-managed-settings: {"permissions":{"deny":["Bash(ssh:*)"]}}`
|
|
shown := ask.Explanation
|
|
if ask.Whole != "" {
|
|
shown = ask.Whole
|
|
}
|
|
if !strings.Contains(shown, exact) || !strings.Contains(ask.Details+ask.WholeDetails, a.Proposal) {
|
|
t.Errorf("the ask does not show the value whole:\n%s\n%s", shown, ask.WholeDetails)
|
|
}
|
|
// Each option binds the level, the key and the value.
|
|
p := storedProposal(t, m, a.Proposal)
|
|
for _, o := range ask.Options {
|
|
if o.Level != asks.Approve {
|
|
t.Errorf("the option %s is at %s", o.ID, o.Level)
|
|
}
|
|
act := p.Act(o.ID)
|
|
want, _ := asks.ActDigest(act)
|
|
if o.Binds != want || act["level"] != LevelMesh || act["key"] != "agent-managed-settings" ||
|
|
act["value"] != valueDigest(json.RawMessage(`{"permissions":{"deny":["Bash(ssh:*)"]}}`), true) ||
|
|
act["was"] != valueDigest(nil, false) {
|
|
t.Errorf("the option %s binds %s for %v", o.ID, o.Binds, act)
|
|
}
|
|
}
|
|
// Still nothing while the ask is open.
|
|
if said, _ := c.Decide(); len(said) != 0 {
|
|
t.Errorf("an open ask decided %v", said)
|
|
}
|
|
|
|
r.choose(t, a.Proposal, OptionApprove, nil)
|
|
said, err := c.Decide()
|
|
if err != nil || len(said) != 1 || !strings.Contains(said[0], "approved by the operator and set") {
|
|
t.Fatalf("the Approve decided %v (%v)", said, err)
|
|
}
|
|
v, set := storedValue(t, m, "mesh.agent-managed-settings")
|
|
if !set || string(v.Value) != `{"permissions":{"deny":["Bash(ssh:*)"]}}` || v.By != "the operator" ||
|
|
v.Via != "g14.node-tools" || !strings.Contains(v.Approved, "approved by the operator, as telegram identity 4242, "+
|
|
"via telegram (user id verified)") || !strings.Contains(v.Approved, "ask "+a.Proposal) ||
|
|
!strings.Contains(v.Approved, "proposed by g14.node-tools") {
|
|
t.Fatalf("the value is %+v", v)
|
|
}
|
|
if res := m.resolved(t, "g14", "claude-code", "managed"); res.Source != SourceMesh ||
|
|
string(res.Value) != string(v.Value) {
|
|
t.Errorf("claude-code's managed settings resolve as %+v", res)
|
|
}
|
|
if len(m.events) != 1 || m.events[0].Approved != v.Approved || len(m.handActs) != 1 ||
|
|
!strings.Contains(m.handActs[0], "the operator's Approve of proposal "+a.Proposal) {
|
|
t.Errorf("the set raised %+v and recorded %q", m.events, m.handActs)
|
|
}
|
|
listed, _ := c.List(Call{Key: "agent-managed-settings"})
|
|
if len(listed.Set) != 1 || listed.Set[0].Approved != v.Approved {
|
|
t.Errorf("the listing says %+v", listed.Set)
|
|
}
|
|
if p = storedProposal(t, m, a.Proposal); p.State != ProposalApproved || !p.Recorded ||
|
|
len(r.warranted) != 1 || r.warranted[0] != a.Proposal {
|
|
t.Errorf("the proposal is %+v; warrants recorded %v", p, r.warranted)
|
|
}
|
|
// A warrant heard again, or read again, sets nothing twice.
|
|
if said, _ = c.Decide(); len(said) != 0 || len(m.events) != 1 {
|
|
t.Errorf("the warrant read again decided %v", said)
|
|
}
|
|
}
|
|
|
|
// **An Approve against a changed value is refused** (ADR 0348 §8, ADR 0277 §13; design 57 K4's proof): the value
|
|
// stored at the level changed after the operator was shown the change against it, so nothing is set.
|
|
func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
// Set at the terminal meanwhile.
|
|
set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0}, Caller: ControllerCaller, By: "jochen"})
|
|
m.events = nil
|
|
r.choose(t, a.Proposal, OptionApprove, nil)
|
|
said, _ := c.Decide()
|
|
if len(said) != 1 || !strings.Contains(said[0], "it changed since, so nothing is set") {
|
|
t.Fatalf("the Approve decided %v", said)
|
|
}
|
|
if v, _ := storedValue(t, m, "mesh.agent-managed-settings"); string(v.Value) != `{"b":2}` || v.Approved != "" {
|
|
t.Errorf("the value is %+v", v)
|
|
}
|
|
if len(m.events) != 0 {
|
|
t.Errorf("a refused Approve raised %+v", m.events)
|
|
}
|
|
if p := storedProposal(t, m, a.Proposal); p.State != ProposalRefused {
|
|
t.Errorf("the proposal is %s: %s", p.State, p.Outcome)
|
|
}
|
|
|
|
// And a clear since: the value it was shown against (a value) is no longer stored.
|
|
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
|
a = propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
|
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
|
|
r.choose(t, a.Proposal, OptionApprove, nil)
|
|
if said, _ = c.Decide(); len(said) != 1 || !strings.Contains(said[0], "no value set") {
|
|
t.Fatalf("the Approve after a clear decided %v", said)
|
|
}
|
|
if _, isSet := storedValue(t, m, "node.g14.font-size"); isSet {
|
|
t.Error("an Approve against a cleared value set it")
|
|
}
|
|
}
|
|
|
|
// **A record that changed after it was asked is refused on Approve** (ADR 0277 §12): its value, or the value it was
|
|
// shown against, no longer what the ask bound.
|
|
func TestAnApproveIsRefusedWhenTheProposalChangedAfterItWasAsked(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
tamper func(p *Proposal)
|
|
want string
|
|
}{
|
|
{"its value", func(p *Proposal) { p.Value = json.RawMessage(`{"evil":true}`) }, "its value is not the value"},
|
|
{"its value and digest", func(p *Proposal) {
|
|
p.Value = json.RawMessage(`{"evil":true}`)
|
|
p.Digest = valueDigest(p.Value, true)
|
|
}, "it was not the ask the person was shown"},
|
|
{"the value it was shown against", func(p *Proposal) {
|
|
p.Was, p.HadValue = json.RawMessage(`{"x":1}`), true
|
|
}, "the value it was shown against"},
|
|
{"its level", func(p *Proposal) { p.Level = Level{Kind: LevelNode, Node: "g14"} }, "not the ask the person was shown"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
r.choose(t, a.Proposal, OptionApprove, nil)
|
|
p := storedProposal(t, m, a.Proposal)
|
|
tc.tamper(&p)
|
|
_ = m.Put(ProposalsState, p.ID, p)
|
|
said, _ := c.Decide()
|
|
if len(said) != 1 || !strings.Contains(said[0], tc.want) {
|
|
t.Fatalf("decided %v, want %q", said, tc.want)
|
|
}
|
|
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
|
|
t.Errorf("a changed proposal set %v", keys)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// **A warrant that is not the answer to the ask is refused** (ADR 0277 §11): another ask's digest, another level, an
|
|
// answer after expiry, nobody verified.
|
|
func TestAWarrantThatDoesNotAnswerTheAskIsRefused(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
tweak func(w *asks.Warrant)
|
|
}{
|
|
{"another ask's digest", func(w *asks.Warrant) { w.AskDigest = "sha256:00" }},
|
|
{"another level", func(w *asks.Warrant) { w.Level = asks.Acknowledge }},
|
|
{"after its expiry", func(w *asks.Warrant) { w.At = time.Date(2026, 10, 16, 0, 0, 0, 0, time.UTC) }},
|
|
{"an option it did not offer", func(w *asks.Warrant) { w.Option = "force" }},
|
|
{"no person", func(w *asks.Warrant) { w.By = nil }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
r.choose(t, a.Proposal, OptionApprove, tc.tweak)
|
|
said, _ := c.Decide()
|
|
if len(said) != 1 || !strings.Contains(said[0], "refused") {
|
|
t.Fatalf("decided %v", said)
|
|
}
|
|
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
|
|
t.Errorf("a warrant that does not hold set %v", keys)
|
|
}
|
|
if len(r.warranted) != 0 {
|
|
t.Errorf("a refused warrant was recorded as acted on: %v", r.warranted)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// **On Decline, at expiry, or on the router's refusal nothing is set** (ADR 0277 §9, §16, §20), and a Decline is a
|
|
// warrant the hand-act log holds.
|
|
func TestDeclineExpiryAndTheRoutersRefusalSetNothing(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
r.choose(t, a.Proposal, OptionDecline, nil)
|
|
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "declined by the operator") {
|
|
t.Fatalf("the Decline decided %v", said)
|
|
}
|
|
if p := storedProposal(t, m, a.Proposal); p.State != ProposalDeclined || p.Approved != "" || !p.Recorded {
|
|
t.Errorf("the declined proposal is %+v", p)
|
|
}
|
|
|
|
// Ended without a choice: the router's word.
|
|
a = propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
|
c.HeardEnd(asks.Warrant{Ask: a.Proposal, Asker: Module, Outcome: asks.OutcomeExpired, At: time.Now()})
|
|
if p := storedProposal(t, m, a.Proposal); p.State != ProposalEnded || !strings.Contains(p.Outcome, "expired") {
|
|
t.Errorf("the expired proposal is %+v", p)
|
|
}
|
|
|
|
// Ended, and the router never recorded it: discarded once its time is past.
|
|
r.noRecord = true
|
|
a = propose(t, c, Call{Key: "font-size", Value: 13.0, Node: "novox"})
|
|
if !strings.Contains(a.Router, "has not said yet") {
|
|
t.Errorf("an ask the router has not taken answered %q", a.Router)
|
|
}
|
|
later := time.Date(2026, 10, 16, 0, 0, 0, 0, time.UTC)
|
|
c.Now = func() time.Time { return later }
|
|
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "keeps no record") {
|
|
t.Errorf("an ask never recorded decided %v", said)
|
|
}
|
|
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
|
|
t.Errorf("an unanswered proposal set %v", keys)
|
|
}
|
|
|
|
// Refused by the router at once: said to the proposer.
|
|
c2, m2, r2 := aProposingMesh(t)
|
|
r2.refuseWith = "no channel can carry an approval"
|
|
_, err := c2.Propose(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "g14.node-tools", Why: "w"})
|
|
if err == nil || !strings.Contains(err.Error(), "no channel can carry an approval") {
|
|
t.Fatalf("the router's refusal answered %v", err)
|
|
}
|
|
all, _ := c2.proposals()
|
|
if len(all) != 1 || all[0].State != ProposalEnded {
|
|
t.Errorf("the refused proposal is %+v", all)
|
|
}
|
|
if keys, _ := m2.Keys(ValuesState); len(keys) != 0 {
|
|
t.Errorf("a refused proposal set %v", keys)
|
|
}
|
|
}
|
|
|
|
// **A proposal is judged as a set before anyone is asked, and refused at once when it cannot be asked** (ADR 0277 §4,
|
|
// §19, §21): a value the verb would refuse; nothing that would change; a value shaped like a secret; no channel that
|
|
// proves who answered; a publish that fails; the same change twice; a fourth open ask.
|
|
func TestAProposalIsJudgedAndRefusedBeforeTheOperatorIsAsked(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
set(t, c, Call{Key: "font-size", Value: 12.0})
|
|
for _, tc := range []struct {
|
|
name string
|
|
call Call
|
|
want string
|
|
}{
|
|
{"a key not declared", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"}, "it declares:"},
|
|
{"a value failing validation", Call{Key: "font-size", Value: 99.0}, "above the maximum 72"},
|
|
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"}, "no module value for the whole mesh"},
|
|
{"nothing that would change", Call{Key: "font-size", Value: 12.0}, "already holds exactly 12"},
|
|
{"a clear of nothing", Call{Key: "font-size", Clear: true, Node: "g14"}, "nothing to clear"},
|
|
{"a clear with a value", Call{Key: "font-size", Clear: true, Value: 1.0}, "a clear carries no value"},
|
|
{"a secret's shape", Call{Key: "agent-managed-settings", Value: map[string]any{"env": map[string]any{
|
|
"API_TOKEN": "ghp_abcdefghijklmnopqrstuvwxyz0123456789"}}}, "leaves the mesh nowhere"},
|
|
{"by", Call{Key: "font-size", Value: 13.0, By: "jochen"}, "by names who gave"},
|
|
{"no why", Call{Key: "font-size", Value: 13.0, Why: " "}, "say why"},
|
|
{"an unproven caller", Call{Key: "font-size", Value: 13.0, Caller: Unproven}, "no proven caller"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
call := tc.call
|
|
call.Action = "propose"
|
|
if call.Caller == "" {
|
|
call.Caller = "g14.node-tools"
|
|
}
|
|
if call.Why == "" {
|
|
call.Why = "a test"
|
|
}
|
|
if _, err := c.Propose(call); err == nil || !strings.Contains(err.Error(), tc.want) ||
|
|
!strings.Contains(err.Error(), "Nothing was proposed") {
|
|
t.Fatalf("refused with %v, want %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
if len(r.subjects) != 0 {
|
|
t.Fatalf("a refused proposal was asked: %v", r.subjects)
|
|
}
|
|
|
|
r.kinds = []stdio.KindHeld{{Seat: asks.ChannelSeat, Kind: "desktop", Capabilities: []string{"choice"}}}
|
|
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 13.0, Caller: "g", Why: "w"}); err == nil ||
|
|
!strings.Contains(err.Error(), "no channel that proves who answered") {
|
|
t.Errorf("no approving channel: %v", err)
|
|
}
|
|
r.kinds = newStandIn().kinds
|
|
r.refuse = errors.New("the bus is away")
|
|
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 13.0, Caller: "g", Why: "w"}); err == nil ||
|
|
!strings.Contains(err.Error(), "could not be asked") {
|
|
t.Errorf("a failed publish: %v", err)
|
|
}
|
|
r.refuse = nil
|
|
|
|
first := propose(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
|
|
if _, err := c.Propose(Call{Action: "propose", Key: "font-size", Value: 14.0, Node: "g14", Caller: "g", Why: "w"}); err == nil ||
|
|
!strings.Contains(err.Error(), "proposed already, as "+first.Proposal) {
|
|
t.Errorf("the same change twice: %v", err)
|
|
}
|
|
// A newer proposal for the same place replaces the older (ADR 0277 §10), and is not counted twice.
|
|
newer := propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "g14"})
|
|
if len(newer.Replaced) != 1 || newer.Replaced[0] != first.Proposal ||
|
|
storedProposal(t, m, first.Proposal).State != ProposalEnded {
|
|
t.Errorf("the newer proposal replaced %v", newer.Replaced)
|
|
}
|
|
propose(t, c, Call{Key: "font-size", Value: 15.0, Node: "novox"})
|
|
propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
if _, err := c.Propose(Call{Action: "propose", Key: "width", Value: 400.0, Node: "g14", Module: "dunst",
|
|
Caller: "g", Why: "w"}); err == nil || !strings.Contains(err.Error(), "3 proposals already wait") {
|
|
t.Errorf("a fourth open ask: %v", err)
|
|
}
|
|
if keys, _ := m.Keys(ValuesState); len(keys) != 1 {
|
|
t.Errorf("proposals set %v", keys)
|
|
}
|
|
}
|
|
|
|
// **A clear is proposed and approved the same way**, and a trusted value set or cleared through the verb is refused,
|
|
// naming the proposal as the way (ADR 0277 §2).
|
|
func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) {
|
|
c, m, r := aProposingMesh(t)
|
|
_, err := c.Change(Call{Action: "set", Key: "agent-managed-settings", Value: map[string]any{}, Caller: "g14.node-tools",
|
|
Why: "w"})
|
|
if err == nil || !strings.Contains(err.Error(), "propose it instead") {
|
|
t.Errorf("a trusted set through the verb: %v", err)
|
|
}
|
|
set(t, c, Call{Key: "managed", Value: map[string]any{"a": 1.0}, Node: "g14", Module: "claude-code",
|
|
Caller: ControllerCaller, By: "jochen"})
|
|
a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"})
|
|
ask := r.asks[a.Proposal]
|
|
if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") ||
|
|
!strings.Contains(ask.Explanation, `- managed (was: {"a":1})`) {
|
|
t.Errorf("the clear asks %q:\n%s", ask.Headline, ask.Explanation)
|
|
}
|
|
r.choose(t, a.Proposal, OptionApprove, nil)
|
|
if said, _ := c.Decide(); len(said) != 1 || !strings.Contains(said[0], "is cleared") {
|
|
t.Fatalf("the clear decided %v", said)
|
|
}
|
|
if _, isSet := storedValue(t, m, "module.g14.claude-code.managed"); isSet {
|
|
t.Error("an approved clear left the value")
|
|
}
|
|
h, _ := c.History(Call{Key: "managed"})
|
|
if last := h[len(h)-1]; last.Action != "clear" || !strings.Contains(last.Approved, "approved by the operator") {
|
|
t.Errorf("the history keeps %+v", last)
|
|
}
|
|
}
|
|
|
|
// **The proposals are listed, and one is read whole** with whether the router's ask is the one composed from it.
|
|
func TestTheProposalsAreListedAndOneIsReadWhole(t *testing.T) {
|
|
c, m, _ := aProposingMesh(t)
|
|
a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}})
|
|
b := propose(t, c, Call{Key: "font-size", Value: 12.0, Node: "g14"})
|
|
rows, err := c.Do(Call{Action: "proposals"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l := rows.([]ProposalRow); len(l) != 2 || l[0].ID != b.Proposal || l[1].State != ProposalWaiting || l[0].Value != nil {
|
|
t.Errorf("listed %+v", l)
|
|
}
|
|
one, err := c.Proposals(Call{Proposal: a.Proposal})
|
|
if err != nil || len(one) != 1 || string(one[0].Value) != `{"a":1}` || one[0].Fingerprint != a.Fingerprint ||
|
|
!strings.Contains(one[0].Phone, "exactly this proposal") {
|
|
t.Fatalf("read whole %+v (%v)", one, err)
|
|
}
|
|
p := storedProposal(t, m, a.Proposal)
|
|
p.Value, p.Digest = json.RawMessage(`{"a":2}`), valueDigest(json.RawMessage(`{"a":2}`), true)
|
|
_ = m.Put(ProposalsState, p.ID, p)
|
|
if one, _ = c.Proposals(Call{Proposal: a.Proposal}); !strings.Contains(one[0].Phone, "ANOTHER ask") {
|
|
t.Errorf("a changed proposal reads %q", one[0].Phone)
|
|
}
|
|
if _, err := c.Proposals(Call{Proposal: "cfg-none"}); err == nil {
|
|
t.Error("an unknown proposal was read")
|
|
}
|
|
}
|
|
|
|
// **The verb's new arguments are read strictly.**
|
|
func TestTheProposalArgumentsAreReadStrictly(t *testing.T) {
|
|
call, err := callOf(map[string]any{"action": "propose", "key": "k", "clear": true, "proposal": " cfg-1 "})
|
|
if err != nil || !call.Clear || call.Proposal != "cfg-1" {
|
|
t.Errorf("read %+v (%v)", call, err)
|
|
}
|
|
if _, err := callOf(map[string]any{"clear": "yes"}); err == nil {
|
|
t.Error("a clear that is not a boolean was read")
|
|
}
|
|
c, _ := aMesh(t)
|
|
if _, err := c.Do(Call{Action: "set", Key: "font-size", Value: 12.0, Clear: true, Caller: "g", Why: "w"}); err == nil {
|
|
t.Error("a set took clear")
|
|
}
|
|
if _, err := c.Do(Call{Action: "propose", Key: "font-size", Value: 12.0, Caller: "g", Why: "w"}); err == nil ||
|
|
!strings.Contains(err.Error(), "cannot be asked from here") {
|
|
t.Errorf("a proposal with no channel: %v", err)
|
|
}
|
|
}
|