Files
mesh-delivery/modules/mesh-delivery-sdk/buildstep/stepoutput_test.go
T
jschoubben 386095bd53 A-290 B1: a step answer over the steps state's bound is kept as the object its key names
A step's key keeps its answer only within what the steps state holds per value (kvlimit.ModuleStateValueBytes less
the headers' room). Over it, finish keeps the answer with the holder's KeepOutput as the object output.<ask> in its
outputs store and the key names it in output-object: a check's whole answer, its result emptied; a run's output, the
key keeping the run answer without it. An answer that cannot be kept ends the key failed, saying why. The Puller says
step-ended from the end finish wrote, so the key and the event say one outcome: a run whose program exited other than
0 ends its key failed as well, its answer kept; step-ended names the object finish kept instead of putting it twice.
Step.Whole reads a key's object back through the seat's verb output on the step's node. The part build-step's Step
gains output-object, the verb output says it answers a step's answer; fixtures and the generated contracts follow.

Builds: design 47 G A-290, A-254, §467

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 16:44:07 +01:00

112 lines
4.7 KiB
Go

package buildstep
import (
"encoding/base64"
"encoding/json"
"errors"
"strings"
"testing"
)
// Design 47 G A-254: one reader of a run's answer from a step-ended, inline or through the seat's verb output on the node that ran it.
func TestStepEndedReadOutput(t *testing.T) {
const ask = "m2kx9a1b3c4d5f"
var key string
var body map[string]any
verb := func(answer string, err error) AskVerb {
return func(k string, b any) (json.RawMessage, error) {
key = k
body, _ = b.(map[string]any)
return json.RawMessage(answer), err
}
}
for _, c := range []struct {
name string
end StepEnded
ask AskVerb
want string
wantKey string
fail string
}{
{name: "inline", end: StepEnded{Ask: ask, Node: "ace", Output: `{"a":1}`}, want: `{"a":1}`},
{name: "an object is read through the verb on its node", end: StepEnded{Ask: ask, Node: "ace", OutputObject: OutputObjectName(ask)},
ask: verb(`{"ask":"`+ask+`","file":"eyJhIjoxfQ=="}`, nil), want: `{"a":1}`, wantKey: "seat:node-build-go.output@ace"},
{name: "no answer kept", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(`{"ask":"`+ask+`","chunks":0}`, nil), fail: "keeps no answer"},
{name: "another ask's answer", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(`{"ask":"x","file":"e30="}`, nil), fail: "answered the ask"},
{name: "the verb fails", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(``, errors.New("no seat")), fail: "no seat"},
{name: "no verb", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, fail: "no verb"},
} {
t.Run(c.name, func(t *testing.T) {
key, body = "", nil
got, err := c.end.ReadOutput("go", c.ask)
if c.fail != "" {
if err == nil || !strings.Contains(err.Error(), c.fail) {
t.Fatalf("err %v, want %q", err, c.fail)
}
return
}
if err != nil || string(got) != c.want || key != c.wantKey {
t.Fatalf("got %q, %v on %q, want %q on %q", got, err, key, c.want, c.wantKey)
}
if c.wantKey != "" && (body["ask"] != ask || len(body["call"].(string)) != 32) {
t.Fatalf("asked with %v, want the ask and 128 random bits as call", body)
}
})
}
}
// A step-ended names its answer inline or as an object, never both.
func TestStepEndedRefusesAnswerTwice(t *testing.T) {
e := StepEnded{ID: "m2kx9a1b3c4d5f", Ask: "m2kx9a1b3c4d5f", Asker: "a", Node: "ace", Outcome: OutcomePassed, Output: "1", OutputObject: "o"}
if _, err := EncodeStepEnded(e); err == nil || !strings.Contains(err.Error(), "inline and as an object") {
t.Fatalf("err %v", err)
}
}
// Design 47 G A-290: a step whose key names its answer as an object is read whole through the seat's verb output on its
// node: a check's answer as its result, a run's output put back in its run answer; a key holding its answer is answered as it is.
func TestStepWholeReadsTheObjectItsKeyNames(t *testing.T) {
const ask = "m2kx9a1b3c4d5f"
var key string
verb := func(file string, err error) AskVerb {
return func(k string, _ any) (json.RawMessage, error) {
key = k
return json.RawMessage(`{"ask":"` + ask + `","file":"` + file + `"}`), err
}
}
b64 := func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
named := Step{ID: ask, Node: "ace", State: StepPassed, OutputObject: OutputObjectName(ask)}
for _, c := range []struct {
name string
step Step
run bool
ask AskVerb
want, fail string
}{
{name: "the key holds it", step: Step{ID: ask, Node: "ace", Result: json.RawMessage(`{"a":1}`)}, want: `{"a":1}`},
{name: "a check's answer", step: named, ask: verb(b64(`{"fetch":{"workspace":"w"}}`), nil), want: `{"fetch":{"workspace":"w"}}`},
{name: "a run's output", step: func() Step { s := named; s.Result = json.RawMessage(`{"built":true,"key":"k","exit":0}`); return s }(), run: true,
ask: verb(b64(`{"verdict":"ok"}`), nil), want: `{"built":true,"key":"k","exit":0,"output":{"verdict":"ok"}}`},
{name: "a check's answer that is no JSON", step: named, ask: verb(b64(`not json`), nil), fail: "not JSON"},
{name: "the verb fails", step: named, ask: verb("", errors.New("no responders")), fail: "no responders"},
{name: "no verb", step: named, fail: "no verb"},
} {
t.Run(c.name, func(t *testing.T) {
key = ""
got, err := c.step.Whole("go", c.run, c.ask)
if c.fail != "" {
if err == nil || !strings.Contains(err.Error(), c.fail) {
t.Fatalf("err %v, want %q", err, c.fail)
}
return
}
if err != nil || string(got.Result) != c.want || got.OutputObject != "" {
t.Fatalf("got %s (object %q), %v; want %s", got.Result, got.OutputObject, err, c.want)
}
if c.ask != nil && key != "seat:node-build-go.output@ace" {
t.Fatalf("asked %q", key)
}
})
}
}