mesh/merge-gate head f09daabec3a8 merged into trunk 9c166398f204 — pass: builds claude-code, desk-channel, mesh-knowledge, messenger, telegram → ace, g14, …
mesh/repo-check head f09daabec3a8 merged into trunk 9c166398f204 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance delivered: every member is delivered
The router's fallback said a refused note line as "(a line carrying … is withheld here)", so the operator could not tell which setting held a value they were not shown. A change's line now keeps its key; any other line, or one whose key is refused too, is said withheld whole. The SDK pin moves to mesh-sdk 5933eb7, where no two asks digest the same.
231 lines
11 KiB
Go
231 lines
11 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||
)
|
||
|
||
// novox/hq issue 383: a settings proposal's values are what the operator approves, so where the sender is proven
|
||
// they are shown whole; where nothing is proven, the explanation under the whole content rule stands.
|
||
|
||
// proposalAsk is a proposal as the controller composes it: the explanation with its values masked, the whole
|
||
// with them whole, and Details with the fingerprint and where to read it, which the note shows.
|
||
func proposalAsk(c *clock, id string) asks.Ask {
|
||
a := heldAsk(c, id)
|
||
a.Headline = "Set mounts on shanks?"
|
||
a.Options = a.Options[:2]
|
||
a.Explanation = "+ shares: media=‹path›\n+ sources: recalbox=‹address›@‹path›\nProposed by an agent at 15:11."
|
||
a.Whole = "+ shares: media=/storage/media\n+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox\nProposed by an agent at 15:11."
|
||
a.Details = "Fingerprint 1234 5678 9abc def0 1234 5678.\nRead it whole: settings proposals " + id + " at the controller's terminal."
|
||
return a
|
||
}
|
||
|
||
func TestAnAsksWholeWordsAreShownWhereTheSenderIsProvenAndMaskedElsewhere(t *testing.T) {
|
||
h, tg, _, c, store, w := newRouter(t)
|
||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||
submit(t, h, "mesh-controller", proposalAsk(c, "s-1"))
|
||
settle(h, c)
|
||
if len(tg.sends) != 1 {
|
||
t.Fatalf("telegram got %d messages", len(tg.sends))
|
||
}
|
||
phone := tg.sends[0]
|
||
for _, want := range []string{"+ shares: media=/storage/media", "recalbox=smb://nas.lan/recalbox@/mnt/recalbox", "Proposed by an agent at 15:11."} {
|
||
if !strings.Contains(phone.Body, want) {
|
||
t.Errorf("the phone, which proves who answers, lacks %q:\n%s", want, phone.Body)
|
||
}
|
||
}
|
||
if strings.Contains(phone.Body, "‹") || strings.Contains(phone.Body, "Fingerprint") || strings.Contains(phone.Body, "settings proposals") {
|
||
t.Errorf("the phone shows a masked value, or the Details' words, in the body:\n%s", phone.Body)
|
||
}
|
||
// The Details are the note's, after what the answers do (issue 639): no answer of their own.
|
||
if !strings.Contains(phone.Note, "Fingerprint 1234 5678 9abc def0 1234 5678.") ||
|
||
!strings.Contains(phone.Note, "Read it whole: settings proposals s-1 at the controller's terminal.") {
|
||
t.Errorf("the phone's note lacks the Details:\n%s", phone.Note)
|
||
}
|
||
if phone.Title != "Set mounts on shanks?" || ticketFor(phone, "Release") == "" || ticketFor(phone, DetailsLabel) != "" {
|
||
t.Errorf("the phone's message: %q offering %s", phone.Title, labelsOf(phone))
|
||
}
|
||
// The desk proves nothing: the same record composed for it shows the explanation, its values masked.
|
||
r := h.open["ask.mesh-controller.s-1"]
|
||
if r == nil {
|
||
t.Fatal("no record of the ask")
|
||
}
|
||
desk := h.sayOn(r, "raised", "desktop")
|
||
if !strings.Contains(desk.Body, "+ shares: media=‹path›") || strings.Contains(desk.Text(), "/storage/media") || strings.Contains(desk.Text(), "nas.lan") {
|
||
t.Errorf("the desk shows a value whole:\n%s", desk.Text())
|
||
}
|
||
if got := w.of("mesh-controller"); len(got) != 0 || store.asks["mesh-controller.s-1"].State != StateOpen {
|
||
t.Errorf("the ask was answered: %+v / %s", got, store.asks["mesh-controller.s-1"].State)
|
||
}
|
||
// Approved, the message is edited to one line that says so, with the headline; the body is not said again
|
||
// (issue 639).
|
||
if out, err := h.Chosen("telegram", choiceOf(ticketFor(phone, "Release"), "42", c.now())); err != nil || out != "chosen" {
|
||
t.Fatalf("Release: %q %v", out, err)
|
||
}
|
||
edited, ok := tg.edits["1"]
|
||
if !ok {
|
||
t.Fatalf("the phone's message was not edited: %+v", tg.edits)
|
||
}
|
||
if !strings.HasPrefix(edited.Title, "You chose Release at ") || !strings.HasSuffix(edited.Title, " · Set mounts on shanks?") ||
|
||
edited.Body != "" || len(edited.Actions) != 0 {
|
||
t.Errorf("the edited message: %q\n%s\nactions %+v", edited.Title, edited.Body, edited.Actions)
|
||
}
|
||
}
|
||
|
||
// The mutation on the switch: the same ask without whole words is shown on the phone as on the desk, from its
|
||
// explanation; the whole words alone make the difference, and only where the sender is proven.
|
||
func TestAnAskWithoutWholeWordsIsShownFromItsExplanationEverywhere(t *testing.T) {
|
||
h, tg, _, c, store, _ := newRouter(t)
|
||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||
a := proposalAsk(c, "s-2")
|
||
a.Whole, a.Details = "", ""
|
||
submit(t, h, "mesh-controller", a)
|
||
settle(h, c)
|
||
if len(tg.sends) != 1 || !strings.Contains(tg.sends[0].Body, "+ shares: media=‹path›") || strings.Contains(tg.sends[0].Body, "/storage/media") {
|
||
t.Fatalf("telegram: %+v", tg.sends)
|
||
}
|
||
if ticketFor(tg.sends[0], DetailsLabel) != "" || strings.Contains(tg.sends[0].Note, "Fingerprint") {
|
||
t.Errorf("an ask that says nothing for Details offers or notes it: %s\n%s", labelsOf(tg.sends[0]), tg.sends[0].Note)
|
||
}
|
||
// A kind that proves nothing never shows the whole words, whatever the ask says.
|
||
a = proposalAsk(c, "s-3")
|
||
submit(t, h, "mesh-controller", a)
|
||
settle(h, c)
|
||
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "desktop"); whole != "" {
|
||
t.Errorf("the desk is given the whole words: %q", whole)
|
||
}
|
||
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "telegram"); whole != a.Whole {
|
||
t.Errorf("the phone is not given the whole words: %q", whole)
|
||
}
|
||
}
|
||
|
||
// A secret's shape leaves the mesh nowhere: an ask whose whole words carry one is refused as a whole, so the
|
||
// phone never sees it, with or without the proven sender.
|
||
func TestAnAskWhoseWholeWordsCarryASecretIsRefused(t *testing.T) {
|
||
// A path is read as a path, whatever its case and digits; a random run outside one is still a secret.
|
||
for text, may := range map[string]bool{
|
||
"+ shares: photos=/mnt/Photos_2024/Jochen": true,
|
||
"+ sources: games=smb://nas.lan/Recalbox_Games2024": true,
|
||
"+ library: /srv/media/Series_Archive/Season01": true,
|
||
"+ key: xK9mQ2vL8pR4tW7yB3nF6hJ1dG5sA0zC": false,
|
||
} {
|
||
if _, ok := CheckSecret(text); ok != may {
|
||
t.Errorf("CheckSecret(%q) lets it leave: %v", text, ok)
|
||
}
|
||
}
|
||
// A condition's record never shows an ask's whole words, whichever kind: its words are its own.
|
||
{
|
||
h, _, _, c, store, _ := newRouter(t)
|
||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||
k := cond("delivery.hq.held", Warning, "the delivery of hq is held")
|
||
k.Raised = c.now()
|
||
h.Condition(EventRaised, k)
|
||
settle(h, c)
|
||
a := proposalAsk(c, "s-5")
|
||
a.About = "delivery.hq.held"
|
||
submit(t, h, "mesh-controller", a)
|
||
settle(h, c)
|
||
if r := h.open["delivery.hq.held"]; r == nil || r.Ask != "mesh-controller.s-5" || h.wholeOf(r, "telegram") != "" {
|
||
t.Errorf("a condition's record is given an ask's whole words")
|
||
}
|
||
}
|
||
for _, secret := range []string{
|
||
"+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789",
|
||
"+ password=hunter2",
|
||
"+ key: 0123456789abcdef0123456789abcdef",
|
||
"-----BEGIN PRIVATE KEY-----",
|
||
} {
|
||
h, tg, _, c, store, w := newRouter(t)
|
||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||
a := proposalAsk(c, "s-4")
|
||
a.Whole = "+ shares: media=/storage/media\n" + secret
|
||
submit(t, h, "mesh-controller", a)
|
||
settle(h, c)
|
||
got := w.of("mesh-controller")
|
||
if len(tg.sends) != 0 || len(got) != 1 || got[0].Outcome != asks.OutcomeRefused || !strings.Contains(got[0].Words, "whole words carry secret") {
|
||
t.Errorf("%q: sent %d, told %+v", secret, len(tg.sends), got)
|
||
}
|
||
if _, kept := store.asks["mesh-controller.s-4"]; kept {
|
||
t.Errorf("%q: the refused ask was recorded", secret)
|
||
}
|
||
}
|
||
}
|
||
|
||
// The review of mesh-controller #304: a proposal of five keys, one of them 300 characters long, shows two keys at
|
||
// a glance; every key and every value, whole, is in the note on a kind that proves who answers, so the operator can
|
||
// read exactly what Approve sets. The desk, which proves nothing, shows them masked.
|
||
func TestEveryKeyAndItsWholeValueIsInTheNoteWhereTheSenderIsProven(t *testing.T) {
|
||
h, tg, _, c, store, _ := newRouter(t)
|
||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||
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]
|
||
keys := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "shares": value}
|
||
masked := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "shares": "library=‹path›"}
|
||
lines := func(values map[string]string) string {
|
||
var out []string
|
||
for _, k := range []string{"a", "b", "c", "d", "shares"} {
|
||
out = append(out, "+ "+k+": "+values[k])
|
||
}
|
||
return strings.Join(out, "\n")
|
||
}
|
||
a := proposalAsk(c, "s-9")
|
||
a.Explanation = "Changes 5 setting(s) of mounts on shanks.\n+ a: 1\n+ b: 2\nand 3 more, each whole below: c, d, shares"
|
||
a.Whole = ""
|
||
a.Details = "Fingerprint 1234 5678 9abc def0 1234 5678.\n" + lines(masked)
|
||
a.WholeDetails = "Fingerprint 1234 5678 9abc def0 1234 5678.\n" + lines(keys)
|
||
submit(t, h, "mesh-controller", a)
|
||
settle(h, c)
|
||
if len(tg.sends) != 1 {
|
||
t.Fatalf("telegram got %d messages", len(tg.sends))
|
||
}
|
||
phone := tg.sends[0]
|
||
for k, v := range keys {
|
||
if !strings.Contains(phone.Note, "+ "+k+": "+v) {
|
||
t.Errorf("the phone's note lacks %s whole:\n%s", k, phone.Note)
|
||
}
|
||
}
|
||
if strings.Contains(phone.Body, "/storage") || strings.Count(phone.Body, "\n") > 3 {
|
||
t.Errorf("the body is not the glance:\n%s", phone.Body)
|
||
}
|
||
desk := h.sayOn(h.open["ask.mesh-controller.s-9"], "raised", "desktop")
|
||
if strings.Contains(desk.Text(), "/storage") || !strings.Contains(desk.Note, "+ shares: library=‹path›") {
|
||
t.Errorf("the desk shows a value whole, or not every key:\n%s", desk.Note)
|
||
}
|
||
// A secret's shape in the whole details refuses the ask as a whole.
|
||
b := proposalAsk(c, "s-10")
|
||
b.WholeDetails = "+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
||
submit(t, h, "mesh-controller", b)
|
||
settle(h, c)
|
||
if _, kept := store.asks["mesh-controller.s-10"]; kept || len(tg.sends) != 1 {
|
||
t.Errorf("an ask whose whole details carry a secret was taken")
|
||
}
|
||
}
|
||
|
||
// A change's line the rule refuses keeps its key's name in the note (the review of mesh-controller #304): the
|
||
// operator knows which setting holds a value they are not shown. A line whose key the rule refuses too is said
|
||
// withheld whole.
|
||
func TestAWithheldChangeLineKeepsItsKey(t *testing.T) {
|
||
for line, want := range map[string]string{
|
||
"+ password: hunter2": "+ password (a value carrying a value given to a secret's name is withheld here)",
|
||
"~ shares: /mnt/media (was: x)": "~ shares (a value carrying a file path is withheld here)",
|
||
"Read it at /etc/x": "(a line carrying a file path is withheld here)",
|
||
"+ ghp_abcdefghijklmnopqrstuvwxyz0123456789: 1": "(a line carrying ",
|
||
} {
|
||
refusal, ok := Check(line)
|
||
if ok {
|
||
t.Fatalf("%q passes the content rule", line)
|
||
}
|
||
if got := withheldLine(line, refusal, Check); !strings.HasPrefix(got, want) {
|
||
t.Errorf("%q: %q, want %q", line, got, want)
|
||
}
|
||
}
|
||
}
|