mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
348 lines
17 KiB
Go
348 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.
|
|
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.waitFor("the refusal of a replayed tap", operatorAccount, tapped, 90*time.Second,
|
|
func(m tgMessage) bool { return strings.Contains(m.Text, "That answer was not taken") })
|
|
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())
|
|
}
|