package stateobjects import ( "errors" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/kvlimit" "git.novox.be/novox/mesh-sdk/go/views" ) type acts struct { done []views.HandAct err error } func (a *acts) RecordHandAct(h views.HandAct) error { if a.err != nil { return a.err } a.done = append(a.done, h) return nil } // ADR 0666 ยง25: the four tools read what each store holds and drop only what is no longer live, each drop only with who // asks and why, recorded as a hand act first. func subTheObjectToolsReadEverythingAndDropOnlyWhatIsNotLive(t *testing.T) { m := newMemory() m.modified = map[string]time.Time{} v := m.values() big := big(2 * kvlimit.ModuleStateValueBytes) for _, k := range []string{"live", "gone"} { if err := v.Put(k, big); err != nil { t.Fatal(err) } } // the two keys hold one object each (different keys): delete one key if err := v.Delete("gone"); err != nil { t.Fatal(err) } now := time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC) rec := &acts{} sentStore := newMemory() sentStore.modified = map[string]time.Time{"reply.list.aa": now.Add(-48 * time.Hour), "event.done.bb": now.Add(-time.Hour)} sentStore.objects = map[string][]byte{"reply.list.aa": []byte("1"), "event.done.bb": []byte("22")} tools := Tools("m", []State{{Name: "views", Values: v}}, &Sent{Objects: memObjects{sentStore}}, rec, func() time.Time { return now }) by := map[string]func(map[string]any) (any, error){} for _, tl := range tools { by[tl.Name] = tl.Run } for _, name := range []string{"m_state_objects", "m_state_objects_drop", "m_sent_objects", "m_sent_objects_drop"} { if by[name] == nil { t.Fatalf("no tool %s", name) } } read, err := by["m_state_objects"](map[string]any{}) if err != nil || read.(map[string]any)["key-gone"] != 1 { t.Fatalf("the state's objects: %v, %v", read, err) } listed, err := by["m_sent_objects"](map[string]any{}) if objs := listed.(map[string]any)["objects"].([]SentObject); err != nil || len(objs) != 2 { t.Fatalf("the sent objects: %v, %v", listed, err) } // each drop refuses without by and why, and records nothing for _, tc := range []struct { tool string args map[string]any }{ {"m_state_objects_drop", map[string]any{"why": "x"}}, {"m_state_objects_drop", map[string]any{"by": "a@b"}}, {"m_sent_objects_drop", map[string]any{"why": "x", "older-than": "24h"}}, {"m_sent_objects_drop", map[string]any{"by": "a@b", "why": "x", "older-than": "soon"}}, } { if _, err := by[tc.tool](tc.args); err == nil { t.Errorf("%s %v was accepted", tc.tool, tc.args) } } if len(rec.done) != 0 { t.Fatalf("a refused drop recorded %v", rec.done) } // the hand act is recorded first: one that cannot be recorded drops nothing rec.err = errors.New("hand acts are down") if _, err := by["m_state_objects_drop"](map[string]any{"by": "a@b", "why": "tidy"}); err == nil || !strings.Contains(err.Error(), "nothing was dropped") { t.Fatalf("an unrecorded drop: %v", err) } if kept, _ := v.Kept(); len(kept) != 2 { t.Fatalf("an unrecorded drop removed objects: %d left", len(kept)) } rec.err = nil // the state drop takes only the key-gone object out, err := by["m_state_objects_drop"](map[string]any{"by": "a@b", "why": "tidy"}) if err != nil || len(out.(map[string]any)["dropped"].([]string)) != 1 { t.Fatalf("the state drop: %v, %v", out, err) } if kept, _ := v.Kept(); len(kept) != 1 || kept[0].KeyGone { t.Fatalf("the live object did not survive: %+v", kept) } // the sent drop takes only objects past the age out, err = by["m_sent_objects_drop"](map[string]any{"by": "a@b", "why": "tidy", "older-than": "24h"}) if err != nil || len(out.(map[string]any)["dropped"].([]string)) != 1 { t.Fatalf("the sent drop: %v, %v", out, err) } if _, ok := sentStore.objects["event.done.bb"]; !ok || len(sentStore.objects) != 1 { t.Fatalf("the young sent object did not survive: %v", sentStore.objects) } if len(rec.done) != 2 { t.Fatalf("%d hand acts, want 2", len(rec.done)) } }