package catalogue import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-host/validate" ) // A long-running container may say it is busy (novox/hq ADR 0335 ยง1): `module check` holds the field to its // shape near its author, and it is sent only to a node-engine that reads it, in words the engine's own // validator accepts. func busyManifest(t *testing.T, on string, busy any) []byte { t.Helper() extra := map[string]any{"id": "agent", "type": "container", "name": "app-agent", "image": "registry.example/app" + healthDigest} var m map[string]any if err := json.Unmarshal(healthManifest(t, "", nil, extra), &m); err != nil { t.Fatal(err) } for _, r := range m["resources"].([]any) { if r := r.(map[string]any); r["id"] == on { r[BusyField] = busy } } out, err := json.Marshal(m) if err != nil { t.Fatal(err) } return out } func TestABusyIsHeldToItsShape(t *testing.T) { if _, err := ParseManifest(busyManifest(t, "agent", map[string]any{"command": "mesh-builder busy"})); err != nil { t.Fatalf("a well-formed busy was refused: %v", err) } if _, err := ParseManifest(busyManifest(t, "agent", map[string]any{"command": "x", "timeout": "30s"})); err != nil { t.Fatalf("a busy with its timeout was refused: %v", err) } for name, c := range map[string]struct { on string busy any says string }{ "text": {"agent", "mesh-builder busy", "it is an object"}, "no command": {"agent", map[string]any{"timeout": "5s"}, "names no command"}, "two lines": {"agent", map[string]any{"command": "a\nb"}, "one line"}, "a long timeout": {"agent", map[string]any{"command": "x", "timeout": "5m"}, "at most 1m0s"}, "an unknown key": {"agent", map[string]any{"command": "x", "every": "1m"}, `says "every"`}, "on a step": {"seed", map[string]any{"command": "x"}, "not a container that stays up"}, "on a service": {"daemon", map[string]any{"command": "x"}, "not a container that stays up"}, } { _, err := ParseManifest(busyManifest(t, c.on, c.busy)) if err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%s: want a refusal saying %q, got %v", name, c.says, err) } } } func TestABusyIsSentOnlyToAnEngineThatReadsIt(t *testing.T) { m, err := ParseManifest(busyManifest(t, "agent", map[string]any{"command": "mesh-builder busy"})) if err != nil { t.Fatal(err) } compose := func(with Rendering) []map[string]any { out, err := Resolution{Node: "laptop", Modules: []Manifest{m}}.Declaration(with) if err != nil { t.Fatal(err) } return out } older := compose(Rendering{}) if b, sent := older[indexOfID(older, "app.agent")][BusyField]; sent { t.Fatalf("busy was sent to an engine that does not read it: %v", b) } newer := compose(Rendering{ReadsBusy: true}) got, _ := newer[indexOfID(newer, "app.agent")][BusyField].(map[string]any) if got["command"] != "mesh-builder busy" || got["timeout"] != "10s" { t.Fatalf("busy composed as %v", got) } // And the engine's own validator takes the container with it. body, err := json.Marshal(map[string]any{"declaration": validate.Version, "resources": newer}) if err != nil { t.Fatal(err) } if problems := validate.Declaration(body); len(problems) > 0 { t.Fatalf("the node-engine's validator refuses the composed busy: %v", problems) } if _, has := m.Resources[len(m.Resources)-1][BusyField].(map[string]any)["timeout"]; has { t.Error("composing wrote the timeout into the catalogue's own manifest") } }