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
130 lines
6.9 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|