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) } // At the tap, the phone says it was recorded, and never before the warrant was (issue 372). ack := l.tg.waitForToast("the approval confirmed", tapped, 30*time.Second, func(s string) bool { return strings.HasPrefix(s, "You chose Approve: recorded") }) if ack.At.Before(w.At) { t.Errorf("the phone said recorded at %s, before the warrant at %s", ack.At, w.At) } // Every copy says how it ended — as its title, read at a glance — its buttons gone. edited := 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 }) if !strings.HasPrefix(edited.Text, "You chose Approve at ") || !strings.Contains(firstLine(edited.Text), " on Telegram · ") { t.Errorf("the question does not open with how it ended: %q", firstLine(edited.Text)) } // 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. 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")) // The same answer tapped again is what was recorded: the phone says so, and nothing more is done (issue 372: // a refusal here read as the approval failing). l.tg.waitForToast("the replayed tap said as already recorded", tapped, 30*time.Second, func(s string) bool { return strings.HasPrefix(s, "You already chose Approve at ") && strings.HasSuffix(s, ": 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()) }