Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/run_output_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

130 lines
6.9 KiB
Go

package steprun
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/kvlimit"
)
// Design 47 G A-254, A-290: a build seat's step-ended of a run carries the run's answer, the last line of its program's standard
// output: inline as `output` up to the event bound, else as `output-object`, the object the holder keeps, which the verb `output`
// answers. The step's key holds the answer only within what the steps state holds per value: over it, a check's whole answer or a
// run's output is the object output.<ask> the key names, kept once; the key and step-ended always say the same outcome, a run
// whose program exited other than 0 failed in both, its answer kept (A-290).
func TestStepEndedOfARunCarriesItsAnswer(t *testing.T) {
big := `"` + strings.Repeat("x", sdkstep.StepEndedInlineMost) + `"`
overKey := `"` + strings.Repeat("y", kvlimit.ModuleStateValueBytes) + `"` // over the steps state, within the event
checkAnswer := map[string]any{"fetch": map[string]any{"workspace": "/w"}, "levels": []any{map[string]any{"level": "unit", "said": strings.Repeat("z", kvlimit.ModuleStateValueBytes)}}}
for _, c := range []struct {
name string
check bool // a check ask answering checkAnswer, not a run
answer sdkstep.RunAnswer
keepErr error
outcome string
inline string
objectKept bool // step-ended names the object
keyObject string // what the object the key names holds: "" none, else the answer's bytes
said string
}{
{name: "a small answer is inline", answer: sdkstep.RunAnswer{Output: json.RawMessage(`{"verdict":true}`)}, outcome: sdkstep.OutcomePassed, inline: `{"verdict":true}`},
{name: "an answer over the event bound is an object", answer: sdkstep.RunAnswer{Output: json.RawMessage(big)}, outcome: sdkstep.OutcomePassed, objectKept: true, keyObject: big},
{name: "an answer over the steps state within the event is inline and the object the key names", answer: sdkstep.RunAnswer{Built: true, Key: "k", Output: json.RawMessage(overKey)},
outcome: sdkstep.OutcomePassed, inline: overKey, keyObject: overKey},
{name: "a program that exits other than 0 failed, its answer kept", answer: sdkstep.RunAnswer{Exit: 3, Output: json.RawMessage(`"no"`), Said: "bad input"},
outcome: sdkstep.OutcomeFailed, inline: `"no"`, said: "the program exited 3: bad input"},
{name: "an answer the holder cannot keep is said", answer: sdkstep.RunAnswer{Output: json.RawMessage(big)}, keepErr: errors.New("store full"),
outcome: sdkstep.OutcomeFailed, said: "store full"},
{name: "a check's answer over the steps state is the object the key names", check: true, outcome: sdkstep.OutcomePassed, keyObject: "check"},
{name: "a check's answer the holder cannot keep fails in its key and its end alike", check: true, keepErr: errors.New("store full"),
outcome: sdkstep.OutcomeFailed, said: "store full"},
} {
t.Run(c.name, func(t *testing.T) {
src, st, sy := NewFakeAsks(), newSteps(), &sayer{}
a := testAsk(sdkstep.AskCheck)
var answer any = c.answer
if c.check {
answer = checkAnswer
} else {
a.Check = nil
in, _ := sdkstep.RunInputOf("seat:mesh-catalog.input", []byte(`{"v":1}`))
commit := "0123456789abcdef0123456789abcdef01234567"
a.Run = &sdkstep.RunJob{Program: sdkstep.RunProgram{Repository: "novox/mesh-delivery", Commit: commit, Dir: "modules/x", Package: "./cmd/x"}, Input: in}
}
_ = src.PublishAsk(a)
var mu sync.Mutex
kept, keeps := map[string][]byte{}, 0
p := &Puller{Source: src, Language: sdkstep.AskGo, Node: "shanks", Put: st.put, Capacity: func() int { return 1 },
PullWait: 10 * time.Millisecond, Heartbeat: time.Hour, Say: sy.say,
KeepOutput: func(object string, body []byte) error {
mu.Lock()
defer mu.Unlock()
keeps++
if c.keepErr != nil {
return c.keepErr
}
kept[object] = body
return nil
},
Run: func(ctx context.Context, a sdkstep.StepAsk) (any, error) { return answer, nil }}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go p.Pull(ctx)
waitFor(t, "step-ended", func() bool { return len(sy.of("step-ended")) == 1 })
end := sy.of("step-ended")[0].body
if end["outcome"] != c.outcome {
t.Fatalf("outcome %v, want %s (%v)", end["outcome"], c.outcome, end)
}
if c.inline != "" && end["output"] != c.inline {
t.Fatalf("output %.80v, want %.80s inline", end["output"], c.inline)
}
if c.said != "" && !strings.Contains(end["said"].(string), c.said) {
t.Fatalf("said %v, want it to hold %q", end["said"], c.said)
}
key, _ := sdkstep.StepKey("shanks", a.ID)
step, _ := st.get(key)
if raw, err := sdkstep.EncodeStep(step); err != nil || kvlimit.Check("steps", key, len(raw), kvlimit.ModuleStateValueBytes) != nil {
t.Fatalf("the step's key is %d bytes (%v), over what the steps state holds", len(raw), err)
}
if want := map[string]string{sdkstep.OutcomePassed: sdkstep.StepPassed, sdkstep.OutcomeFailed: sdkstep.StepFailed}[c.outcome]; step.State != want {
t.Fatalf("the key ended %s (%s), step-ended %s: no step ends passed in one place and failed in another", step.State, step.Reason, c.outcome)
}
if c.answer.Exit != 0 && (!strings.Contains(step.Reason, c.said) || !strings.Contains(string(step.Result), c.inline)) {
t.Fatalf("the key's reason %q and answer %s, want the exit said and the answer kept", step.Reason, step.Result)
}
mu.Lock()
defer mu.Unlock()
name := sdkstep.OutputObjectName(a.ID)
if c.objectKept {
if end["output-object"] != name || end["output"] != nil || string(kept[name]) != big {
t.Fatalf("end %v and kept %d bytes, want the object %s holding the whole answer and no inline output", end["output-object"], len(kept[name]), name)
}
} else if end["output-object"] != nil {
t.Fatalf("an object %v was named, want none", end["output-object"])
}
switch {
case c.keyObject == "":
if step.OutputObject != "" || (c.keepErr == nil && keeps != 0) {
t.Fatalf("the key names %q and %d objects were kept, want none", step.OutputObject, keeps)
}
case c.check:
var whole map[string]any
if step.OutputObject != name || len(step.Result) != 0 || keeps != 1 || json.Unmarshal(kept[name], &whole) != nil || whole["fetch"] == nil {
t.Fatalf("the key names %q with result %.80s, %d keeps; want the object %s holding the whole check answer, kept once", step.OutputObject, step.Result, keeps, name)
}
default:
var rest sdkstep.RunAnswer
if step.OutputObject != name || keeps != 1 || string(kept[name]) != c.keyObject || json.Unmarshal(step.Result, &rest) != nil || rest.Output != nil || rest.Key != c.answer.Key {
t.Fatalf("the key names %q with result %.80s, %d keeps; want the object %s holding the output, kept once, and the run answer without it", step.OutputObject, step.Result, keeps, name)
}
}
})
}
}