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
The bus the controller composes and raises, at its commit, for one machine; the mesh's runtime serving the router and the Telegram channel on their own credentials; a fake Bot API as the phone. It links the operator's account with the code shown on the desk, refuses an approval in the hour after a link, performs an approval exactly once with who and how in the warrant and the router's record, refuses a replayed tap and a redelivered warrant, tells the operator when another account answers, drops a tap from a group, does nothing on Decline and on expiry, offers no approval while an agent can become root where the channel runs, and has the bus refuse every forged warrant, ask and tap. live-acceptance.sh is the same after rollout, with a drill the operator approves on the phone.
319 lines
16 KiB
Go
319 lines
16 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") != "" })
|
|
if !strings.Contains(firstMessage.Text, "Flip the lab's switch?") || !strings.Contains(firstMessage.Text, "approve or decline") {
|
|
t.Errorf("the question does not say what is asked and why: %q", 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, "can approve from") })
|
|
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]["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:
|
|
// refused, and the operator is told which account tried. The stranger is told nothing.
|
|
tapped := time.Now()
|
|
l.tg.tapped(strangerAccount, strangerAccount, "private", m.ID, button(m, "Approve"))
|
|
l.tg.waitFor("the operator told another account tried", operatorAccount, tapped, 90*time.Second,
|
|
func(m tgMessage) bool {
|
|
return strings.Contains(m.Text, "Another account tried to answer") && strings.Contains(m.Text, fmt.Sprint(strangerAccount))
|
|
})
|
|
if got := l.tg.since(strangerAccount, since); len(got) != 0 {
|
|
t.Errorf("the bot answered the stranger: %+v", got)
|
|
}
|
|
// 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, or from a group, was a warrant")
|
|
}
|
|
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.
|
|
for _, try := range []struct{ user, subject string }{
|
|
{machine + ".lab-asker", "mesh.seat.operator-channel.accept.cancel.lab-asker"},
|
|
{machine + ".telegram", asks.IntakeSubject(asks.StandingWhat, "telegram")},
|
|
} {
|
|
if l.refusedToPublish(try.user, try.subject, []byte(`{"id":"none","ready":true}`)) {
|
|
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 an agent can become root on the router's machine, approve is not offered", func(t *testing.T) {
|
|
key := "machine." + machine + ".agent-root"
|
|
l.raise(map[string]any{"key": key, "kind": "agent-root", "severity": "urgent",
|
|
"summary": "an agent can become root on anchor without a person",
|
|
"headline": "Root without you on anchor", "explanation": "A program working for you can become root.",
|
|
"subject": map[string]any{"scope": "machine", "id": machine, "machine": machine},
|
|
"raised": time.Now().UTC().Format(time.RFC3339Nano)})
|
|
time.Sleep(2 * time.Second)
|
|
since := time.Now()
|
|
a.ask("a3", "Flip it while root is open?", time.Hour)
|
|
m := l.tg.waitFor("the third question", operatorAccount, since, 90*time.Second,
|
|
func(m tgMessage) bool { return !m.Edited && button(m, "Decline") != "" })
|
|
if button(m, "Approve") != "" {
|
|
t.Errorf("approve was offered on Telegram while an agent can become root there: %v", labels(m))
|
|
}
|
|
l.clear(key)
|
|
if err := a.client.Cancel("a3"); 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())
|
|
}
|