The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
728 lines
32 KiB
Go
728 lines
32 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
)
|
|
|
|
const typed = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
|
|
|
|
// aDesk is the desk path with a prompt the test answers as the operator would, and what it was asked kept.
|
|
func aDesk(t *testing.T, answer func(args map[string]any) (json.RawMessage, error)) (deskGive, *[]string, *[]link.HandAct, *[]map[string]any) {
|
|
t.Helper()
|
|
var accepted []string
|
|
var acts []link.HandAct
|
|
var asked []map[string]any
|
|
return deskGive{
|
|
declares: func(module, name string) error {
|
|
if module != "telegram" || name != "telegram-token" {
|
|
return errors.New(module + " does not declare " + name + " as an own secret")
|
|
}
|
|
return nil
|
|
},
|
|
ask: func(machine string, args map[string]any) (json.RawMessage, error) {
|
|
asked = append(asked, args)
|
|
return answer(args)
|
|
},
|
|
accept: func(value string) (bool, error) { accepted = append(accepted, value); return false, nil },
|
|
record: func(a link.HandAct) error { acts = append(acts, a); return nil },
|
|
}, &accepted, &acts, &asked
|
|
}
|
|
|
|
func sealedTo(t *testing.T, value string) func(args map[string]any) (json.RawMessage, error) {
|
|
return func(args map[string]any) (json.RawMessage, error) {
|
|
sealed, err := secrets.Seal(args["seal_to"].(string), []byte(value+"\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
|
|
return raw, nil
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0259 §10: the value typed at the desk is sealed as `secret accept` seals it, and is in no
|
|
// answer, no prompt argument and no act recorded.
|
|
func TestASecretGivenAtTheDeskIsSealedAndSaidNowhere(t *testing.T) {
|
|
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
words, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*accepted) != 1 || (*accepted)[0] != typed {
|
|
t.Fatalf("the value sealed is not what was typed, its line ending taken off")
|
|
}
|
|
if len(*acts) != 1 || (*acts)[0].Verb != "secret accept" || (*acts)[0].Cause != "given-at-the-desk" ||
|
|
!strings.Contains((*acts)[0].Why, "at the desk on laptop") {
|
|
t.Errorf("the act: %+v", *acts)
|
|
}
|
|
raw, _ := json.Marshal(struct {
|
|
Words string
|
|
Acts []link.HandAct
|
|
Asked []map[string]any
|
|
}{words, *acts, *asked})
|
|
if strings.Contains(string(raw), typed) || strings.Contains(string(raw), "AAEhBP0") {
|
|
t.Fatal("the value appears in what was said, asked or recorded")
|
|
}
|
|
if !strings.Contains(words, "push anchor") || !strings.Contains(words, "given at the desk on laptop") {
|
|
t.Errorf("%q", words)
|
|
}
|
|
if p := (*asked)[0]; p["seal_to"] == "" || p["timeout_seconds"] != deskPromptWithin {
|
|
t.Errorf("the prompt was asked %v", p)
|
|
}
|
|
}
|
|
|
|
func TestNothingIsAskedForASecretTheMeshWouldRefuse(t *testing.T) {
|
|
for _, c := range [][2]string{{"telegram", "chat-id"}, {"nobody", "telegram-token"}} {
|
|
d, accepted, _, asked := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", c[0], c[1], "laptop"); err == nil || !strings.Contains(err.Error(), "nobody was asked") {
|
|
t.Errorf("%v: %v", c, err)
|
|
}
|
|
if len(*asked) != 0 || len(*accepted) != 0 {
|
|
t.Errorf("%v: the operator was asked anyway", c)
|
|
}
|
|
}
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", ""); err == nil {
|
|
t.Error("no desk was refused nowhere")
|
|
}
|
|
}
|
|
|
|
func TestADismissedEmptyLateOrForeignAnswerTakesNothing(t *testing.T) {
|
|
for want, answer := range map[string]func(map[string]any) (json.RawMessage, error){
|
|
"not answered within 25 seconds": func(map[string]any) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"cancelled":true,"timed_out":true}`), nil
|
|
},
|
|
"was dismissed": func(map[string]any) (json.RawMessage, error) { return json.RawMessage(`{"cancelled":true}`), nil },
|
|
"answered empty": sealedTo(t, " "),
|
|
"not sealed to this call": func(map[string]any) (json.RawMessage, error) {
|
|
other, _, _ := secrets.Keypair()
|
|
sealed, _ := secrets.Seal(other, []byte(typed))
|
|
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
|
|
return raw, nil
|
|
},
|
|
"could not be asked": func(map[string]any) (json.RawMessage, error) { return nil, errors.New("no session answers") },
|
|
} {
|
|
d, accepted, acts, _ := aDesk(t, answer)
|
|
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), want) || strings.Contains(err.Error(), typed) {
|
|
t.Errorf("want %q, got %v", want, err)
|
|
}
|
|
if len(*accepted) != 0 || len(*acts) != 0 {
|
|
t.Errorf("%s: something was taken or recorded", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheGiveVerbRunsTheDeskPathAndTheControllerMayAskTheDesk(t *testing.T) {
|
|
argv, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token", "at": "laptop"})
|
|
if err != nil || strings.Join(argv, " ") != "secret ask anchor telegram telegram-token --at laptop" {
|
|
t.Fatalf("%v %v", argv, err)
|
|
}
|
|
if _, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token"}); err == nil {
|
|
t.Error("give without a desk was taken")
|
|
}
|
|
// secret-ask is the same line, with the desk the module's machine unless named (novox/hq ADR 0277).
|
|
argv, err = argvFor("secret-ask", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token"})
|
|
if err != nil || strings.Join(argv, " ") != "secret ask anchor telegram telegram-token" {
|
|
t.Fatalf("%v %v", argv, err)
|
|
}
|
|
argv, err = argvFor("secret-ask", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token", "at": "laptop"})
|
|
if err != nil || strings.Join(argv, " ") != "secret ask anchor telegram telegram-token --at laptop" {
|
|
t.Fatalf("%v %v", argv, err)
|
|
}
|
|
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, p := range perms.Publish {
|
|
found = found || p == "mesh.seat.node-launcher.tool.secret.*"
|
|
}
|
|
if !found {
|
|
t.Error("the controller may not ask the desk's prompt")
|
|
}
|
|
}
|
|
|
|
// The review of 2026-10-09 (M4): the desk's prompt says who asks in words the caller does not choose — the
|
|
// controller, which the bus alone lets ask it — and what for, from names the controller checked; the prompt
|
|
// carries no free text of the caller's.
|
|
func TestThePromptIsAskedByNameNeverByWordsTheCallerChose(t *testing.T) {
|
|
d, _, _, asked := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := (*asked)[0]
|
|
if p["module"] != "telegram" || p["secret"] != "telegram-token" || p["node"] != "anchor" {
|
|
t.Errorf("the prompt was not asked by name: %v", p)
|
|
}
|
|
for _, free := range []string{"prompt", "message"} {
|
|
if _, there := p[free]; there {
|
|
t.Errorf("the prompt carries the caller's %s: %v", free, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every value given for a module's own secret is announced as a condition, on every channel (the review of
|
|
// 2026-10-09, M4): a bot token changed by somebody else is a channel that now answers for them.
|
|
func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) {
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
var said []string
|
|
d.announce = func(node, module, name, how string) error {
|
|
said = append(said, node+" "+module+" "+name+" "+how)
|
|
return nil
|
|
}
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(said) != 1 || !strings.Contains(said[0], "anchor telegram telegram-token") || !strings.Contains(said[0], "laptop") {
|
|
t.Fatalf("announced %v", said)
|
|
}
|
|
o := secretGivenObservation("anchor", "telegram", "telegram-token", "at the desk on laptop", time.Date(2026, 10, 9, 12, 3, 0, 0, time.UTC))
|
|
if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram token") ||
|
|
len(o.Actions) == 0 || o.Key() == "" {
|
|
t.Errorf("the announcement %+v", o)
|
|
}
|
|
if strings.Contains(o.Summary+o.Explanation+o.Said, typed) {
|
|
t.Error("the announcement carries the value")
|
|
}
|
|
}
|
|
|
|
// **The secret-given condition says itself in its own plain words** (hq issue 359): the words it carries pass
|
|
// the plain rule, from the controller's terminal and from a desk, so the keeper keeps them — they once held a
|
|
// clock time, and the operator read "Novox needs a look … a problem it calls secret given" instead. Its
|
|
// severity and its answer stay: it is heard on every channel, and silenced by the operator.
|
|
func TestTheSecretGivenConditionSaysItselfInPlainWords(t *testing.T) {
|
|
at := time.Date(2026, 10, 9, 23, 32, 0, 0, time.Local)
|
|
for _, how := range []string{"at the controller's terminal", "at the desk on laptop"} {
|
|
o := secretGivenObservation("anchor", "telegram", "telegram-token", how, at)
|
|
w := conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs,
|
|
Actions: o.Actions}
|
|
if why, ok := conditions.PlainWords(w, o.Machine); !ok {
|
|
t.Fatalf("given %s, the words are not plain: %s", how, why)
|
|
}
|
|
k, _ := withConditionsInMemory(t)
|
|
c, err := k.Observe(t.Context(), o)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Headline != "New secret given for telegram on anchor" || c.Severity != conditions.Urgent ||
|
|
!strings.HasPrefix(c.Explanation, conditions.NeedsYou+" silence this") ||
|
|
!strings.Contains(c.Explanation, "telegram token of telegram on anchor was given "+how) ||
|
|
len(c.Actions) != 1 || c.Actions[0].Label != "Silence for a week" {
|
|
t.Errorf("given %s, the keeper said %q / %q (%s, %v)", how, c.Headline, c.Explanation, c.Severity, c.Actions)
|
|
}
|
|
if strings.Contains(c.Headline+c.Explanation+c.Resolved+c.Needs, typed) {
|
|
t.Error("the words carry the value")
|
|
}
|
|
}
|
|
// A long module name keeps the headline and the resolved line within their bounds.
|
|
for _, module := range []string{"a-module-with-a-rather-long-name-indeed",
|
|
"a-module-with-a-name-so-long-that-no-headline-could-ever-hold-it"} {
|
|
o := secretGivenObservation("anchor", module, "api-key", "at the controller's terminal", at)
|
|
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
|
|
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions}, o.Machine); !ok {
|
|
t.Errorf("the module %s: %s", module, why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The bus lets the controller alone ask the desk's prompt (the review of 2026-10-09, M4): the runtime, which
|
|
// carries every agent's calls, and a person granted every tool are denied it, however wide their grant.
|
|
func TestOnlyTheControllerMayAskTheDesksPrompt(t *testing.T) {
|
|
for _, p := range []broker.Principal{
|
|
{Kind: broker.KindNodeTools, Node: "laptop"},
|
|
{Kind: broker.KindPerson, Module: "operator", Invokes: []string{"*"}},
|
|
{Kind: broker.KindModule, Node: "laptop", Module: "lab", Invokes: []string{"seat:node-launcher.secret"}},
|
|
} {
|
|
perms, err := broker.PermissionsFor(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, subject := range []string{"mesh.seat.node-launcher.tool.secret.laptop", "mesh.seat.node-launcher.tool.secret",
|
|
"mesh.mod.rofi.tool.node-launcher.secret", "mesh.mod.rofi.tool.node-launcher.secret.laptop"} {
|
|
if broker.MayPublish(perms, subject) {
|
|
t.Errorf("%s may publish %s", p.Username(), subject)
|
|
}
|
|
}
|
|
}
|
|
perms, _ := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
|
|
if !broker.MayPublish(perms, "mesh.seat.node-launcher.tool.secret.laptop") {
|
|
t.Error("the controller may not ask the desk's prompt")
|
|
}
|
|
}
|
|
|
|
// A value for a secret comes from the terminal or the desk, never through a verb (the review of 2026-10-09,
|
|
// M4): `secret accept` with a value, run for a verb, is refused before anything is read.
|
|
func TestASecretValueIsNeverAcceptedThroughAVerb(t *testing.T) {
|
|
t.Setenv(verbVar, "mesh-controller.command")
|
|
for _, args := range [][]string{
|
|
{"accept", "anchor", "telegram", "telegram-token", "--from", "/dev/null"},
|
|
{"accept", "anchor", "app", "db", "--from", "/dev/null", "--provider", "store"},
|
|
} {
|
|
err := secretCommand(context.Background(), args)
|
|
if err == nil || !strings.Contains(err.Error(), "never through a verb") {
|
|
t.Errorf("%v: %v", args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The `give` and `secret-ask` verbs' own line passes the terminal-only rule of ADR 0266, and no `secret accept`
|
|
// does: a value, a file, a provider or an extra word is still the terminal's alone (novox/hq ADR 0277).
|
|
func TestOnlyTheGiveLinePassesTheTerminalRuleForSecrets(t *testing.T) {
|
|
for _, argv := range [][]string{
|
|
{"secret", "ask", "anchor", "telegram", "telegram-token", "--at", "laptop"},
|
|
{"secret", "ask", "anchor", "telegram", "telegram-token"},
|
|
} {
|
|
if err := terminalOnly(argv); err != nil {
|
|
t.Errorf("%v refused: %v", argv, err)
|
|
}
|
|
}
|
|
for _, argv := range [][]string{
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"},
|
|
{"secret", "ask", "anchor", "telegram", "telegram-token", "--from", "/tmp/x"},
|
|
{"secret", "ask", "anchor", "telegram", "telegram-token", "--at", "laptop", "--local"},
|
|
{"secret", "ask", "anchor", "telegram", "--at", "laptop"},
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token"},
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "/tmp/x"},
|
|
{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop", "--local"},
|
|
{"secret", "accept", "anchor", "telegram", "--provider", "--at-desk", "laptop"},
|
|
{"secret", "export", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"},
|
|
} {
|
|
if err := terminalOnly(argv); err == nil {
|
|
t.Errorf("%v passed the terminal rule", argv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The confirmation review of 2026-10-09, N1-give: a desk's prompt is answered over the desk machine's bus, and
|
|
// on a desk machine agents run as the operator, who holds its runtime's credential — so a trusted party's
|
|
// secret (a module running as an account of its own: the Telegram bot's token) is never taken at a desk.
|
|
// Refused before anybody is asked to type, whoever called, naming the terminal's line.
|
|
func TestATrustedPartysSecretIsNeverTakenAtADesk(t *testing.T) {
|
|
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
d.trusted = func(module string) (bool, error) { return module == "telegram", nil }
|
|
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), "controller's terminal alone") ||
|
|
!strings.Contains(err.Error(), "secret accept anchor telegram telegram-token") {
|
|
t.Fatalf("a trusted party's secret was taken at the desk, or refused without the line: %v", err)
|
|
}
|
|
if len(*asked)+len(*accepted)+len(*acts) != 0 {
|
|
t.Errorf("asked %v, accepted %d, recorded %v", *asked, len(*accepted), *acts)
|
|
}
|
|
d.trusted = func(string) (bool, error) { return false, errors.New("the store did not answer") }
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || len(*asked) != 0 {
|
|
t.Errorf("a module not known to be untrusted was asked at the desk: %v", err)
|
|
}
|
|
}
|
|
|
|
// And who may answer the desk's prompt at all: only the runtime of the machine it is asked on, carrying the
|
|
// launcher that holds the seat there — never the controller, another machine's runtime, or a module's own
|
|
// account (the confirmation review of 2026-10-09, N1-give).
|
|
func TestOnlyTheDeskMachinesLauncherMayAnswerItsPrompt(t *testing.T) {
|
|
launcher := broker.Declared{Module: "rofi", Holds: []broker.Seat{{Name: "node-launcher", Scope: "node",
|
|
Serves: []string{"run", "secret"}}}}
|
|
subject := "mesh.seat.node-launcher.tool.secret.laptop"
|
|
for _, c := range []struct {
|
|
p broker.Principal
|
|
answers bool
|
|
}{
|
|
{broker.Principal{Kind: broker.KindNodeTools, Node: "laptop", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, true},
|
|
{broker.Principal{Kind: broker.KindNodeTools, Node: "anchor", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, false},
|
|
{broker.Principal{Kind: broker.KindController}, false},
|
|
{broker.Principal{Kind: broker.KindModule, Node: "laptop", Module: "lab"}, false},
|
|
{broker.Principal{Kind: broker.KindNode, Node: "laptop"}, false},
|
|
} {
|
|
perms, err := broker.PermissionsFor(c.p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := broker.MaySubscribe(perms, subject); got != c.answers {
|
|
t.Errorf("%s may answer %s: %v, want %v", c.p.Username(), subject, got, c.answers)
|
|
}
|
|
}
|
|
}
|
|
|
|
// N1-give at the controller's terminal (the confirmation review of 2026-10-09): a trusted party's secret is
|
|
// announced before it is kept, and not kept when the announcement fails; another module's is kept first and
|
|
// a failed announcement is said, not undone.
|
|
func TestATrustedPartysSecretGivenAtTheTerminalIsAnnouncedBeforeItIsKept(t *testing.T) {
|
|
var order []string
|
|
announce := func(fail bool) func() error {
|
|
return func() error {
|
|
order = append(order, "announce")
|
|
if fail {
|
|
return errors.New("no channel")
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
keep := func() (bool, error) { order = append(order, "keep"); return false, nil }
|
|
|
|
order = nil
|
|
if _, unannounced, err := keepGiven(true, announce(false), keep); err != nil || unannounced != nil ||
|
|
strings.Join(order, ",") != "announce,keep" {
|
|
t.Errorf("trusted: %v %v, order %v; want announced, then kept", unannounced, err, order)
|
|
}
|
|
order = nil
|
|
if _, _, err := keepGiven(true, announce(true), keep); err == nil || strings.Join(order, ",") != "announce" {
|
|
t.Errorf("trusted, announcement failed: %v, order %v; want refused and nothing kept", err, order)
|
|
}
|
|
order = nil
|
|
if _, unannounced, err := keepGiven(false, announce(true), keep); err != nil || unannounced == nil ||
|
|
strings.Join(order, ",") != "keep,announce" {
|
|
t.Errorf("not trusted: %v %v, order %v; want kept, then the failed announcement said", unannounced, err, order)
|
|
}
|
|
order = nil
|
|
failing := func() (bool, error) { order = append(order, "keep"); return false, errors.New("store away") }
|
|
if _, _, err := keepGiven(false, announce(false), failing); err == nil || strings.Join(order, ",") != "keep" {
|
|
t.Errorf("not trusted, keep failed: %v, order %v; want refused and nothing announced", err, order)
|
|
}
|
|
}
|
|
|
|
// A machine the mesh does not know, as the secret's or as the desk, is refused before anybody is asked to type.
|
|
func TestAGiveNamingAMachineTheMeshDoesNotKnowAsksNobody(t *testing.T) {
|
|
for _, unknown := range []string{"elsewhere", "nodesk"} {
|
|
d, accepted, acts, asked := aDesk(t, func(map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("the desk was asked")
|
|
return nil, nil
|
|
})
|
|
d.known = func(machine string) error {
|
|
if machine == unknown {
|
|
return errors.New("no node " + machine)
|
|
}
|
|
return nil
|
|
}
|
|
node, desk := "anchor", "laptop"
|
|
if unknown == "elsewhere" {
|
|
node = unknown
|
|
} else {
|
|
desk = unknown
|
|
}
|
|
_, err := d.give(node, "telegram", "telegram-token", desk)
|
|
if err == nil || !strings.Contains(err.Error(), "nobody was asked") || !strings.Contains(err.Error(), unknown) {
|
|
t.Errorf("%s: %v", unknown, err)
|
|
}
|
|
if len(*accepted)+len(*acts)+len(*asked) != 0 {
|
|
t.Errorf("%s: something happened: %v %v %v", unknown, *accepted, *acts, *asked)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0277: the prompt names who asked — the controller's word on the bus's caller, cut to a name's
|
|
// characters — and never a word the caller chose: there is no argument for it.
|
|
func TestThePromptNamesWhoAskedFromTheBussWordAlone(t *testing.T) {
|
|
d, _, acts, asked := aDesk(t, sealedTo(t, typed))
|
|
d.askedBy = "g14/claude-code, through the mesh-controller seat"
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := (*asked)[0]["asked_by"]; got != "g14/claude-code" {
|
|
t.Errorf("asked_by %q", got)
|
|
}
|
|
if len(*acts) != 1 || !strings.Contains((*acts)[0].Why, "asked by g14/claude-code") {
|
|
t.Errorf("the record: %+v", *acts)
|
|
}
|
|
for in, want := range map[string]string{
|
|
"jochen at a shell on novox": "jochen at a shell on novox",
|
|
"laptop/agent": "laptop/agent",
|
|
"Your bank asks\nType your PIN, now": "Your bank asks-Type your PIN",
|
|
"": "an unnamed caller",
|
|
"<b>x</b>": "an unnamed caller",
|
|
strings.Repeat("a", 100): strings.Repeat("a", 80),
|
|
"--prompt, something else": "an unnamed caller",
|
|
} {
|
|
if got := askedByName(in); got != want {
|
|
t.Errorf("askedByName(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
// The verb's schema has no argument that reaches the prompt's words.
|
|
for _, verb := range []string{"give", "secret-ask"} {
|
|
for _, free := range []string{"asked_by", "prompt", "message", "value", "from"} {
|
|
if _, err := argvFor(verb, map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token",
|
|
"at": "laptop", free: "x"}); err == nil {
|
|
t.Errorf("%s takes %s", verb, free)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The desk's prompt names which account of a family is asked for, from the catalogue's own words (novox/hq
|
|
// issue 789): a family declaring `prompt` with `{member}` sends `what` with the member's suffix filled in.
|
|
func TestThePromptNamesWhichAccountOfAFamilyIsAsked(t *testing.T) {
|
|
d, _, _, asked := aDesk(t, sealedTo(t, typed))
|
|
d.declares = func(module, name string) error {
|
|
if module != "mesh-mail" || name != "mail-credential-gmail" {
|
|
return errors.New(module + " does not declare " + name + " as an own secret")
|
|
}
|
|
return nil
|
|
}
|
|
d.promptFor = func(module, name string) (string, error) {
|
|
if module == "mesh-mail" && name == "mail-credential-gmail" {
|
|
return "the password of the mail account gmail", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
if _, err := d.give("anchor", "mesh-mail", "mail-credential-gmail", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := (*asked)[0]["what"]; got != "the password of the mail account gmail" {
|
|
t.Errorf(`"what" = %v`, got)
|
|
}
|
|
}
|
|
|
|
// Without a declared prompt, no `what` is sent: an old module, or one that declares none, is asked by name alone,
|
|
// as the desk does today (novox/hq issue 789).
|
|
func TestWithNoDeclaredPromptNoWhatIsSent(t *testing.T) {
|
|
d, _, _, asked := aDesk(t, sealedTo(t, typed))
|
|
d.promptFor = func(module, name string) (string, error) { return "", nil }
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, there := (*asked)[0]["what"]; there {
|
|
t.Errorf(`"what" was sent with no declared prompt: %v`, (*asked)[0])
|
|
}
|
|
// And with no promptFor wired at all (an older caller of deskGive), same thing: `give` still works, asking
|
|
// nothing more than it does today.
|
|
d2, _, _, asked2 := aDesk(t, sealedTo(t, typed))
|
|
if _, err := d2.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, there := (*asked2)[0]["what"]; there {
|
|
t.Errorf(`"what" was sent with promptFor nil: %v`, (*asked2)[0])
|
|
}
|
|
}
|
|
|
|
// An ask for a secret is bounded before anybody is asked to type (ADR 0277): the record refuses it, nothing is
|
|
// asked; and how every ask ends is recorded.
|
|
func TestASecretAskIsBoundedAndItsEndRecorded(t *testing.T) {
|
|
d, accepted, _, asked := aDesk(t, sealedTo(t, typed))
|
|
var ended []string
|
|
d.open = func(node, module, name, desk string) (int64, error) { return 7, nil }
|
|
d.end = func(id int64, outcome string) error {
|
|
ended = append(ended, fmt.Sprintf("%d %s", id, outcome))
|
|
return nil
|
|
}
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d.open = func(node, module, name, desk string) (int64, error) {
|
|
return 0, errors.New("an ask for telegram-token of telegram on anchor is still open")
|
|
}
|
|
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
|
|
if err == nil || !strings.Contains(err.Error(), "nobody was asked to type anything") || !strings.Contains(err.Error(), "still open") {
|
|
t.Errorf("a second ask: %v", err)
|
|
}
|
|
if len(*asked) != 1 || len(*accepted) != 1 {
|
|
t.Errorf("asked %d, accepted %d", len(*asked), len(*accepted))
|
|
}
|
|
d.open = func(node, module, name, desk string) (int64, error) { return 8, nil }
|
|
d.ask = func(string, map[string]any) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"cancelled":true}`), nil
|
|
}
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); !errors.Is(err, errNothingGiven) {
|
|
t.Errorf("a dismissed prompt: %v", err)
|
|
}
|
|
if strings.Join(ended, "; ") != "7 given; 8 dismissed" {
|
|
t.Errorf("ended: %v", ended)
|
|
}
|
|
}
|
|
|
|
// A secret ask cannot be turned into a secret read: the line carries no value, the answer carries none, and every
|
|
// other `secret` line is the terminal's.
|
|
func TestASecretAskIsNeverASecretRead(t *testing.T) {
|
|
t.Setenv(verbVar, "mesh-controller.secret-ask")
|
|
for _, args := range [][]string{
|
|
{"ask", "anchor", "telegram", "telegram-token", "the-value"},
|
|
{"ask", "anchor", "telegram"},
|
|
{"ask", "anchor", "telegram", "telegram-token", "--from", "/dev/null"},
|
|
} {
|
|
if err := secretCommand(context.Background(), args); err == nil {
|
|
t.Errorf("secret %v was taken", args)
|
|
}
|
|
}
|
|
for _, args := range [][]string{{"recover", "anchor", "telegram", "telegram-token"}, {"export"}} {
|
|
if err := terminalOnly(append([]string{"secret"}, args...)); err == nil {
|
|
t.Errorf("secret %v passed the terminal rule", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// askOperatorChannelFake replaces askOperatorChannel for the test and records every call it was given.
|
|
func askOperatorChannelFake(t *testing.T, answer func(args map[string]any) (json.RawMessage, error)) *[]map[string]any {
|
|
t.Helper()
|
|
var calls []map[string]any
|
|
was := askOperatorChannel
|
|
askOperatorChannel = func(_ context.Context, args map[string]any) (json.RawMessage, error) {
|
|
calls = append(calls, args)
|
|
return answer(args)
|
|
}
|
|
t.Cleanup(func() { askOperatorChannel = was })
|
|
return &calls
|
|
}
|
|
|
|
// announceWithRealTell wires a deskGive's announce to the real announceSecretGiven, as askAtDesk does,
|
|
// rather than the fakes the tests above use to look at what `give` does around it.
|
|
func announceWithRealTell(ctx context.Context, askedBy string) func(node, module, name, how string) error {
|
|
return func(node, module, name, how string) error {
|
|
return announceSecretGiven(ctx, node, module, name, how, askedBy)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0380 clauses 1-2: a secret given at the desk is told to the operator once, in a plain
|
|
// message through the operator channel's `tell`, naming the secret, the module, where and when and who
|
|
// asked, and opens no condition — the kind the review of 2026-10-09 (M4) raised is retired.
|
|
func TestASecretGivenAtTheDeskIsToldOnceAndOpensNoCondition(t *testing.T) {
|
|
ctx := context.Background()
|
|
k, _ := withConditionsInMemory(t)
|
|
calls := askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, nil })
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
d.announce = announceWithRealTell(ctx, "mail-seats-835@novox")
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*calls) != 1 {
|
|
t.Fatalf("told the operator %d times, want exactly 1: %v", len(*calls), *calls)
|
|
}
|
|
call := (*calls)[0]
|
|
if call["headline"] != "A password was entered for telegram's telegram token on anchor" {
|
|
t.Errorf("headline %v", call["headline"])
|
|
}
|
|
if call["subject"] != "anchor" {
|
|
t.Errorf("subject %v", call["subject"])
|
|
}
|
|
lines, _ := call["lines"].([]string)
|
|
if len(lines) != 2 || !strings.Contains(lines[0], "at the desk on laptop at") ||
|
|
!strings.Contains(lines[0], "asked by mail-seats-835@novox") ||
|
|
lines[1] != "If that was not you, give it again yourself at the controller's terminal." {
|
|
t.Errorf("lines %v", lines)
|
|
}
|
|
open, err := k.Open(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(open) != 0 {
|
|
t.Errorf("a condition was raised though the operator was told: %+v", open)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0380 §9: until the operator channel's `tell` is delivered — nothing yet serves it, or the
|
|
// controller's own grant is refused — the secret-given condition is raised exactly as before.
|
|
func TestASecretGivenAtTheDeskRaisesTheConditionWhileNothingServesTell(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, failure := range []error{link.ErrNothingServes, link.ErrGrantRefused} {
|
|
k, _ := withConditionsInMemory(t)
|
|
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, failure })
|
|
d, _, _, _ := aDesk(t, sealedTo(t, typed))
|
|
d.announce = announceWithRealTell(ctx, "mesh-mail-835, novox")
|
|
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
open, err := k.Open(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(open) != 1 || open[0].Kind != kindSecretGiven {
|
|
t.Fatalf("%v: the condition was not raised as before: %+v", failure, open)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0380 §10: once the operator channel is told successfully, every secret-given condition
|
|
// still open from before this change is retired, without a resolved line of its own.
|
|
func TestASuccessfulTellRetiresEveryOpenSecretGivenCondition(t *testing.T) {
|
|
ctx := context.Background()
|
|
k, _ := withConditionsInMemory(t)
|
|
for _, module := range []string{"telegram", "mesh-mail"} {
|
|
if _, err := k.Observe(ctx, secretGivenObservation("anchor", module, "a-secret", "at the desk on laptop",
|
|
time.Now())); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, nil })
|
|
if err := announceSecretGiven(ctx, "anchor", "telegram", "telegram-token", "at the desk on laptop", "me"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
open, err := k.Open(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(open) != 0 {
|
|
t.Fatalf("the open secret-given conditions were not retired: %+v", open)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0380 clause 7: a trusted party's secret given at the controller's terminal is announced
|
|
// before it is kept, and not kept when the announcement fails. It still fails when telling the operator
|
|
// AND raising the fallback condition both fail — nothing silently keeps a trusted party's value unheard.
|
|
func TestATrustedPartysSecretAtTheTerminalIsNotKeptWhenTellAndTheConditionBothFail(t *testing.T) {
|
|
ctx := context.Background()
|
|
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, link.ErrNothingServes })
|
|
// conditionsFrom is left nil and there is no bus to dial here, so the fallback condition cannot be
|
|
// raised either: both of ADR 0380's ways to tell the operator fail.
|
|
kept := false
|
|
if _, _, err := keepGiven(true,
|
|
func() error {
|
|
return announceSecretGiven(ctx, "anchor", "telegram", "telegram-token", "at the controller's terminal", "me")
|
|
},
|
|
func() (bool, error) { kept = true; return false, nil }); err == nil {
|
|
t.Error("a trusted party's secret was kept though telling the operator and raising the condition both failed")
|
|
}
|
|
if kept {
|
|
t.Error("the value was kept despite the failure")
|
|
}
|
|
}
|
|
|
|
// Review round 2 of mesh-controller (feat/mail-seats, head 5f24a47d): ADR 0380 §9's fallback — raising the
|
|
// secret-given condition — answers only link.ErrNothingServes and link.ErrGrantRefused. Any other failure of
|
|
// the operator channel's `tell` (the holder itself refusing, or answering something unreadable) is the
|
|
// announcement's own failure, surfaced to the caller, and opens no condition.
|
|
func TestAnOperatorChannelFailureOtherThanNothingServesOrGrantRefusedSurfacesAndRaisesNoCondition(t *testing.T) {
|
|
ctx := context.Background()
|
|
k, _ := withConditionsInMemory(t)
|
|
refused := errors.New("operator-channel.tell refused: too many lines")
|
|
askOperatorChannelFake(t, func(map[string]any) (json.RawMessage, error) { return nil, refused })
|
|
if err := announceSecretGiven(ctx, "anchor", "telegram", "telegram-token", "at the desk on laptop", "me"); err != refused {
|
|
t.Fatalf("announceSecretGiven = %v, want the refusal itself surfaced", err)
|
|
}
|
|
open, err := k.Open(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(open) != 0 {
|
|
t.Errorf("a condition was raised though the failure was neither ErrNothingServes nor ErrGrantRefused: %+v", open)
|
|
}
|
|
}
|
|
|
|
// Review round 2 of mesh-controller: a secret accepted at the controller's terminal writes a hand act naming
|
|
// who asked, where and when it was given — and never the value (novox/hq ADR 0380 §6).
|
|
func TestASecretAcceptedAtTheTerminalWritesAHandActNamingWhoWhereAndNeverTheValue(t *testing.T) {
|
|
act := terminalGiveHandAct("anchor", "telegram", "telegram-token", "mail-seats-835@novox")
|
|
if act.Verb != "secret accept" || act.Cause != "given-at-the-terminal" {
|
|
t.Fatalf("act %+v", act)
|
|
}
|
|
if !strings.Contains(act.Why, "at the controller's terminal") {
|
|
t.Errorf("the act does not name where: %q", act.Why)
|
|
}
|
|
if !strings.Contains(act.Why, "asked by mail-seats-835@novox") {
|
|
t.Errorf("the act does not name who: %q", act.Why)
|
|
}
|
|
if strings.Contains(act.Why, typed) {
|
|
t.Error("the act carries the value")
|
|
}
|
|
// When is RecordHandAct's own job (it stamps At with time.Now when the act leaves none, and By with the
|
|
// caller when the act leaves that empty too): this act leaves both for it to fill.
|
|
if act.By != "" || !act.At.IsZero() {
|
|
t.Errorf("the act sets its own who/when instead of leaving them to RecordHandAct: %+v", act)
|
|
}
|
|
}
|