package runtime import ( "encoding/json" "errors" "strings" "testing" "time" "github.com/novox/mesh-tools/node-tools/internal/bus" "github.com/novox/mesh-tools/node-tools/internal/launch" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // historyBuses are three modules on one runtime, each reached as a bundle reaches it (novox/hq ADR // 0307): issues keeps a state `issues` in a bucket keeping 8 values a key, triage reads it as // `issues.issues`, and other was issued no state. func historyBuses(t *testing.T) (owner, reader, other launch.Bus) { t.Helper() mesh := mt.New(t) mesh.BucketKeeping(t, "issues_issues", 8) m := mt.MembershipOf("issues", "anchor", false, nil) m.State = []bus.StateIssued{{Name: "issues", Bucket: "issues_issues", Writes: true}} mesh.Issue(t, m) r := mt.MembershipOf("triage", "anchor", false, nil) r.State = []bus.StateIssued{{Name: "issues.issues", Bucket: "issues_issues"}} mesh.Issue(t, r) mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil)) conn := connect(t, "node-tools", "anchor") for _, module := range []string{"issues", "triage", "other"} { conn.Follow(module) } mt.Until(t, func() error { for _, module := range []string{"issues", "triage", "other"} { if conn.Membership(module) == nil { return errors.New(module + " not issued yet") } } return nil }) all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} return all.forModule("issues"), all.forModule("triage"), all.forModule("other") } // kept is one value of a history answer, by the field names on the wire. type kept struct { Key string `json:"key"` Value json.RawMessage `json:"value"` Revision uint64 `json:"revision"` Op string `json:"op"` At string `json:"at"` } func historyOf(t *testing.T, b launch.Bus, state, key string) []kept { t.Helper() raw, err := ask(b, "history", map[string]any{"state": state, "key": key}) if err != nil { t.Fatalf("history of %s %s: %v", state, key, err) } var h []kept if err := json.Unmarshal(raw, &h); err != nil || h == nil { t.Fatalf("history answered %s, not an array", raw) } return h } // A key's history is every value the bucket keeps, oldest first, each with its revision, op and time // in RFC 3339 — a delete with no value; an unknown key's is []. The owner and a reader are served it, // and reading it writes nothing. func TestAKeysHistoryIsEveryValueKeptOldestFirst(t *testing.T) { owner, reader, _ := historyBuses(t) before := time.Now().Add(-time.Second) var revisions []uint64 for _, v := range []string{`{"state":"open"}`, `{"state":"fixing"}`, `{"state":"resolved"}`} { raw, err := ask(owner, "put", map[string]any{"state": "issues", "key": "502", "value": json.RawMessage(v)}) if err != nil { t.Fatal(err) } var r struct { Revision uint64 `json:"revision"` } _ = json.Unmarshal(raw, &r) revisions = append(revisions, r.Revision) } if _, err := ask(owner, "put", map[string]any{"state": "issues", "key": "503", "value": 1}); err != nil { t.Fatal(err) } if _, err := ask(owner, "delete", map[string]any{"state": "issues", "key": "502"}); err != nil { t.Fatal(err) } for who, b := range map[string]launch.Bus{"the owner": owner, "a reader": reader} { name := "issues" if b == reader { name = "issues.issues" } h := historyOf(t, b, name, "502") if len(h) != 4 { t.Fatalf("%s read %d values of 502, not 4: %+v", who, len(h), h) } for i, v := range []string{`{"state":"open"}`, `{"state":"fixing"}`, `{"state":"resolved"}`} { if h[i].Op != "put" || string(h[i].Value) != v || h[i].Revision != revisions[i] || h[i].Key != "502" { t.Errorf("%s: value %d is %+v", who, i, h[i]) } } if h[3].Op != "delete" || len(h[3].Value) != 0 && string(h[3].Value) != "null" || h[3].Revision <= revisions[2] { t.Errorf("%s: the delete reads %+v", who, h[3]) } for i, k := range h { at, err := time.Parse(time.RFC3339, k.At) if err != nil || at.Before(before) || at.After(time.Now().Add(time.Second)) { t.Errorf("%s: value %d's time %q is not RFC 3339 and now: %v", who, i, k.At, err) } if i > 0 && k.Revision <= h[i-1].Revision { t.Errorf("%s: not oldest first: %+v", who, h) } } if h := historyOf(t, b, name, "999"); len(h) != 0 { t.Errorf("%s: an unknown key's history is %+v, not []", who, h) } } // The history is read, never written: the key is still deleted and keeps the same four values. if raw, err := ask(owner, "get", map[string]any{"state": "issues", "key": "502"}); err != nil || string(raw) != "null" { t.Fatalf("after history, 502 reads %s %v", raw, err) } if h := historyOf(t, owner, "issues", "502"); len(h) != 4 { t.Fatalf("reading the history changed it: %+v", h) } // A key the bucket holds nothing for answers [] on the wire, not null. if raw, _ := ask(owner, "history", map[string]any{"state": "issues", "key": "999"}); string(raw) != "[]" { t.Fatalf("an unknown key's history is %s on the wire", raw) } } // A module the bucket was not issued to is refused its history with the reason, as it is a get. func TestAKeysHistoryIsRefusedToAModuleNotIssuedTheState(t *testing.T) { owner, _, other := historyBuses(t) if _, err := ask(owner, "put", map[string]any{"state": "issues", "key": "1", "value": 1}); err != nil { t.Fatal(err) } for _, name := range []string{"issues", "issues.issues"} { _, err := ask(other, "history", map[string]any{"state": name, "key": "1"}) if err == nil || !strings.Contains(err.Error(), "keeps and reads no state called") || !strings.Contains(err.Error(), "issued: none") { t.Errorf("history of %s to a module not issued it: %v", name, err) } } if _, err := ask(owner, "history", map[string]any{"state": "issues", "key": "a b"}); err == nil || !strings.Contains(err.Error(), "not a key") { t.Errorf("history of a key the bus cannot hold: %v", err) } }