Add the node-engine verb composition to the seat's contract, and the resources two compositions differ in (issue 774)

A provider waiting for a yes said why only in its have's words; nobody could read the composition that waits beside
what runs. The engine's read verb composition answers that, and ChangedResources lists what a move changes, a
received file naming the consumers that join or leave.
This commit is contained in:
2026-10-31 00:23:42 +01:00
parent 409c56685d
commit 6bce1fb0d2
4 changed files with 60 additions and 1 deletions
+12
View File
@@ -49,6 +49,18 @@ func EngineVerbs() []Verb {
"hands its lease away, beside the controller's mirror for this machine and their difference. With write, the " +
"key written again from what the engine composed.",
Input: schema(map[string]string{"write": "\"true\": write this machine's key again from what the engine composed"}, nil, "write")},
// Read-only (issue 774): why a module waits for a yes, or what its next apply would change. Optional until every
// engine serves it.
{Name: "composition", Optional: true, Description: "One module's composition on this machine beside what runs of " +
"it, read-only: the composition its want's build has in the bucket composed-<node> (the latest revision, or the " +
"one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs " +
"(applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, " +
"and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for " +
"a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers " +
"that join or leave.",
Input: schema(map[string]string{"module": "the module, as placed on this machine; node for the node's own",
"digest": "a composition's digest (sha256:…), to read that revision rather than the latest",
"diff": "\"true\": list the resources that differ from what runs"}, []string{"module"})},
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[5].Input)
}
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages" ||
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition" ||
strings.Join(engine.Emits, ",") != "asked,done,refused" {
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits)
}
+37
View File
@@ -139,6 +139,43 @@ func judged(from, to Composed) (Judgement, string) {
return JudgedNeither, "it recreates no container and moves no data"
}
// ResourceChange is one resource that differs between two compositions of a module: its id, its kind, whether it is
// added, removed or changed, and for a file the mesh makes for a provision (received-, bound-, secret-) the consumers
// that join or leave, in words.
type ResourceChange struct {
ID string `json:"id"`
Kind string `json:"kind"`
Change string `json:"change"`
Consumers []string `json:"consumers,omitempty"`
}
// ChangedResources is every resource added, removed or changed from running to composed, by id, each compared
// canonical (issue 774: the engine's verb composition shows what a move waiting for a yes would change). Running nil
// is every resource added.
func ChangedResources(running *Composed, composed Composed) []ResourceChange {
var from Composed
if running != nil {
from = *running
}
before, after := judgedResourcesByID(from), judgedResourcesByID(composed)
var out []ResourceChange
for _, id := range changedOf(before, after, func(string) bool { return true }) {
was, now := before[id], after[id]
c := ResourceChange{ID: id, Change: "changed"}
switch {
case was == nil:
c.Change, c.Kind = "added", now.kind
case now == nil:
c.Change, c.Kind = "removed", was.kind
default:
c.Kind = now.kind
}
c.Consumers = consumersOf(id, was, now)
out = append(out, c)
}
return out
}
// consumersOf is, for a changed file the mesh makes for a provision (received-, bound- or secret-, catalogue.ReceivedID,
// BoundID, SecretID), what changes of it in words: each consumer that joins or leaves a received file, by the given
// list's from and node; else that what it is given for the provision changes. Nil for any other resource.
+10
View File
@@ -2,6 +2,7 @@ package delivery
import (
"encoding/json"
"fmt"
"strings"
"testing"
)
@@ -78,6 +79,15 @@ func TestJudgeMove(t *testing.T) {
t.Errorf("%s: the flag's difference is not said: %q", c.name, got.Words)
}
}
// The resources that differ, as the engine's verb composition lists them (issue 774): a provider's received file
// names its consumers, a file changed and one added are said as such, and nothing equal is listed.
changes := ChangedResources(mail('a', true, container, spool, received, config), *mail('b', true, container, spool, received2, config2, tools))
if want := `[{mesh-mail.conf file changed []} {mesh-mail.received-smtp file changed [cloud on novox leaves smtp wiki on ace joins smtp]} {mesh-mail.tools file added []}]`; fmt.Sprint(changes) != want {
t.Errorf("changed resources: %v, want %s", changes, want)
}
if got := ChangedResources(nil, *mail('b', true, spool)); len(got) != 1 || got[0].Change != "added" {
t.Errorf("never ran: %v, want its one resource added", got)
}
if got := JudgeMove(nil, Composed{Node: "ace"}); got.Judgement != JudgedNeither {
t.Errorf("the node's own composition: %+v", got)
}