The node seat node-engine served on the engine's link: its verbs with their proven caller, its acts kept in its own state, a hand-over or access act asked of the operator as node-engine-<node> and performed only on the warrant (issue 774, ADR 0563 §1-§3)

This commit is contained in:
2026-10-28 20:35:33 +01:00
parent 479ac71050
commit aaffc19232
9 changed files with 1195 additions and 7 deletions
+101
View File
@@ -3,9 +3,14 @@ package link
import (
"crypto/ed25519"
"encoding/json"
"path/filepath"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// The take act's ask (novox/hq ADR 0325 §8): a hand-over's fields and the act's own in one signed object, under a
@@ -68,6 +73,102 @@ func TestTheTakeActRunsOnlyOnAWarrant(t *testing.T) {
if a.Said != "started" || len(given) != 1 || given[0].Warrant != "w-1" || given[0].Path != "/srv/media/movies" {
t.Fatalf("a warranted ask did not reach the act whole: %+v %+v", a, given)
}
// 774 (ADR 0563 §3): a hand-over asked through the node seat is the engine's own ask, performed only on the
// operator's warrant to it: never on the call, not on a warrant for another digest, once on a warrant heard twice,
// and not when the disk changed since it was asked.
t.Run("the node seat's hand-over acts only on the operator's warrant", func(t *testing.T) {
for _, row := range []struct {
name string
warrant func(asked asks.Ask, w asks.Warrant) asks.Warrant
diskAfter string
twice bool
done int
state string
}{
{name: "no warrant", done: 0, state: HandAsked},
{name: "the operator's yes", warrant: yes, done: 1, state: HandDone},
{name: "the yes heard twice", warrant: yes, twice: true, done: 1, state: HandDone},
{name: "a warrant for another digest", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
w = yes(a, w)
w.AskDigest = "sha256:another"
return w
}, done: 0, state: HandAsked},
{name: "a no", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
w = yes(a, w)
w.Option, w.Label, w.Level = "no", "No, leave it", asks.Acknowledge
return w
}, done: 0, state: HandRefused},
{name: "the disk changed", warrant: yes, diskAfter: "0:0 700 9:2", done: 0, state: HandRefused},
{name: "expired", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
return asks.Warrant{Ask: a.ID, Asker: w.Asker, Outcome: asks.OutcomeExpired, At: w.At}
}, done: 0, state: HandRefused},
} {
t.Run(row.name, func(t *testing.T) {
h, asked, done := anEngine(t)
disk := "1000:1000 755 9:1"
h.JudgeHandOver = func(path string) (Judged, error) {
return Judged{Subject: path, Path: path, Sets: "owner jo, group jo, mode 0750", Disk: disk,
Words: "A directory used as found."}, nil
}
if _, err := h.Call(EngineCall{Verb: ActHandOver, Caller: "shanks.node-tools",
Args: map[string]any{"path": "/srv/x", "why": "a test"}}); err != nil {
t.Fatal(err)
}
if len(*asked) != 1 || *done != 0 {
t.Fatalf("the call asked %d and performed %d", len(*asked), *done)
}
if _, err := h.Call(EngineCall{Verb: ActHandOver, Caller: "x", Args: map[string]any{"path": "/srv/x"}}); err == nil {
t.Fatal("a second ask of the same path was asked while one is open")
}
if row.diskAfter != "" {
disk = row.diskAfter
}
if row.warrant != nil {
w := row.warrant((*asked)[0], asks.Warrant{Asker: asks.EngineAsker("n1"), At: time.Now()})
h.Heard(w)
if row.twice {
h.Heard(w)
}
}
got, _ := h.Acts(0)
acts := got.(map[string]any)["acts"].([]HandAct)
if *done != row.done || len(acts) != 1 || acts[0].State != row.state {
t.Fatalf("performed %d (want %d), acts %+v (want %s)", *done, row.done, acts, row.state)
}
if row.state == HandDone && (acts[0].By != "operator" || acts[0].Via == "" || acts[0].Ask == "") {
t.Fatalf("the act does not record the warrant: %+v", acts[0])
}
})
}
})
}
// yes is the operator's yes to an ask, as the router writes it.
func yes(a asks.Ask, w asks.Warrant) asks.Warrant {
w.Ask, w.Outcome, w.Option, w.Label, w.Level = a.ID, asks.OutcomeChosen, "yes", "Yes", asks.Approve
w.By = &asks.Person{Who: asks.Operator, Kind: "telegram", Verified: "user id verified"}
w.AskDigest, w.Proofs = a.Digest(), []string{"P1"}
return w
}
// anEngine is a node seat with its own state in a temporary folder, a router that keeps what it was asked, and a
// hand-over that counts what it performed.
func anEngine(t *testing.T) (*HandActs, *[]asks.Ask, *int) {
t.Helper()
var asked []asks.Ask
done := 0
state := &FileState{Path: filepath.Join(t.TempDir(), HandActsFile)}
h := &HandActs{Node: "n1", State: state,
HandOver: func(path, by string) (string, error) { done++; return path + " handed over by " + by, nil }}
h.Asker = &asker.Client{Module: asks.EngineAsker("n1"), State: state,
Publish: func(subject string, body any, id string) error {
if a, ok := body.(asks.Ask); ok && subject == asks.AskSubject(asks.EngineAsker("n1")) {
asked = append(asked, a)
}
return nil
},
Record: func(string) (*stdio.StateEntry, error) { return nil, nil }}
return h, &asked, &done
}
// The controller names an access by its declared resource id exactly, which may hold a `/`: it reaches the act,