mesh-vault: the ledger's tests say ADR 0230's rules, not a withdrawal on the first pass (review 250)
mesh/merge-gate head c63f7d6e5cea merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head c63f7d6e5cea merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head c63f7d6e5cea merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head c63f7d6e5cea merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,15 +53,15 @@ func TestTheLedgerHoldsFingerprintsNeverTheValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithdrawingForgetsAHolderAndListingIsByLogin(t *testing.T) {
|
func TestDeletingForgetsAHolderAndListingIsByLogin(t *testing.T) {
|
||||||
l, _ := NewLedger(t.TempDir())
|
l, _ := NewLedger(t.TempDir())
|
||||||
_, _, _ = l.Record("b-app", "b", "x", time.Now())
|
_, _, _ = l.Record("b-app", "b", "x", time.Now())
|
||||||
_, _, _ = l.Record("a-app", "a", "y", time.Now())
|
_, _, _ = l.Record("a-app", "a", "y", time.Now())
|
||||||
if got := logins(l.List()); got != "a-app,b-app" {
|
if got := logins(l.List()); got != "a-app,b-app" {
|
||||||
t.Errorf("listed %s", got)
|
t.Errorf("listed %s", got)
|
||||||
}
|
}
|
||||||
if !l.Withdraw("a-app") || l.Withdraw("a-app") {
|
if !l.Delete("a-app") || l.Delete("a-app") {
|
||||||
t.Error("withdrawing found the wrong thing")
|
t.Error("deleting found the wrong thing")
|
||||||
}
|
}
|
||||||
if got := logins(l.List()); got != "b-app" {
|
if got := logins(l.List()); got != "b-app" {
|
||||||
t.Errorf("listed %s", got)
|
t.Errorf("listed %s", got)
|
||||||
@@ -120,49 +120,230 @@ func TestWhatTheMeshDeliversIsFingerprintedNeverReturned(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The custody pass: a delivered value is recorded and announced by fingerprint, a changed one is a rotation,
|
// custodyAt is a custody pass over a contributions file in dir, on a clock the test moves.
|
||||||
// a login no longer asked for is withdrawn — and an unreadable contributions file withdraws nobody.
|
type custodyAt struct {
|
||||||
func TestTheCustodyPass(t *testing.T) {
|
*Custody
|
||||||
|
bus *published
|
||||||
|
receives string
|
||||||
|
dir string
|
||||||
|
clock *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCustody(t *testing.T) custodyAt {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
l, _ := NewLedger(filepath.Join(dir, "ledger"))
|
l, _ := NewLedger(filepath.Join(dir, "ledger"))
|
||||||
bus := &published{}
|
bus := &published{}
|
||||||
secret := filepath.Join(dir, "a.secret")
|
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
|
||||||
receives := filepath.Join(dir, "mesh.json")
|
c := custodyAt{bus: bus, receives: filepath.Join(dir, "mesh.json"), dir: dir, clock: &clock}
|
||||||
write := func(given ...map[string]string) {
|
c.Custody = &Custody{Ledger: l, Receives: c.receives, Emit: bus.emit, Now: func() time.Time { return *c.clock }, Log: t.Logf}
|
||||||
doc, _ := json.Marshal(map[string]any{"given": given})
|
return c
|
||||||
_ = os.WriteFile(receives, doc, 0o600)
|
}
|
||||||
}
|
|
||||||
c := Custody{Ledger: l, Receives: receives, Emit: bus.emit, Now: time.Now, Log: t.Logf}
|
|
||||||
|
|
||||||
_ = os.WriteFile(secret, []byte("v1"), 0o600)
|
// ask writes a contributions file asking for these logins, each with its value.
|
||||||
write(map[string]string{"node": "anchor", "as": "anchor-redis", "secret": secret})
|
func (c custodyAt) ask(values map[string]string) {
|
||||||
c.Pass()
|
given := []map[string]string{}
|
||||||
_ = os.WriteFile(secret, []byte("v2"), 0o600)
|
for as, v := range values {
|
||||||
c.Pass()
|
f := filepath.Join(c.dir, as+".secret")
|
||||||
_ = os.WriteFile(receives, []byte("{not json"), 0o600)
|
_ = os.WriteFile(f, []byte(v), 0o600)
|
||||||
c.Pass()
|
given = append(given, map[string]string{"node": "anchor", "as": as, "secret": f})
|
||||||
if _, ok := l.Get("anchor-redis"); !ok {
|
|
||||||
t.Fatal("an unreadable contributions file withdrew a holder")
|
|
||||||
}
|
}
|
||||||
write()
|
doc, _ := json.Marshal(map[string]any{"requirement": "secret", "given": given})
|
||||||
c.Pass()
|
_ = os.WriteFile(c.receives, doc, 0o600)
|
||||||
if _, ok := l.Get("anchor-redis"); ok {
|
}
|
||||||
t.Fatal("a holder no longer asked for was kept")
|
|
||||||
|
// passes runs n passes a minute apart.
|
||||||
|
func (c custodyAt) passes(n int) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
c.Pass()
|
||||||
|
*c.clock = c.clock.Add(time.Minute)
|
||||||
}
|
}
|
||||||
var said []string
|
}
|
||||||
for _, e := range bus.events {
|
|
||||||
said = append(said, e["event"].(string))
|
func (c custodyAt) changes() []string {
|
||||||
|
var out []string
|
||||||
|
for _, e := range c.bus.events {
|
||||||
|
if e["event"] == EventRetirement {
|
||||||
|
out = append(out, e["body"].(map[string]any)["change"].(string))
|
||||||
|
} else {
|
||||||
|
out = append(out, e["event"].(string))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.Join(said, ",") != "provisioned,rotated,deprovisioned" {
|
return out
|
||||||
t.Errorf("announced %v", said)
|
}
|
||||||
|
|
||||||
|
// The custody pass: a delivered value is recorded and announced by fingerprint, a changed one is a rotation.
|
||||||
|
func TestTheCustodyPassRecordsAndRotates(t *testing.T) {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(map[string]string{"anchor-redis": "v1"})
|
||||||
|
c.passes(1)
|
||||||
|
c.ask(map[string]string{"anchor-redis": "v2"})
|
||||||
|
c.passes(1)
|
||||||
|
if got := strings.Join(c.changes(), ","); got != "provisioned,rotated" {
|
||||||
|
t.Errorf("announced %s", got)
|
||||||
}
|
}
|
||||||
for _, raw := range bus.raw {
|
for _, raw := range c.bus.raw {
|
||||||
if strings.Contains(raw, `"v1"`) || strings.Contains(raw, `"v2"`) {
|
if strings.Contains(raw, `"v1"`) || strings.Contains(raw, `"v2"`) {
|
||||||
t.Errorf("announced a value: %s", raw)
|
t.Errorf("announced a value: %s", raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A contributions file that does not say who is asked for — unreadable, not JSON, without a given list, a
|
||||||
|
// null one, or for another requirement — is no answer: nobody is retired, and the count starts again.
|
||||||
|
func TestAFileThatSaysNothingRetiresNobody(t *testing.T) {
|
||||||
|
for _, doc := range []string{"{not json", `{"requirement":"secret"}`, `{"requirement":"secret","given":null}`,
|
||||||
|
`{"requirement":"redis-cache","given":[]}`} {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(map[string]string{"anchor-redis": "v1"})
|
||||||
|
c.passes(1)
|
||||||
|
_ = os.WriteFile(c.receives, []byte(doc), 0o600)
|
||||||
|
c.passes(30)
|
||||||
|
if h, ok := c.Ledger.Get("anchor-redis"); !ok || h.Retired != nil {
|
||||||
|
t.Errorf("%s: the holder was retired or lost: %+v %v", doc, h, ok)
|
||||||
|
}
|
||||||
|
if _, err := Contributions(c.receives); err == nil {
|
||||||
|
t.Errorf("%s was read as an answer", doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := Contributions(filepath.Join(t.TempDir(), "missing.json")); err == nil {
|
||||||
|
t.Error("a missing file was read as an answer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A holder the mesh stops asking for is retired only after five passes AND ten minutes, is never deleted
|
||||||
|
// by a pass, keeps its history, and is re-enabled when asked for again.
|
||||||
|
func TestAHolderNoLongerAskedForIsRetiredNotDeleted(t *testing.T) {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"})
|
||||||
|
c.passes(1)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2"})
|
||||||
|
c.Pass() // the first pass that sees c-app unasked
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
*c.clock = c.clock.Add(10 * time.Second) // five passes, but in under ten minutes
|
||||||
|
c.Pass()
|
||||||
|
}
|
||||||
|
if h, _ := c.Ledger.Get("c-app"); h.Retired != nil {
|
||||||
|
t.Fatal("retired before ten minutes had passed")
|
||||||
|
}
|
||||||
|
*c.clock = c.clock.Add(10 * time.Minute)
|
||||||
|
c.Pass()
|
||||||
|
h, ok := c.Ledger.Get("c-app")
|
||||||
|
if !ok || h.Retired == nil || h.Retired.Why == "" {
|
||||||
|
t.Fatalf("not retired, or retired without a why: %+v %v", h, ok)
|
||||||
|
}
|
||||||
|
if h.Fingerprint != Fingerprint("3") {
|
||||||
|
t.Error("retiring lost what the ledger knew of the holder")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(c.dir, "ledger", "c-app.json")); err != nil {
|
||||||
|
t.Error("a pass deleted a ledger entry")
|
||||||
|
}
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"})
|
||||||
|
c.Pass()
|
||||||
|
if h, _ := c.Ledger.Get("c-app"); h.Retired != nil {
|
||||||
|
t.Error("asked for again, the holder stayed retired")
|
||||||
|
}
|
||||||
|
if got := strings.Join(c.changes(), ","); got != "provisioned,provisioned,provisioned,retired,reenabled" {
|
||||||
|
t.Errorf("announced %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A different set, or a file that says nothing, starts the count again.
|
||||||
|
func TestTheCountStartsAgain(t *testing.T) {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"})
|
||||||
|
c.passes(1)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2"})
|
||||||
|
c.passes(4)
|
||||||
|
_ = os.WriteFile(c.receives, []byte(`{"given":null}`), 0o600)
|
||||||
|
c.passes(1)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2"})
|
||||||
|
c.passes(4)
|
||||||
|
*c.clock = c.clock.Add(time.Hour)
|
||||||
|
if h, _ := c.Ledger.Get("c-app"); h.Retired != nil {
|
||||||
|
t.Fatal("retired though the count was broken")
|
||||||
|
}
|
||||||
|
c.passes(1)
|
||||||
|
if h, _ := c.Ledger.Get("c-app"); h.Retired == nil {
|
||||||
|
t.Fatal("not retired after five unbroken passes and an hour")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Too many at once waits for a person: approve retires exactly that set, reject keeps it.
|
||||||
|
func TestTooManyAtOnceWaitsForAPerson(t *testing.T) {
|
||||||
|
all := map[string]string{"a-app": "1", "b-app": "2", "c-app": "3", "d-app": "4", "e-app": "5"}
|
||||||
|
for _, decide := range []string{"approve", "reject"} {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(all)
|
||||||
|
c.passes(1)
|
||||||
|
c.ask(map[string]string{"a-app": "1"})
|
||||||
|
c.passes(5)
|
||||||
|
*c.clock = c.clock.Add(time.Hour)
|
||||||
|
c.passes(1)
|
||||||
|
r := c.Retirement()
|
||||||
|
if r["waiting"] == nil {
|
||||||
|
t.Fatalf("four of five no longer asked for did not wait: %v", r)
|
||||||
|
}
|
||||||
|
for _, h := range c.Ledger.List() {
|
||||||
|
if h.Retired != nil {
|
||||||
|
t.Fatalf("%s retired while the set waited", h.As)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := c.Approve("person.jochen", ""); err == nil {
|
||||||
|
t.Error("an approval without a why was taken")
|
||||||
|
}
|
||||||
|
if decide == "approve" {
|
||||||
|
set, err := c.Approve("person.jochen", "the four apps are gone")
|
||||||
|
if err != nil || len(set) != 4 {
|
||||||
|
t.Fatalf("%v %v", set, err)
|
||||||
|
}
|
||||||
|
if h, _ := c.Ledger.Get("b-app"); h.Retired == nil {
|
||||||
|
t.Error("approved, not retired")
|
||||||
|
}
|
||||||
|
if h, _ := c.Ledger.Get("a-app"); h.Retired != nil {
|
||||||
|
t.Error("an asked-for holder was retired")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := c.Reject("person.jochen", "they come back"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c.passes(10)
|
||||||
|
if h, _ := c.Ledger.Get("b-app"); h.Retired != nil {
|
||||||
|
t.Error("rejected, but retired")
|
||||||
|
}
|
||||||
|
if c.Retirement()["waiting"] != nil {
|
||||||
|
t.Error("a rejected set was asked about again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a person deletes, with a why, and only a retired holder the mesh no longer asks for.
|
||||||
|
func TestOnlyAPersonDeletesARetiredHolder(t *testing.T) {
|
||||||
|
c := newCustody(t)
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2", "c-app": "3"})
|
||||||
|
c.passes(1)
|
||||||
|
if err := c.Delete("c-app", "person.jochen", "gone"); err == nil {
|
||||||
|
t.Error("an active holder was deleted")
|
||||||
|
}
|
||||||
|
c.ask(map[string]string{"a-app": "1", "b-app": "2"})
|
||||||
|
c.passes(5)
|
||||||
|
*c.clock = c.clock.Add(time.Hour)
|
||||||
|
c.passes(1)
|
||||||
|
if err := c.Delete("c-app", "person.jochen", ""); err == nil {
|
||||||
|
t.Error("deleted without a why")
|
||||||
|
}
|
||||||
|
if err := c.Delete("c-app", "person.jochen", "gitea's redis is gone for good"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := c.Ledger.Get("c-app"); ok {
|
||||||
|
t.Error("deleted, but still in the ledger")
|
||||||
|
}
|
||||||
|
last := c.changes()
|
||||||
|
if last[len(last)-1] != "deleted" {
|
||||||
|
t.Errorf("announced %v", last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func logins(hs []Held) string {
|
func logins(hs []Held) string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, h := range hs {
|
for _, h := range hs {
|
||||||
|
|||||||
Reference in New Issue
Block a user