mesh/merge-gate head deef9421a276 merged into trunk cef22a25264b — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head deef9421a276 merged into trunk cef22a25264b — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance delivered: every member is delivered
{a:1, b:2, c:3, password:hunter2} was proposed with "+ password: hunter2" in its Details: the router
withheld the line unnamed and Approve set a value the operator never saw (ADR 0347 §1, §3, §4). Each line is
now judged as the router judges it, masked under the content rule and whole under the secret rule; a line
refused is said "+ password (value withheld)", and any line the router would still refuse is refused at
propose in the controller's words, never echoing a secret-shaped key. SDK at mesh-sdk 5933eb7 (no two asks
digest the same).
980 lines
43 KiB
Go
980 lines
43 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"slices"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||
|
||
"github.com/novox/mesh-controller/internal/catalogue"
|
||
"github.com/novox/mesh-controller/internal/conditions"
|
||
"github.com/novox/mesh-controller/internal/outward"
|
||
)
|
||
|
||
// novox/hq ADR 0277: a trusted setting is proposed through a verb by anyone the bus admits, asked of the
|
||
// operator at the level approve with the exact change, and set only on the operator's warrant — once, for the
|
||
// exact values the option bound; declined, expired or refused, it is discarded; nothing is asked when nothing
|
||
// can carry the ask.
|
||
|
||
// aProposer is the propose path with its reaches faked: a layer as it stands, a judge that records what it
|
||
// judged, a memory store, and what was published.
|
||
type proposerRig struct {
|
||
pr proposer
|
||
store memAskedStore
|
||
sent []published
|
||
judged []map[string]any
|
||
before map[string]any
|
||
had bool
|
||
refusedBy string
|
||
now time.Time
|
||
}
|
||
|
||
func newProposerRig(t *testing.T) *proposerRig {
|
||
r := &proposerRig{store: memAskedStore{}, now: time.Date(2026, 10, 10, 14, 5, 0, 0, time.UTC)}
|
||
r.pr = proposer{
|
||
layer: func(_ context.Context, node, module string) (map[string]any, bool, error) {
|
||
return r.before, r.had, nil
|
||
},
|
||
judge: func(_ context.Context, node, module string, values map[string]any) error {
|
||
r.judged = append(r.judged, values)
|
||
if r.refusedBy != "" {
|
||
return errors.New(r.refusedBy)
|
||
}
|
||
return nil
|
||
},
|
||
machines: func(context.Context) ([]string, error) { return []string{"anchor", "laptop", "shanks"}, nil },
|
||
store: r.store,
|
||
publish: func(_ context.Context, subject string, body []byte, id string) error {
|
||
r.sent = append(r.sent, published{subject, id, body})
|
||
return nil
|
||
},
|
||
now: func() time.Time { return r.now },
|
||
caller: "g14/claude-code, through the mesh-controller seat",
|
||
waitFor: time.Millisecond,
|
||
waitEvery: time.Millisecond,
|
||
}
|
||
return r
|
||
}
|
||
|
||
func (r *proposerRig) askSent(t *testing.T) asks.Ask {
|
||
t.Helper()
|
||
if len(r.sent) != 1 || r.sent[0].subject != asks.AskSubject("mesh-controller") {
|
||
t.Fatalf("published %+v", r.sent)
|
||
}
|
||
var q asks.Ask
|
||
if err := json.Unmarshal(r.sent[0].body, &q); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return q
|
||
}
|
||
|
||
func (r *proposerRig) theProposal(t *testing.T) asked {
|
||
t.Helper()
|
||
for _, a := range r.store {
|
||
if a.Proposal != nil {
|
||
return a
|
||
}
|
||
}
|
||
t.Fatal("no proposal is kept")
|
||
return asked{}
|
||
}
|
||
|
||
var mountsSources = map[string]any{"sources": "recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro", "shares": "library=/mnt/library"}
|
||
|
||
// The ask: at the level approve on both answers, each binding the proposal's act, with every key and its exact new
|
||
// value as far as the content rule lets it leave the mesh, the layer it was shown against, and nothing set.
|
||
func TestAProposalAsksAtTheLevelApproveWithTheExactChange(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
r.before, r.had = map[string]any{"shares": "none", "old": "x"}, true
|
||
words, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: mountsSources, replace: true})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
if err := q.Check(r.now); err != nil {
|
||
t.Fatalf("the ask is refused: %v", err)
|
||
}
|
||
if q.Headline != "Set mounts on shanks?" || q.About != "settings.mounts.shanks" || q.Who != asks.Operator ||
|
||
!q.Expires.Equal(r.now.Add(askApproveFor)) {
|
||
t.Errorf("the ask: %+v", q)
|
||
}
|
||
if len(q.Options) != 2 {
|
||
t.Fatalf("options %+v", q.Options)
|
||
}
|
||
for _, o := range q.Options {
|
||
if o.Level != asks.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
|
||
t.Errorf("the option %s is %s and binds %q", o.ID, o.Level, o.Binds)
|
||
}
|
||
}
|
||
if q.Options[0].Binds == q.Options[1].Binds {
|
||
t.Error("Approve and Decline bind the same act")
|
||
}
|
||
// The message reads at a glance (issues 383 and 639): a summary, then one point per key — and nothing else:
|
||
// who proposed it, the fingerprint and the how-to are the Details, which the router notes after the answers.
|
||
summary := "Changes 3 setting(s) of mounts on shanks; approved, the layer is set and sent at once by a walk to shanks, one machine at a time, each judged."
|
||
if q.Explanation != summary+"\n+ sources: recalbox=‹address›@‹path›:ro\n~ shares: library=‹path› (was: none)\n- old (was: x)" {
|
||
t.Errorf("the explanation:\n%s", q.Explanation)
|
||
}
|
||
if err := asks.Glance(q.Explanation); err != nil {
|
||
t.Error(err)
|
||
}
|
||
for _, leak := range []string{"nas.lan", "/mnt/recalbox", "/mnt/library", "smb://"} {
|
||
if strings.Contains(q.Explanation, leak) {
|
||
t.Errorf("the explanation carries %q, which may not leave the mesh", leak)
|
||
}
|
||
}
|
||
// Where the sender is proven, the values whole: the mount point and the share are what is approved.
|
||
if q.Whole != summary+"\n+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro\n~ shares: library=/mnt/library (was: none)\n- old (was: x)" ||
|
||
asks.Glance(q.Whole) != nil {
|
||
t.Errorf("the whole words:\n%s", q.Whole)
|
||
}
|
||
for _, line := range []string{
|
||
"Proposed by g14/claude-code, through the mesh-controller seat at " + r.now.Local().Format("15:04 on 2 Jan") +
|
||
". Fingerprint " + fingerprint(layerDigest(mountsSources)) + ".",
|
||
"Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.",
|
||
"Read it whole, with this fingerprint: settings proposals " + kept(r).ID + " at the controller's terminal",
|
||
} {
|
||
if !strings.Contains(q.Details, line) {
|
||
t.Errorf("Details lack %q:\n%s", line, q.Details)
|
||
}
|
||
}
|
||
if refusal, ok := outward.Check(q.Details, "anchor", "laptop", "shanks"); !ok {
|
||
t.Errorf("the router would withhold a line of the details: %s", refusal)
|
||
}
|
||
for _, howTo := range []string{"ingerprint", "settings proposals", "settings verb", "not proven", "Proposed by"} {
|
||
if strings.Contains(q.Explanation, howTo) || strings.Contains(q.Whole, howTo) {
|
||
t.Errorf("the message carries the how-to %q", howTo)
|
||
}
|
||
}
|
||
all := []string{q.Headline, q.Explanation, q.OnExpiry}
|
||
for _, o := range q.Options {
|
||
all = append(all, o.Label, o.Does)
|
||
}
|
||
if refusal, ok := outward.Check(strings.Join(all, "\n"), "anchor", "laptop", "shanks"); !ok {
|
||
t.Errorf("the router would refuse the ask: %s", refusal)
|
||
}
|
||
if refusal, ok := outward.Secret(q.Whole, "anchor", "laptop", "shanks"); !ok {
|
||
t.Errorf("the router would refuse the whole words: %s", refusal)
|
||
}
|
||
// Kept as the controller's own ask, about no condition, with the proposal whole and its digests.
|
||
kept := r.theProposal(t)
|
||
p := kept.Proposal
|
||
if kept.State != askOpen || kept.Ask.Digest() != q.Digest() || p.Module != "mounts" || p.Node != "shanks" ||
|
||
p.Digest != layerDigest(mountsSources) || p.BeforeDigest != layerDigest(r.before) || !p.Replace || !p.HadLayer ||
|
||
p.From != r.pr.caller || kept.ofACondition() {
|
||
t.Errorf("kept %+v / %+v", kept, p)
|
||
}
|
||
// Each option binds exactly the act the controller will perform (boundAct over the kept action).
|
||
for i, act := range kept.Actions {
|
||
binds, _ := asks.ActDigest(boundAct(act))
|
||
if q.Options[i].Binds != binds || act.Arguments["values"] != p.Digest || act.Arguments["before"] != p.BeforeDigest ||
|
||
act.Verb != proposalVerb || act.Level != conditions.LevelApprove {
|
||
t.Errorf("the option %s does not bind the kept act: %+v", q.Options[i].ID, act)
|
||
}
|
||
}
|
||
if len(r.judged) != 1 || r.judged[0]["sources"] != mountsSources["sources"] {
|
||
t.Errorf("judged %v", r.judged)
|
||
}
|
||
if !strings.Contains(words, "nothing changes") || !strings.Contains(words, kept.ID) {
|
||
t.Errorf("the caller is told: %s", words)
|
||
}
|
||
}
|
||
|
||
// A layer for the whole mesh is proposed too.
|
||
func TestAMeshWideLayerIsProposed(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", values: map[string]any{"x": "1"}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
if q.Headline != "Set notes on the whole mesh?" || q.About != "settings.notes.mesh" ||
|
||
q.Explanation != "Changes 1 setting(s) of notes on the whole mesh; approved, the layer is set and sent at once by a walk to every machine running notes, one machine at a time, each judged.\n+ x: 1" ||
|
||
q.Whole != "" {
|
||
t.Errorf("%+v", q)
|
||
}
|
||
}
|
||
|
||
// kept is the one proposal the rig keeps.
|
||
func kept(r *proposerRig) asked {
|
||
for _, a := range r.store {
|
||
if a.Proposal != nil {
|
||
return a
|
||
}
|
||
}
|
||
return asked{}
|
||
}
|
||
|
||
// The switch between the masked and the whole change (issue 383): the same change, under the content rule and
|
||
// exact, differs in the masked values alone; a value shaped like a secret is withheld in both, whole, with its
|
||
// key still named; and a change no value of which is masked carries no whole words of its own.
|
||
func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t *testing.T) {
|
||
machines := []string{"anchor", "laptop", "shanks"}
|
||
r := newProposerRig(t)
|
||
values := map[string]any{"shares": "media=/storage/media", "sources": "recalbox=smb://nas.lan/recalbox@/mnt/recalbox", "font": "Inter 13"}
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: values}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
masked, _, _ := kept(r).Proposal.change(machines, false)
|
||
whole, _, _ := kept(r).Proposal.change(machines, true)
|
||
if !strings.HasSuffix(q.Explanation, "\n"+strings.Join(masked, "\n")) || !strings.HasSuffix(q.Whole, "\n"+strings.Join(whole, "\n")) {
|
||
t.Fatalf("the ask does not carry the change masked and whole:\n%s\n--\n%s", q.Explanation, q.Whole)
|
||
}
|
||
if want := []string{"+ font: Inter 13", "+ shares: media=‹path›", "+ sources: recalbox=‹address›@‹path›"}; strings.Join(masked, "\n") != strings.Join(want, "\n") {
|
||
t.Errorf("masked: %q", masked)
|
||
}
|
||
if want := []string{"+ font: Inter 13", "+ shares: media=/storage/media", "+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox"}; strings.Join(whole, "\n") != strings.Join(want, "\n") {
|
||
t.Errorf("whole: %q", whole)
|
||
}
|
||
if _, ok := outward.Secret(q.Whole, "shanks"); !ok {
|
||
t.Error("the router would refuse the whole words")
|
||
}
|
||
// Mutation: the masked and the whole change differ in exactly the lines whose value was masked.
|
||
differ := 0
|
||
for i := range masked {
|
||
if masked[i] != whole[i] {
|
||
differ++
|
||
if !strings.Contains(masked[i], "‹") || strings.Contains(whole[i], "‹") {
|
||
t.Errorf("the lines differ otherwise than by the mask:\n%s\n%s", masked[i], whole[i])
|
||
}
|
||
}
|
||
}
|
||
if differ != 2 {
|
||
t.Errorf("%d lines differ, and the mask covered 2", differ)
|
||
}
|
||
// A value shaped like a secret is withheld in both, whole, with its key still named.
|
||
r1 := newProposerRig(t)
|
||
secrets := map[string]any{"github": "ghp_abcdefghijklmnopqrstuvwxyz0123456789", "cert": "-----BEGIN CERTIFICATE-----"}
|
||
if _, err := r1.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: secrets}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q1 := r1.askSent(t)
|
||
for _, line := range []string{"+ cert: ‹withheld›", "+ github: ‹withheld›"} {
|
||
if !strings.Contains(q1.Explanation, line) {
|
||
t.Errorf("lacks %q:\n%s", line, q1.Explanation)
|
||
}
|
||
}
|
||
for _, secret := range []string{"ghp_", "BEGIN CERTIFICATE"} {
|
||
if strings.Contains(q1.Explanation, secret) || strings.Contains(q1.Whole, secret) || strings.Contains(q1.Details, secret) {
|
||
t.Errorf("the secret %q reaches the phone", secret)
|
||
}
|
||
}
|
||
// More keys than points (issue 639): the first two, then the keys of the rest, read whole at the terminal.
|
||
r3 := newProposerRig(t)
|
||
five := map[string]any{"a": "1", "b": "2", "c": "3", "d": "4", "e": "5"}
|
||
if _, err := r3.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: five}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if q3 := r3.askSent(t); !strings.HasSuffix(q3.Explanation, "\n+ a: 1\n+ b: 2\nand 3 more, each in the note: c, d, e") || asks.Glance(q3.Explanation) != nil ||
|
||
!strings.Contains(q3.Details, "settings proposals") {
|
||
t.Errorf("five keys:\n%s", q3.Explanation)
|
||
}
|
||
// No value masked: the message is the same everywhere, and the ask says no whole words.
|
||
r2 := newProposerRig(t)
|
||
if _, err := r2.pr.propose(context.Background(), proposeInput{module: "dunst", node: "laptop", values: map[string]any{"font-size": 13, "width": 500}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if q2 := r2.askSent(t); q2.Whole != "" || !strings.HasSuffix(q2.Explanation, "\n+ font-size: 13\n+ width: 500") ||
|
||
strings.Contains(q2.Details, "not proven") {
|
||
t.Errorf("%+v", q2)
|
||
}
|
||
}
|
||
|
||
// A proposal expired unanswered is kept so by the reconciling, and a warrant for it afterwards sets nothing.
|
||
func TestAnExpiredProposalIsKeptExpired(t *testing.T) {
|
||
r := newAskerRig(t)
|
||
calls := withSetLayer(r)
|
||
a := aProposalAsked(t, r, "s7", aProposal(r.now))
|
||
r.now = r.now.Add(askApproveFor + time.Minute)
|
||
if err := r.a.reconcile(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := r.store["s7"]; got.State != string(asks.OutcomeExpired) || !strings.HasPrefix(got.Acted, "nothing") {
|
||
t.Errorf("kept as %+v", got)
|
||
}
|
||
answerWith(t, r, warrantOn(a, "approve", r.now.Add(-2*time.Minute)))
|
||
if len(*calls) != 0 {
|
||
t.Errorf("set after expiry: %+v", *calls)
|
||
}
|
||
}
|
||
|
||
// A clear is proposed too, and shows the layer it removes.
|
||
func TestAClearIsProposedAndShowsWhatItRemoves(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
r.before, r.had = map[string]any{"places": map[string]any{"data": map[string]any{"path": "/srv/notes", "owner": "1001:1001"}}}, true
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", clear: true}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
if q.Headline != "Clear notes on laptop?" || !strings.Contains(q.Explanation, "- places.data.owner: 1001:1001") ||
|
||
!strings.Contains(q.Explanation, "- places.data.path: ‹path›") || !strings.Contains(q.Whole, "- places.data.path: /srv/notes") ||
|
||
!strings.HasPrefix(q.Explanation, "Removes the 2 setting(s) of notes set on laptop; approved, the layer is removed and that is sent at once by a walk to laptop, one machine at a time") {
|
||
t.Errorf("%+v", q)
|
||
}
|
||
if p := r.theProposal(t).Proposal; !p.Clear || p.Digest != layerDigest(r.before) || len(r.judged) != 0 {
|
||
t.Errorf("a clear: %+v, judged %v", p, r.judged)
|
||
}
|
||
}
|
||
|
||
// What the phone is shown keeps a value's shape and passes the content rule: an address, a path, a secret's
|
||
// shape each replaced in place; a value every word of which may leave the mesh shown whole.
|
||
func TestAValueIsShownInItsShapeAndPassesTheContentRule(t *testing.T) {
|
||
for in, want := range map[any]string{
|
||
"recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro": "recalbox=‹address›@‹path›:ro",
|
||
"library=/mnt/library": "library=‹path›",
|
||
"none": "none",
|
||
"Inter 13": "Inter 13",
|
||
"10.77.0.9:53": "‹address›",
|
||
"jochen@example.com": "‹address›",
|
||
"nas.lan": "‹address›",
|
||
"anchor": "anchor",
|
||
"-----BEGIN CERTIFICATE-----": "‹withheld›",
|
||
42: "42",
|
||
true: "true",
|
||
} {
|
||
got, whole := sayableValue(in, []string{"anchor"})
|
||
if got != want {
|
||
t.Errorf("%v shown as %q, want %q", in, got, want)
|
||
}
|
||
if whole != (got == want && !strings.Contains(want, "‹")) {
|
||
t.Errorf("%v: whole %v", in, whole)
|
||
}
|
||
if _, ok := outward.Check(got, "anchor"); !ok {
|
||
t.Errorf("%v shown as %q, which the router refuses", in, got)
|
||
}
|
||
}
|
||
for _, v := range []any{[]any{"a", "/etc/x"}, map[string]any{"path": "/srv/x", "owner": "1001:1001"}} {
|
||
got, _ := sayableValue(v, nil)
|
||
if _, ok := outward.Check(got); !ok || strings.Contains(got, "/srv") || strings.Contains(got, "/etc") {
|
||
t.Errorf("%v shown as %q", v, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// A proposal that the mesh would refuse to set, or that would silently remove a key, is refused before anybody is
|
||
// asked (ADR 0217 holds for a proposal as for a set).
|
||
func TestAProposalTheMeshWouldRefuseIsNotAsked(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
r.before, r.had = map[string]any{"a": 1, "b": 2}, true
|
||
_, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"a": 1}})
|
||
if err == nil || !strings.Contains(err.Error(), "would no longer set b") || !strings.Contains(err.Error(), "ADR 0217") {
|
||
t.Errorf("a silent removal: %v", err)
|
||
}
|
||
r.refusedBy = "refused: notes on laptop cannot compose"
|
||
_, err = r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"a": 1, "b": 3}})
|
||
if err == nil || !strings.Contains(err.Error(), "cannot compose") {
|
||
t.Errorf("a layer the mesh refuses: %v", err)
|
||
}
|
||
if len(r.sent) != 0 || len(r.store) != 0 {
|
||
t.Errorf("asked anyway: %+v %+v", r.sent, r.store)
|
||
}
|
||
}
|
||
|
||
// Fail closed: no router, no grant, a publish that fails, or the router's refusal each leave nothing waiting for
|
||
// an answer that cannot come, and name the terminal's line.
|
||
func TestAProposalFailsClosedWhenNothingCanCarryIt(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
in := proposeInput{module: "mounts", node: "shanks", values: mountsSources}
|
||
r.pr.routerHere = func(context.Context) (bool, error) { return false, nil }
|
||
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "no router") ||
|
||
!strings.Contains(err.Error(), "mesh-cli settings set mounts") {
|
||
t.Errorf("without a router: %v", err)
|
||
}
|
||
r.pr.routerHere = nil
|
||
r.pr.grantHeld = func(context.Context) (bool, string, error) { return false, "the bus's user list is behind", nil }
|
||
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "user list is behind") {
|
||
t.Errorf("without the grant: %v", err)
|
||
}
|
||
if len(r.sent) != 0 || len(r.store) != 0 {
|
||
t.Fatalf("asked anyway: %+v %+v", r.sent, r.store)
|
||
}
|
||
r.pr.grantHeld = nil
|
||
r.pr.publish = func(context.Context, string, []byte, string) error { return errors.New("the bus is away") }
|
||
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "the bus is away") ||
|
||
!strings.Contains(err.Error(), "never become active") {
|
||
t.Errorf("a publish that fails: %v", err)
|
||
}
|
||
if kept := r.theProposal(t); kept.State != askUnsent {
|
||
t.Errorf("an unpublished proposal is %s", kept.State)
|
||
}
|
||
// The router's refusal, heard by the serving controller and kept here, is said to the caller.
|
||
r = newProposerRig(t)
|
||
r.pr.publish = func(_ context.Context, _ string, _ []byte, id string) error {
|
||
ask := strings.TrimPrefix(id, "ask.")
|
||
r.store[ask] = func() asked {
|
||
a := r.store[ask]
|
||
a.State, a.Acted = string(asks.OutcomeRefused), "nothing: the ask refused: no channel can carry any of its answers now"
|
||
return a
|
||
}()
|
||
return nil
|
||
}
|
||
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "did not ask the operator") ||
|
||
!strings.Contains(err.Error(), "no channel can carry") {
|
||
t.Errorf("the router's refusal: %v", err)
|
||
}
|
||
}
|
||
|
||
// The bounds: at most three asks open for the controller, and the same change not proposed twice.
|
||
func TestAProposalIsBounded(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
in := proposeInput{module: "mounts", node: "shanks", values: mountsSources}
|
||
if _, err := r.pr.propose(context.Background(), in); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "already proposed") {
|
||
t.Errorf("the same change twice: %v", err)
|
||
}
|
||
for _, node := range []string{"laptop", "anchor"} {
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: node, values: mountsSources}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
_, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"x": 1}})
|
||
if err == nil || !strings.Contains(err.Error(), "3 questions already wait") {
|
||
t.Errorf("a fourth: %v", err)
|
||
}
|
||
if len(r.sent) != 3 {
|
||
t.Errorf("published %d", len(r.sent))
|
||
}
|
||
}
|
||
|
||
// ---- the warrant ------------------------------------------------------------------------------------
|
||
|
||
// aProposalAsked keeps a proposal's ask in the asker rig's store, as propose keeps it.
|
||
func aProposalAsked(t *testing.T, r *askerRig, id string, p settingsProposal) asked {
|
||
t.Helper()
|
||
q, options := p.ask(id, nil)
|
||
if err := q.Check(r.now); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a := asked{ID: id, Condition: q.About, Ask: q, Actions: p.actions(id), Options: options, State: askOpen, Opened: r.now, Proposal: &p}
|
||
r.store[id] = a
|
||
return a
|
||
}
|
||
|
||
func aProposal(now time.Time) settingsProposal {
|
||
before := map[string]any{"shares": "none"}
|
||
return settingsProposal{Module: "mounts", Node: "shanks", Values: mountsSources, Replace: true, Before: before, HadLayer: true,
|
||
From: "g14/claude-code", At: now, Digest: layerDigest(mountsSources), BeforeDigest: layerDigest(before)}
|
||
}
|
||
|
||
// warrantOn is the router's warrant for a kept ask, choosing an option by id.
|
||
func warrantOn(a asked, option string, now time.Time) asks.Warrant {
|
||
o, _ := a.Ask.Option(option)
|
||
return asks.Warrant{Ask: a.ID, Asker: "mesh-controller", About: a.Ask.About, Outcome: asks.OutcomeChosen, Option: o.ID,
|
||
Label: o.Label, Level: o.Level, Channel: "telegram", Proofs: []string{"P1"}, At: now, AskDigest: a.Ask.Digest(),
|
||
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
|
||
}
|
||
|
||
type setCall struct {
|
||
p settingsProposal
|
||
ask string
|
||
setBy string
|
||
}
|
||
|
||
func withSetLayer(r *askerRig) *[]setCall {
|
||
var calls []setCall
|
||
r.a.setLayer = func(_ context.Context, p settingsProposal, askID, setBy string) (string, error) {
|
||
calls = append(calls, setCall{p, askID, setBy})
|
||
return "+ sources, ~ shares", nil
|
||
}
|
||
return &calls
|
||
}
|
||
|
||
// On Approve the layer is set once, with who approved it and through which channel kept beside it, and the warrant
|
||
// is recorded as the operator's decision; heard again, nothing more happens.
|
||
func TestTheLayerIsSetOnceOnTheOperatorsApproval(t *testing.T) {
|
||
r := newAskerRig(t)
|
||
calls := withSetLayer(r)
|
||
a := aProposalAsked(t, r, "s1", aProposal(r.now))
|
||
if err := r.a.reconcile(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := r.store["s1"]; got.State != askOpen {
|
||
t.Fatalf("the reconciling of conditions ended the proposal: %+v", got)
|
||
}
|
||
w := warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
answerWith(t, r, w)
|
||
answerWith(t, r, w) // heard again, or replayed
|
||
if len(*calls) != 1 {
|
||
t.Fatalf("set %d time(s): %+v", len(*calls), *calls)
|
||
}
|
||
c := (*calls)[0]
|
||
if c.ask != "s1" || c.p.Digest != layerDigest(mountsSources) ||
|
||
!strings.HasPrefix(c.setBy, "approved by the operator, as telegram identity 42 via telegram (telegram), user id verified at ") ||
|
||
!strings.Contains(c.setBy, "(ask s1, proposed by g14/claude-code)") {
|
||
t.Errorf("set by %q for %+v", c.setBy, c.p)
|
||
}
|
||
if len(r.called)+len(r.silenced) != 0 {
|
||
t.Errorf("a verb was called: %v %v", r.called, r.silenced)
|
||
}
|
||
if len(r.acts) != 1 {
|
||
t.Fatalf("hand-acts %+v", r.acts)
|
||
}
|
||
act := r.acts[0]
|
||
if act.Verb != handActWarrant || act.By != "the operator, as telegram identity 42" || act.Ask != "s1" ||
|
||
!slices.Contains(act.Args, "answer=approve") || !slices.Contains(act.Args, "values="+layerDigest(mountsSources)) ||
|
||
!strings.HasPrefix(act.Outcome, "done") || !personsDecision(act) {
|
||
t.Errorf("the record: %+v", act)
|
||
}
|
||
if got := r.store["s1"]; got.State != string(asks.OutcomeChosen) || !strings.HasPrefix(got.Acted, "done") {
|
||
t.Errorf("kept as %+v", got)
|
||
}
|
||
}
|
||
|
||
// Decline, expiry, the router's refusal, a cancel: nothing is set, and the proposal is recorded as ended.
|
||
func TestDeclineExpiryAndRefusalDiscardAProposal(t *testing.T) {
|
||
for name, outcome := range map[string]asks.Outcome{"declined": asks.OutcomeChosen, "expired": asks.OutcomeExpired,
|
||
"refused": asks.OutcomeRefused, "cancelled": asks.OutcomeCancelled, "replaced": asks.OutcomeReplaced} {
|
||
t.Run(name, func(t *testing.T) {
|
||
r := newAskerRig(t)
|
||
calls := withSetLayer(r)
|
||
a := aProposalAsked(t, r, "s2", aProposal(r.now))
|
||
w := warrantOn(a, "decline", r.now.Add(time.Hour))
|
||
if outcome != asks.OutcomeChosen {
|
||
w = asks.Warrant{Ask: a.ID, Asker: "mesh-controller", Outcome: outcome, Words: "its time passed", At: r.now.Add(time.Hour)}
|
||
}
|
||
answerWith(t, r, w)
|
||
if len(*calls) != 0 {
|
||
t.Fatalf("set: %+v", *calls)
|
||
}
|
||
got := r.store["s2"]
|
||
if got.State != string(outcome) || got.Acted == "" || !strings.HasPrefix(got.Acted, "nothing") {
|
||
t.Errorf("kept as %+v", got)
|
||
}
|
||
if outcome == asks.OutcomeChosen && (len(r.acts) != 1 || !strings.Contains(r.acts[0].Outcome, "declined")) {
|
||
t.Errorf("a decline is a decision too: %+v", r.acts)
|
||
}
|
||
// An approval after it ended is refused.
|
||
answerWith(t, r, warrantOn(a, "approve", r.now.Add(2*time.Hour)))
|
||
if len(*calls) != 0 {
|
||
t.Fatalf("set after the end: %+v", *calls)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// The warrant binds the exact values: a record whose values changed after the ask, an action changed, a warrant for
|
||
// another ask's digest, at another level, or after expiry each set nothing.
|
||
func TestAWarrantSetsOnlyTheExactValuesTheOperatorWasShown(t *testing.T) {
|
||
cases := map[string]func(r *askerRig, a asked) asks.Warrant{
|
||
"the values changed in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Values = map[string]any{"sources": "recalbox=smb://evil/recalbox@/mnt/recalbox", "shares": "library=/mnt/library"}
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the layer it was shown against changed in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Before = map[string]any{"shares": "other"}
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the module changed in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Module = "sshd"
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the machine changed in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Node = "anchor"
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the removal became meant in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Replace = !a.Proposal.Replace
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the set became a clear in the record": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Proposal.Clear = true
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"the action's digest was changed": func(r *askerRig, a asked) asks.Warrant {
|
||
a.Actions[0].Arguments["values"] = layerDigest(map[string]any{"sources": "x"})
|
||
r.store[a.ID] = a
|
||
return warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
},
|
||
"another ask's digest": func(r *askerRig, a asked) asks.Warrant {
|
||
w := warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
w.AskDigest = "sha256:0000"
|
||
return w
|
||
},
|
||
"at the level acknowledge": func(r *askerRig, a asked) asks.Warrant {
|
||
w := warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
w.Level = asks.Acknowledge
|
||
return w
|
||
},
|
||
"after expiry": func(r *askerRig, a asked) asks.Warrant {
|
||
return warrantOn(a, "approve", r.now.Add(askApproveFor+time.Minute))
|
||
},
|
||
"nobody chose": func(r *askerRig, a asked) asks.Warrant {
|
||
w := warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
w.By = nil
|
||
return w
|
||
},
|
||
"another asker's": func(r *askerRig, a asked) asks.Warrant {
|
||
w := warrantOn(a, "approve", r.now.Add(time.Hour))
|
||
w.Asker = "claude-code"
|
||
return w
|
||
},
|
||
}
|
||
for name, tamper := range cases {
|
||
t.Run(name, func(t *testing.T) {
|
||
r := newAskerRig(t)
|
||
calls := withSetLayer(r)
|
||
a := aProposalAsked(t, r, "s3", aProposal(r.now))
|
||
answerWith(t, r, tamper(r, a))
|
||
if len(*calls) != 0 {
|
||
t.Fatalf("set: %+v", *calls)
|
||
}
|
||
if got := r.store["s3"]; strings.HasPrefix(got.Acted, "done") {
|
||
t.Errorf("kept as done: %+v", got)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// A proposal counts toward what the controller holds open, so a fourth ask is not attempted while three are.
|
||
func TestOpenProposalsCountTowardTheAsksHeldOpen(t *testing.T) {
|
||
r := newAskerRig(t)
|
||
for _, id := range []string{"s4", "s5", "s6"} {
|
||
aProposalAsked(t, r, id, aProposal(r.now))
|
||
}
|
||
r.open = []conditions.Condition{heldCondition()}
|
||
if err := r.a.reconcile(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(r.sent) != 0 {
|
||
t.Errorf("asked beyond the bound: %d", len(r.sent))
|
||
}
|
||
for _, id := range []string{"s4", "s5", "s6"} {
|
||
if r.store[id].State != askOpen {
|
||
t.Errorf("%s was ended by the reconciling of conditions: %+v", id, r.store[id])
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- the verb ---------------------------------------------------------------------------------------
|
||
|
||
func TestTheSettingsVerbComposesProposeAndProposals(t *testing.T) {
|
||
for name, c := range map[string]struct {
|
||
args map[string]any
|
||
want string
|
||
}{
|
||
"propose on a machine": {map[string]any{"module": "mounts", "node": "shanks", "values": `{"sources":"x"}`, "propose": "true"},
|
||
"settings propose mounts {\"sources\":\"x\"} --node shanks"},
|
||
"propose with replace": {map[string]any{"module": "mounts", "values": `{"a":1}`, "propose": "true", "replace": "true"},
|
||
"settings propose mounts {\"a\":1} --replace"},
|
||
"propose a clear": {map[string]any{"module": "mounts", "node": "shanks", "propose": "true", "clear": "true"},
|
||
"settings propose mounts --clear --node shanks"},
|
||
"the proposals": {map[string]any{"proposals": "true"}, "settings proposals"},
|
||
"one proposal": {map[string]any{"proposal": "s1"}, "settings proposals s1"},
|
||
} {
|
||
argv, err := argvFor("settings", c.args)
|
||
if err != nil || strings.Join(argv, " ") != c.want {
|
||
t.Errorf("%s: %v %v", name, argv, err)
|
||
}
|
||
}
|
||
for name, args := range map[string]map[string]any{
|
||
"propose without a module": {"values": `{"a":1}`, "propose": "true"},
|
||
"propose without values": {"module": "mounts", "propose": "true"},
|
||
"propose values and clear": {"module": "mounts", "values": `{"a":1}`, "clear": "true", "propose": "true"},
|
||
"proposals with a module": {"proposals": "true", "module": "mounts"},
|
||
"proposals and a proposal": {"proposals": "true", "proposal": "s1"},
|
||
"a proposal with values": {"proposal": "s1", "values": `{"a":1}`},
|
||
"proposals with a listing": {"proposals": "true", "list": "preferences"},
|
||
} {
|
||
if _, err := argvFor("settings", args); err == nil {
|
||
t.Errorf("%s was composed", name)
|
||
}
|
||
}
|
||
// The generic command verb proposes nothing (it is the settings verb's), and lists proposals (a read).
|
||
if err := refusedAsTheGenericCommand([]string{"settings", "propose", "mounts", "{}", "--node", "shanks"}); err == nil {
|
||
t.Error("the generic command proposed")
|
||
}
|
||
if err := refusedAsTheGenericCommand([]string{"settings", "proposals"}); err != nil {
|
||
t.Errorf("the generic command may not list proposals: %v", err)
|
||
}
|
||
}
|
||
|
||
// ---- on the real stores -----------------------------------------------------------------------------
|
||
|
||
// setLayerIn sets the layer as the terminal does — judged, the removal meant, the history kept — with who approved it
|
||
// beside it; and refuses once the layer is no longer the one the operator was shown the change against.
|
||
func TestSetOnAWarrantJudgesTheLayerAndKeepsWhoApprovedIt(t *testing.T) {
|
||
open := aMesh(t)
|
||
ctx := t.Context()
|
||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
||
// y is a preference with a default, so a layer may leave it out; x is the operator's own (ADR 0262).
|
||
Settings: map[string]catalogue.SettingDeclaration{"y": {Kind: "preference", Default: "10", Why: "a size"}},
|
||
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
|
||
// A trusted file (unmarked), so its keys are the terminal's — and now the warrant's (novox/hq ADR 0277).
|
||
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "content": "x = ${setting:x}\ny = ${setting:y}\n"}}})
|
||
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
set := setLayerIn(open)
|
||
now := time.Now()
|
||
first := map[string]any{"x": "1", "y": "2"}
|
||
p := settingsProposal{Module: "notes", Node: "laptop", Values: first, From: "g14/claude-code", At: now,
|
||
Digest: layerDigest(first), BeforeDigest: layerDigest(nil)}
|
||
changed, err := set(ctx, p, "s9", "approved by the operator, as telegram identity 42 via telegram at 14:05 (ask s9)")
|
||
if err != nil || !strings.Contains(changed, "+ x") {
|
||
t.Fatalf("%q %v", changed, err)
|
||
}
|
||
layer, has, err := open.inventory.Layer(ctx, "laptop", "notes")
|
||
if err != nil || !has || layer["x"] != "1" {
|
||
t.Fatalf("the layer: %v %v %v", layer, has, err)
|
||
}
|
||
setBy, _, has, err := open.inventory.LayerOrigin(ctx, "laptop", "notes")
|
||
if err != nil || !has || !strings.HasPrefix(setBy, "approved by the operator, as telegram identity 42 via telegram") {
|
||
t.Fatalf("who set it: %q %v", setBy, err)
|
||
}
|
||
// Shown by `settings show`, at the terminal and through the verb.
|
||
out := captureStdout(t, func() {
|
||
if err := atTheTerminal(t, "settings", "show", "notes", "--node", "laptop"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
if !strings.Contains(out, "approved by the operator, as telegram identity 42 via telegram") {
|
||
t.Errorf("settings show says:\n%s", out)
|
||
}
|
||
// The same proposal again: the layer is no longer the one the operator was shown it against.
|
||
if _, err := set(ctx, p, "s9", "approved …"); err == nil || !strings.Contains(err.Error(), "changed since the operator was shown") {
|
||
t.Errorf("a stale proposal: %v", err)
|
||
}
|
||
// A removal not meant is refused; one meant is taken, and the history says who had set the layer.
|
||
second := map[string]any{"x": "1"}
|
||
q := settingsProposal{Module: "notes", Node: "laptop", Values: second, Before: first, HadLayer: true,
|
||
From: "g14/claude-code", At: now, Digest: layerDigest(second), BeforeDigest: layerDigest(first)}
|
||
if _, err := set(ctx, q, "s10", "approved …"); err == nil || !strings.Contains(err.Error(), "removal was not meant") {
|
||
t.Errorf("a silent removal: %v", err)
|
||
}
|
||
// A layer the mesh refuses is refused here too, with the same words as at the terminal.
|
||
bad := q
|
||
bad.Values, bad.Digest = map[string]any{"x": "a\nb", "y": "2"}, layerDigest(map[string]any{"x": "a\nb", "y": "2"})
|
||
if _, err := set(ctx, bad, "s11", "approved …"); err == nil || !strings.Contains(err.Error(), "line break") {
|
||
t.Errorf("a line break on a warrant: %v", err)
|
||
}
|
||
if layer, _, _ := open.inventory.Layer(ctx, "laptop", "notes"); layer["y"] != "2" {
|
||
t.Fatalf("a refused act changed the layer: %v", layer)
|
||
}
|
||
q.Replace = true
|
||
if _, err := set(ctx, q, "s10", "approved later"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
past, err := open.inventory.SettingsHistory(ctx, "laptop", "notes")
|
||
if err != nil || len(past) != 1 || !strings.HasPrefix(past[0].SetBy, "approved by the operator") {
|
||
t.Errorf("the history: %+v %v", past, err)
|
||
}
|
||
// And a clear, keeping who cleared it with the copy.
|
||
c := settingsProposal{Module: "notes", Node: "laptop", Clear: true, Before: second, HadLayer: true, From: "x", At: now,
|
||
Digest: layerDigest(second), BeforeDigest: layerDigest(second)}
|
||
if _, err := set(ctx, c, "s12", "approved by the operator at 15:00"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, has, _ := open.inventory.Layer(ctx, "laptop", "notes"); has {
|
||
t.Error("the layer was not cleared")
|
||
}
|
||
past, _ = open.inventory.SettingsHistory(ctx, "laptop", "notes")
|
||
if len(past) != 2 || !strings.Contains(past[0].SetBy, "cleared approved by the operator at 15:00") {
|
||
t.Errorf("the history after a clear: %+v", past)
|
||
}
|
||
}
|
||
|
||
// A layer set at the terminal says so, and one set through a verb names the verb (novox/hq ADR 0277).
|
||
func TestALayerSaysWhoSetIt(t *testing.T) {
|
||
open := aMesh(t)
|
||
ctx := t.Context()
|
||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
||
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false,
|
||
"content": "x = ${setting:x}\n"}}})
|
||
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := atTheTerminal(t, "settings", "set", "notes", `{"x":"1"}`, "--node", "laptop"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setBy, _, _, _ := open.inventory.LayerOrigin(ctx, "laptop", "notes")
|
||
if !strings.HasPrefix(setBy, "set at the controller's terminal by ") {
|
||
t.Errorf("at the terminal: %q", setBy)
|
||
}
|
||
if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": `{"x":"2"}`}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setBy, _, _, _ = open.inventory.LayerOrigin(ctx, "laptop", "notes")
|
||
if !strings.HasPrefix(setBy, "set by ") || !strings.Contains(setBy, "through settings") {
|
||
t.Errorf("through the verb: %q", setBy)
|
||
}
|
||
}
|
||
|
||
// A trusted setting is still refused through the settings verb (novox/hq issue 339), and the refusal now names the
|
||
// proposal as the way.
|
||
func TestATrustedSettingThroughAVerbNamesTheProposal(t *testing.T) {
|
||
open := aMesh(t)
|
||
ctx := t.Context()
|
||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
||
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"}}})
|
||
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop",
|
||
"values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}}}`})
|
||
if err == nil || !strings.Contains(err.Error(), "issue 339") || !strings.Contains(err.Error(), "propose") {
|
||
t.Errorf("%v", err)
|
||
}
|
||
if _, has, _ := open.inventory.Layer(ctx, "laptop", "notes"); has {
|
||
t.Error("a layer was kept")
|
||
}
|
||
}
|
||
|
||
// captureStdout runs f and answers what it printed to standard output.
|
||
func captureStdout(t *testing.T, f func()) string {
|
||
t.Helper()
|
||
before := os.Stdout
|
||
r, w, err := os.Pipe()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
os.Stdout = w
|
||
done := make(chan string)
|
||
go func() {
|
||
var b strings.Builder
|
||
_, _ = io.Copy(&b, r)
|
||
done <- b.String()
|
||
}()
|
||
f()
|
||
os.Stdout = before
|
||
_ = w.Close()
|
||
return <-done
|
||
}
|
||
|
||
// The review of #304: Approve sets exactly what the operator can read. A proposal of five keys, one value 300
|
||
// characters long, shows two keys at a glance, says what it cut, and carries every key with its whole value in its
|
||
// Details for a channel that proves who answers (WholeDetails), and every key masked for one that does not.
|
||
func TestEveryKeyAndItsWholeValueReachTheOperator(t *testing.T) {
|
||
var long strings.Builder
|
||
long.WriteString("library=/storage")
|
||
for i := 0; long.Len() < 300; i++ {
|
||
long.WriteString(fmt.Sprintf("/album-%02d", i))
|
||
}
|
||
value := long.String()[:300]
|
||
values := map[string]any{"a": "1", "b": "2", "c": "3", "d": "4", "shares": value}
|
||
r := newProposerRig(t)
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: values}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
if err := q.Check(r.now); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// What the operator reads on a proving channel: the whole words, then the note's whole details.
|
||
visible := q.Whole + "\n" + q.WholeDetails
|
||
if q.Whole == "" {
|
||
visible = q.Explanation + "\n" + q.WholeDetails
|
||
}
|
||
for k, v := range values {
|
||
if !strings.Contains(visible, "+ "+k+": "+v.(string)) {
|
||
t.Errorf("the operator never reads %s whole:\n%s", k, visible)
|
||
}
|
||
}
|
||
// The masked glance points to a note that holds the values masked, and says no more; the whole glance, shown only
|
||
// beside the whole details, says they are whole there.
|
||
if asks.Glance(q.Explanation) != nil || !strings.HasSuffix(q.Explanation, "\nand 3 more, each in the note: c, d, shares") ||
|
||
strings.Contains(q.Explanation, "whole in the note") || !strings.HasSuffix(q.Whole, "\nand 3 more, each whole in the note: c, d, shares") {
|
||
t.Errorf("the glance:\n%s", q.Explanation)
|
||
}
|
||
for k := range values {
|
||
if !strings.Contains(q.Details, "+ "+k+": ") {
|
||
t.Errorf("the masked details lack %s:\n%s", k, q.Details)
|
||
}
|
||
}
|
||
if strings.Contains(q.Details, "/storage") {
|
||
t.Errorf("the masked details show a path:\n%s", q.Details)
|
||
}
|
||
if refusal, ok := outward.Check(q.Details, "shanks"); !ok {
|
||
t.Errorf("the router would withhold the masked details: %s", refusal)
|
||
}
|
||
if _, ok := outward.Secret(q.WholeDetails, "shanks"); !ok {
|
||
t.Error("the router would refuse the whole details")
|
||
}
|
||
// A value too long for a glance is cut there, and says so.
|
||
r2 := newProposerRig(t)
|
||
if _, err := r2.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: map[string]any{"shares": value}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q2 := r2.askSent(t)
|
||
// The cut point is 160 characters, its marker inside the bound.
|
||
if last := q2.Whole[strings.LastIndex(q2.Whole, "\n")+1:]; len([]rune(last)) != asks.PointLength {
|
||
t.Errorf("the cut point is %d characters, not %d: %q", len([]rune(last)), asks.PointLength, last)
|
||
}
|
||
if !strings.HasSuffix(q2.Whole, "… (cut: whole in the note)") || !strings.HasSuffix(q2.Explanation, "‹path›") && !strings.HasSuffix(q2.Explanation, "… (cut: in the note)") || !strings.Contains(q2.WholeDetails, "+ shares: "+value) || asks.Glance(q2.Whole) != nil {
|
||
t.Errorf("the cut is not said, or the note lacks the whole:\n%s\n--\n%s", q2.Whole, q2.WholeDetails)
|
||
}
|
||
}
|
||
|
||
// The review of #304: a value given to a secret's name, past the second key, is said with its key named and its
|
||
// value withheld, in the note on every channel; the router has nothing left to withhold unnamed or refuse. A line
|
||
// the router would refuse even so is refused at propose, in the controller's words.
|
||
func TestASecretNamedKeyIsNamedAndItsValueWithheld(t *testing.T) {
|
||
r := newProposerRig(t)
|
||
values := map[string]any{"a": "1", "b": "2", "c": "3", "password": "hunter2"}
|
||
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: values}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
q := r.askSent(t)
|
||
for _, form := range []string{q.Details, q.WholeDetails, q.Explanation, q.Whole} {
|
||
if strings.Contains(form, "hunter2") {
|
||
t.Errorf("the value reaches the phone:\n%s", form)
|
||
}
|
||
}
|
||
if !strings.Contains(q.Details, "+ password (value withheld)") {
|
||
t.Errorf("the key is not named with its value withheld:\n%s", q.Details)
|
||
}
|
||
if q.WholeDetails != "" && !strings.Contains(q.WholeDetails, "+ password (value withheld)") {
|
||
t.Errorf("the whole details do not name the key:\n%s", q.WholeDetails)
|
||
}
|
||
if line, why := judgeLines(q.Details, false, nil); line != "" {
|
||
t.Errorf("the router would withhold %q: %s", line, why)
|
||
}
|
||
if line, why := judgeLines(q.WholeDetails, true, nil); line != "" {
|
||
t.Errorf("the router would refuse %q: %s", line, why)
|
||
}
|
||
if !strings.HasSuffix(q.Explanation, "\nand 2 more, each in the note: c, password") {
|
||
t.Errorf("the glance:\n%s", q.Explanation)
|
||
}
|
||
// A key shaped like a secret cannot be named on any channel: refused at propose, in the controller's words, and
|
||
// the key itself not echoed.
|
||
r2 := newProposerRig(t)
|
||
secretKey := "ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
||
_, err := r2.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop",
|
||
values: map[string]any{"a": "1", "b": "2", secretKey: "3"}})
|
||
if err == nil || !strings.Contains(err.Error(), "Nothing was proposed") || strings.Contains(err.Error(), secretKey) {
|
||
t.Errorf("a secret-shaped key: %v", err)
|
||
}
|
||
if len(r2.sent) != 0 {
|
||
t.Errorf("the refused proposal was asked")
|
||
}
|
||
}
|
||
|
||
// keyOfLine names a change's key, and never one shaped like a secret.
|
||
func TestARefusalNamesTheKeyAndNeverASecret(t *testing.T) {
|
||
for line, want := range map[string]string{
|
||
"+ password (value withheld)": "password",
|
||
"~ shares: ‹path› (was: none)": "shares",
|
||
"+ ghp_abcdefghijklmnopqrstuvwxyz0123456789: 1": "whose key is shaped like a secret",
|
||
"Proposed by g14 at 15:04.": "Proposed by g14 at 15:04.",
|
||
} {
|
||
if got := keyOfLine(line); got != want {
|
||
t.Errorf("%q: %q, want %q", line, got, want)
|
||
}
|
||
}
|
||
}
|