Issue 1585, S11 (mesh-sdk half): Case.Verdicts carries the indexer's verdicts; a refused module is held as its stub and named; an unknown field is set aside and named, never refused. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
155 lines
7.0 KiB
Go
155 lines
7.0 KiB
Go
package composer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files"}]}`
|
|
|
|
// A manifest named by its digest goes on the wire as {"digest": ...}, and Fill puts the bytes the indexer keeps in its
|
|
// place; a raw manifest goes as itself.
|
|
func TestAManifestIsItsBytesOrItsDigest(t *testing.T) {
|
|
r := Request{V: Version, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{
|
|
"demo": {Digest: "d1"}, "raw": {Raw: json.RawMessage(`{"module":"raw","version":"1"}`)}},
|
|
Kept: map[string]map[string]Manifest{"laptop": {"demo": {Digest: "d1"}}}}}}
|
|
wire, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(wire), `"demo":{"digest":"d1"}`) || !strings.Contains(string(wire), `"raw":{"module":"raw","version":"1"}`) {
|
|
t.Fatalf("on the wire: %s", wire)
|
|
}
|
|
back, err := Decode(wire)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Run(back); !errors.Is(err, ErrUnfilled) {
|
|
t.Fatalf("a digest left unfilled runs: %v", err)
|
|
}
|
|
asked := 0
|
|
if err := Fill(&back, func(d string) ([]byte, error) {
|
|
asked++
|
|
if d != "d1" {
|
|
return nil, errors.New("no such digest")
|
|
}
|
|
return []byte(demo), nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asked != 2 || string(back.Cases[0].Manifests["demo"].Raw) != demo || string(back.Cases[0].Kept["laptop"]["demo"].Raw) != demo {
|
|
t.Fatalf("filled %d times: %+v", asked, back.Cases[0])
|
|
}
|
|
bad := Request{V: Version, Cases: []Case{{Name: "c", Manifests: map[string]Manifest{"x": {Digest: "nope"}}}}}
|
|
if err := Fill(&bad, func(string) ([]byte, error) { return nil, errors.New("not there") }); err == nil || !strings.Contains(err.Error(), "nope") {
|
|
t.Fatalf("a digest the indexer does not hold: %v", err)
|
|
}
|
|
}
|
|
|
|
// The input travels as JSON and comes back equal, so the command composes what its caller meant.
|
|
func TestTheInputTravelsAsJSON(t *testing.T) {
|
|
in := compose.Input{
|
|
Nodes: []compose.Node{{Name: "anchor", Assigned: []string{"nats"}, Capabilities: map[string]bool{"systemd": true},
|
|
Place: compose.Place{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1"},
|
|
Pins: map[string]catalogue.Chosen{"bus": {Node: "anchor", Module: "nats"}},
|
|
Settings: map[string][]catalogue.Layer{"nats": {{Values: map[string]any{"port": float64(4222)}}}}}},
|
|
Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: "mesh", Node: "anchor", Module: "nats"}},
|
|
Issued: []compose.Issued{{Provision: "bus", Consumer: "laptop", ConsumerModule: "x", Provider: "anchor"}},
|
|
}
|
|
raw, err := json.Marshal(Request{V: Version, Input: in})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := Decode(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(back.Input, in) {
|
|
t.Fatalf("the input came back as\n%+v\nwant\n%+v", back.Input, in)
|
|
}
|
|
}
|
|
|
|
// The seats a node's assigned modules claim are held there when the request says the holdings come from the claims.
|
|
func TestHoldingsComeFromTheClaims(t *testing.T) {
|
|
m, err := catalogue.ParseManifest([]byte(demo))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := HoldingsOf(map[string]catalogue.Manifest{"demo": m}, []compose.Node{{Name: "b", Assigned: []string{"demo"}}, {Name: "a", Assigned: []string{"demo"}}})
|
|
if len(got) != 2 || got[0].Node != "a" || got[0].Claim != "node-files" || got[0].Scope != "node" {
|
|
t.Fatalf("holdings = %+v", got)
|
|
}
|
|
}
|
|
|
|
// A request the command cannot run is refused, never answered as a verdict: another version, a case named twice, an
|
|
// unknown depth.
|
|
func TestARequestItCannotRunIsRefused(t *testing.T) {
|
|
if _, err := Decode([]byte(`{"v": 2}`)); err == nil {
|
|
t.Error("a request of version 2 is read")
|
|
}
|
|
if _, err := Run(Request{V: Version, Cases: []Case{{Name: "a"}, {Name: "a"}}}); err == nil {
|
|
t.Error("a case named twice runs")
|
|
}
|
|
if _, err := Run(Request{V: Version, Depth: "deep"}); err == nil {
|
|
t.Error("an unknown depth runs")
|
|
}
|
|
// A case's own placement (issue 1126) composes over a node the input holds, and is refused for one it does not.
|
|
placed := Request{V: Version, Input: compose.Input{Nodes: []compose.Node{{Name: "a"}}}, Machines: []string{"a"}, Depth: DepthResolve,
|
|
Cases: []Case{{Name: "trunk", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}},
|
|
{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}},
|
|
Placements: map[string]*delivery.Placement{"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "ghost"}}}}}}}
|
|
if v, err := Run(placed); err != nil || !v.Cases[0].Machines["a"].Composes || v.Cases[1].Machines["a"].Composes ||
|
|
!strings.Contains(v.Cases[1].Machines["a"].Reason, "ghost") {
|
|
t.Errorf("a case composes its own placement: %+v, %v", v, err)
|
|
}
|
|
placed.Cases[1].Placements = map[string]*delivery.Placement{"z": {Node: "z"}}
|
|
if _, err := Run(placed); err == nil || !strings.Contains(err.Error(), "holds no node for") {
|
|
t.Errorf("a case placing a node the input lacks: %v", err)
|
|
}
|
|
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
|
|
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
|
|
t.Fatalf("a request with no machines: %+v, %v", v, err)
|
|
}
|
|
}
|
|
|
|
// The composer judges no manifest (novox/hq ADR 0578 §5): xterm's module.json from mesh-catalog at 835bf22b, whose
|
|
// config value falls back to "font-size", is read whatever list of predefined keys a request carries (a group touching
|
|
// xterm failed its composition on this, group check 18df81380db3); a module whose verdict in the input refuses it is
|
|
// held, composed as its stub, and named in the verdict's words.
|
|
func TestTheComposerTakesTheVerdictsOfItsInput(t *testing.T) {
|
|
raw, err := os.ReadFile("../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := Request{V: Version, Predefined: catalogue.PredefinedList{"something-else": {Type: "string", Description: "x"}},
|
|
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"xterm": {Raw: raw}}}}}
|
|
v, err := Run(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := v.Cases[0].Manifests["xterm"]; !got.Accepted {
|
|
t.Fatalf("xterm: %+v, want accepted", got)
|
|
}
|
|
r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"xterm": {Module: "xterm", State: delivery.VerdictRefused,
|
|
Problems: []string{"falls back to a key not listed"}, Judge: "src2:j"}}
|
|
shelf, err := parse(r.Cases[0].Manifests, map[string]ManifestRead{}, r.Cases[0].Verdicts)
|
|
if err != nil || !shelf["xterm"].Stubbed {
|
|
t.Fatalf("the refused module is not held as its stub: %+v %v", shelf["xterm"], err)
|
|
}
|
|
if v, err = Run(r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := v.Cases[0].Manifests["xterm"]; got.Accepted ||
|
|
!strings.Contains(got.Refused, "the indexer's judge (src2:j) refused the manifest of xterm: falls back to a key not listed") {
|
|
t.Fatalf("xterm refused by its verdict: %+v, want refused in the verdict's words", got)
|
|
}
|
|
}
|