ADR 0380 §8: the operator channel's notify is shaped for conditions (a key that stays open, escalation, a plain rule that rewrites a subject), so a module has no way to say a plain notice such as new mail. tell sends a headline and one or two lines at once to every channel, bypassing routing, the burst window and the walk hold; it offers Details like any message, is kept in the history, but its record is marked cleared at once so it never stays open. Its cap (10 a channel an hour) is its own, apart from notify's and the conditions'.
143 lines
4.9 KiB
Go
143 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// The manifest says what the code does (novox/hq ADR 0259): the seats it declares — the operator's, and
|
|
// the two kinded benches its channels hold — the one it holds and the verbs it serves, the bench it sends
|
|
// on, the events it consumes, the one it emits, the tool it calls, its state, and its own tools. It holds
|
|
// no channel's secret, and names nothing of one installation.
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Seats []seat `json:"seats"`
|
|
Claims []seat `json:"claims"`
|
|
Uses []string
|
|
Consumes []string
|
|
Emits []string
|
|
Invokes []string
|
|
State []json.RawMessage
|
|
Own map[string]string `json:"own-secrets"`
|
|
RunsAs string `json:"runs-as"`
|
|
SecretsOwner string `json:"secrets-owner"`
|
|
Tools []string
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
type seat struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
Accepts []string `json:"accepts"`
|
|
Emits []string `json:"emits"`
|
|
Kinded bool `json:"kinded"`
|
|
ByCaller []string `json:"by-caller"`
|
|
Proofs []string `json:"proofs"`
|
|
Records []string `json:"records"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) (manifest, string) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, string(raw)
|
|
}
|
|
|
|
func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
verbs := []string{"open", "history", "notify", "tell", "asks"}
|
|
want := []seat{
|
|
{Name: asks.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"},
|
|
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"asks"}},
|
|
{Name: asks.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{asks.Show, asks.Edit, asks.Send}},
|
|
{Name: asks.IntakeSeat, Scope: "mesh", Kinded: true,
|
|
Emits: []string{asks.Chosen, asks.Link, asks.FailedWhat, asks.StandingWhat}, Proofs: []string{asks.CodeProof}},
|
|
}
|
|
if !reflect.DeepEqual(m.Seats, want) {
|
|
t.Fatalf("seats %+v", m.Seats)
|
|
}
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != asks.Seat || !reflect.DeepEqual(m.Claims[0].Serves, verbs) {
|
|
t.Fatalf("claims %+v", m.Claims)
|
|
}
|
|
if !reflect.DeepEqual(m.Uses, []string{asks.ChannelSeat}) {
|
|
t.Fatalf("uses %v", m.Uses)
|
|
}
|
|
if !reflect.DeepEqual(m.Consumes, []string{
|
|
ControllerSeat + "." + EventRaised, ControllerSeat + "." + EventChanged, ControllerSeat + "." + EventCleared,
|
|
ControllerSeat + "." + EventPlanMoved, "intake.choice", "intake.link", "intake.failed", "intake.standing"}) {
|
|
t.Fatalf("consumes %v", m.Consumes)
|
|
}
|
|
// It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues
|
|
// track which conditions.
|
|
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions",
|
|
"seat:" + ControllerSeat + ".root-free", TrackingVerb}) {
|
|
t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes)
|
|
}
|
|
var state []string
|
|
for _, s := range m.State {
|
|
var name string
|
|
if json.Unmarshal(s, &name) != nil {
|
|
var full struct{ Name string }
|
|
_ = json.Unmarshal(s, &full)
|
|
name = full.Name
|
|
}
|
|
state = append(state, name)
|
|
}
|
|
if !reflect.DeepEqual(state, []string{"open", "sent", "asks", "identities", "walks"}) {
|
|
t.Fatalf("state %v", state)
|
|
}
|
|
// It holds no channel's secret: only its own bus account, read by its own account alone (ADR 0259 §8).
|
|
if len(m.Own) != 1 || m.Own["broker"] == "" || m.RunsAs != "messenger" || m.SecretsOwner != "messenger" {
|
|
t.Fatalf("own secrets %v, runs as %q, secrets owned by %q", m.Own, m.RunsAs, m.SecretsOwner)
|
|
}
|
|
}
|
|
|
|
func TestTheToolsAgreeWithTheManifest(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
var own, verbs []string
|
|
for _, tool := range tools(&Holder{}, &listening{}) {
|
|
if strings.TrimSpace(tool.Description) == "" {
|
|
t.Errorf("%s has no description", tool.Name)
|
|
}
|
|
if seatName, verb, ok := strings.Cut(tool.Name, "."); ok {
|
|
if seatName != "operator-channel" {
|
|
t.Errorf("%s is a verb of a seat this does not hold", tool.Name)
|
|
}
|
|
verbs = append(verbs, verb)
|
|
continue
|
|
}
|
|
own = append(own, tool.Name)
|
|
}
|
|
listed := append([]string(nil), m.Tools...)
|
|
sort.Strings(own)
|
|
sort.Strings(listed)
|
|
if !reflect.DeepEqual(own, listed) {
|
|
t.Fatalf("serves %v, lists %v", own, listed)
|
|
}
|
|
if !reflect.DeepEqual(verbs, m.Claims[0].Serves) {
|
|
t.Fatalf("verbs %v, claim %v", verbs, m.Claims[0].Serves)
|
|
}
|
|
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox", "zurag", "telegram"} {
|
|
if strings.Contains(strings.ToLower(raw), never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|