172 lines
9.0 KiB
Go
172 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// 774/AC9 (novox/hq ADR 0561 §2, ADR 0230 §15, §18): a wanted account whose key is deleted is retired (its hash taken
|
|
// out of bus-accounts, its value kept, marked) only once the same set has stood for five passes and ten minutes, and never
|
|
// more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted
|
|
// again, it is re-enabled at once; and a retired account is not hashed again at a start.
|
|
func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
|
|
v, accounts, _ := mintingVault(t)
|
|
clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC)
|
|
v.Now = func() time.Time { return clock }
|
|
modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"}
|
|
for _, m := range modules {
|
|
w := aWant(m)
|
|
v.WantHeard(w.User, wantRaw(t, w), false)
|
|
}
|
|
v.WantsRead()
|
|
if len(accounts.keys) != 8 {
|
|
t.Fatalf("minted %d", len(accounts.keys))
|
|
}
|
|
pass := func(n int, every time.Duration) {
|
|
for i := 0; i < n; i++ {
|
|
clock = clock.Add(every)
|
|
v.RetirePass()
|
|
}
|
|
}
|
|
v.WantHeard("ace.m1", nil, true)
|
|
pass(5, time.Second)
|
|
if _, ok := accounts.keys["ace.m1"]; !ok {
|
|
t.Fatal("retired after five passes in five seconds: ADR 0230 waits ten minutes too")
|
|
}
|
|
pass(1, 10*time.Minute)
|
|
if _, ok := accounts.keys["ace.m1"]; ok {
|
|
t.Fatalf("not retired after five passes and ten minutes: %+v", v.RetireState())
|
|
}
|
|
if rec, held, _ := v.Store.Read("own/ace/m1/broker"); !held || rec.Retired == nil {
|
|
t.Fatalf("the retired account's value is %+v (held %v): kept, and marked", rec, held)
|
|
}
|
|
if written, _ := v.HashHeld(); len(written) != 0 {
|
|
t.Errorf("a start hashed a retired account again: %+v", written)
|
|
}
|
|
// Wanted again: re-enabled at once.
|
|
w := aWant("m1")
|
|
v.WantHeard(w.User, wantRaw(t, w), false)
|
|
if rec, _, _ := v.Store.Read("own/ace/m1/broker"); rec.Retired != nil || accounts.keys["ace.m1"].Hash == "" {
|
|
t.Fatalf("wanted again, ace.m1 is %+v with hash %v", rec.Retired, accounts.keys["ace.m1"])
|
|
}
|
|
// Four gone at once is over the bound: it waits for a person, who retires them by hand.
|
|
for _, m := range modules[4:] {
|
|
v.WantHeard("ace."+m, nil, true)
|
|
}
|
|
pass(6, 3*time.Minute)
|
|
st := v.RetireState()
|
|
if len(accounts.keys) != 8 || len(st.Waiting) != 4 || !strings.Contains(st.Why, "more than 3") {
|
|
t.Fatalf("over the bound: %d keys, %+v", len(accounts.keys), st)
|
|
}
|
|
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, ""); err == nil {
|
|
t.Error("retired by hand with no why")
|
|
}
|
|
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil {
|
|
t.Error("retired by hand an account still wanted")
|
|
}
|
|
// Never before the wants are read whole; no lease is asked (issue 1126).
|
|
v.minting.mu.Lock()
|
|
v.minting.retiring.wantsRead = false
|
|
v.minting.mu.Unlock()
|
|
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "not read whole") {
|
|
t.Errorf("retired by hand before the wants were read: %v", err)
|
|
}
|
|
v.WantsRead()
|
|
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5", "ace.m6"}, "the modules left"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := accounts.keys["ace.m5"]; ok || len(accounts.keys) != 6 {
|
|
t.Errorf("after a person's retirement %d keys remain", len(accounts.keys))
|
|
}
|
|
}
|
|
|
|
// 1966/CAT-V1 (design 32): `retire` removes one module's own secret on a node, and says own-retired, only for a caller on
|
|
// the vault's list retire-callers who carries a hand act it recorded for this verb naming exactly this node, module and
|
|
// secret. Without the act, with another name's, with another caller's or an older one, from a caller not on the list, or
|
|
// for a bus account, nothing is removed and nothing is said; a waiter on rotated or own-replaced is not woken.
|
|
func TestRetireRemovesAnOwnSecretOnlyOnTheWarrantOfTheListedCaller(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
const caller = "novox.mesh-config"
|
|
v.RetireCallers = stdio.Callers{"mesh-config"}
|
|
acts := map[string]*HandActSeen{}
|
|
v.ReadAct = func(id string) (*HandActSeen, error) { return acts[id], nil }
|
|
act := func(id, by, verb, what string, age time.Duration) {
|
|
acts[id] = &HandActSeen{ID: id, By: by, At: v.Now().Add(-age), Verb: verb, Args: []string{what}}
|
|
}
|
|
act("ok", caller, "mesh-vault.retire", "g14 dunst token ret-1", time.Minute)
|
|
act("other-name", caller, "mesh-vault.retire", "g14 dunst other ret-1", time.Minute)
|
|
act("other-node", caller, "mesh-vault.retire", "ace dunst token ret-1", time.Minute)
|
|
act("other-caller", "g14.claude-code", "mesh-vault.retire", "g14 dunst token ret-1", time.Minute)
|
|
act("other-verb", caller, "mesh-vault.rotate", "g14 dunst token ret-1", time.Minute)
|
|
act("ledgers", caller, "mesh-vault.retire", "g14 node-tools ledgers ret-1", time.Minute)
|
|
act("no-ask", caller, "mesh-vault.retire", "g14 dunst token", time.Minute)
|
|
act("old", caller, "mesh-vault.retire", "g14 dunst token ret-1", 25*time.Hour)
|
|
const secret = "own/g14/dunst/token"
|
|
for _, name := range []string{secret, "own/g14/dunst/other", "own/g14/node-tools/ledgers"} {
|
|
if _, err := v.keep(name, "a-value-of-"+name, "the test", "g14.dunst", OriginMade); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
name, caller string
|
|
args RetireArgs
|
|
want string
|
|
}{
|
|
{"no act", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Ask: "ret-1"}, "carries the hand act"},
|
|
{"an act that does not exist", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "nope", Ask: "ret-1"}, "holds no hand act"},
|
|
{"an act for another name", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-name", Ask: "ret-1"}, "a warrant retires nothing else"},
|
|
{"an act for another node", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-node", Ask: "ret-1"}, "a warrant retires nothing else"},
|
|
{"an act of another caller", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-caller", Ask: "ret-1"}, "was recorded by g14.claude-code"},
|
|
{"an act for another verb", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-verb", Ask: "ret-1"}, "warrants no retirement"},
|
|
{"an ask other than the act's", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok", Ask: "ret-2"}, "a warrant retires nothing else"},
|
|
{"no ask", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok"}, "names the ask"},
|
|
{"an act that names no ask", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "no-ask", Ask: "ret-1"}, "a warrant retires nothing else"},
|
|
{"an act older than a day", caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "old", Ask: "ret-1"}, "older than"},
|
|
{"a caller not on the list", "g14.claude-code", RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "other-caller", Ask: "ret-1"}, "retire-callers"},
|
|
{"an unproven caller", stdio.Unproven, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok", Ask: "ret-1"}, "retire-callers"},
|
|
{"the tool runner's ledgers", caller, RetireArgs{Node: "g14", Module: "node-tools", Name: "ledgers", Warrant: "ledgers", Ask: "ret-1"}, "bus account or the tool runner's ledgers"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
before := len(v.bus.events)
|
|
if _, err := v.RetireOwn(tc.caller, tc.args); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("answered %v, want a refusal saying %q", err, tc.want)
|
|
}
|
|
if _, held, _ := v.Store.Read(secret); !held {
|
|
t.Error("a refused retire removed the secret")
|
|
}
|
|
if n := len(v.bus.events) - before; n != 0 {
|
|
t.Errorf("a refused retire said %d events", n)
|
|
}
|
|
})
|
|
}
|
|
before := len(v.bus.events)
|
|
got, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok", Ask: "ret-1"})
|
|
if err != nil || !got.Retired {
|
|
t.Fatalf("retire answered %+v, %v", got, err)
|
|
}
|
|
if _, held, _ := v.Store.Read(secret); held {
|
|
t.Error("the secret is still held")
|
|
}
|
|
if _, held, _ := v.Store.Read("own/g14/dunst/other"); !held {
|
|
t.Error("another secret of the module was removed")
|
|
}
|
|
if n := eventsNamed(v, before, OwnRetiredEvent); n != 1 || len(v.bus.events)-before != 1 {
|
|
t.Fatalf("said %d own-retired among %d events, want one and no other", n, len(v.bus.events)-before)
|
|
}
|
|
if body, _ := v.bus.events[before]["body"].(map[string]any); len(body) != 5 || body["ask"] != "ret-1" || body["caller"] != caller || body["node"] != "g14" ||
|
|
body["module"] != "dunst" || body["name"] != "token" {
|
|
t.Errorf("own-retired said %+v", body)
|
|
}
|
|
if OwnRetiredEvent == OwnReplacedEvent || OwnRetiredEvent == "rotated" {
|
|
t.Error("own-retired shares a name with an event a waiter hears")
|
|
}
|
|
// Asked again for what is gone: nothing to retire.
|
|
if _, err := v.RetireOwn(caller, RetireArgs{Node: "g14", Module: "dunst", Name: "token", Warrant: "ok", Ask: "ret-1"}); err == nil ||
|
|
!strings.Contains(err.Error(), "nothing to retire") {
|
|
t.Errorf("a second retire answered %v", err)
|
|
}
|
|
}
|