Files
mesh-controller/cmd/mesh-controller/desk_secret_test.go
T
jochen f7928a2bf7 Let an own secret's manifest declare a desk prompt (issue 789)
A family like mesh-mail's mail-credential-* names only the module and the secret's key at the desk, so the operator cannot tell which account is asked. An own secret (or a family) may now declare a one-line prompt in its manifest, with {member} filled by a family member's suffix; the module check refuses one that is not one line, over 120 characters, or holds another placeholder. desk_secret.go reads it from the controller's own catalogue and sends it to the desk as what, never from the call or the module's runtime; omitted when none is declared, so an old desk or module behaves as today.
2026-10-15 13:47:20 +02:00

559 lines
25 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)
}
}
}