1062 lines
41 KiB
Go
1062 lines
41 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// The router of novox/hq ADR 0259: it holds every ask, judges each answer and issues a warrant; it
|
|
// performs nothing. These tests hold it to every rule of the record's §4, §5 and §7.
|
|
|
|
// memAsks is the router's state in memory, with the compare-and-set the bus gives.
|
|
type memAsks struct {
|
|
mu sync.Mutex
|
|
asked map[string][]time.Time
|
|
asks map[string]AskRecord
|
|
revs map[string]uint64
|
|
ids map[string]Identity
|
|
links map[string]PendingLink
|
|
unlinked map[string]Unlinked
|
|
rev uint64
|
|
}
|
|
|
|
func newMemAsks() *memAsks {
|
|
return &memAsks{asks: map[string]AskRecord{}, revs: map[string]uint64{}, ids: map[string]Identity{},
|
|
links: map[string]PendingLink{}, unlinked: map[string]Unlinked{}, asked: map[string][]time.Time{}}
|
|
}
|
|
|
|
func clone(r AskRecord) AskRecord {
|
|
raw, _ := json.Marshal(r)
|
|
var out AskRecord
|
|
_ = json.Unmarshal(raw, &out)
|
|
return out
|
|
}
|
|
|
|
func (m *memAsks) GetAsk(k string) (*AskRecord, uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
r, ok := m.asks[k]
|
|
if !ok {
|
|
return nil, 0, nil
|
|
}
|
|
c := clone(r)
|
|
return &c, m.revs[k], nil
|
|
}
|
|
func (m *memAsks) CreateAsk(k string, r AskRecord) (uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, ok := m.asks[k]; ok {
|
|
return 0, ErrChanged
|
|
}
|
|
m.rev++
|
|
m.asks[k], m.revs[k] = clone(r), m.rev
|
|
return m.rev, nil
|
|
}
|
|
func (m *memAsks) UpdateAsk(k string, r AskRecord, rev uint64) (uint64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.revs[k] != rev {
|
|
return 0, ErrChanged
|
|
}
|
|
m.rev++
|
|
m.asks[k], m.revs[k] = clone(r), m.rev
|
|
return m.rev, nil
|
|
}
|
|
func (m *memAsks) AskKeys() ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []string
|
|
for k := range m.asks {
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (m *memAsks) GetIdentity(k string) (*Identity, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
id, ok := m.ids[k]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &id, nil
|
|
}
|
|
func (m *memAsks) PutIdentity(k string, id Identity) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.ids[k] = id
|
|
return nil
|
|
}
|
|
func (m *memAsks) DeleteIdentity(k string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.ids, k)
|
|
return nil
|
|
}
|
|
func (m *memAsks) IdentityKeys() ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []string
|
|
for k := range m.ids {
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
func (m *memAsks) GetLink(kind string) (*PendingLink, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
l, ok := m.links[kind]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &l, nil
|
|
}
|
|
func (m *memAsks) PutLink(kind string, l PendingLink) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.links[kind] = l
|
|
return nil
|
|
}
|
|
func (m *memAsks) DeleteLink(kind string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.links, kind)
|
|
return nil
|
|
}
|
|
func (m *memAsks) GetUnlinked(kind string) (*Unlinked, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
u, ok := m.unlinked[kind]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &u, nil
|
|
}
|
|
func (m *memAsks) PutUnlinked(kind string, u Unlinked) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.unlinked[kind] = u
|
|
return nil
|
|
}
|
|
func (m *memAsks) GetLinksAsked(key string) ([]time.Time, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return append([]time.Time(nil), m.asked[key]...), nil
|
|
}
|
|
func (m *memAsks) PutLinksAsked(key string, times []time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.asked[key] = append([]time.Time(nil), times...)
|
|
return nil
|
|
}
|
|
|
|
// rootFreeOf is each test router's machines said not root-free, by why.
|
|
var rootFreeOf = map[*Holder]map[string]string{}
|
|
|
|
// told is what the router published to askers, by subject.
|
|
type told struct{ by map[string][]asks.Warrant }
|
|
|
|
func (w *told) of(asker string) []asks.Warrant { return w.by[asks.DecidedSubject(asker)] }
|
|
|
|
// newRouter is a router with two kinds: telegram, which reaches the operator away and verifies its sender,
|
|
// and the desk, which does neither — as the controller would issue them from the claims.
|
|
func newRouter(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memAsks, *told) {
|
|
t.Helper()
|
|
h, tg, dt, c, _ := newHolder(t)
|
|
store := newMemAsks()
|
|
w := &told{by: map[string][]asks.Warrant{}}
|
|
bench := NewBench(c.now)
|
|
bench.SetKinds([]KindInfo{
|
|
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit",
|
|
"verified-sender", "exact-render"}},
|
|
{Kind: "desktop", Module: "desk-channel", Node: "anchor", Capabilities: []string{"deliver", "choice", "edit", "exact-render", "private"}},
|
|
})
|
|
bench.To = func(kind string) string {
|
|
if ids := h.identitiesOf(kind); len(ids) > 0 {
|
|
return ids[0].Identity
|
|
}
|
|
return ""
|
|
}
|
|
bench.Stand("telegram", Standing{Ready: true, At: c.now()})
|
|
bench.Stand("desktop", Standing{Ready: true, At: c.now()})
|
|
h.Bench, h.Asks = bench, store
|
|
h.Node = "anchor"
|
|
// The controller's word on root, as its verb root-free answers it: free unless a test says otherwise.
|
|
notFree := map[string]string{}
|
|
rootFreeOf[h] = notFree
|
|
h.RootFree = func(machines []string) (map[string]RootVerdict, error) {
|
|
out := map[string]RootVerdict{}
|
|
for _, m := range machines {
|
|
why, no := notFree[m]
|
|
out[m] = RootVerdict{Machine: m, Free: !no, Why: why}
|
|
}
|
|
return out, nil
|
|
}
|
|
h.Zone = func() *time.Location { return time.UTC }
|
|
h.Publish = func(subject string, body any, id string) error {
|
|
if wt, ok := body.(asks.Warrant); ok {
|
|
w.by[subject] = append(w.by[subject], wt)
|
|
}
|
|
return nil
|
|
}
|
|
return h, tg, dt, c, store, w
|
|
}
|
|
|
|
// stand keeps the channels' word fresh as the clock moves.
|
|
func stand(h *Holder, c *clock) {
|
|
// The controller's word on the kinds, read again as the router does every half minute.
|
|
h.Bench.SetKinds(h.Bench.Kinds())
|
|
h.Bench.Stand("telegram", Standing{Ready: true, At: c.now()})
|
|
h.Bench.Stand("desktop", Standing{Ready: true, At: c.now()})
|
|
}
|
|
|
|
// bound is what an option of the held delivery's ask binds: the asker's act, digested.
|
|
func bound(verb string) string {
|
|
d, err := asks.ActDigest(asks.Act{"verb": "mesh-delivery." + verb, "id": "novox/hq@0555"})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func heldAsk(c *clock, id string) asks.Ask {
|
|
return asks.Ask{ID: id, Headline: "Delivery of hq held for 36 hours",
|
|
Explanation: "Needs you: release it, or stop it. A delivery of hq has been held for 36 hours.",
|
|
Who: asks.Operator, Expires: c.now().Add(2 * time.Hour), OnExpiry: "the delivery stays held",
|
|
Options: []asks.Option{
|
|
{ID: "release", Label: "Release", Does: "the delivery goes on", Level: asks.Approve, Binds: bound("release")},
|
|
{ID: "stop", Label: "Stop", Does: "the delivery ends", Level: asks.Approve, Binds: bound("stop")},
|
|
{ID: "silence", Label: "Silence for a week", Does: "nothing more is said of it for a week", Level: asks.Acknowledge},
|
|
}}
|
|
}
|
|
|
|
func submit(t *testing.T, h *Holder, asker string, a asks.Ask) {
|
|
t.Helper()
|
|
raw, _ := json.Marshal(a)
|
|
if err := h.TakeAsk("ask", asker, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func link(h *Holder, store *memAsks, c *clock, identity string, at time.Time) {
|
|
store.ids[identityKey("telegram", identity)] = Identity{Kind: "telegram", Identity: identity, Display: "Jo",
|
|
Linked: at, ApproveFrom: at.Add(LinkedApproveAfter)}
|
|
}
|
|
|
|
func choiceOf(ticket, identity string, at time.Time) asks.Choice {
|
|
return asks.Choice{ID: "c-" + ticket + "-" + identity, Ticket: ticket, At: at,
|
|
Sender: asks.Sender{Identity: identity, Display: "Jo", Authenticated: true}}
|
|
}
|
|
|
|
func ticketFor(m Message, label string) string {
|
|
for _, a := range m.Actions {
|
|
if a.Label == label {
|
|
return a.Token
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func labelsOf(m Message) string {
|
|
var out []string
|
|
for _, a := range m.Actions {
|
|
out = append(out, a.Label)
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
// ackAsk is an ask of acknowledgements alone: silencing the held delivery.
|
|
func ackAsk(c *clock, id string) asks.Ask {
|
|
a := heldAsk(c, id)
|
|
a.Options = a.Options[2:]
|
|
a.Explanation = "Needs you: silence it, or leave it. A delivery of hq has been held for 36 hours."
|
|
return a
|
|
}
|
|
|
|
// An ask that offers an approval is answered only where it could be approved, whichever option is chosen (the
|
|
// review of 2026-10-09): the desk, which proves nothing of who clicked, is offered none of it. An ask of
|
|
// acknowledgements alone is offered on every kind that carries a choice.
|
|
func TestAnAskIsOfferedOnEachKindAsFarAsItCanCarryIt(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
a := heldAsk(c, "d-1")
|
|
a.Urgent = true
|
|
submit(t, h, "mesh-delivery", a)
|
|
settle(h, c)
|
|
if len(tg.sends) != 1 || len(dt.sends) != 0 {
|
|
t.Fatalf("telegram %d, desk %d", len(tg.sends), len(dt.sends))
|
|
}
|
|
if got := labelsOf(tg.sends[0]); got != "Release, Stop, Silence for a week" {
|
|
t.Errorf("telegram offers %q", got)
|
|
}
|
|
if rec := store.asks["mesh-delivery.d-1"]; rec.State != StateOpen || len(rec.Tickets) != 3 {
|
|
t.Errorf("recorded %+v", rec)
|
|
}
|
|
b := ackAsk(c, "d-2")
|
|
b.Urgent = true
|
|
b.About = "other"
|
|
submit(t, h, "mesh-delivery", b)
|
|
settle(h, c)
|
|
if len(dt.sends) != 1 || labelsOf(dt.sends[0]) != "Silence for a week" {
|
|
t.Fatalf("the desk was not offered an acknowledgement: %+v", dt.sends)
|
|
}
|
|
}
|
|
|
|
func TestWithoutALinkedAccountOnlyTheMeshMCPServerApproves(t *testing.T) {
|
|
h, tg, dt, c, _, w := newRouter(t)
|
|
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
// Telegram says only to the account linked as the operator: none linked, it is not used at all.
|
|
if len(tg.sends) != 0 {
|
|
t.Errorf("an unlinked Telegram was sent %d messages", len(tg.sends))
|
|
}
|
|
if err := h.Bench.Ready("telegram"); err == nil || !strings.Contains(err.Error(), "send /start to the bot") {
|
|
t.Errorf("an unlinked Telegram is ready: %v", err)
|
|
}
|
|
// The ask offers an approval no channel can carry: refused to its asker, in words.
|
|
if got := w.of("mesh-delivery"); len(got) != 1 || got[0].Outcome != asks.OutcomeRefused ||
|
|
!strings.Contains(got[0].Words, "no channel can carry") {
|
|
t.Fatalf("not refused: %+v", got)
|
|
}
|
|
if len(dt.sends) != 0 {
|
|
t.Errorf("the desk was offered an ask with an approval: %+v", dt.sends)
|
|
}
|
|
}
|
|
|
|
func TestAnApproveFromTheLinkedAccountIsAWarrantForTheAskerAlone(t *testing.T) {
|
|
h, tg, _, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
asked := heldAsk(c, "d-1")
|
|
submit(t, h, "mesh-delivery", asked)
|
|
settle(h, c)
|
|
release := ticketFor(tg.sends[0], "Release")
|
|
out, err := h.Chosen("telegram", choiceOf(release, "42", c.now()))
|
|
if err != nil || out != "chosen" {
|
|
t.Fatalf("%s %v", out, err)
|
|
}
|
|
got := w.of("mesh-delivery")
|
|
if len(got) != 1 || got[0].Outcome != asks.OutcomeChosen || got[0].Option != "release" || got[0].Channel != "telegram" ||
|
|
got[0].Says() != "the operator, via telegram (user id verified), chose Release" || strings.Join(got[0].Proofs, ",") != "P1" {
|
|
t.Fatalf("the warrant: %+v", got)
|
|
}
|
|
if _, err := got[0].For("mesh-delivery", asked); err != nil {
|
|
t.Errorf("the asker would not act on it: %v", err)
|
|
}
|
|
// It answers the ask as it was taken, and no other ask under that id (ADR 0259 §6).
|
|
if got[0].AskDigest != asked.Digest() {
|
|
t.Errorf("the warrant names the digest %s, not the ask's %s", got[0].AskDigest, asked.Digest())
|
|
}
|
|
if _, err := got[0].For("mesh-delivery", heldAsk(c, "d-1")); err == nil {
|
|
t.Error("a warrant was taken for an ask under the same id with another expiry")
|
|
}
|
|
if rec := store.asks["mesh-delivery.d-1"]; rec.State != string(asks.OutcomeChosen) || !rec.Told || !rec.Tickets[release].Used {
|
|
t.Errorf("the record: %+v", rec)
|
|
}
|
|
// Every copy says how it ended, its buttons gone.
|
|
for _, ch := range []*fakeChannel{tg} {
|
|
var edited Message
|
|
for _, m := range ch.edits {
|
|
edited = m
|
|
}
|
|
// The question stays whole (issue 714); below it, how it ended and what the router knows of the asker
|
|
// (issue 372).
|
|
if len(edited.Actions) != 0 || edited.Title != "Delivery of hq held for 36 hours" ||
|
|
edited.Body != heldAsk(c, "d-1").Explanation ||
|
|
edited.Note != "You chose Release at 12:00 on Telegram.\nTold to mesh-delivery, which acts on it itself." {
|
|
t.Errorf("%s still offers %q, or does not keep the question and say how it ended (issue 714): %q / %q / %q", ch.name, labelsOf(edited), edited.Title, edited.Body, edited.Note)
|
|
}
|
|
}
|
|
// The tap is confirmed where it was made, in reply to it, once the warrant stood — and before the question
|
|
// was edited, so the confirmation is what the operator sees first.
|
|
var confirmed []Message
|
|
for _, m := range tg.sends {
|
|
if m.Reply == choiceOf(release, "42", c.now()).ID {
|
|
confirmed = append(confirmed, m)
|
|
}
|
|
}
|
|
if len(confirmed) != 1 || confirmed[0].Title != "You chose Release: recorded, and mesh-delivery was told." ||
|
|
confirmed[0].Body != "" || !confirmed[0].Quiet {
|
|
t.Errorf("the tap was confirmed with %+v", confirmed)
|
|
}
|
|
// One warrant: the same answer again, and another answer after it, are refused and said.
|
|
if out, _ := h.Chosen("telegram", choiceOf(release, "42", c.now())); out != "chosen" {
|
|
t.Errorf("a redelivery of the same answer was acted on again: %s", out)
|
|
}
|
|
// The same answer from the same account, as a new tap, is what was recorded: said so, nothing more done
|
|
// (issue 372). From another account it is refused.
|
|
again := choiceOf(release, "42", c.now())
|
|
again.ID = "another"
|
|
if out, err := h.Chosen("telegram", again); err != nil || out != "chosen" ||
|
|
!strings.HasPrefix(tg.sends[len(tg.sends)-1].Title, "You already chose Release at ") {
|
|
t.Errorf("the same answer again was not said as recorded: %s %v %q", out, err, tg.sends[len(tg.sends)-1].Title)
|
|
}
|
|
stranger := choiceOf(release, "666", c.now())
|
|
if out, err := h.Chosen("telegram", stranger); err == nil || out != "refused" {
|
|
t.Errorf("a used ticket was taken from another account: %s", out)
|
|
}
|
|
stop := ticketFor(tg.sends[0], "Stop")
|
|
if out, err := h.Chosen("telegram", choiceOf(stop, "42", c.now())); err == nil || !strings.Contains(err.Error(), "already answered") {
|
|
t.Errorf("a second answer stood: %s %v", out, err)
|
|
}
|
|
if len(w.of("mesh-delivery")) != 1 {
|
|
t.Errorf("more than one warrant: %+v", w.of("mesh-delivery"))
|
|
}
|
|
if len(w.of("mesh-controller")) != 0 {
|
|
t.Error("another asker heard the warrant")
|
|
}
|
|
}
|
|
|
|
func TestEveryAnswerThatIsNotTheOperatorsIsRefusedAndSaid(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
kind, identity string
|
|
authenticated bool
|
|
linkedAgo time.Duration
|
|
label string
|
|
pass time.Duration
|
|
want string
|
|
}{
|
|
"an account not linked": {"telegram", "666", true, 2 * time.Hour, "Release", 0, "not linked as the operator"},
|
|
"a sender the service did not say": {"telegram", "42", false, 2 * time.Hour, "Release", 0, "did not authenticate"},
|
|
"a link younger than an hour": {"telegram", "42", true, 59 * time.Minute, "Release", 0, "can answer this question from"},
|
|
"an ask that expired": {"telegram", "42", true, 2 * time.Hour, "Release", 3 * time.Hour, "expired"},
|
|
"a ticket shown on another kind": {"desktop", "g14", true, 2 * time.Hour, "Release", 0, "not offered here"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
h, tg, _, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-tc.linkedAgo))
|
|
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
ticket := ticketFor(tg.sends[0], tc.label)
|
|
if ticket == "" {
|
|
t.Fatalf("no %s offered: %q", tc.label, labelsOf(tg.sends[0]))
|
|
}
|
|
c.pass(tc.pass)
|
|
ch := choiceOf(ticket, tc.identity, c.now())
|
|
ch.Sender.Authenticated = tc.authenticated
|
|
before := len(tg.sends)
|
|
out, err := h.Chosen(tc.kind, ch)
|
|
if err == nil || out != "refused" || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("%s %v, want refused for %q", out, err, tc.want)
|
|
}
|
|
if len(w.of("mesh-delivery")) != 0 {
|
|
t.Error("a warrant was issued")
|
|
}
|
|
title := "That answer was not taken"
|
|
if tc.identity == "666" {
|
|
// Another account's answer is said to the operator, naming that account (the lab's proof found
|
|
// the plain refusal reached the operator and read as their own answer refused).
|
|
title = "Another account tried to answer for you"
|
|
}
|
|
if tc.kind == "telegram" && (len(tg.sends) != before+1 || tg.sends[len(tg.sends)-1].Title != title) {
|
|
t.Errorf("the refusal was not said on the channel: %+v", tg.sends[before:])
|
|
}
|
|
if tc.identity == "666" && !strings.Contains(tg.sends[len(tg.sends)-1].Body, "number 666") {
|
|
t.Errorf("the notice does not name the other account: %q", tg.sends[len(tg.sends)-1].Body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestALinkApprovesAfterAnHourAndAcknowledgesAtOnce(t *testing.T) {
|
|
h, tg, _, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now())
|
|
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
release, silence := ticketFor(tg.sends[0], "Release"), ticketFor(tg.sends[0], "Silence for a week")
|
|
c.pass(59*time.Minute - BurstWindow)
|
|
if out, err := h.Chosen("telegram", choiceOf(release, "42", c.now())); err == nil || out != "refused" {
|
|
t.Fatalf("approved at 59 minutes: %s", out)
|
|
}
|
|
c.pass(time.Minute)
|
|
stand(h, c)
|
|
late := choiceOf(release, "42", c.now().Add(time.Second))
|
|
late.ID = "at-sixty"
|
|
if out, err := h.Chosen("telegram", late); err != nil || out != "chosen" {
|
|
t.Fatalf("refused at 60 minutes: %s %v", out, err)
|
|
}
|
|
// An acknowledgement is taken at once from a fresh link, on an ask of acknowledgements alone.
|
|
h2, tg2, _, c2, store2, w2 := newRouter(t)
|
|
link(h2, store2, c2, "42", c2.now())
|
|
submit(t, h2, "mesh-delivery", ackAsk(c2, "d-2"))
|
|
settle(h2, c2)
|
|
silence = ticketFor(tg2.sends[0], "Silence for a week")
|
|
if out, err := h2.Chosen("telegram", choiceOf(silence, "42", c2.now())); err != nil || out != "chosen" {
|
|
t.Fatalf("an acknowledgement from a fresh link was refused: %s %v", out, err)
|
|
}
|
|
if got := w2.of("mesh-delivery"); len(got) != 1 || got[0].Level != asks.Acknowledge {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
_ = w
|
|
}
|
|
|
|
// An acknowledgement does not end an ask that waits for an approval from where an approval is not proven
|
|
// (the review of 2026-10-09): a fresh link's Silence on the held delivery's ask is refused, and the ask stays
|
|
// open for Release or Stop once the hour has passed.
|
|
func TestAnAcknowledgementDoesNotEndAnAskThatWaitsForAnApproval(t *testing.T) {
|
|
h, tg, _, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now())
|
|
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
silence := ticketFor(tg.sends[0], "Silence for a week")
|
|
if out, err := h.Chosen("telegram", choiceOf(silence, "42", c.now())); err == nil || out != "refused" {
|
|
t.Fatalf("a fresh link's acknowledgement ended an ask that offers an approval: %s %v", out, err)
|
|
}
|
|
if len(w.of("mesh-delivery")) != 0 || store.asks["mesh-delivery.d-1"].State != StateOpen {
|
|
t.Fatal("the ask ended")
|
|
}
|
|
// Nor while the controller does not say root is free there.
|
|
c.pass(time.Hour)
|
|
stand(h, c)
|
|
rootFreeOf[h]["anchor"] = "not measured"
|
|
again := choiceOf(silence, "42", c.now())
|
|
again.ID = "again"
|
|
if out, _ := h.Chosen("telegram", again); out != "refused" || store.asks["mesh-delivery.d-1"].State != StateOpen {
|
|
t.Fatalf("an acknowledgement ended the ask while root is not free: %s", out)
|
|
}
|
|
delete(rootFreeOf[h], "anchor")
|
|
release := choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now())
|
|
if out, err := h.Chosen("telegram", release); err != nil || out != "chosen" {
|
|
t.Fatalf("the approval after the hour: %s %v", out, err)
|
|
}
|
|
}
|
|
|
|
func TestTheDeskAcknowledgesAndNeverApproves(t *testing.T) {
|
|
h, _, dt, c, _, w := newRouter(t)
|
|
submit(t, h, "mesh-delivery", ackAsk(c, "d-1"))
|
|
settle(h, c)
|
|
silence := ticketFor(dt.sends[0], "Silence for a week")
|
|
ch := choiceOf(silence, "g14", c.now())
|
|
ch.Sender.Authenticated = false
|
|
if out, err := h.Chosen("desktop", ch); err != nil || out != "chosen" {
|
|
t.Fatalf("%s %v", out, err)
|
|
}
|
|
got := w.of("mesh-delivery")
|
|
if len(got) != 1 || got[0].By.Verified != "a desk click: whoever was at the operator's session on g14" || len(got[0].Proofs) != 0 {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
// An approve ticket forged onto the desk is refused: the kind cannot carry it.
|
|
h2, tg2, _, c2, store2, _ := newRouter(t)
|
|
link(h2, store2, c2, "42", c2.now().Add(-2*time.Hour))
|
|
submit(t, h2, "mesh-delivery", heldAsk(c2, "d-1"))
|
|
settle(h2, c2)
|
|
rec := store2.asks["mesh-delivery.d-1"]
|
|
release := ticketFor(tg2.sends[0], "Release")
|
|
tk := rec.Tickets[release]
|
|
tk.Kind = "desktop"
|
|
rec.Tickets[release] = tk
|
|
store2.asks["mesh-delivery.d-1"] = rec
|
|
h2.keepAsk("mesh-delivery.d-1", rec, store2.revs["mesh-delivery.d-1"])
|
|
if out, err := h2.Chosen("desktop", choiceOf(release, "g14", c2.now())); err == nil || out != "refused" ||
|
|
!strings.Contains(err.Error(), "cannot answer a question that asks for an approval") {
|
|
t.Errorf("the desk approved: %s %v", out, err)
|
|
}
|
|
// Nor does an acknowledgement forged onto the desk end that ask.
|
|
silence = ticketFor(tg2.sends[0], "Silence for a week")
|
|
tk = rec.Tickets[silence]
|
|
tk.Kind = "desktop"
|
|
rec.Tickets[silence] = tk
|
|
store2.asks["mesh-delivery.d-1"] = rec
|
|
h2.keepAsk("mesh-delivery.d-1", rec, store2.revs["mesh-delivery.d-1"])
|
|
if out, _ := h2.Chosen("desktop", choiceOf(silence, "g14", c2.now())); out != "refused" ||
|
|
store2.asks["mesh-delivery.d-1"].State != StateOpen {
|
|
t.Errorf("a desk click ended an ask that waits for an approval: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestAnAskIsRefusedInWordsToItsAsker(t *testing.T) {
|
|
h, _, _, c, store, w := newRouter(t)
|
|
bad := heldAsk(c, "d-1")
|
|
bad.Options[0].Level = asks.Destroy
|
|
submit(t, h, "mesh-delivery", bad)
|
|
carrying := heldAsk(c, "d-2")
|
|
carrying.Explanation = "Needs you: see /etc/passwd on the machine."
|
|
submit(t, h, "mesh-delivery", carrying)
|
|
for i := 3; i <= 6; i++ {
|
|
a := ackAsk(c, "d-"+string(rune('0'+i)))
|
|
a.About = "about-" + string(rune('0'+i))
|
|
submit(t, h, "mesh-delivery", a)
|
|
}
|
|
got := w.of("mesh-delivery")
|
|
var refused []string
|
|
for _, g := range got {
|
|
if g.Outcome == asks.OutcomeRefused {
|
|
refused = append(refused, g.Ask+": "+g.Words)
|
|
}
|
|
}
|
|
all := strings.Join(refused, "\n")
|
|
for _, want := range []string{"d-1: the ask is refused: an ask that destroys lasts at most", "d-2: its words carry",
|
|
"d-6: its asker already has 3 open asks"} {
|
|
if !strings.Contains(all, want) {
|
|
t.Errorf("not refused: %q in\n%s", want, all)
|
|
}
|
|
}
|
|
if _, kept := store.asks["mesh-delivery.d-1"]; kept {
|
|
t.Error("a refused ask was recorded")
|
|
}
|
|
// No channel can carry anything: refused, never queued in silence.
|
|
h2, _, _, c2, _, w2 := newRouter(t)
|
|
h2.Bench.Stand("telegram", Standing{Ready: false, Why: "no bot token", At: c2.now()})
|
|
h2.Bench.Stand("desktop", Standing{Ready: false, Why: "no session", At: c2.now()})
|
|
submit(t, h2, "mesh-delivery", heldAsk(c2, "d-1"))
|
|
if got := w2.of("mesh-delivery"); len(got) != 1 || !strings.Contains(got[0].Words, "no channel can carry any of its answers now") {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAnAskExpiresAndNeverDefaults(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-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
c.pass(2 * time.Hour)
|
|
stand(h, c)
|
|
h.Tick()
|
|
got := w.of("mesh-delivery")
|
|
if len(got) != 1 || got[0].Outcome != asks.OutcomeExpired || got[0].Option != "" || !strings.Contains(got[0].Words, "the delivery stays held") {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
var edited Message
|
|
for _, m := range tg.edits {
|
|
edited = m
|
|
}
|
|
if len(edited.Actions) != 0 || !strings.HasPrefix(edited.Title, "Delivery of hq") ||
|
|
edited.Body == "" || edited.Note != "No longer answerable: nobody answered in time.\nThe delivery stays held." {
|
|
t.Errorf("the copy still offers answers, or does not say how it ended: %+v", edited)
|
|
}
|
|
}
|
|
|
|
func TestANewerAskAboutTheSameThingReplacesTheOlderAndACancelEndsIt(t *testing.T) {
|
|
h, _, _, c, store, w := newRouter(t)
|
|
a := ackAsk(c, "d-1")
|
|
a.About = "delivery.hq"
|
|
submit(t, h, "mesh-delivery", a)
|
|
b := ackAsk(c, "d-2")
|
|
b.About = "delivery.hq"
|
|
submit(t, h, "mesh-delivery", b)
|
|
if store.asks["mesh-delivery.d-1"].State != string(asks.OutcomeReplaced) {
|
|
t.Errorf("the older ask was not replaced: %s", store.asks["mesh-delivery.d-1"].State)
|
|
}
|
|
if err := h.TakeAsk("cancel", "mesh-delivery", json.RawMessage(`{"id":"d-2"}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := w.of("mesh-delivery")
|
|
if len(got) != 2 || got[0].Outcome != asks.OutcomeReplaced || got[1].Outcome != asks.OutcomeCancelled {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
// Another asker cannot cancel it: its cancel names its own ask, under its own name.
|
|
submit(t, h, "mesh-delivery", ackAsk(c, "d-3"))
|
|
_ = h.TakeAsk("cancel", "mesh-controller", json.RawMessage(`{"id":"d-3"}`))
|
|
if store.asks["mesh-delivery.d-3"].State != StateOpen {
|
|
t.Error("another asker's cancel ended it")
|
|
}
|
|
}
|
|
|
|
func TestAnAskAboutAConditionIsAnsweredOnItsMessage(t *testing.T) {
|
|
h, tg, dt, c, store, w := 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 := ackAsk(c, "d-1")
|
|
a.About = "delivery.hq.held"
|
|
submit(t, h, "mesh-controller", a)
|
|
settle(h, c)
|
|
// Its first message was on the desk alone (a warning, the desk ready); the ask re-shows it with answers.
|
|
var shown Message
|
|
for _, m := range dt.edits {
|
|
shown = m
|
|
}
|
|
if got := labelsOf(shown); got != "Silence for a week, Details" {
|
|
t.Errorf("the desk's copy offers %q: %+v", got, shown)
|
|
}
|
|
if store.asks["mesh-controller.d-1"].Record != "delivery.hq.held" {
|
|
t.Errorf("the ask is shown on %q", store.asks["mesh-controller.d-1"].Record)
|
|
}
|
|
silence := ticketFor(shown, "Silence for a week")
|
|
if out, err := h.Chosen("desktop", choiceOf(silence, "g14", c.now())); err != nil || out != "chosen" {
|
|
t.Fatalf("%s %v", out, err)
|
|
}
|
|
if got := w.of("mesh-controller"); len(got) != 1 || got[0].About != "delivery.hq.held" {
|
|
t.Errorf("%+v", got)
|
|
}
|
|
_ = tg
|
|
}
|
|
|
|
func TestAnAskerNotToldIsToldAgain(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-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
pub := h.Publish
|
|
h.Publish = func(string, any, string) error { return errorf("the bus is away") }
|
|
if out, _ := h.Chosen("telegram", choiceOf(ticketFor(tg.sends[0], "Stop"), "42", c.now())); out != "chosen" {
|
|
t.Fatal(out)
|
|
}
|
|
if store.asks["mesh-delivery.d-1"].Told {
|
|
t.Fatal("told while the bus was away")
|
|
}
|
|
h.Publish = pub
|
|
c.pass(time.Minute)
|
|
stand(h, c)
|
|
h.Tick()
|
|
if got := w.of("mesh-delivery"); len(got) != 1 || got[0].Option != "stop" || !store.asks["mesh-delivery.d-1"].Told {
|
|
t.Errorf("not told again: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAnAnswerReplayedLongAfterIsNotActedOn(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-delivery", heldAsk(c, "d-1"))
|
|
settle(h, c)
|
|
old := choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now().Add(-time.Hour))
|
|
if out, _ := h.Chosen("telegram", old); out != "too old" || len(w.of("mesh-delivery")) != 0 {
|
|
t.Errorf("%s", out)
|
|
}
|
|
}
|
|
|
|
func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
asked := asks.LinkAsked{ID: "l1", At: c.now(), Sender: asks.Sender{Identity: "42", Display: "Jo", Authenticated: true}}
|
|
h.Link("telegram", asked)
|
|
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Title, "Link a Telegram account as you") {
|
|
t.Fatalf("no code on the desk: %+v", dt.sends)
|
|
}
|
|
code := strings.ReplaceAll(strings.TrimSuffix(strings.SplitAfter(dt.sends[0].Body, "before ")[1][7:14], "."), " ", "")
|
|
if last := tg.sends[len(tg.sends)-1]; last.Title != "Send the code shown on your desk" {
|
|
t.Errorf("telegram was told %q", last.Title)
|
|
}
|
|
if p := store.links["telegram"]; p.CodeHash == "" || strings.Contains(p.CodeHash, code) {
|
|
t.Errorf("the code is kept in clear: %+v", p)
|
|
}
|
|
// Another account sending the code is refused; a wrong code is refused; the right one links.
|
|
if a := h.Prove("telegram", asks.Code{Sender: asks.Sender{Identity: "666", Authenticated: true}, Code: code}); a.Accepted {
|
|
t.Error("another account linked with the code")
|
|
}
|
|
if a := h.Prove("telegram", asks.Code{Sender: asks.Sender{Identity: "42", Authenticated: true}, Code: "000000x"}); a.Accepted {
|
|
t.Error("a wrong code linked")
|
|
}
|
|
a := h.Prove("telegram", asks.Code{Sender: asks.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"})
|
|
if !a.Accepted || !strings.Contains(a.Words, "approve from 13:00") {
|
|
t.Fatalf("the right code: %+v (code %q)", a, code)
|
|
}
|
|
id := store.ids[identityKey("telegram", "42")]
|
|
if id.ApproveFrom.Sub(id.Linked) != time.Hour {
|
|
t.Errorf("approves from %s, linked %s", id.ApproveFrom, id.Linked)
|
|
}
|
|
// Said on every channel.
|
|
for _, ch := range []*fakeChannel{tg, dt} {
|
|
if last := ch.sends[len(ch.sends)-1]; !strings.Contains(last.Title, "is now linked as you") {
|
|
t.Errorf("%s was not told: %q", ch.name, last.Title)
|
|
}
|
|
}
|
|
// A second account cannot link while one is.
|
|
h.Link("telegram", asks.LinkAsked{ID: "l2", At: c.now(), Sender: asks.Sender{Identity: "666", Authenticated: true}})
|
|
if _, pending := store.links["telegram"]; pending {
|
|
t.Error("a second link was begun while one is linked")
|
|
}
|
|
}
|
|
|
|
func TestALinkCodeExpiresAndFiveWrongTriesEndIt(t *testing.T) {
|
|
h, _, _, c, store, _ := newRouter(t)
|
|
jo := asks.Sender{Identity: "42", Authenticated: true}
|
|
h.Link("telegram", asks.LinkAsked{ID: "l1", At: c.now(), Sender: jo})
|
|
for i := 0; i < LinkTries; i++ {
|
|
h.Prove("telegram", asks.Code{Sender: jo, Code: "nope"})
|
|
}
|
|
if _, pending := store.links["telegram"]; pending {
|
|
t.Error("five wrong codes did not end the link")
|
|
}
|
|
h.Link("telegram", asks.LinkAsked{ID: "l2", At: c.now(), Sender: jo})
|
|
c.pass(LinkCodeFor)
|
|
if a := h.Prove("telegram", asks.Code{Sender: jo, Code: "123456"}); a.Accepted || !strings.Contains(a.Words, "expired") {
|
|
t.Errorf("%+v", a)
|
|
}
|
|
// The desk cannot be linked: it proves nothing about who clicked.
|
|
h.Link("desktop", asks.LinkAsked{ID: "l3", At: c.now(), Sender: asks.Sender{Identity: "g14"}})
|
|
if _, pending := store.links["desktop"]; pending {
|
|
t.Error("the desk began a link")
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0259 §7 (revised): unlinking is an approve ask put to the account that is linked; its notice
|
|
// reaches that account first, by name; nobody else's answer unlinks it.
|
|
func TestUnlinkingIsAskedOfTheLinkedAccountAndToldToItFirst(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
got, err := h.Unlink("telegram")
|
|
if err != nil || got["asked"] != true {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
if len(store.ids) != 1 {
|
|
t.Fatal("unlinked before the account answered")
|
|
}
|
|
ask := tg.sends[len(tg.sends)-1]
|
|
if ask.Title != "Urgent: Unlink this account?" || tg.to[len(tg.to)-1] != "42" || labelsOf(ask) != "Unlink" {
|
|
t.Fatalf("the question: %q to %q offers %q", ask.Title, tg.to[len(tg.to)-1], labelsOf(ask))
|
|
}
|
|
if last := dt.sends[len(dt.sends)-1]; !strings.Contains(last.Title, "An unlink of your Telegram account was asked") {
|
|
t.Errorf("the desk was not told: %q", last.Title)
|
|
}
|
|
unlink := ticketFor(ask, "Unlink")
|
|
// Another account cannot answer it.
|
|
if out, _ := h.Chosen("telegram", choiceOf(unlink, "666", c.now())); out != "refused" || len(store.ids) != 1 {
|
|
t.Fatalf("another account unlinked it: %s", out)
|
|
}
|
|
before := len(tg.sends)
|
|
if out, err := h.Chosen("telegram", choiceOf(unlink, "42", c.now())); err != nil || out != "chosen" {
|
|
t.Fatalf("%s %v", out, err)
|
|
}
|
|
if len(store.ids) != 0 {
|
|
t.Fatal("the account's own approval did not unlink it")
|
|
}
|
|
// The tap is confirmed where it was made (issue 372), then the account is told, by name, before it goes.
|
|
if confirmed := tg.sends[before]; confirmed.Title != "You chose Unlink: recorded." || confirmed.Reply == "" {
|
|
t.Errorf("the tap was not confirmed in reply to it: %+v", confirmed)
|
|
}
|
|
notice := tg.sends[before+1]
|
|
if notice.Title != "This account is no longer linked as you" || tg.to[before+1] != "42" {
|
|
t.Errorf("the account was not told first, by name: %q to %q", notice.Title, tg.to[before+1])
|
|
}
|
|
if last := dt.sends[len(dt.sends)-1]; !strings.Contains(last.Title, "No Telegram account is linked as you") {
|
|
t.Errorf("the desk was not told: %q", last.Title)
|
|
}
|
|
}
|
|
|
|
// A link within a day of an unlink waits for the replaced account's approval.
|
|
func TestARelinkWithinADayWaitsForTheReplacedAccount(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
_, _ = h.Unlink("telegram")
|
|
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
|
|
h.Link("telegram", asks.LinkAsked{ID: "l9", At: c.now(), Sender: asks.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
|
|
code := codeOf(t, dt.sends[len(dt.sends)-1])
|
|
a := h.Prove("telegram", asks.Code{Sender: asks.Sender{Identity: "666", Authenticated: true}, Code: code})
|
|
if !a.Accepted || !strings.Contains(a.Words, "waits for that account's approval") || len(store.ids) != 0 {
|
|
t.Fatalf("linked at once within a day of an unlink: %+v %v", a, store.ids)
|
|
}
|
|
asked := tg.sends[len(tg.sends)-1]
|
|
if asked.Title != "Urgent: Allow another account in your place?" || tg.to[len(tg.to)-1] != "42" {
|
|
t.Fatalf("the replaced account was not asked: %q to %q", asked.Title, tg.to[len(tg.to)-1])
|
|
}
|
|
if out, _ := h.Chosen("telegram", choiceOf(ticketFor(asked, "Allow"), "666", c.now())); out != "refused" {
|
|
t.Error("the new account allowed itself")
|
|
}
|
|
if out, err := h.Chosen("telegram", choiceOf(ticketFor(asked, "Allow"), "42", c.now())); err != nil || out != "chosen" {
|
|
t.Fatalf("%s %v", out, err)
|
|
}
|
|
if id, ok := store.ids[identityKey("telegram", "666")]; !ok || id.ApproveFrom.Sub(id.Linked) != time.Hour {
|
|
t.Errorf("not linked on the replaced account's approval: %+v", store.ids)
|
|
}
|
|
}
|
|
|
|
// Root at the router's machine unlinks, or allows a waiting link, by a file only root makes. It acts from the
|
|
// router's minute (Tick), which holds the work: the review of 2026-10-09 found the minute took the work again
|
|
// there and hung for ever the first time root used it. It is said on every channel, and the router's own open
|
|
// questions end.
|
|
func TestBreakGlassIsAFileRootMadeAndTheMinuteGoesOn(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
h.StateDir = t.TempDir()
|
|
tick := func() {
|
|
t.Helper()
|
|
done := make(chan struct{})
|
|
go func() { h.Tick(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the router's minute hung on a break-glass file")
|
|
}
|
|
}
|
|
// Who owns the file is what the kernel says, faked here both ways: the build seat runs this suite as root, where
|
|
// every file the test makes is root's, and a person runs it as themselves (the forge's check of 2026-10-09 failed
|
|
// on the first and passed on the second).
|
|
was := fileOwner
|
|
t.Cleanup(func() { fileOwner = was })
|
|
fileOwner = func(os.FileInfo) uint32 { return 1000 }
|
|
if err := os.WriteFile(filepath.Join(h.StateDir, "unlink.telegram"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tick()
|
|
if len(store.ids) != 1 || !strings.Contains(dt.sends[len(dt.sends)-1].Title, "was refused") {
|
|
t.Fatalf("a file not made by root unlinked: %v", store.ids)
|
|
}
|
|
// An open question of the router's own: root's act ends it.
|
|
if _, err := h.Unlink("telegram"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileOwner = func(os.FileInfo) uint32 { return 0 }
|
|
_ = os.WriteFile(filepath.Join(h.StateDir, "unlink.telegram"), nil, 0o600)
|
|
tick()
|
|
if len(store.ids) != 0 {
|
|
t.Fatal("root's file did not unlink")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(h.StateDir, "unlink.telegram")); err == nil {
|
|
t.Error("the file was left to act again")
|
|
}
|
|
for _, ch := range []*fakeChannel{tg, dt} {
|
|
said := false
|
|
for _, m := range ch.sends {
|
|
said = said || (m.Title == "Urgent: Root changed who is you" || strings.Contains(m.Title, "Root changed who is you")) &&
|
|
strings.Contains(m.Body, "was unlinked") && strings.Contains(m.Body, "1 open question")
|
|
}
|
|
if !said && ch == dt {
|
|
t.Errorf("%s was not told what root did: %+v", ch.name, ch.sends[len(ch.sends)-1])
|
|
}
|
|
}
|
|
for k, rec := range store.asks {
|
|
if rec.Asker == SelfAsker && rec.State == StateOpen {
|
|
t.Errorf("the router's own question %s is still open after root decided", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// While an agent can become root where the router or the channel runs, nothing proven there approves.
|
|
func TestNoApprovalWhileAnAgentCanBecomeRootThere(t *testing.T) {
|
|
h, tg, _, c, store, w := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
asked := heldAsk(c, "d-1")
|
|
submit(t, h, "mesh-delivery", asked)
|
|
settle(h, c)
|
|
release := ticketFor(tg.sends[0], "Release")
|
|
rootFreeOf[h]["anchor"] = "the agent account is not judged"
|
|
if out, err := h.Chosen("telegram", choiceOf(release, "42", c.now())); err == nil || out != "refused" ||
|
|
!strings.Contains(err.Error(), "may become root on anchor") {
|
|
t.Fatalf("approved while an agent can become root there: %s %v", out, err)
|
|
}
|
|
if len(w.of("mesh-delivery")) != 0 {
|
|
t.Error("a warrant was issued")
|
|
}
|
|
if got := h.carriers(asks.Approve); len(got) != 0 {
|
|
t.Errorf("an approval is still offered on %v", got)
|
|
}
|
|
}
|
|
|
|
// Another asker naming a condition's key does not take that condition's message over.
|
|
func TestOnlyTheConditionsOwnerAsksOnItsMessage(t *testing.T) {
|
|
h, _, _, c, store, _ := newRouter(t)
|
|
k := cond("delivery.hq.held", Warning, "the delivery of hq is held")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
a := heldAsk(c, "d-1")
|
|
a.About = "delivery.hq.held"
|
|
submit(t, h, "mesh-delivery", a)
|
|
if got := store.asks["mesh-delivery.d-1"].Record; got == "delivery.hq.held" {
|
|
t.Error("another asker took the condition's message")
|
|
}
|
|
}
|
|
|
|
// A channel that does not render exactly as the router does carries no answer.
|
|
func TestAnAnswerNeedsAChannelThatRendersExactly(t *testing.T) {
|
|
h, _, _, c, _, _ := newRouter(t)
|
|
h.Bench.SetKinds([]KindInfo{{Kind: "desktop", Module: "desk-channel", Capabilities: []string{"deliver", "choice", "private"}}})
|
|
h.Bench.Stand("desktop", Standing{Ready: true, At: c.now()})
|
|
if got := h.carriers(asks.Acknowledge); len(got) != 0 {
|
|
t.Errorf("a channel without exact-render carries %v", got)
|
|
}
|
|
}
|
|
|
|
// At most three links an hour per kind, and the code is shown only where the words stay private.
|
|
func TestLinksAreBoundedAndTheCodeShownOnlyInPrivate(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
jo := asks.Sender{Identity: "42", Display: "Jo", Authenticated: true}
|
|
for i := 0; i < LinksAnHour; i++ {
|
|
h.Link("telegram", asks.LinkAsked{ID: fmt.Sprint("l", i), At: c.now(), Sender: jo})
|
|
}
|
|
shown := len(dt.sends)
|
|
h.Link("telegram", asks.LinkAsked{ID: "l9", At: c.now(), Sender: jo})
|
|
if len(dt.sends) != shown || !strings.Contains(tg.sends[len(tg.sends)-1].Body, "Try again in an hour") {
|
|
t.Error("a fourth link this hour showed a code")
|
|
}
|
|
if !strings.Contains(dt.sends[0].Body, "(number 42)") {
|
|
t.Errorf("the notice does not name the account by its number: %q", dt.sends[0].Body)
|
|
}
|
|
h2, tg2, _, c2, store2, _ := newRouter(t)
|
|
h2.Bench.SetKinds([]KindInfo{
|
|
{Kind: "telegram", Module: "telegram", Capabilities: []string{"choice", "verified-sender", "exact-render", "reaches-away"}},
|
|
{Kind: "desktop", Module: "desk-channel", Capabilities: []string{"choice", "exact-render"}}})
|
|
h2.Link("telegram", asks.LinkAsked{ID: "l1", At: c2.now(), Sender: jo})
|
|
if _, pending := store2.links["telegram"]; pending || !strings.Contains(tg2.sends[len(tg2.sends)-1].Body, "No private place") {
|
|
t.Error("a code was shown where the words are not private")
|
|
}
|
|
_ = store
|
|
}
|
|
|
|
// A warning the desk could not show goes to the channel that reaches the operator away.
|
|
func TestAWarningTheDeskCouldNotShowGoesAway(t *testing.T) {
|
|
h, tg, dt, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
k := cond("module.x.unhealthy", Warning, "x is not healthy")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
if len(dt.sends) != 1 {
|
|
t.Fatalf("the desk was sent %d", len(dt.sends))
|
|
}
|
|
h.Failed("desktop", asks.Failed{Handle: "g14=1", Why: "no session answers", At: c.now()})
|
|
c.pass(BurstWindow)
|
|
h.Flush()
|
|
if len(tg.sends) == 0 {
|
|
t.Error("a warning the desk could not show did not go to Telegram")
|
|
}
|
|
}
|
|
|
|
// codeOf reads the six digits a link's notice shows.
|
|
func codeOf(t *testing.T, m Message) string {
|
|
t.Helper()
|
|
i := strings.Index(m.Body, ": ")
|
|
for i >= 0 {
|
|
rest := m.Body[i+2:]
|
|
if len(rest) >= 7 && rest[3] == ' ' {
|
|
return rest[:3] + rest[4:7]
|
|
}
|
|
j := strings.Index(rest, ": ")
|
|
if j < 0 {
|
|
break
|
|
}
|
|
i += 2 + j
|
|
}
|
|
t.Fatalf("no code in %q", m.Body)
|
|
return ""
|
|
}
|
|
|
|
// An ask's message goes alone even when it offers one answer: folded into a burst's digest, its one button was
|
|
// lost (found by the lab's proof, mesh-lab asks/, with a condition's message in the same burst).
|
|
func TestAnAsksMessageWithOneAnswerIsNotFoldedIntoADigest(t *testing.T) {
|
|
h, tg, _, c, store, _ := newRouter(t)
|
|
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
|
settle(h, c)
|
|
// Something was just said on the channel, so what follows waits for the burst.
|
|
submit(t, h, "mesh-delivery", heldAsk(c, "d-0"))
|
|
settle(h, c)
|
|
k := cond("machine.anchor.disk", Urgent, "the disk on anchor is full")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
before := len(tg.sends)
|
|
one := heldAsk(c, "d-1")
|
|
one.Options = one.Options[2:] // Silence alone: one answer
|
|
submit(t, h, "mesh-delivery", one)
|
|
settle(h, c)
|
|
var asked *Message
|
|
for i := range tg.sends[before:] {
|
|
m := tg.sends[before+i]
|
|
if ticketFor(m, "Silence for a week") != "" {
|
|
asked = &m
|
|
}
|
|
}
|
|
if asked == nil {
|
|
t.Fatalf("the ask's one answer was not offered on Telegram: %+v", tg.sends[before:])
|
|
}
|
|
if strings.Contains(asked.Body, "disk on anchor") {
|
|
t.Errorf("the ask's message was folded with another: %q", asked.Body)
|
|
}
|
|
}
|