G C-1 (Go): a bundle reads its tool runner's capability words once and names a word not stated
mesh/capabilities with its answer {words}, an unknown-call answer read as none,
the node from MESH_NODE; put-sessions and finding-calls named beside
mesh/publish.
Builds: design 47 G §576, §488, §622
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package buscalls
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Design 47 G §587, §622: the ledger's upkeep calls have their requests and answers, and every family its word.
|
||||
func TestTheLedgerUpkeepCallsAndTheirWords(t *testing.T) {
|
||||
var removal LedgerRemoveRequest
|
||||
if err := json.Unmarshal([]byte(`{"ledger":"records","instance":"r1"}`), &removal); err != nil || removal.Ledger != "records" || removal.Instance != "r1" {
|
||||
t.Errorf("the removal read as %+v (%v)", removal, err)
|
||||
}
|
||||
raw, _ := json.Marshal(LedgerRemoved{Instance: "r1", Operations: 3, Bytes: 900})
|
||||
if string(raw) != `{"instance":"r1","operations":3,"bytes":900}` {
|
||||
t.Errorf("the removal's answer reads %s", raw)
|
||||
}
|
||||
raw, _ = json.Marshal(Fill{Count: 2, Bytes: 10, MaxBytes: 100})
|
||||
if string(raw) != `{"count":2,"bytes":10,"max_bytes":100}` || LedgerFill != "mesh/ledger.fill" || LedgerRemove != "mesh/ledger.remove" {
|
||||
t.Errorf("the fill reads %s, calls %s %s", raw, LedgerFill, LedgerRemove)
|
||||
}
|
||||
for word, want := range map[string]string{BusStateCalls: "bus-state-calls", LedgerCalls: "ledger-calls",
|
||||
LedgerUpkeepCalls: "ledger-upkeep-calls", SentObjectsCalls: "sent-objects-calls"} {
|
||||
if word != want {
|
||||
t.Errorf("the word %q is %q", want, word)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package stdio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The call a bundle reads its tool runner's capability words with, and the words of the families that sit beside
|
||||
// `mesh/publish` and `mesh/subscribe` (design 47 G §576, §622). The words of the bus-state families are in buscalls.
|
||||
const (
|
||||
// CapabilitiesCall is answered with {words}: the capability words the tool runner states.
|
||||
CapabilitiesCall = "mesh/capabilities"
|
||||
// CapPutSessions is the put and get sessions' word (§457, §518), CapFindingCalls the acknowledged publish and the
|
||||
// subscription for new messages the finding writer uses (§559). CapSeatSettleDelay (seat.go) is the third.
|
||||
CapPutSessions = "put-sessions"
|
||||
CapFindingCalls = "finding-calls"
|
||||
)
|
||||
|
||||
// capabilityWords are the words the tool runner states, read once at the first call that needs one and kept for as long
|
||||
// as the bundle runs: a bundle lives no longer than the tool runner that started it (design 47 G §576).
|
||||
type capabilityWords struct {
|
||||
mu sync.Mutex
|
||||
read bool
|
||||
words []string
|
||||
}
|
||||
|
||||
var capabilities capabilityWords
|
||||
|
||||
// get answers the words, asking once with ask. A tool runner of before G answers `mesh/capabilities` as an unknown call,
|
||||
// which reads as no word stated; any other failure is returned and asked again at the next call.
|
||||
func (c *capabilityWords) get(ask func() (json.RawMessage, error)) ([]string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.read {
|
||||
return c.words, nil
|
||||
}
|
||||
raw, err := ask()
|
||||
switch {
|
||||
case errors.Is(err, errors.ErrUnsupported):
|
||||
c.words = nil
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("reading the tool runner's capability words: %w", err)
|
||||
default:
|
||||
var a struct {
|
||||
Words []string `json:"words"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the tool runner's capability words: %w", err)
|
||||
}
|
||||
c.words = a.Words
|
||||
}
|
||||
c.read = true
|
||||
return c.words, nil
|
||||
}
|
||||
|
||||
// require is nil when word is among the stated ones, else an error naming the word and the node (design 47 G §488, §576).
|
||||
func (c *capabilityWords) require(word string, ask func() (json.RawMessage, error)) error {
|
||||
words, err := c.get(ask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(words, word) {
|
||||
return nil
|
||||
}
|
||||
node := os.Getenv("MESH_NODE")
|
||||
if node == "" {
|
||||
node = "unknown"
|
||||
}
|
||||
return fmt.Errorf("the tool runner of node %s does not state the capability word %q: a call of its family is refused until it does", node, word)
|
||||
}
|
||||
|
||||
// RequireCapability fails at once, naming the word and the node, when the tool runner this bundle runs under does not
|
||||
// state word; it reads the words once (design 47 G §488, §576).
|
||||
func RequireCapability(word string) error {
|
||||
return capabilities.require(word, func() (json.RawMessage, error) {
|
||||
c := awaitChannel()
|
||||
if c == nil {
|
||||
return nil, errors.New("a capability is read through the runtime that launched the bundle, and Serve has not run")
|
||||
}
|
||||
return c.ask(CapabilitiesCall, map[string]any{}, 30*time.Second)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package stdio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Design 47 G §576: the words are read once, an unknown-call answer reads as none, and a word not stated fails naming it
|
||||
// and the node (from MESH_NODE).
|
||||
func TestACapabilityWordIsReadOnceAndNamedWhenNotStated(t *testing.T) {
|
||||
t.Setenv("MESH_NODE", "ace")
|
||||
asked := 0
|
||||
var c capabilityWords
|
||||
ask := func() (json.RawMessage, error) { asked++; return json.RawMessage(`{"words":["put-sessions"]}`), nil }
|
||||
if err := c.require(CapPutSessions, ask); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := c.require(CapFindingCalls, ask)
|
||||
if err == nil || !strings.Contains(err.Error(), CapFindingCalls) || !strings.Contains(err.Error(), "ace") || asked != 1 {
|
||||
t.Errorf("a word not stated: %v after %d asks", err, asked)
|
||||
}
|
||||
var old capabilityWords
|
||||
unknown := func() (json.RawMessage, error) {
|
||||
asked++
|
||||
return nil, &refused{message: "no mesh/capabilities", code: CodeNoMethod}
|
||||
}
|
||||
if err := old.require(CapPutSessions, unknown); err == nil || !strings.Contains(err.Error(), CapPutSessions) {
|
||||
t.Errorf("a tool runner of before G stated %v", err)
|
||||
}
|
||||
var failing capabilityWords
|
||||
if err := failing.require(CapPutSessions, func() (json.RawMessage, error) { return nil, errors.New("pipe closed") }); err == nil ||
|
||||
!strings.Contains(err.Error(), "pipe closed") || failing.read {
|
||||
t.Errorf("a failed read: %v, kept %v", err, failing.read)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user