The tool runner now takes a call's caller from the subject it arrived on and hands it to the module through the SDK (stdio.CallerOf); the tracker records that bus user on what each verb writes instead of unproven. Never from the arguments, which still refuse any caller, and unproven where the runtime proves none. Needs mesh-sdk's go/caller.go (pinned at its branch head until merged).
141 lines
5.0 KiB
Go
141 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// served is the tracker's verbs served as the runtime launches them, over the SDK's stdio loop, with the caller
|
|
// read from the runtime (stdio.CallerOf): what reaches the module is what a tools/call carries.
|
|
type served struct {
|
|
in *io.PipeWriter
|
|
lines *bufio.Scanner
|
|
next int
|
|
}
|
|
|
|
func serve(t *testing.T, h *harness) *served {
|
|
t.Helper()
|
|
h.CallerOf = stdio.CallerOf
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
go func() { _ = stdio.ServeOn("mesh-issues", Tools(h.Tracker), inR, outW) }()
|
|
t.Cleanup(func() { _ = inW.Close() })
|
|
return &served{in: inW, lines: bufio.NewScanner(outR)}
|
|
}
|
|
|
|
// call sends one tools/call, with the runtime's `_meta` naming caller when one is given, and answers the
|
|
// verb's result or its refusal.
|
|
func (s *served) call(t *testing.T, verb, caller string, args map[string]any) (map[string]any, string) {
|
|
t.Helper()
|
|
s.next++
|
|
params := map[string]any{"name": Seat + "." + verb, "arguments": args}
|
|
if caller != "" {
|
|
params["_meta"] = map[string]any{stdio.MetaCaller: caller}
|
|
}
|
|
b, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": s.next, "method": "tools/call", "params": params})
|
|
_, _ = s.in.Write(append(b, '\n'))
|
|
done := make(chan []byte, 1)
|
|
go func() {
|
|
if s.lines.Scan() {
|
|
done <- append([]byte(nil), s.lines.Bytes()...)
|
|
}
|
|
}()
|
|
var line []byte
|
|
select {
|
|
case line = <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("%s was not answered", verb)
|
|
}
|
|
var m struct {
|
|
Result struct {
|
|
Content []struct {
|
|
Text string `json:"text"`
|
|
} `json:"content"`
|
|
IsError bool `json:"isError"`
|
|
} `json:"result"`
|
|
}
|
|
_ = json.Unmarshal(line, &m)
|
|
if len(m.Result.Content) == 0 {
|
|
t.Fatalf("%s was answered %s", verb, line)
|
|
}
|
|
text := m.Result.Content[0].Text
|
|
if m.Result.IsError {
|
|
return nil, text
|
|
}
|
|
var out map[string]any
|
|
_ = json.Unmarshal([]byte(text), &out)
|
|
return out, ""
|
|
}
|
|
|
|
// **Who acted is the caller the runtime proved** (novox/hq issue 365, ADR 0275 §5): a call whose subject named
|
|
// its caller is recorded as that bus user — on the issue it opened, the note, the link, the move and its event,
|
|
// the value set, and the number reserved.
|
|
func TestWhoActedIsTheCallerTheRuntimeProved(t *testing.T) {
|
|
h, _ := reserveHarness(t)
|
|
s := serve(t, h)
|
|
const who = "shanks.node-tools"
|
|
opened, refused := s.call(t, "open", who, map[string]any{"title": "a title", "report": "a report"})
|
|
if refused != "" || opened["by"] != who {
|
|
t.Fatalf("open answered %v %q, want by %s", opened, refused, who)
|
|
}
|
|
n := uint64(opened["number"].(float64))
|
|
other, _ := s.call(t, "open", who, map[string]any{"title": "another", "report": "another report"})
|
|
is, _ := h.Get(n)
|
|
if is.OpenedBy != who {
|
|
t.Errorf("the issue was opened by %q", is.OpenedBy)
|
|
}
|
|
if noted, refused := s.call(t, "note", who, map[string]any{"number": n, "text": "seen again"}); refused != "" || noted["by"] != who {
|
|
t.Errorf("note answered %v %q", noted, refused)
|
|
}
|
|
if linked, refused := s.call(t, "link", who, map[string]any{"number": n, "kind": "relates-to", "target": fmt.Sprint(other["number"])}); refused != "" || linked["by"] != who {
|
|
t.Errorf("link answered %v %q", linked, refused)
|
|
}
|
|
if _, refused := s.call(t, "set", who, map[string]any{"number": n, "severity": "minor"}); refused != "" {
|
|
t.Errorf("set refused: %s", refused)
|
|
}
|
|
if _, refused := s.call(t, "move", who, map[string]any{"number": n, "status": "fixing", "by": "fix-1@g14"}); refused != "" {
|
|
t.Errorf("move refused: %s", refused)
|
|
}
|
|
if reserved, refused := s.call(t, "reserve", who, map[string]any{"kind": "decision", "for": "a decision"}); refused != "" || reserved["by"] != who {
|
|
t.Errorf("reserve answered %v %q", reserved, refused)
|
|
}
|
|
is, _ = h.Get(n)
|
|
for _, note := range is.Notes {
|
|
if note.By != who {
|
|
t.Errorf("a note is by %q", note.By)
|
|
}
|
|
}
|
|
for _, e := range h.said() {
|
|
if e.Event == EventMoved && e.Body["caller"] != who {
|
|
t.Errorf("the move was said as by caller %v", e.Body["caller"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// A caller named in the arguments is refused, whatever it names — an argument is the caller's to write — and a
|
|
// call the runtime proved no caller for is recorded Unproven, as it always was.
|
|
func TestACallerInTheArgumentsIsRefusedAndAnUnprovenCallSaysSo(t *testing.T) {
|
|
h := newHarness(t)
|
|
s := serve(t, h)
|
|
for _, args := range []map[string]any{
|
|
{"title": "t", "report": "r", "caller": "novox.node-tools"},
|
|
{"title": "t", "report": "r", "_meta": map[string]any{stdio.MetaCaller: "novox.node-tools"}},
|
|
{"title": "t", "report": "r", "by": "novox.node-tools"},
|
|
} {
|
|
if out, refused := s.call(t, "open", "shanks.node-tools", args); refused == "" || !strings.Contains(refused, "does not take") {
|
|
t.Errorf("an open naming a caller in its arguments %v was answered %v", args, out)
|
|
}
|
|
}
|
|
opened, refused := s.call(t, "open", "", map[string]any{"title": "t", "report": "r"})
|
|
if refused != "" || opened["by"] != Unproven {
|
|
t.Fatalf("an open the runtime named no caller for answered %v %q, want by %s", opened, refused, Unproven)
|
|
}
|
|
}
|