The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold, no summary. A settings proposal was the same wall of text, its values masked so the approval was blind. - messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each answer does, the expiry, where else it is answered and the asker's Details travel as the message's note (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone (from the earlier work on issue 383, #178). - telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message shown before is still compared by the digest it was kept under. - desk-channel: the note follows the body. - mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is composed again as it was asked (form), so its warrant still holds. - claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the module's), its text in the Details; a question's context is at most 300 characters and its recommendation one point. Older proposals and questions keep their form. The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and asks.Glance.
156 lines
7.2 KiB
Go
156 lines
7.2 KiB
Go
package main
|
||
|
||
import (
|
||
"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)
|
||
}
|
||
}
|
||
}
|