Files
mesh-controller/cmd/mesh-controller/states_announced_integration_test.go
T
jschoubben e419d0182f Delete the builder, the build asks, the build queue and the merge gate: the builder's keys are the only way a module is built (issues 1851, 1299, 1318; C2b-C5, C10)
The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the
builder's done key; a key not done yet waits, named in the plan, and never fails it.

- C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an
  outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired
  seats' built subjects only to let go of them, because the installer's carried user list names them.
- C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name.
- C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and
  their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go).
- C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the
  controller nothing but to tell mesh-config what its head declares.
- C5: the builds bucket is written only by key_takein.
- C10 (help and refusals): the terminal's help lists only what is served.

Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every
module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
2026-10-23 15:55:15 +02:00

119 lines
4.1 KiB
Go

//go:build integration
package main
import (
"context"
"slices"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/testbus"
)
// D1-D3 whole against a real bus and the store (ADR 0495 checks §2, §3, §5; ADR 0513 §3; issue 1790): the controller
// says what it keeps and keeps what every module says; a reader of a state its owner announced readable is granted it,
// one its owner announced and did not make readable is refused and said, and one of an owner that announced nothing
// yet is granted as before and named as not yet announced.
func TestTheStatesAnnouncedGrantTheReads(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
js, err := broker.Dial(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(js.Close)
if err := broker.AssertMeshStreams(js); err != nil {
t.Fatal(err)
}
if err := js.EnsureControllerBuckets(); err != nil {
t.Fatal(err)
}
servingBus.Store(js)
t.Cleanup(func() { servingBus.Store(nil) })
was := inventory.Announcements
inventory.Announcements = readAnnouncements
t.Cleanup(func() { inventory.Announcements = was })
// The owner keeps two states, one readable; the reader reads both and a state of a module that says nothing.
register := func(m catalogue.Manifest) {
t.Helper()
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git",
Path: "modules/" + m.Module, BuiltFrom: "c0"}); err != nil {
t.Fatal(err)
}
}
register(catalogue.Manifest{Module: "owner", Version: "1", State: []catalogue.StateDeclaration{{Name: "open"}, {Name: "closed"}}})
register(catalogue.Manifest{Module: "quiet", Version: "1", State: []catalogue.StateDeclaration{{Name: "any"}}})
register(catalogue.Manifest{Module: "reader", Version: "1", Reads: []string{"owner.open", "owner.closed", "quiet.any"}})
if _, err := inv.AddNode(ctx, "one"); err != nil {
t.Fatal(err)
}
for _, m := range []string{"owner", "quiet", "reader"} {
if _, err := inv.Assign(ctx, "one", m); err != nil {
t.Fatal(err)
}
}
watching, stop := context.WithCancel(context.Background())
t.Cleanup(stop)
if err := announceOwnStates(ctx, js); err != nil {
t.Fatal(err)
}
if err := keepAnnouncements(watching, js, open); err != nil {
t.Fatal(err)
}
api, _ := jetstream.New(js.Conn())
word, _ := announce.Encode(announce.States{Module: "owner", Node: "one",
States: []announce.State{{Name: "open", Readable: true}, {Name: "closed"}}})
if _, err := api.Publish(ctx, announce.Subject("owner"), word); err != nil {
t.Fatal(err)
}
becomes(t, 10*time.Second, "both words kept", func() bool {
a, err := readAnnouncements(ctx)
return err == nil && len(a) == 2 && a["owner"].Readable("open") && a[broker.ControllerSeat].Readable(broker.ChainBuilds)
})
records, err := inv.BusRecords(ctx)
if err != nil {
t.Fatal(err)
}
var reader broker.Declared
for _, d := range records.Assigned["one"] {
if d.Module == "reader" {
reader = d
}
}
if !slices.Equal(reader.Reads, []string{"owner.open", "quiet.any"}) || !slices.Equal(reader.ReadsRefused, []string{"owner.closed"}) {
t.Fatalf("0495 §3: the reader is granted %v and refused %v", reader.Reads, reader.ReadsRefused)
}
if got := broker.NotAnnounced(records); !slices.Equal(got, []string{"quiet"}) {
t.Fatalf("not yet announced: %v", got)
}
users, err := broker.Users(records)
if err != nil {
t.Fatal(err)
}
for _, u := range users {
if u.Kind != broker.KindModule || u.Module != "reader" {
continue
}
perms, err := broker.PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
if slices.ContainsFunc(perms.Publish, func(s string) bool { return s == "$JS.API.DIRECT.GET.KV_owner_closed.>" }) {
t.Fatal("0495 §3: the reader may read the state its owner did not announce readable")
}
if !slices.Contains(perms.Publish, "$JS.API.DIRECT.GET.KV_owner_open.>") {
t.Fatal("0495 §3: the reader may not read the state its owner announced readable")
}
}
}