Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/seat_tools_test.go
T
jschoubben 7125a9f92f C1c-2a: the step runner says a run's answer in step-ended, inline or as an object, and serves it
The puller's end of a run carries the run's program's last line of standard output as `output` up to StepEndedInlineMost, else keeps
it with KeepOutput and names the object in `output-object`; a program that exits other than 0 ends failed. OutputTool serves the
seat's verb `output` from the kept object, as the whole file the tool runner sends in chunks; OutputsStore names the store.

Builds: design 47 G A-254, A-257, §602

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:37:51 +01:00

77 lines
3.1 KiB
Go

package steprun
import (
"context"
"testing"
"time"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
)
// 1887/A9a, ADR 0525 §10: a build seat's verbs are <seat>.current, .pause and .resume; current names the ask running,
// pause stops the puller taking asks, resume lets it take them again.
func TestSeatToolsCurrentPauseResume(t *testing.T) {
src, st := NewFakeAsks(), newSteps()
release := make(chan struct{})
p := &Puller{Source: src, Language: sdkstep.AskGo, Node: "shanks", Put: st.put, Capacity: func() int { return 1 },
PullWait: 10 * time.Millisecond, Run: func(ctx context.Context, a sdkstep.StepAsk) (any, error) { <-release; return nil, nil }}
tools := map[string]func(map[string]any) (any, error){}
for _, tool := range SeatTools("node-build-go", "Go", p) {
tools[tool.Name] = tool.Run
}
for _, name := range []string{"node-build-go.current", "node-build-go.pause", "node-build-go.resume"} {
if tools[name] == nil {
t.Fatalf("no %s", name)
}
}
a := testAsk(sdkstep.AskDelivery)
_ = src.PublishAsk(a)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go p.Pull(ctx)
waitFor(t, "the ask running", func() bool { return p.InHand() == 1 })
out, _ := tools["node-build-go.current"](nil)
cur := out.(map[string]any)
running := cur["running"].([]RunningAsk)
if len(running) != 1 || running[0].ID != a.ID || running[0].Module != "m" || cur["capacity"] != 1 || cur["paused"] != false {
t.Fatalf("current = %+v", cur)
}
if out, err := tools["node-build-go.pause"](nil); err != nil || out.(map[string]any)["paused"] != true {
t.Fatalf("pause = %v, %v", out, err)
}
close(release)
_ = src.PublishAsk(testAsk(sdkstep.AskDelivery))
time.Sleep(50 * time.Millisecond)
if src.Waiting(sdkstep.AskDelivery) != 1 {
t.Fatal("a paused node took an ask")
}
if _, err := tools["node-build-go.resume"](nil); err != nil {
t.Fatal(err)
}
waitFor(t, "the ask taken after the resume", func() bool { return src.Waiting(sdkstep.AskDelivery) == 0 })
}
// Design 47 G A-254: `<seat>.output` answers a run's answer by its ask as the whole file (base64), which the tool runner sends in
// chunks (A-257); an ask this node keeps no answer of answers no chunks.
func TestOutputToolAnswersTheKeptAnswer(t *testing.T) {
kept := map[string][]byte{sdkstep.OutputObjectName("a1"): []byte(`{"verdict":"x"}`)}
tool := OutputTool("node-build-go", func(object string) ([]byte, error) { return kept[object], nil })
if tool.Name != "node-build-go.output" {
t.Fatalf("name %q", tool.Name)
}
got, err := tool.Run(map[string]any{"ask": "a1", "call": "c"})
if err != nil {
t.Fatal(err)
}
if m := got.(map[string]any); m["ask"] != "a1" || m["file"] != "eyJ2ZXJkaWN0IjoieCJ9" {
t.Fatalf("answer %v, want the ask and its answer's bytes as the file", got)
}
got, _ = tool.Run(map[string]any{"ask": "none", "call": "c"})
if m := got.(map[string]any); m["ask"] != "none" || m["chunks"] != 0 || m["file"] != nil {
t.Fatalf("answer %v, want no chunks and no file", got)
}
if _, err := tool.Run(map[string]any{"call": "c"}); err == nil {
t.Fatal("a call with no ask was answered")
}
}