On 2026-10-31 two merge asks of pull requests merged by another path held two of the router's three places for the teller and a placement ask was refused for want of room. The gate's fix ends such asks by itself; this tool is the recovery for any ask that still outlives its question: it cancels the open ask at the router, or drops one waiting for room, by its router id or its key, and logs who and why. The tools bundle asks through the teller's new state ends, as indexer_take_in does.
216 lines
8.2 KiB
Go
216 lines
8.2 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
|
|
// Invokes :590, Streams :613, Reads :659; StreamDeclaration, streams.go), an internal package of another repository.
|
|
// These tests decode the manifest strictly into the fields those types name (DisallowUnknownFields) and hold the
|
|
// rules of issue 1240 (ADR 0437 §4). The merge gate's module check proves the controller takes the manifest.
|
|
|
|
type state struct {
|
|
Name string `json:"name"`
|
|
History int `json:"history,omitempty"`
|
|
}
|
|
|
|
type artifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
System string `json:"system"`
|
|
From string `json:"from"`
|
|
Binary string `json:"binary"`
|
|
Loads []string `json:"loads"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Tools []string `json:"tools"`
|
|
Uses []string `json:"uses"`
|
|
State []state `json:"state"`
|
|
Reads []string `json:"reads"`
|
|
OwnSecrets map[string]string `json:"own-secrets"`
|
|
SecretsOwner string `json:"secrets-owner"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []artifact `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func load(t *testing.T) (manifest, map[string]json.RawMessage) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("the controller's reader refuses a key this manifest has: %v", err)
|
|
}
|
|
var top map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &top); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, top
|
|
}
|
|
|
|
func sorted(s []string) []string {
|
|
s = slices.Clone(s)
|
|
slices.Sort(s)
|
|
return s
|
|
}
|
|
|
|
func TestManifestDeclarations(t *testing.T) { // 1240/AC1
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-teller" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
// answers is the teller's answer to each ask; asked is what it asked of the router, which condition it raised and which
|
|
// gate key and revision each ask was made at (issue 1343); ends is the ends asked by hand with teller_end_ask (issue 774).
|
|
if want := []state{{Name: "answers", History: 5}, {Name: "asked", History: 1}, {Name: "ends", History: 1}}; !reflect.DeepEqual(m.State, want) {
|
|
t.Errorf("state = %+v, want %+v", m.State, want)
|
|
}
|
|
// The assigner's asks and the view's answers to them are read so the view's answer fills an ask that needs no proof (1297/B10).
|
|
if want := []string{"mesh-delivery-assigner.asks", "mesh-delivery-gate.asks", "mesh-delivery-view.answers"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
|
t.Errorf("reads = %v, want %v", m.Reads, want)
|
|
}
|
|
if want := []string{"operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
|
t.Errorf("uses = %v, want %v", m.Uses, want)
|
|
}
|
|
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
|
|
t.Errorf("own-secrets = %v, want exactly broker", m.OwnSecrets)
|
|
}
|
|
if m.SecretsOwner != "mesh-delivery-teller" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
// 774: teller_end_ask, served by its tools bundle through the state ends; the process serves none.
|
|
if want := []string{"teller_end_ask"}; !reflect.DeepEqual(m.Tools, want) {
|
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
|
}
|
|
// No consumes, invokes, emits, streams, bus or seat (ADR 0437 §4).
|
|
for _, k := range []string{"consumes", "invokes", "streams", "logs", "emits", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the teller has none", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestProcess(t *testing.T) { // 1240/AC1
|
|
m, _ := load(t)
|
|
var procs []map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" {
|
|
procs = append(procs, r)
|
|
}
|
|
if r["type"] == "tool" || r["type"] == "seat" {
|
|
t.Errorf("resource %v: the teller serves no tool and holds no seat", r)
|
|
}
|
|
}
|
|
if len(procs) != 1 {
|
|
t.Fatalf("%d process resources, want one", len(procs))
|
|
}
|
|
p := procs[0]
|
|
if p["artifact"] != "program" || !reflect.DeepEqual(p["run"], []any{"./mesh-delivery-teller"}) {
|
|
t.Errorf("process = %v", p)
|
|
}
|
|
env, _ := p["env"].(map[string]any)
|
|
if len(env) != 1 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" {
|
|
t.Errorf("env = %v", p["env"])
|
|
}
|
|
if _, bus := p["bus"]; bus {
|
|
t.Errorf("the process declares a bus: it runs on its own broker credential")
|
|
}
|
|
if len(m.Build.Artifacts) != 2 {
|
|
t.Fatalf("artifacts = %+v, want the program and the tools", m.Build.Artifacts)
|
|
}
|
|
a := m.Build.Artifacts[0]
|
|
if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-teller" || !strings.HasSuffix(a.From, a.Binary) {
|
|
t.Errorf("artifact = %+v", a)
|
|
}
|
|
if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Binary != "mesh-delivery-teller-tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-teller-tools"}) {
|
|
t.Errorf("tools artifact = %+v", a) // 774
|
|
} else if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
|
|
t.Errorf("the tools' source %s: %v", a.From, err)
|
|
}
|
|
}
|
|
|
|
func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1240/AC3 (ADR 0437 §8)
|
|
forbidden := []string{"Subscribe(", "QueueSubscribe", "ChanSubscribe", "Respond(", "RespondMsg(", "time.NewTicker", "time.Tick", "time.NewTimer", "time.AfterFunc", "time.After(", "time.Sleep"}
|
|
// The one subscription the teller makes is to its own `decided` event, the router's word on its asks (ADR 0259, ADR 0437
|
|
// §6): an event it hears, not a request it answers. It is allowed in decided.go and there only on that subject.
|
|
const decidedFile = "decided.go"
|
|
var files int
|
|
for _, root := range []string{"cmd", "internal"} {
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if os.IsNotExist(err) {
|
|
return fs.SkipDir
|
|
}
|
|
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
files++
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range forbidden {
|
|
if f == "Subscribe(" && filepath.Base(path) == decidedFile {
|
|
if n := strings.Count(string(src), "conn.Subscribe("); n != 1 || !strings.Contains(string(src), "conn.Subscribe(asks.DecidedSubject(module),") ||
|
|
strings.Count(string(src), f) != n {
|
|
t.Errorf("%s holds %d subscriptions, and not just one to asks.DecidedSubject(module)", path, strings.Count(string(src), f))
|
|
}
|
|
continue
|
|
}
|
|
if strings.Contains(string(src), f) {
|
|
t.Errorf("%s holds %s: the teller serves no request subject and holds no timer", path, f)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if files < 1 {
|
|
t.Fatalf("read %d source files, want the program's", files)
|
|
}
|
|
}
|
|
|
|
// Issue 1242: no delivery part hand-writes its bus connection. A part's own credential, its pinned dial, the subjects of its
|
|
// streams and its durable consumer come from mesh-sdk's process package (ReadBrokerAccount, Account.Dial, Account.EventConsumer,
|
|
// ModuleStreamSubject); a part whose main.go dials with nats.Connect, pins with its own certificate check or binds the
|
|
// consumer name itself is refused here. mesh-grants is outside it until its own seats change (issue 1945) lands.
|
|
func TestNoPartHandWritesItsBusConnection(t *testing.T) {
|
|
mains, err := filepath.Glob("../mesh-delivery-*/cmd/*/*.go")
|
|
if err != nil || len(mains) == 0 {
|
|
t.Fatalf("no delivery part found: %v", err)
|
|
}
|
|
for _, f := range mains {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, banned := range []string{"nats.Connect(", "VerifyPeerCertificate", `cred.Node+"_"+cred.Module`, `"MESH_BROKER_FILE"`} {
|
|
if bytes.Contains(raw, []byte(banned)) {
|
|
t.Errorf("%s hand-writes the bus connection (%s): use mesh-sdk's process.ReadBrokerAccount, Account.Dial, Account.EventConsumer or ModuleStreamSubject", f, banned)
|
|
}
|
|
}
|
|
}
|
|
}
|