Merge pull request 'A seat says what it receives, and its holder places every module's contribution (hq ADR 0212)' (#269) from feat/0212-contributions-to-a-seat into main
This commit is contained in:
@@ -110,6 +110,8 @@ const (
|
||||
var (
|
||||
ofEnvironment = regexp.MustCompile(`\$\{environment:([^}]*)\}`)
|
||||
ofShell = regexp.MustCompile(`\$\{shell:([^}]*)\}`)
|
||||
// ofContributed is either kind of contributed text, matched together so both fill in one pass.
|
||||
ofContributed = regexp.MustCompile(`\$\{(shell|contribution):([^}]*)\}`)
|
||||
)
|
||||
|
||||
// variableName is a POSIX shell variable name, which is also what environment.d accepts.
|
||||
@@ -223,6 +225,7 @@ func (m Manifest) contributionPlaceholderProblems() []string {
|
||||
var problems []string
|
||||
for _, r := range m.Resources {
|
||||
problems = append(problems, placeholderProblems(m, r)...)
|
||||
problems = append(problems, seatPlaceholderProblems(m, r)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
@@ -545,7 +548,7 @@ func shellCode(modules []Manifest, shell, slot string) string {
|
||||
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
|
||||
// wrote, so a contributed piece is never scanned again.
|
||||
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string) error {
|
||||
if problems := placeholderProblems(m, resource); len(problems) > 0 {
|
||||
if problems := append(placeholderProblems(m, resource), seatPlaceholderProblems(m, resource)...); len(problems) > 0 {
|
||||
return fmt.Errorf("%s", problems[0])
|
||||
}
|
||||
content, ok := resource["content"].(string)
|
||||
@@ -564,10 +567,17 @@ func contributionsInto(resource map[string]any, m Manifest, modules []Manifest,
|
||||
return env.posix()
|
||||
})
|
||||
}
|
||||
if ofShell.MatchString(content) {
|
||||
content = ofShell.ReplaceAllStringFunc(content, func(placeholder string) string {
|
||||
shell, slot, _ := strings.Cut(ofShell.FindStringSubmatch(placeholder)[1], ":")
|
||||
return shellCode(modules, shell, slot)
|
||||
// Shell code and seat contributions in one pass (novox/hq ADR 0212): both are contributed text
|
||||
// the controller does not read, so neither may be scanned after the other is in place — a
|
||||
// contributed line that happened to spell the other's placeholder would be filled.
|
||||
if ofContributed.MatchString(content) {
|
||||
content = ofContributed.ReplaceAllStringFunc(content, func(placeholder string) string {
|
||||
found := ofContributed.FindStringSubmatch(placeholder)
|
||||
first, second, _ := strings.Cut(found[2], ":")
|
||||
if found[1] == "contribution" {
|
||||
return seatContributions(modules, first, second)
|
||||
}
|
||||
return shellCode(modules, first, second)
|
||||
})
|
||||
}
|
||||
resource["content"] = content
|
||||
|
||||
@@ -41,16 +41,20 @@ func graphicalSessionSeats() []Seat {
|
||||
"name": "the profile to save or apply (save and apply only)",
|
||||
}, []string{"action"}), "action", "list", "save", "apply")},
|
||||
}},
|
||||
{Name: DisplaySessionSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
|
||||
{Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.",
|
||||
Input: schema(map[string]string{}, nil)},
|
||||
{Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " +
|
||||
"it is visible or focused.",
|
||||
Input: schema(map[string]string{}, nil)},
|
||||
{Name: "windows", Description: "The session's windows: each one's title, class, workspace and " +
|
||||
"whether it has focus; narrowed to one workspace when named.",
|
||||
Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)},
|
||||
}},
|
||||
// It receives window-manager configuration lines from every other module (novox/hq ADR 0212):
|
||||
// bindings, start-up commands, rules — placed by the session's holder, never written into
|
||||
// its directory by the contributor.
|
||||
{Name: DisplaySessionSeat, Scope: ScopeNode, Decision: decided,
|
||||
Receives: []Receivable{{Kind: "config", Comment: "#"}}, Serves: []Verb{
|
||||
{Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.",
|
||||
Input: schema(map[string]string{}, nil)},
|
||||
{Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " +
|
||||
"it is visible or focused.",
|
||||
Input: schema(map[string]string{}, nil)},
|
||||
{Name: "windows", Description: "The session's windows: each one's title, class, workspace and " +
|
||||
"whether it has focus; narrowed to one workspace when named.",
|
||||
Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)},
|
||||
}},
|
||||
{Name: TerminalEmulatorSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
|
||||
{Name: "open", Description: "Open a terminal window in the operator's session, running a command " +
|
||||
"or the login shell, in a directory or the account's home.",
|
||||
|
||||
@@ -539,6 +539,11 @@ type Manifest struct {
|
||||
// the holder of node-login-shell put the slot's placeholder.
|
||||
Shell []ShellCode `json:"shell,omitempty"`
|
||||
|
||||
// Contributions are configuration this module gives the holder of a seat it does not hold, in
|
||||
// that tool's own grammar (novox/hq ADR 0212): a seat, a kind the seat receives, and the text. The
|
||||
// holder places them; the module depends on the seat (ADR 0210 §3).
|
||||
Contributions []SeatContribution `json:"contributions,omitempty"`
|
||||
|
||||
// Guards are ports of this module's the mesh refuses on an adopted node except from the
|
||||
// private network and from the machine itself (novox/hq ADR 0100) — the store's port and the
|
||||
// broker's management port. The ports the software uses; the mesh guards where the machine
|
||||
@@ -1846,6 +1851,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, m.environmentProblems()...)
|
||||
problems = append(problems, m.shellProblems()...)
|
||||
problems = append(problems, m.contributionPlaceholderProblems()...)
|
||||
problems = append(problems, m.seatContributionProblems()...)
|
||||
|
||||
for i, r := range m.Resources {
|
||||
id, _ := r["id"].(string)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A module contributes to a seat it does not hold (novox/hq ADR 0212).
|
||||
//
|
||||
// ADR 0210 made a tool's configuration its seat holder's, and every other module's way in a
|
||||
// contribution to the seat. The environment, the shell's slots and the power moments each became a
|
||||
// field of their own; this is the general form, so a new seat that takes contributions is a row in
|
||||
// the seat table rather than a change to the manifest: a contribution names a seat, a kind that
|
||||
// seat receives, and text in the tool's own grammar, which the controller never reads.
|
||||
|
||||
// HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5).
|
||||
const HotkeysSeat = "node-hotkeys"
|
||||
|
||||
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
|
||||
type SeatContribution struct {
|
||||
// Seat is the seat whose holder places it.
|
||||
Seat string `json:"seat"`
|
||||
// Kind is which of the seat's receivable kinds it is.
|
||||
Kind string `json:"kind"`
|
||||
// Content is the text, in the tool's own grammar. Never interpreted.
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// ofContribution is where a holder places a kind: ${contribution:<seat>:<kind>}. Loose inside the
|
||||
// braces, so a misspelt seat or kind is found and refused rather than written out as text.
|
||||
var ofContribution = regexp.MustCompile(`\$\{contribution:([^}]*)\}`)
|
||||
|
||||
// receivable is what a seat receives of a kind, with the seat's canonical name; false when the seat
|
||||
// is unknown or does not receive it.
|
||||
func receivable(seat, kind string) (Seat, Receivable, bool) {
|
||||
s, known := SeatNamed(seat)
|
||||
if !known {
|
||||
return Seat{}, Receivable{}, false
|
||||
}
|
||||
for _, r := range s.Receives {
|
||||
if r.Kind == kind {
|
||||
return s, r, true
|
||||
}
|
||||
}
|
||||
return s, Receivable{}, false
|
||||
}
|
||||
|
||||
// kindsOf names a seat's receivable kinds for a refusal.
|
||||
func kindsOf(s Seat) string {
|
||||
if len(s.Receives) == 0 {
|
||||
return "it receives no contributions"
|
||||
}
|
||||
var kinds []string
|
||||
for _, r := range s.Receives {
|
||||
kinds = append(kinds, r.Kind)
|
||||
}
|
||||
return "it receives " + strings.Join(kinds, ", ")
|
||||
}
|
||||
|
||||
// seatContributionProblems is what is wrong with this module's contributions, from the manifest
|
||||
// alone (novox/hq ADR 0212 §2).
|
||||
func (m Manifest) seatContributionProblems() []string {
|
||||
var problems []string
|
||||
for i, c := range m.Contributions {
|
||||
s, _, ok := receivable(c.Seat, c.Kind)
|
||||
switch {
|
||||
case s.Name == "":
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's contribution %d is to the seat %q, which the mesh does not define", m.Module, i+1, c.Seat))
|
||||
case !ok:
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's contribution %d to %s is of the kind %q; %s (novox/hq ADR 0212)",
|
||||
m.Module, i+1, s.Name, c.Kind, kindsOf(s)))
|
||||
}
|
||||
if strings.TrimSpace(c.Content) == "" {
|
||||
problems = append(problems, fmt.Sprintf("%s's contribution %d has no content", m.Module, i+1))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// seatPlaceholderProblems is what is wrong with one resource's ${contribution:…}: placed only in a
|
||||
// file's content, naming a seat and a kind it receives, and only by a module that claims that seat
|
||||
// — another would be a second writer of a file there is one of (novox/hq ADR 0212 §3).
|
||||
func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
|
||||
var problems []string
|
||||
for _, field := range sortedKeys(r) {
|
||||
v, ok := r[field].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
found := ofContribution.FindAllStringSubmatch(v, -1)
|
||||
if len(found) == 0 {
|
||||
continue
|
||||
}
|
||||
if field != "content" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's resource %v names %s in its %s; contributions are placed only in a file's content",
|
||||
m.Module, r["id"], found[0][0], field))
|
||||
continue
|
||||
}
|
||||
for _, f := range found {
|
||||
seat, kind, two := strings.Cut(f[1], ":")
|
||||
s, _, ok := receivable(seat, kind)
|
||||
if !two || !ok {
|
||||
detail := "the mesh defines no seat " + fmt.Sprintf("%q", seat)
|
||||
if s.Name != "" {
|
||||
detail = s.Name + ": " + kindsOf(s)
|
||||
}
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's resource %v names %s; a contribution is ${contribution:<seat>:<kind>} (%s)",
|
||||
m.Module, r["id"], f[0], detail))
|
||||
continue
|
||||
}
|
||||
if !m.ClaimsSeat(s.Name) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s's resource %v names %s and %s does not claim %s; every module's contributions to a "+
|
||||
"seat are placed by its holder alone (novox/hq ADR 0212)",
|
||||
m.Module, r["id"], f[0], m.Module, s.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
|
||||
// in module order, each module's pieces in the order it declared them, each module's preceded by a
|
||||
// comment line naming it in the tool's grammar, and empty when nothing is contributed.
|
||||
func seatContributions(modules []Manifest, seat, kind string) string {
|
||||
s, r, ok := receivable(seat, kind)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, m := range inModuleOrder(modules) {
|
||||
named := false
|
||||
for _, c := range m.Contributions {
|
||||
if c.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name {
|
||||
continue
|
||||
}
|
||||
if !named {
|
||||
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
|
||||
named = true
|
||||
}
|
||||
b.WriteString(c.Content)
|
||||
if !strings.HasSuffix(c.Content, "\n") {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0212: a seat says what it receives, its holder places it, and a contribution
|
||||
// depends on the seat.
|
||||
|
||||
func hotkeysHolder() Manifest {
|
||||
return Manifest{Module: "triggerhappy", Claims: []Claim{{Name: HotkeysSeat}}, Resources: []map[string]any{
|
||||
{"id": "triggers", "type": "file", "path": "/etc/triggerhappy/triggers.d/mesh.conf",
|
||||
"content": "# the mesh's triggers\n${contribution:node-hotkeys:trigger}"},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestAContributionReachesTheHoldersFileInModuleOrderNamedByModule(t *testing.T) {
|
||||
laptop := Manifest{Module: "laptop", Contributions: []SeatContribution{
|
||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_PROG1 1 play ${machine:account-home}"},
|
||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_F21 1 touchpad\n"},
|
||||
}}
|
||||
another := Manifest{Module: "another", Contributions: []SeatContribution{
|
||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_MUTE 1 mute ${shell:zsh:first}"},
|
||||
}}
|
||||
unrelated := Manifest{Module: "bar", Contributions: []SeatContribution{
|
||||
{Seat: DisplaySessionSeat, Kind: "config", Content: "bar { }"},
|
||||
}}
|
||||
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder(), laptop, another, unrelated}}
|
||||
out, err := r.Declaration(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var file map[string]any
|
||||
for _, res := range out {
|
||||
if res["id"] == "triggerhappy.triggers" {
|
||||
file = res
|
||||
}
|
||||
}
|
||||
if file == nil {
|
||||
t.Fatalf("the holder's file was not composed: %v", out)
|
||||
}
|
||||
// Module order; each module named once; text never read, so neither ${machine:…} nor ${shell:…}
|
||||
// inside a contribution is filled.
|
||||
want := "# the mesh's triggers\n" +
|
||||
"# another\nKEY_MUTE 1 mute ${shell:zsh:first}\n" +
|
||||
"# laptop\nKEY_PROG1 1 play ${machine:account-home}\nKEY_F21 1 touchpad\n"
|
||||
if got := file["content"]; got != want {
|
||||
t.Fatalf("the holder's file is\n%s\nnot\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoContributionPlacesNothing(t *testing.T) {
|
||||
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder()}}
|
||||
out, err := r.Declaration(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, res := range out {
|
||||
if res["id"] == "triggerhappy.triggers" && res["content"] != "# the mesh's triggers\n" {
|
||||
t.Fatalf("an empty kind left %q", res["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAContributionToASeatThatDoesNotReceiveItIsRefused(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`{"seat":"node-hotkeys","kind":"config","content":"x"}`: "it receives trigger",
|
||||
`{"seat":"node-nothing","kind":"trigger","content":"x"}`: "the mesh does not define",
|
||||
`{"seat":"node-hotkeys","kind":"trigger","content":" "}`: "has no content",
|
||||
`{"seat":"node-display-session","kind":"trigger","content":"x"}`: "it receives config",
|
||||
}
|
||||
for c, want := range cases {
|
||||
raw := `{"module":"laptop","contributions":[` + c + `]}`
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAContributionPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
|
||||
raw := `{"module":"laptop","resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:trigger}"}]}`
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "laptop does not claim node-hotkeys") {
|
||||
t.Errorf("a placeholder outside the holder was accepted: %v", err)
|
||||
}
|
||||
raw = `{"module":"triggerhappy","claims":[{"name":"node-hotkeys"}],"resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:keys}"}]}`
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "it receives trigger") {
|
||||
t.Errorf("a placeholder for a kind the seat does not receive was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAContributionDependsOnItsSeat(t *testing.T) {
|
||||
m := Manifest{Module: "laptop", Contributions: []SeatContribution{
|
||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
||||
{Seat: DisplaySessionSeat, Kind: "config", Content: "y"},
|
||||
}}
|
||||
if got, want := DependsOn(m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("depends on %v, want %v", got, want)
|
||||
}
|
||||
catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()}
|
||||
if _, err := AssignRefusal(catalogue, "laptop", nil, []string{"laptop"}); err == nil ||
|
||||
!strings.Contains(err.Error(), HotkeysSeat) {
|
||||
t.Errorf("a contributor without the holder was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheHotkeysSeatIsTheMeshsAndReceivesTriggers(t *testing.T) {
|
||||
s, ok := SeatNamed(HotkeysSeat)
|
||||
if !ok || s.Scope != ScopeNode || len(s.Receives) != 1 || s.Receives[0].Kind != "trigger" {
|
||||
t.Fatalf("%+v %v", s, ok)
|
||||
}
|
||||
d, _ := SeatNamed(DisplaySessionSeat)
|
||||
if len(d.Receives) != 1 || d.Receives[0].Kind != "config" {
|
||||
t.Fatalf("the display session receives %+v", d.Receives)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,13 @@ func contributedTo(m Manifest) []string {
|
||||
for _, c := range m.Shell {
|
||||
out = append(out, placerOf(c.For))
|
||||
}
|
||||
// Any other seat's contribution (novox/hq ADR 0212 §4), by the seat's canonical name; one the
|
||||
// mesh does not define is refused at registration and depends on nothing here.
|
||||
for _, c := range m.Contributions {
|
||||
if s, known := SeatNamed(c.Seat); known {
|
||||
out = append(out, s.Name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,22 @@ type Seat struct {
|
||||
// Serves carries each verb in full — name, description, schema — because a role's tools are the
|
||||
// mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2).
|
||||
Serves []Verb
|
||||
// Receives is what other modules may contribute to the seat's holder, by kind (novox/hq ADR
|
||||
// 0212): each kind is text in the tool's own grammar, placed by the holder with
|
||||
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
|
||||
// definition of the role, and the store's rows carry no column for it.
|
||||
Receives []Receivable
|
||||
// Decision is the record that made it a seat.
|
||||
Decision string
|
||||
}
|
||||
|
||||
// Receivable is one kind of contribution a seat receives (novox/hq ADR 0212 §2): its name, and the
|
||||
// comment prefix of the tool's grammar, with which the controller names each contributing module.
|
||||
type Receivable struct {
|
||||
Kind string
|
||||
Comment string
|
||||
}
|
||||
|
||||
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
|
||||
// fallback when it has none (novox/hq ADR 0122). It is the one place the closed set 0110 defines is
|
||||
// written; the store's table is seeded from it and thereafter is the live, editable copy.
|
||||
@@ -176,6 +188,11 @@ var defaultSeats = append([]Seat{
|
||||
// code modules contribute for the power moments, and publishes the machine's power states as
|
||||
// its events. Every machine has one — every machine boots and shuts down. No verbs yet.
|
||||
{Name: PowerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0211"},
|
||||
// The machine's hotkeys (novox/hq ADR 0212): the daemon that sees the keys the window manager
|
||||
// does not — a laptop's vendor keys — run by one module per machine, which owns its
|
||||
// configuration. Every other module with keys contributes trigger lines to it.
|
||||
{Name: HotkeysSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0212",
|
||||
Receives: []Receivable{{Kind: "trigger", Comment: "#"}}},
|
||||
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
|
||||
// model change, not a rename, so it stays until that is built.
|
||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
@@ -238,6 +255,10 @@ func UseSeats(s []Seat) {
|
||||
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
|
||||
}
|
||||
}
|
||||
// What a seat receives is never stored (novox/hq ADR 0212), so it is always the compiled one.
|
||||
if d, known := byName[row.Name]; known {
|
||||
row.Receives = d.Receives
|
||||
}
|
||||
merged = append(merged, row)
|
||||
}
|
||||
seats = merged
|
||||
|
||||
@@ -46,13 +46,13 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
// Thirty-three with node-power (novox/hq ADR 0211); thirty-two since the graphical session's
|
||||
// Thirty-four with node-hotkeys (novox/hq ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the graphical session's
|
||||
// eleven (ADR 0208); twenty-one with
|
||||
// node-package-manager and node-container-runtime (ADR 0207); nineteen with node-environment and
|
||||
// node-login-shell (ADR 0203, ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer
|
||||
// once the retired mesh-build-machine row goes, when no registered manifest claims it any more.
|
||||
if len(Seats()) != 33 {
|
||||
t.Errorf("the mesh defines %d seats rather than 33; the set is closed, so a change here is "+
|
||||
if len(Seats()) != 34 {
|
||||
t.Errorf("the mesh defines %d seats rather than 34; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user