Let a module-declared seat carry needs, so mail-notifier can need node-notifier (issue 802)

ADR 0220's dependency mechanism served compiled seats only and retired its one user with
node-resolver-config (commit f68521da). ADR 0378 S8 revives it for module-declared seats:
SeatDeclaration gains Needs, judged by the catalogue check (node scope only, a name that
resolves to a node seat somewhere in the catalogue, never itself), and DependsOn now takes
the catalogue so a claimed seat's needs are found wherever they are declared.
This commit is contained in:
jochen
2026-10-15 17:18:23 +02:00
parent 0ab61d1be8
commit b5a441a978
9 changed files with 188 additions and 23 deletions
+47
View File
@@ -116,6 +116,53 @@ func TestTheMeshsOwnSeatsAreStillClaimable(t *testing.T) {
}
}
// Only a node seat may carry needs (novox/hq ADR 0378 §8): a per-mesh seat's holder is not on any
// one node to depend on anything beside it there.
func TestOnlyANodeSeatMayCarryNeeds(t *testing.T) {
m := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
Name: "mail-notifier", Scope: ScopeMesh, Needs: []string{"node-notifier"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
if !strings.Contains(got, "needing") || !strings.Contains(got, "node seat") {
t.Fatalf("a mesh-scope seat's needs were accepted: %q", got)
}
}
// A seat needing itself names nothing (novox/hq ADR 0378 §8).
func TestASeatCannotNeedItself(t *testing.T) {
m := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{"mail-notifier"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
if !strings.Contains(got, "needing itself") {
t.Fatalf("a seat needing itself was accepted: %q", got)
}
}
// A need naming nothing node-scope — undeclared, or declared at another scope — is the whole
// catalogue's to say (novox/hq ADR 0378 §8).
func TestANeedMustNameANodeSeatSomewhereInTheCatalogue(t *testing.T) {
mail := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{"desk-beeper"}}}}
// Nobody declares desk-beeper at all.
got := problemsFor(t, Shelf{"mail": mail})
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "no module declares") {
t.Fatalf("needing an undeclared seat was accepted: %q", got)
}
// Declared, but at mesh scope: still not a node seat to hold beside it.
notNode := Manifest{Module: "dunst", DefinesSeats: []SeatDeclaration{{Name: "desk-beeper", Scope: ScopeMesh}}}
got = problemsFor(t, Shelf{"mail": mail, "dunst": notNode})
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "not a node seat") {
t.Fatalf("needing a mesh-scope seat was accepted: %q", got)
}
// Declared at node scope: accepted.
dunst := Manifest{Module: "dunst", DefinesSeats: []SeatDeclaration{{Name: "desk-beeper", Scope: ScopeNode}}}
if got := problemsFor(t, Shelf{"mail": mail, "dunst": dunst}); got != "" {
t.Fatalf("needing a node seat another module declares was refused: %q", got)
}
}
// A refusal that reorders itself between runs is a refusal nobody can diff.
func TestTheProblemsAreStable(t *testing.T) {
shelf := Shelf{"telegram": telegram(), "shop": {Module: "shop", Uses: []string{"nope"}},