package runtime import ( "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // novox/hq ADR 0201: a module keeps its current state in buckets it declares, and its code reaches // them through the runtime. The owner's instances write and read; a reader only reads; a watch hands // the current values — none that is deleted — and then every change; the runtime refuses, with the // reason, what the module was not issued, a write to state it only reads, and a value naming a // credential. func TestABundleKeepsAndWatchesItsStateThroughTheRuntime(t *testing.T) { mesh := mt.New(t) kv := mesh.Bucket(t, "keeper_servers") // What was there before anything started: one value, and one key since deleted. if _, err := kv.Put("all.one", []byte(`{"url":"http://one"}`)); err != nil { t.Fatal(err) } if _, err := kv.Put("all.gone", []byte(`{}`)); err != nil { t.Fatal(err) } if err := kv.Delete("all.gone"); err != nil { t.Fatal(err) } keeper := mt.MembershipOf("keeper", "anchor", false, nil) keeper.State = []bus.StateIssued{{Name: "servers", Bucket: "keeper_servers", Writes: true}} peeker := mt.MembershipOf("peeker", "anchor", false, nil) peeker.State = []bus.StateIssued{{Name: "keeper.servers", Bucket: "keeper_servers"}} mesh.Issue(t, keeper) mesh.Issue(t, peeker) nodeTools := connect(t, "node-tools", "anchor") asker := connect(t, "console", "workstation") dir := t.TempDir() keeperLog, peekerLog := filepath.Join(dir, "keeper.log"), filepath.Join(dir, "peeker.log") said := &lines{} stop, err := Run(nodeTools, []Served{ {Module: "keeper", Entrypoints: []string{mt.Fixture("state-keeper.mjs")}}, {Module: "peeker", Entrypoints: []string{mt.Fixture("state-keeper.mjs")}}, }, map[string]map[string]string{ "keeper": {"STATE_NAME": "servers", "STATE_LOG": keeperLog}, "peeker": {"STATE_NAME": "keeper.servers", "STATE_LOG": peekerLog}, }, said.logf) if err != nil { t.Fatal(err) } t.Cleanup(stop) // Both read the whole current state at start: the value, not the deleted key, then the end of it. for _, log := range []string{keeperLog, peekerLog} { waitFor(t, log, "current") lines := read(t, log) if lines[0] != `was put all.one {"url":"http://one"}` || lines[1] != "current" || strings.Contains(strings.Join(lines, "\n"), "all.gone") { t.Fatalf("the current state was not handed over as it is:\n%s\nruntime:\n%s", strings.Join(lines, "\n"), said.all()) } } // The owner writes; every watch sees the change. got, err := call(t, asker, "keeper.put@anchor", map[string]any{"state": "servers", "key": "anchor.two", "value": map[string]any{"url": "http://two"}}) if err != nil { t.Fatal(err) } if !strings.Contains(got, `"revision"`) { t.Fatalf("a put answered %s", got) } waitFor(t, keeperLog, `now put anchor.two {"url":"http://two"}`) waitFor(t, peekerLog, `now put anchor.two {"url":"http://two"}`) // Get and keys, by the owner and by the reader. got, err = call(t, asker, "peeker.get@anchor", map[string]any{"state": "keeper.servers", "key": "anchor.two"}) if err != nil { t.Fatal(err) } if !strings.Contains(got, `"value":{"url":"http://two"}`) { t.Fatalf("the reader's get answered %s", got) } got, err = call(t, asker, "peeker.get@anchor", map[string]any{"state": "keeper.servers", "key": "nothing.here"}) if err != nil || got != "null" { t.Fatalf("a key with no value answered %s, %v", got, err) } got, err = call(t, asker, "keeper.keys@anchor", map[string]any{"state": "servers"}) if err != nil { t.Fatal(err) } same(t, got, `["all.one","anchor.two"]`) // Refused, with the reason, before anything is sent. for _, c := range []struct { key, why string args map[string]any }{ {"peeker.put@anchor", "peeker reads keeper.servers and does not keep it", map[string]any{"state": "keeper.servers", "key": "x", "value": 1}}, {"keeper.get@anchor", `keeper keeps and reads no state called "other"`, map[string]any{"state": "other", "key": "x"}}, {"keeper.put@anchor", `carries a field "accessToken", which names a credential`, map[string]any{"state": "servers", "key": "x", "value": map[string]any{"headers": map[string]any{"accessToken": "s"}}}}, {"keeper.put@anchor", "is not a key the bus can hold", map[string]any{"state": "servers", "key": "a..b", "value": 1}}, } { if _, err := call(t, asker, c.key, c.args); err == nil || !strings.Contains(err.Error(), c.why) { t.Errorf("%s %v: want refused for %q, got %v", c.key, c.args, c.why, err) } } // A delete reaches every watch. if _, err := call(t, asker, "keeper.del@anchor", map[string]any{"state": "servers", "key": "all.one"}); err != nil { t.Fatal(err) } waitFor(t, peekerLog, "now delete all.one") waitFor(t, keeperLog, "now delete all.one") } func read(t *testing.T, path string) []string { t.Helper() b, _ := os.ReadFile(path) return strings.Split(strings.TrimSpace(string(b)), "\n") } func waitFor(t *testing.T, path, line string) { t.Helper() mt.Until(t, func() error { for _, l := range read(t, path) { if l == line { return nil } } return errorf("%s has no %q yet: %s", filepath.Base(path), line, strings.Join(read(t, path), " | ")) }) }