Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/proposals_test.go
T

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)
}
}