ADR 0578 §2 and §3: a case's accepted verdict, or none (the running checker drops the change's own), lifts the input's hold on a manifest the composer reads whole; a case's refusal and bytes that do not decode stay held. It is composed at the build the input carries (Input.Holds). Without that build the node is not judged and the verdict names the module, why it was held and that its build is awaited; where it cannot compose without it, it is composed held as its node-engine holds it, and said so. Never a silent pass (the lead's Q4 ruling). The issue 774 row now gives the runner the refused verdict the checker gives a manifest the change does not bring. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
356 lines
20 KiB
Go
356 lines
20 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)
|
|
}
|
|
}
|
|
})
|
|
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)
|
|
}
|
|
}
|