Files
mesh-lab/asks/proof_test.go
T
jochen af0a32a780
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), replays/merge-check.sh (go), asks/merge-check.sh …
mesh/delivery delivered
asks: a replayed tap is said as the answer already recorded, on its acknowledgement (issue 393)
Since issue 372 the router answers the same answer again from the same
account with 'You already chose Approve at HH:MM: recorded.', on the
tap's acknowledgement rather than in the chat. The proof from #74 still
waited for a chat refusal, so it failed on every mesh-lab pull request.
The lab's phone now keeps a tap's acknowledgement words, and the replay
subtest waits for them; its checks that nothing more is done stay.
2026-10-10 18:22:22 +02:00

352 lines
17 KiB
Go

package asks
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// The operator's own Telegram account in the lab, and somebody else's.
const (
operatorAccount = 42
strangerAccount = 99
groupChat = -1001
)
// TestTheOperatorsAnswerEndToEnd is the operator's acceptance of novox/hq ADR 0259, run before anybody uses it:
//
// "I get a Telegram message from the mesh with what is asked, why, by whom, and Approve/Decline buttons;
// tapping Approve makes the mesh do exactly that one thing once; Decline or no answer does nothing;
// nobody else (no agent, no other Telegram user, no replay of an old message) can produce an approval;
// and I can see in the record who approved what."
//
// What is real: the bus, of the release the mesh runs, with the accounts the controller at its commit composes
// for one machine and raised by that controller's own code (its fixture TestTheAsksLabBus); the mesh's runtime
// (mesh-tools node-tools) serving the router (messenger) and the Telegram channel (telegram), each on its own
// credential, as `runs-as` places them; the router's and the channel's code at their commits; the SDK's asker.
//
// What is the lab's: Telegram itself (a fake Bot API, the operator's phone), the desk (the desktop kind at the
// bus, which shows the link's code), the controller's conditions verb (a list the lab keeps), and the asker
// (the module `lab-asker`, the SDK's client in this process). The controller as an asker — that it acts on a
// warrant once, through the verb the action names, and records it — is its own repository's test
// (asker_test.go); the live acceptance after rollout runs it for real.
func TestTheOperatorsAnswerEndToEnd(t *testing.T) {
repos := clonesFor(t)
dir := os.Getenv("MESH_LAB_ASKS_KEEP")
if dir == "" {
dir = t.TempDir()
} else if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
l := &lab{t: t, repos: repos, dir: dir}
token := "123456789:" + strings.Repeat("A", 35)
l.tg = newFakeTelegram(t, token)
// The bus, as the controller composes and raises it.
l.compose("")
l.startBus()
l.compose(l.url)
l.playController()
// The programs, at their commits. The channel is built to call the lab's phone in place of Telegram.
runtimeBin := l.build(filepath.Join(repos.tools, "node-tools"), "./cmd/node-tools", "node-tools", "")
routerBin := l.build(filepath.Join(repos.catalogue, "modules", "messenger"), "./cmd/messenger", "messenger", "")
channelBin := l.build(filepath.Join(repos.catalogue, "modules", "telegram"), "./cmd/telegram", "telegram",
"-X main.API="+l.tg.URL())
routerState := filepath.Join(dir, "messenger")
if err := os.MkdirAll(routerState, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(routerState, "settings.json"), []byte(`{"time-zone": "UTC"}`), 0o600); err != nil {
t.Fatal(err)
}
tokenFile := filepath.Join(dir, "telegram", "telegram-token")
if err := os.MkdirAll(filepath.Dir(tokenFile), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(tokenFile, []byte(token+"\n"), 0o600); err != nil {
t.Fatal(err)
}
ctx, stop := context.WithCancel(context.Background())
t.Cleanup(stop)
l.playDesk(ctx)
l.runtime("messenger", runtimeBin, routerBin, map[string]string{
"MESH_MESSENGER_SETTINGS": filepath.Join(routerState, "settings.json"), "MESH_MESSENGER_STATE": routerState})
l.runtime("telegram", runtimeBin, channelBin, map[string]string{"MESH_TELEGRAM_TOKEN_FILE": tokenFile})
l.waitSaid("messenger", "taking the asks submitted", time.Minute)
l.waitSaid("telegram", "taking the router's messages for this kind", time.Minute)
a := newLabAsker(t, l)
// --- 1. The operator links their account: /start on the phone, the code from the desk.
t.Run("the operator links their account with the code shown on the desk", func(t *testing.T) {
since := time.Now()
l.tg.typed(operatorAccount, operatorAccount, "private", "/start")
code := l.codeOnTheDesk(90 * time.Second)
l.tg.typed(operatorAccount, operatorAccount, "private", code[:3]+" "+code[3:])
l.tg.waitFor("the link said done", operatorAccount, since, 90*time.Second, func(m tgMessage) bool {
return strings.Contains(m.Text, "Linked")
})
})
if t.Failed() {
return
}
// --- 2. Within the hour after a link, an approval is refused; the button is not spent.
var first asks.Ask
var firstMessage tgMessage
t.Run("an approval in the hour after linking is refused and nothing is done", func(t *testing.T) {
since := time.Now()
first = a.ask("a1", "Flip the lab's switch?", time.Hour)
firstMessage = l.tg.waitFor("the first question with its buttons", operatorAccount, since, 90*time.Second,
func(m tgMessage) bool { return !m.Edited && button(m, "Approve") != "" && button(m, "Decline") != "" })
for _, says := range []string{"Flip the lab's switch?", "approve or decline", "lab-asker",
"the switch is flipped, once", "nothing is flipped"} {
if !strings.Contains(firstMessage.Text, says) {
t.Errorf("the question does not say %q (what is asked, why, by whom, what each answer does, what "+
"no answer does): %q", says, firstMessage.Text)
}
}
tapped := time.Now()
l.tg.tapped(operatorAccount, operatorAccount, "private", firstMessage.ID, button(firstMessage, "Approve"))
l.tg.waitFor("the refusal of an approval inside the hour", operatorAccount, tapped, 90*time.Second,
func(m tgMessage) bool {
return strings.Contains(m.Text, "That answer was not taken") && strings.Contains(m.Text, "linked at")
})
if !a.noWordOn("a1", 2*time.Second) || len(a.performed()) != 0 {
t.Fatalf("an approval inside the hour was a warrant: %v", a.performed())
}
})
// The hour passes: the lab ages the link in the router's own record (the hour itself is the router's
// unit test: an approve at 59 minutes refused, at 60 taken).
t.Run("the hour after the link passes", func(t *testing.T) {
e, kv := l.routerRecord("messenger_identities", fmt.Sprintf("telegram.%d", operatorAccount))
var id map[string]any
if err := json.Unmarshal(e.Value(), &id); err != nil {
t.Fatal(err)
}
past := time.Now().Add(-61 * time.Minute).UTC()
id["linked"], id["approve-from"] = past.Format(time.RFC3339Nano), past.Add(time.Hour).Format(time.RFC3339Nano)
raw, _ := json.Marshal(id)
if _, err := kv.Update(context.Background(), e.Key(), raw, e.Revision()); err != nil {
t.Fatal(err)
}
})
// --- 3. Approve: the warrant, for this ask alone, performed once.
t.Run("approve does exactly the one thing, once, and says who approved", func(t *testing.T) {
tapped := time.Now()
l.tg.tapped(operatorAccount, operatorAccount, "private", firstMessage.ID, button(firstMessage, "Approve"))
w := a.warrantFor("a1", 30*time.Second)
if w.Outcome != asks.OutcomeChosen || w.Option != "approve" || w.Level != asks.Approve {
t.Fatalf("the warrant: %+v", w)
}
if w.By == nil || w.By.Kind != "telegram" || w.By.Identity != fmt.Sprint(operatorAccount) ||
w.By.Verified != "user id verified" || w.Channel != "telegram" || strings.Join(w.Proofs, ",") != "P1" {
t.Errorf("the warrant does not say who approved, how and where: %+v by %+v", w, w.By)
}
if w.AskDigest != first.Digest() {
t.Errorf("the warrant names the digest %s, not the ask's %s", w.AskDigest, first.Digest())
}
if got := a.performed(); len(got) != 1 || got[0]["arg.switch"] != "approve" {
t.Fatalf("performed %v, want the one bound act once", got)
}
// Every copy says how it ended, its buttons gone.
l.tg.waitFor("the question edited to its outcome", operatorAccount, tapped, 90*time.Second,
func(m tgMessage) bool { return m.Edited && m.ID == firstMessage.ID && len(m.Buttons) == 0 })
// And the router's record holds who approved what.
e, _ := l.routerRecord("messenger_asks", "lab-asker.a1")
var rec struct {
State string `json:"state"`
Warrant *asks.Warrant `json:"warrant"`
}
_ = json.Unmarshal(e.Value(), &rec)
if rec.State != "chosen" || rec.Warrant == nil || rec.Warrant.By == nil || rec.Warrant.By.Identity != fmt.Sprint(operatorAccount) {
t.Errorf("the router's record: %s", e.Value())
}
t.Logf("the record reads: %s", w.Says())
})
// --- 4. A replayed tap, and a redelivered warrant, do nothing more.
// The replay is the operator's own answer again: the router says it is the one already recorded (on the tap's
// acknowledgement, issue 372), not refused as if the approval failed, and it is no second warrant or act.
t.Run("a replayed tap and a redelivered warrant do nothing more", func(t *testing.T) {
tapped := time.Now()
l.tg.tapped(operatorAccount, operatorAccount, "private", firstMessage.ID, button(firstMessage, "Approve"))
l.tg.toldTapper("that a replayed tap is the answer already recorded", operatorAccount, tapped, 90*time.Second,
func(text string) bool {
return strings.Contains(text, "You already chose Approve") && strings.Contains(text, "recorded")
})
time.Sleep(2 * time.Second)
if n := a.wordsOn("a1"); n != 1 {
t.Errorf("the router said %d words on one ask", n)
}
w := a.warrantFor("a1", time.Second)
if h := a.take(w); h != asker.Done {
t.Errorf("a warrant heard again was %s", h)
}
if got := a.performed(); len(got) != 1 {
t.Fatalf("performed %v after a replay", got)
}
})
// --- 5. Somebody else's account, and a group, cannot answer; Decline does nothing.
t.Run("another account and a group cannot answer, and decline does nothing", func(t *testing.T) {
since := time.Now()
a.ask("a2", "Flip it once more?", time.Hour)
m := l.tg.waitFor("the second question", operatorAccount, since, 90*time.Second,
func(m tgMessage) bool { return !m.Edited && button(m, "Approve") != "" })
// The buttons' data reached a stranger (a forwarded question), who taps in their own chat with the bot:
// the channel never showed that message there, so the tap goes nowhere, and the stranger is told
// nothing. (A stranger's answer that does reach the router is refused there and told to the operator by
// name: the router's own test.)
l.tg.tapped(strangerAccount, strangerAccount, "private", m.ID, button(m, "Approve"))
// The operator taps a message whose words are not the ones the channel put there.
l.tg.tappedOn(operatorAccount, operatorAccount, "private", m.ID, button(m, "Approve"), "Flip ALL the switches?")
// The operator taps in a group the bot is in: not their own chat, dropped by the channel.
l.tg.tapped(operatorAccount, groupChat, "supergroup", m.ID, button(m, "Approve"))
if !a.noWordOn("a2", 3*time.Second) {
t.Fatal("a tap from another account, on changed words, or from a group, was a warrant")
}
if got := l.tg.since(strangerAccount, since); len(got) != 0 {
t.Errorf("the bot answered the stranger: %+v", got)
}
if got := l.tg.since(groupChat, since); len(got) != 0 {
t.Errorf("the bot answered in a group: %+v", got)
}
l.tg.tapped(operatorAccount, operatorAccount, "private", m.ID, button(m, "Decline"))
w := a.warrantFor("a2", 30*time.Second)
if w.Option != "decline" || w.Level != asks.Acknowledge {
t.Errorf("decline's warrant: %+v", w)
}
if got := a.performed(); len(got) != 1 {
t.Fatalf("decline performed something: %v", got)
}
})
// --- 6. No agent can produce an approval: the bus refuses every forgery, as the controller composed it.
t.Run("no agent, channel or module but the router can say a warrant, and none asks in another's name", func(t *testing.T) {
warrant, _ := json.Marshal(asks.Warrant{Ask: "a9", Asker: "lab-asker", Outcome: asks.OutcomeChosen,
Option: "approve", Level: asks.Approve, AskDigest: first.Digest(),
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}})
decided := asks.DecidedSubject("lab-asker")
// The control: what is granted is not refused, so a refusal below is the bus's word and not the lab's.
// The channel's control is a true standing, now: a false one would hold the router's messages.
standing, _ := json.Marshal(asks.Standing{Ready: true, EditsSilently: true, At: time.Now().UTC()})
for _, try := range []struct {
user, subject string
body []byte
}{
{machine + ".lab-asker", "mesh.seat.operator-channel.accept.cancel.lab-asker", []byte(`{"id":"none"}`)},
{machine + ".telegram", asks.IntakeSubject(asks.StandingWhat, "telegram"), standing},
} {
if l.refusedToPublish(try.user, try.subject, try.body) {
t.Fatalf("the control failed: %s was refused %s, which the controller grants it", try.user, try.subject)
}
}
for _, user := range []string{
machine + ".node-tools", // the machine's runtime: every agent's tool calls go through one
machine + ".lab-asker", // the asker itself
machine + ".lab-bystander", // any other module
machine + ".telegram", // the channel
"node." + machine, // the machine's node-engine
"controller", // the controller asks; it does not decide
} {
if !l.refusedToPublish(user, decided, warrant) {
t.Errorf("%s may say a warrant on %s", user, decided)
}
}
for _, try := range []struct{ user, subject, what string }{
{machine + ".lab-asker", asks.AskSubject("mesh-controller"), "an ask in another asker's name"},
{machine + ".lab-bystander", asks.AskSubject("lab-bystander"), "an ask from a module that may not ask"},
{machine + ".node-tools", asks.AskSubject("mesh-controller"), "an agent asking as the controller through the runtime"},
{machine + ".node-tools", asks.IntakeSubject(asks.Chosen, "telegram"), "an agent saying a Telegram tap"},
{machine + ".lab-asker", asks.IntakeSubject(asks.Chosen, "telegram"), "an asker saying a Telegram tap"},
{machine + ".node-tools", "$KV.messenger_identities.telegram.666", "an agent linking an account of its own"},
{machine + ".lab-asker", "$KV.messenger_asks.lab-asker.a1", "an asker rewriting the router's record"},
{machine + ".telegram", asks.ProofSubject(asks.CodeProof, "desktop"), "a channel speaking for another kind"},
} {
if !l.refusedToPublish(try.user, try.subject, []byte(`{}`)) {
t.Errorf("%s: %s may publish %s", try.what, try.user, try.subject)
}
}
time.Sleep(time.Second)
if n := a.wordsOn("a9"); n != 0 || len(a.performed()) != 1 {
t.Fatalf("a forged warrant reached the asker: %d words, performed %v", n, a.performed())
}
})
// --- 7. While an agent can become root where the router runs, nothing proven there approves.
t.Run("while the controller cannot say root is free where the router runs, nothing approves", func(t *testing.T) {
// An approval the operator already sees on the phone, then root stops being free.
since := time.Now()
a.ask("a5", "Flip it while root is checked?", time.Hour)
m := l.tg.waitFor("the question asked while root was free", operatorAccount, since, 90*time.Second,
func(m tgMessage) bool { return !m.Edited && button(m, "Approve") != "" })
l.mu.Lock()
l.notFree = map[string]string{machine: "an agent can become root without a person"}
asked := l.rootAsked
l.mu.Unlock()
tapped := time.Now()
l.tg.tapped(operatorAccount, operatorAccount, "private", m.ID, button(m, "Approve"))
l.tg.waitFor("the refusal of an approval while root is not free", operatorAccount, tapped, 90*time.Second,
func(m tgMessage) bool { return strings.Contains(m.Text, "That answer was not taken") })
l.mu.Lock()
askedAgain := l.rootAsked > asked
l.mu.Unlock()
if !askedAgain {
t.Error("the router judged an approval without asking the controller whether root is free")
}
if !a.noWordOn("a5", 2*time.Second) {
t.Fatal("an approval was a warrant while root was not free")
}
// A new ask that needs an approval is refused to its asker, in words: no channel can carry it now.
a.ask("a3", "Flip it while root is not free?", time.Hour)
w := a.warrantFor("a3", 30*time.Second)
if w.Outcome != asks.OutcomeRefused || w.By != nil {
t.Errorf("an approving ask while root is not free: %+v", w)
}
if got := a.performed(); len(got) != 1 {
t.Fatalf("performed %v while root was not free", got)
}
l.mu.Lock()
l.notFree = nil
l.mu.Unlock()
if err := a.client.Cancel("a5"); err != nil {
t.Errorf("taking the ask back: %v", err)
}
})
// --- 8. No answer does nothing: the ask expires.
t.Run("an ask nobody answers expires and nothing is done", func(t *testing.T) {
since := time.Now()
a.ask("a4", "Flip it if you answer in time?", 20*time.Second)
m := l.tg.waitFor("the fourth question", operatorAccount, since, 90*time.Second,
func(m tgMessage) bool { return !m.Edited && button(m, "Approve") != "" })
w := a.warrantFor("a4", 2*time.Minute)
if w.Outcome != asks.OutcomeExpired || w.By != nil {
t.Errorf("the end of an unanswered ask: %+v", w)
}
late := time.Now()
l.tg.tapped(operatorAccount, operatorAccount, "private", m.ID, button(m, "Approve"))
l.tg.waitFor("the refusal of a late tap", operatorAccount, late, 90*time.Second,
func(m tgMessage) bool { return strings.Contains(m.Text, "That answer was not taken") })
time.Sleep(2 * time.Second)
if got := a.performed(); len(got) != 1 {
t.Fatalf("an expired ask performed something: %v", got)
}
})
t.Logf("performed, over the whole run: %v (one approval, one act)", a.performed())
}