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
112 lines
4.7 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|