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"}, 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) } } // §2: a module may claim one of them, and may not declare it as its own. func TestNoModuleMayDeclareAGraphicalSessionSeat(t *testing.T) { raw := `{"module":"xorg","seats":[{"name":"node-display-server","scope":"node"}]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "mesh's own namespace") { t.Fatalf("a module declared node-display-server as its own: %v", 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 TestTheMachinesReachIsAProvisionsOnlyReachAndIsNodeScoped(t *testing.T) { for _, c := range []struct{ provides, want string }{ {`{"name":"x11-display","reach":"internal"}`, `with reach "internal"; a provision's reach is "machine" or nothing`}, {`{"name":"x11-display","scope":"mesh","reach":"machine"}`, `at scope "mesh" with the machine's reach`}, } { _, err := ParseManifest([]byte(`{"module":"xorg","provides":[` + c.provides + `]}`)) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: want %q, got %v", c.provides, c.want, err) } } m, err := ParseManifest([]byte(`{"module":"xorg","provides":[{"name":"x11-display","reach":"machine"}]}`)) if err != nil { t.Fatal(err) } if !m.Provides[0].MachineReach() { t.Fatal("the reach was not read") } back, _ := json.Marshal(m.Provides[0]) if string(back) != `{"name":"x11-display","reach":"machine"}` { t.Errorf("written back as %s", back) } } 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) } } // §4: xinitrc and xresources slots, placed by the display server's holder alone. func TestTheSessionsFilesArePlacedByTheDisplayServersHolderAlone(t *testing.T) { xorg := Manifest{Module: "xorg", Claims: []Claim{{Name: DisplayServerSeat}}, Shell: []ShellCode{{For: "xinitrc", Slot: "first", Code: "xset s off"}}, Resources: []map[string]any{ {"id": "xinitrc", "type": "file", "path": "/home/op/.xinitrc", "content": "${shell:xinitrc:first}${shell:xinitrc:normal}${shell:xinitrc:last}"}, {"id": "xresources", "type": "file", "path": "/home/op/.Xresources", "content": "${shell:xresources:normal}"}, }} i3 := Manifest{Module: "i3", Shell: []ShellCode{{For: "xinitrc", Slot: "last", Code: "exec i3"}}} theme := Manifest{Module: "theme", Shell: []ShellCode{ {For: "xresources", Slot: "normal", Code: "Xft.dpi: 96"}, {For: "zsh", Slot: "normal", Code: "not for the session"}, }} r := Resolution{Node: "workstation", Account: "op", Modules: []Manifest{xorg, i3, theme}} out, err := r.Declaration(Rendering{}) if err != nil { t.Fatal(err) } by := map[string]any{} for _, res := range out { by[res["id"].(string)] = res["content"] } if got := by["xorg.xinitrc"]; got != "# xorg\nxset s off\n# i3\nexec i3\n" { t.Errorf("the .xinitrc is %q", got) } if got := by["xorg.xresources"]; got != "# theme\nXft.dpi: 96\n" { t.Errorf("the .Xresources is %q", got) } // The contributions parse; the placeholders parse only in the holder. if _, err := ParseManifest([]byte(`{"module":"i3","shell":[{"for":"xinitrc","slot":"last","code":"exec i3"},` + `{"for":"xresources","slot":"normal","code":"i3.font: x"}]}`)); err != nil { t.Fatalf("a session contribution was refused: %v", err) } for _, c := range []struct{ claims, content, want string }{ {``, "${shell:xinitrc:normal}", "does not claim node-display-server; every module's xinitrc is placed by the display server's holder alone"}, {`{"name":"node-login-shell"}`, "${shell:xresources:normal}", "does not claim node-display-server"}, {`{"name":"node-display-server"}`, "${shell:zsh:normal}", "does not claim node-login-shell"}, {`{"name":"node-display-server"}`, "${shell:xsession:normal}", "the session's file one of xinitrc, xresources"}, } { raw := `{"module":"holder","claims":[` + c.claims + `],"resources":[{"id":"rc","type":"file","path":"/etc/rc","content":"` + c.content + `"}]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s with claims [%s]: want %q, got %v", c.content, c.claims, c.want, err) } } if _, err := ParseManifest([]byte(`{"module":"xorg","claims":[{"name":"node-display-server"}],` + `"resources":[{"id":"rc","type":"file","path":"/home/op/.xinitrc","content":"${shell:xinitrc:last}"}]}`)); err != nil { t.Errorf("the display server's holder could not place the session's slots: %v", err) } }