Issue 1585, S19: the strict parser, every manifest problem function and the judge's tables (192 declarations, about 5,200 lines) and their 244 tests leave; composition keeps its own refusals and the helpers it uses. The catalogue's tests decode the manifests they build. ParseManifest, ParseManifestWith, ParseManifestForInputs, CatalogueProblems and the rest are gone; mesh-delivery-indexer/judge holds them. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
129 lines
4.9 KiB
Go
129 lines
4.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0208: the graphical session is one module per piece, on the mesh's seats.
|
|
|
|
// §2: the eleven roles are the mesh's own node seats, each with the verbs it starts with.
|
|
func TestTheGraphicalSessionsSeatsAreTheMeshsOwnWithTheirVerbs(t *testing.T) {
|
|
want := map[string][]string{
|
|
LoginManagerSeat: {"sessions"},
|
|
DisplayServerSeat: {"displays", "layout"},
|
|
DisplaySessionSeat: {"reload", "workspaces", "windows"},
|
|
TerminalEmulatorSeat: {"open"},
|
|
LauncherSeat: {"menu", "secret"},
|
|
NotifierSeat: {"send", "history"},
|
|
LockScreenSeat: {"lock"},
|
|
ClipboardSeat: {"history", "copy"},
|
|
BarSeat: nil,
|
|
CompositorSeat: nil,
|
|
SecretServiceSeat: nil,
|
|
}
|
|
for name, verbs := range want {
|
|
s, ok := SeatNamed(name)
|
|
if !ok {
|
|
t.Errorf("%s is not in the mesh's set", name)
|
|
continue
|
|
}
|
|
if s.Scope != ScopeNode || s.Decision != "novox/hq ADR 0208" {
|
|
t.Errorf("%s is %s-scoped under %q", name, s.Scope, s.Decision)
|
|
}
|
|
var got []string
|
|
for _, v := range s.Serves {
|
|
got = append(got, v.Name)
|
|
if v.Description == "" || v.Input["type"] != "object" {
|
|
t.Errorf("%s.%s has no description or no object schema", name, v.Name)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(got, verbs) {
|
|
t.Errorf("%s serves %v, want %v", name, got, verbs)
|
|
}
|
|
}
|
|
// The launcher's menu takes a list, and the layout verb says its actions.
|
|
menu, _ := SeatNamed(LauncherSeat)
|
|
choices := menu.Serves[0].Input["properties"].(map[string]any)["choices"].(map[string]any)
|
|
if choices["type"] != "array" {
|
|
t.Errorf("menu's choices are %v, not a list", choices["type"])
|
|
}
|
|
display, _ := SeatNamed(DisplayServerSeat)
|
|
action := display.Serves[1].Input["properties"].(map[string]any)["action"].(map[string]any)
|
|
if !reflect.DeepEqual(action["enum"], []string{"list", "save", "apply"}) {
|
|
t.Errorf("layout's actions are %v", action["enum"])
|
|
}
|
|
// And they survive the store's JSON, which is where the live set comes from.
|
|
if _, err := json.Marshal(graphicalSessionSeats()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func displayServer(name, display string, claims ...string) Manifest {
|
|
m := Manifest{Module: name, Provides: []Offer{{Name: display, Reach: ReachMachine}}}
|
|
for _, c := range claims {
|
|
m.Claims = append(m.Claims, Claim{Name: c})
|
|
}
|
|
return m
|
|
}
|
|
|
|
func windowManager() Manifest {
|
|
return Manifest{Module: "i3", Requires: []string{"x11-display"}}
|
|
}
|
|
|
|
// §3: a display is resolved on the requiring module's own node.
|
|
func TestAMachineReachRequirementResolvesToTheProviderOnItsOwnNode(t *testing.T) {
|
|
cat := shelf(windowManager(), displayServer("xorg", "x11-display", DisplayServerSeat))
|
|
got, err := Resolve(cat, []string{"xorg", "i3"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatalf("i3 beside xorg did not resolve: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(names(got), []string{"xorg", "i3"}) && !reflect.DeepEqual(names(got), []string{"i3", "xorg"}) {
|
|
t.Errorf("resolved %v", names(got))
|
|
}
|
|
}
|
|
|
|
// §3: never answered by installing a provider, and never by another machine's.
|
|
func TestAMachineReachRequirementIsNotPulledInNorAnsweredFromAnotherNode(t *testing.T) {
|
|
cat := shelf(windowManager(),
|
|
displayServer("xorg", "x11-display", DisplayServerSeat),
|
|
displayServer("xwayland", "x11-display"))
|
|
// Another machine runs xorg and says so to the world; it does not count.
|
|
world := World{Offered: map[string][]Provider{
|
|
"x11-display": {{Node: "laptop", At: "laptop.mesh", Module: "xorg"}}}}
|
|
_, err := Resolve(cat, []string{"i3"}, workstation(), world)
|
|
if err == nil {
|
|
t.Fatal("i3 resolved on a machine with no display of its own")
|
|
}
|
|
for _, want := range []string{
|
|
`"x11-display" is wanted by i3`, "usable only on the machine that provides it",
|
|
"assign one to workstation", "xorg (holds node-display-server)", "xwayland",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not say %q:\n%v", want, err)
|
|
}
|
|
}
|
|
// With a single provider in the catalogue too: one candidate is still not a choice to make
|
|
// for somebody, unlike a node-scoped provision without the machine's reach.
|
|
_, err = Resolve(shelf(windowManager(), displayServer("xorg", "x11-display", DisplayServerSeat)),
|
|
[]string{"i3"}, workstation(), World{})
|
|
if err == nil {
|
|
t.Fatal("xorg was pulled in for i3")
|
|
}
|
|
// Not in the first pass, whose refusals take the machine off the network.
|
|
if _, err := Resolve(cat, []string{"i3"}, workstation(), World{Unchecked: true}); err != nil {
|
|
t.Errorf("the first pass refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestACatalogueDisagreeingAboutAProvisionsReachIsRefused(t *testing.T) {
|
|
cat := shelf(windowManager(), displayServer("xorg", "x11-display"),
|
|
Manifest{Module: "fake-x", Provides: Offers("x11-display")})
|
|
_, err := Resolve(cat, []string{"xorg", "i3"}, workstation(), World{})
|
|
if err == nil || !strings.Contains(err.Error(), `the catalogue disagrees about "x11-display"`) {
|
|
t.Fatalf("a provision with and without the machine's reach gave %v", err)
|
|
}
|
|
}
|