Files
mesh-host/internal/composer/composer_test.go
T
jschoubben 53addd73c3
mesh/delivery-group group feat/registry-milestone checking: 7 of 7 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 53addd73c358 on e73aae3686a6 | novox/mesh-host#209 in group feat/registry-milestone
The check's composer composes a module at its build only where that build was made of the change's manifest (issue 1921, milestone 47/G)
The running indexer could not read 17 of the group's manifests, so the running node-engine composed those modules at
their trunk builds and said nothing; the head composer laid those builds over the change's other manifests and ace
failed on "nothing provides artifact-store, wanted by mesh-build-go" (group check 8a80e8e86858). The input a node
answers a check with now names, per module taken in from its build, the content digest of the manifest that build was
made of (ComposedHow.Manifests); the composer compares it with the case's manifest, and, for an input from a
node-engine before it, compares what the two manifests say beyond what a take-in resolves (the lead's ruling, §17).
Each module lifted either way is composed from the case's manifest, its build awaited, and named in NotJudged with
the comparison that lifted it; one that cannot compose without its build is held as its stub. The digest is taken
over one JSON form because a request through a build module arrives compacted.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 07:24:16 +01:00

405 lines
23 KiB
Go

package composer
import (
"context"
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/frombus"
"github.com/novox/mesh-host/internal/compose/frombus/frombustest"
)
const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files"}]}`
// withInput is r with in as its input, as a caller puts it on the wire: the request's shape carries the input as JSON.
func withInput(t *testing.T, r Request, in compose.Input) Request {
t.Helper()
raw, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(raw, &r.Input); err != nil {
t.Fatal(err)
}
return r
}
// 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.Decode([]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: a case named twice, an unknown depth.
func TestARequestItCannotRunIsRefused(t *testing.T) {
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 assignment (issue 1126) composes over a node the input holds, and is refused for one it does not.
placed := withInput(t, Request{V: Version, 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)}},
Assignments: bucketstest.Raw(map[string]meshmoduleassigner.Assignments{"a": {V: 1, Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "ghost"}}}})}}},
compose.Input{Nodes: []compose.Node{{Name: "a"}}})
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 assignment: %+v, %v", v, err)
}
placed.Cases[1].Assignments = bucketstest.Raw(map[string]meshmoduleassigner.Assignments{"z": {V: 1, 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)
}
// Issue 1126 (check of mesh-host #200): the module the mesh provides itself (the private network) comes from mesh-sdk,
// a second source beside the indexer's manifests the cases carry, and the input carries no catalogue: a node it is
// assigned to still composes, and what it provides is provided.
wg := overlay.ProvidedModules()[0]
provided := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}},
compose.Input{Nodes: []compose.Node{{Name: "a", Assigned: []string{wg, "demo"}}}})
if v, err := Run(provided); err != nil || !v.Cases[0].Machines["a"].Composes {
t.Errorf("a node assigned the mesh's own module: %+v, %v", v, err)
}
// Issue 1126 (checks of mesh-host #200, fix-a-bug-met-twice): the sandbox composes one node from the same inputs as
// the bus reader, through one path. A mesh whose restic is built (its files from an archive) and handed to the
// provisioner composes n1 through frombus, and the request a node-engine answers for it (the input as the engine
// sends it, the indexer's manifests in the case) declares the same paths for n1.
t.Run("the sandbox composes a node as the bus reader does", func(t *testing.T) {
b, err := frombustest.Provisioned()
if err != nil {
t.Fatal(err)
}
ctx, scope := context.Background(), frombus.Scope{Node: frombustest.Node}
read, err := frombus.Input(ctx, b, frombus.Mesh{}, scope)
if err != nil {
t.Fatal(err)
}
d, problems, err := compose.Compose(read, frombustest.Node)
if err != nil || len(problems) > 0 {
t.Fatalf("the bus reader's input composes n1: %v %v", err, problems)
}
want := pathsOf(d)
sent := SentInput(read, frombustest.Node)
cases := Case{Name: "change", Manifests: map[string]Manifest{}}
for name, m := range b.Module {
cases.Manifests[name] = Manifest{Raw: b.Manifests[m.Manifest]}
}
r := withInput(t, Request{V: Version, Machines: []string{frombustest.Node}, Declarations: true, Cases: []Case{cases}}, sent)
v, err := Run(r)
if err != nil {
t.Fatal(err)
}
got := v.Cases[0].Machines[frombustest.Node]
if !got.Composes || !reflect.DeepEqual(got.Declared, want) {
t.Errorf("the sandbox composed n1 %+v; the bus reader declared %+v", got, want)
}
})
// Issue 774 (the check of the bus want, the third of a kind: the sandbox's input differs from what the bus reader
// composes): a node like the bus node, its bus module folded in its own composition at a build of the old form and no
// build published for the module's server artifact, composes in the sandbox as its node-engine composes it, from the
// input that says how (compose.Input.ComposedHow); from an older engine's input, which does not, it is named not
// judged, never failed and never passed in silence.
t.Run("a module folded in the node's own composition", func(t *testing.T) {
bus := `{"module": "bus", "version": "1", "claims": [{"name": "node-files"}], ` +
`"resources": [{"id": "server", "type": "container", "name": "bus-server", "artifact": "server"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
man, err := catalogue.Decode([]byte(bus))
if err != nil {
t.Fatal(err)
}
node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true},
RunsOwn: &engcomp.Composed{Node: "a"}}
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"bus": man}, Nodes: []compose.Node{node}}
request := func(in compose.Input) Request {
return withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{}}}}, in)
}
older, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := older.Cases[0].Machines["a"]; m.Composes || !strings.HasPrefix(m.NotJudged, NotJudged+"bus") || m.Reason != "" {
t.Errorf("an older engine's input of a node with a folded module: %+v", m)
}
in.ComposedHow = &compose.ComposedHow{Node: "a", Builds: map[string]string{}}
sent, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := sent.Cases[0].Machines["a"]; !m.Composes || m.Reason != "" {
t.Errorf("the input that says how the node composes, its bus folded: %+v", m)
}
// A module the change refuses beside the folded one fails the node in its words, never passed with the rest
// composed (review of the sandbox input); held already where the node-engine composed, it stays held as there.
app, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", ` +
`"resources": [{"id": "env", "type": "file", "path": "/var/lib/app/env", "mode": "0600", "content": "N=${secret:repository}\n"}]}`))
if err != nil {
t.Fatal(err)
}
in.Catalogue["app"] = app
in.Nodes[0].Assigned = []string{"bus", "app"}
refused, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := refused.Cases[0].Machines["a"]; m.Composes || !strings.Contains(m.Reason, "app: ") || !strings.Contains(m.Reason, "repository") {
t.Errorf("a module the change refuses on a node composed as its node-engine does: %+v", m)
}
// Held composing without the change, blamed for a refusal beyond the read (issue 1960's unset names): no refusal of
// the change either (review round 1).
in.ComposedHow.HeldBefore = map[string]string{"app": "its route names are unset"}
before, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := before.Cases[0].Machines["a"]; !m.Composes {
t.Errorf("a module the node holds composing without the change: %+v", m)
}
in.ComposedHow.HeldBefore = nil
in.ComposedHow.Held = map[string]string{"app": "held on the node"}
held, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := held.Cases[0].Machines["a"]; !m.Composes {
t.Errorf("a module the node-engine holds too: %+v", m)
}
})
// Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's
// verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses
// it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way. The
// case gives the runner its refused verdict, as the checker does for a manifest the change does not bring.
t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) {
raw := `{"module": "runner", "version": "1", "tool-runner": true, ` +
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
man, err := catalogue.Decode([]byte(raw))
if err != nil {
t.Fatal(err)
}
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"runner": compose.Stub(man)},
Refused: map[string]string{"runner": "the indexer's judge refused the manifest of runner"},
Nodes: []compose.Node{{Name: "a", Assigned: []string{"runner"}}}}
wire, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
var back compose.Input
if err := json.Unmarshal(wire, &back); err != nil || !back.Catalogue["runner"].Stubbed || !strings.Contains(string(wire), `"Held"`) {
t.Fatalf("the input's held mark across the wire: %s, %v", wire, err)
}
older := strings.Replace(string(wire), `"Held":{"Catalogue":["runner"]}`, `"Unsaid":null`, 1)
for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} {
var r Request
r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}},
Verdicts: bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"runner": {V: 1, Module: "runner",
State: meshcatalogclient.VerdictRefused, Problems: []string{"p"}}})}}}
if err := json.Unmarshal([]byte(sent), &r.Input); err != nil {
t.Fatal(err)
}
v, err := Run(r)
if err != nil {
t.Fatal(err)
}
if m := v.Cases[0].Machines["a"]; !m.Composes || strings.Contains(m.Reason, "bundle") {
t.Errorf("%s: the node with its tool runner held: %+v", name, m)
}
if read := v.Cases[0].Manifests["runner"]; read.Accepted || !strings.Contains(read.Refused, "the indexer's judge refused the manifest of runner") {
t.Errorf("%s: the runner's read names not why its node-engine holds it: %+v", name, read)
}
}
})
// ADR 0578 §2, §3 (issue 1921): a module its node-engine holds by the running indexer's verdict gives way when the case,
// judging the change's manifest itself, gives it an accepted verdict or none (the running checker drops the change's
// own), and this composer reads its bytes whole. It is composed at the build the input carries for it (Input.Holds);
// without one it is composed from the case's manifest and the node is not judged, naming it (never a silent pass), or,
// where it cannot compose without its build (a tool runner's bundle), composed held as its node-engine holds it, named
// so. Bytes that do not decode here, and a case's own refusal, stay held.
t.Run("a held module the case judges itself", func(t *testing.T) {
app := `{"module": "app", "version": "1", "resources": [{"id": "server", "type": "container", "name": "app-server", "artifact": "server"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
// A module that names a build and composes from its manifest without it.
plain := `{"module": "app", "version": "1", "resources": [{"id": "d", "type": "directory", "path": "/var/lib/app"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
runner := `{"module": "app", "version": "1", "tool-runner": true, ` +
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
built, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", "resources": [{"id": "built", "type": "directory", "path": "/var/lib/app-built"}]}`))
if err != nil {
t.Fatal(err)
}
verdict := func(state string) map[string]json.RawMessage {
return bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"app": {V: 1, Module: "app", State: state, Problems: []string{"p"}}})
}
const why = "the indexer's judge refused the manifest of app"
for _, row := range []struct {
name string
raw string
verdicts map[string]json.RawMessage
hold *compose.Hold
composes bool // the machine composes, judged
path string // a path declared on it
words string // in NotJudged, else in the read's refusal
}{
{"an accepted verdict, its build carried", app, verdict(meshcatalogclient.VerdictAccepted),
&compose.Hold{By: compose.HeldByVerdict, Build: "fp1", Built: &built}, true, "/var/lib/app-built", ""},
{"no verdict, its build carried", app, nil, &compose.Hold{By: compose.HeldByVerdict, Build: "fp1", Built: &built}, true, "/var/lib/app-built", ""},
{"an accepted verdict, its build awaited", plain, verdict(meshcatalogclient.VerdictAccepted), &compose.Hold{By: compose.HeldByVerdict, Build: "fp1"}, false, "",
"composed from the change's manifest without its build: app is refused only by the running indexer (" + why + ") and accepted by the check's own validator, and its build fp1 is awaited"},
{"no verdict, from an engine before holds", plain, nil, nil, false, "",
"app is held by the running node-engine (" + why + ") and given no verdict by the check, so taken as accepted"},
{"a container, its image awaited", app, nil, &compose.Hold{By: compose.HeldByVerdict}, false, "",
"composed with it held as its node-engine holds it, since it does not compose without its build: app is refused only by the running indexer"},
{"a tool runner, its bundle awaited", runner, nil, &compose.Hold{By: compose.HeldByVerdict}, false, "",
"composed with it held as its node-engine holds it, since it does not compose without its build: app is refused only by the running indexer"},
{"bytes that do not decode", `{"module": "app", "version": "1", "resources": "none"}`, nil, &compose.Hold{By: compose.HeldByVerdict, Built: &built}, true, "", "does not decode"},
{"the case's own refusal", app, verdict(meshcatalogclient.VerdictRefused), &compose.Hold{By: compose.HeldByVerdict, Built: &built}, true, "", "refused the manifest of app"},
} {
man, _ := catalogue.Decode([]byte(app))
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"app": compose.Stub(man)}, Refused: map[string]string{"app": why},
Nodes: []compose.Node{{Name: "a", Assigned: []string{"app"}}}}
if row.hold != nil {
in.Holds = map[string]compose.Hold{"app": *row.hold}
}
v, err := Run(withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Declarations: true,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"app": {Raw: json.RawMessage(row.raw)}}, Verdicts: row.verdicts}}}, in))
if err != nil {
t.Fatal(err)
}
m, read := v.Cases[0].Machines["a"], v.Cases[0].Manifests["app"]
declared := false
for _, p := range m.Declared {
declared = declared || p.Path == row.path
}
switch {
case m.Composes != row.composes || (row.path != "" && !declared):
t.Errorf("%s: machine %+v", row.name, m)
case !row.composes && (m.NotJudged == "" || !strings.Contains(m.NotJudged, row.words)):
t.Errorf("%s: not judged %q, want it naming %q", row.name, m.NotJudged, row.words)
case row.composes && row.path == "" && (read.Accepted || !strings.Contains(read.Refused, row.words)):
t.Errorf("%s: read %+v, want held naming %q", row.name, read, row.words)
}
}
})
// Issue 1921 (the group check of 8a80e8e86858, ace: "nothing provides artifact-store, wanted by mesh-build-go"): a
// module the input composes at its build, taken in by a node-engine whose indexer cannot read the change's manifest,
// is composed at that build only where the build was made of the case's manifest: by the content digest the input
// names, else, from a node-engine that names none, by what the two manifests say beyond what a take-in resolves.
// Otherwise it is composed from the case's manifest, never silently: the node is not judged, naming it and how.
t.Run("a module composed at a build of another manifest", func(t *testing.T) {
plain := `{"module": "app", "version": "1", "resources": [{"id": "d", "type": "directory", "path": "/var/lib/app"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
built, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", "resources": [{"id": "built", "type": "directory", "path": "/var/lib/app-built"}]}`))
if err != nil {
t.Fatal(err)
}
same, err := compose.ContentDigest([]byte(plain))
if err != nil {
t.Fatal(err)
}
for _, row := range []struct {
name, raw, digest string
words string // in NotJudged, its build's paths not declared; "" composes at its build
}{
{"the digest of the case's manifest", plain, same, ""},
{"another digest, the same contract", plain, strings.Repeat("b", 64), "of another manifest than the change's (its build was made of the manifest " + strings.Repeat("b", 64)},
{"no digest, the same contract", plain, "", ""},
{"no digest, another contract", strings.Replace(plain, `"version": "1"`, `"version": "2"`, 1), "",
"app is composed by the running node-engine at its build fp1, of another manifest than the change's (its node-engine names no manifest"},
{"a tool runner, its bundle awaited", `{"module": "app", "version": "1", "tool-runner": true, ` +
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`, "",
"composed with it held as its node-engine holds it, since it does not compose without its build: app is composed by the running node-engine at its build fp1"},
} {
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"app": built}, Nodes: []compose.Node{{Name: "a", Assigned: []string{"app"}}},
ComposedHow: &compose.ComposedHow{Node: "a", Builds: map[string]string{"app": "fp1"}}}
if row.digest != "" {
in.ComposedHow.Manifests = map[string]string{"app": row.digest}
}
v, err := Run(withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Declarations: true,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"app": {Raw: json.RawMessage(row.raw)}}}}}, in))
if err != nil {
t.Fatal(err)
}
m := v.Cases[0].Machines["a"]
declared := false
for _, p := range m.Declared {
declared = declared || p.Path == "/var/lib/app-built"
}
if row.words == "" && (!m.Composes || !declared) || row.words != "" && (m.Composes || declared || !strings.Contains(m.NotJudged, row.words)) {
t.Errorf("%s: machine %+v, want it naming %q", row.name, m, row.words)
}
}
})
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("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 = bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"xterm": {V: 1, Module: "xterm", State: meshcatalogclient.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)
}
// The verdict as the trunk's checker hands it, version 0 and no judge (group try 1793952957648659842 failed on all
// four nodes: "the verdict of f1-circuits is not one: ... the value is version 0"), holds the module too; a later
// version is refused.
r.Cases[0].Verdicts = map[string]json.RawMessage{"xterm": json.RawMessage(`{"v":0,"module":"xterm","state":"refused","problems":["falls back to a key not listed"],"judge":"","at":"0001-01-01T00:00:00Z"}`)}
if v, err = Run(r); err != nil {
t.Fatalf("a version-0 verdict of the trunk's checker: %v", err)
}
if got := v.Cases[0].Manifests["xterm"]; got.Accepted || !strings.Contains(got.Refused, "refused the manifest of xterm: falls back") {
t.Fatalf("xterm refused by a version-0 verdict: %+v, want refused", got)
}
r.Cases[0].Verdicts["xterm"] = json.RawMessage(`{"v":2,"module":"xterm","state":"refused","problems":["p"]}`)
if _, err = Run(r); !errors.Is(err, keys.ErrUnknownVersion) {
t.Fatalf("a version-2 verdict: %v, want keys.ErrUnknownVersion", err)
}
}