Review of fix/lost-check: an ask no node took yet has no step key, so asking again then would run the check twice (issue 1791's kind). The checker keeps the asks each running check put in a new state asks, and the tool refuses unless every one of them has a step key. Asking again under the same ask id is not done: the stream drops a second put of an id within its de-duplication window and keeps both past it, so the waiting copy is never replaced. Tests: the hand row of the lost-ask table refuses while its ask waits; counts unchanged.
213 lines
8.6 KiB
Go
213 lines
8.6 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 issues 1678 and 1683 (ADR 0453 §4, ADR 0484). 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 stream struct {
|
|
Name string `json:"name"`
|
|
Events []string `json:"events"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Streams []stream `json:"streams"`
|
|
Tools []string `json:"tools"`
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Consumes []string `json:"consumes"`
|
|
State []state `json:"state"`
|
|
Reads []string `json:"reads"`
|
|
Uses []string `json:"uses"`
|
|
Invokes []string `json:"invokes"`
|
|
Config map[string]struct {
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Pattern string `json:"pattern"`
|
|
Default string `json:"default"`
|
|
} `json:"config"`
|
|
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) { // 1678/AC1
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-checker" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
// delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets.
|
|
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) {
|
|
t.Errorf("state = %+v, want %+v", m.State, want)
|
|
}
|
|
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains", "mesh-controller.assignments",
|
|
"mesh-delivery-assigner.compositions", "mesh-delivery-builder.builds", "mesh-delivery-gate.source", "mesh-delivery-gate.turns", "mesh-delivery-indexer.candidates",
|
|
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) {
|
|
t.Errorf("reads = %v, want %v", m.Reads, want)
|
|
}
|
|
// 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no
|
|
// build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4).
|
|
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
|
t.Errorf("invokes = %v, want only the forge's reads for a mesh-sdk pin (ADR 0484 clause 9)", m.Invokes)
|
|
}
|
|
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
|
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
|
|
}
|
|
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
|
|
if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
|
t.Errorf("consumes = %v, want %v", m.Consumes, 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-checker" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
// The checker says each time it wrote a preview or a status, so the indexer's and the forge bridge's watches find the buckets (ADR 0484 clauses 2 and 3).
|
|
if want := []stream{{Name: "checks", Events: []string{"preview.**", "owed.**"}}}; !reflect.DeepEqual(m.Streams, want) {
|
|
t.Errorf("streams = %+v, want %+v", m.Streams, want)
|
|
}
|
|
// The read tools (issue 1686) are served by the tools bundle, not by the checker's process.
|
|
// The try's tools (ADR 0501 §5; the end by hand, issue 734) beside them: mesh-controller's try calls them. A lost check
|
|
// is asked again by hand (checker_ask_again).
|
|
if want := []string{"checker_ask_again", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
|
}
|
|
for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the checker has none", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestProcess(t *testing.T) { // 1678/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 checker 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-checker"}) {
|
|
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, want the broker file alone", p["env"])
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "settings" {
|
|
t.Errorf("a settings resource %v: the checker has no capacity of its own (ADR 0525 §4)", r)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Language != "go" || a.Binary != "mesh-delivery-checker-tools" ||
|
|
!reflect.DeepEqual(a.Loads, []string{"mesh-delivery-checker-tools"}) {
|
|
t.Errorf("tools artifact = %+v", a)
|
|
}
|
|
if st, err := os.Stat(m.Build.Artifacts[1].From); err != nil || !st.IsDir() {
|
|
t.Errorf("the tools' source %s: %v", m.Build.Artifacts[1].From, err)
|
|
}
|
|
a := m.Build.Artifacts[0]
|
|
if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-checker" || !strings.HasSuffix(a.From, a.Binary) || a.Kind != "bundle" || a.Language != "go" {
|
|
t.Errorf("artifact = %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1678/AC6 (ADR 0437 §8: no timer, a wait is a context's bound)
|
|
forbidden := []string{"Subscribe(", "QueueSubscribe", "ChanSubscribe", "time.NewTicker", "time.Tick", "time.NewTimer", "time.AfterFunc", "time.After(", "time.Sleep"}
|
|
var files int
|
|
for _, root := range []string{"cmd", "internal"} {
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
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 strings.Contains(string(src), f) {
|
|
t.Errorf("%s holds %s: the checker 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)
|
|
}
|
|
}
|