mesh-issues: record who acted as the caller the runtime proves (hq issue 365)
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).
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,11 @@ type candidate struct {
|
||||
// number: create-if-absent on the key, the counter untouched. See the file's comment for what it never
|
||||
// does.
|
||||
func (t *Tracker) Import(a ImportAsk, records Records) (*Imported, error) {
|
||||
return t.importAs(t.by(), a, records)
|
||||
}
|
||||
|
||||
// importAs is Import as the one who acts.
|
||||
func (t *Tracker) importAs(by string, a ImportAsk, records Records) (*Imported, error) {
|
||||
settings, err := t.Settings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the settings cannot be read (%v); nothing was imported", err)
|
||||
@@ -241,7 +246,6 @@ func (t *Tracker) Import(a ImportAsk, records Records) (*Imported, error) {
|
||||
}
|
||||
out := &Imported{DryRun: a.DryRun, Commit: commit, ByStatus: map[string]int{}, Rows: []ImportRow{}}
|
||||
at := t.now()
|
||||
by := t.by()
|
||||
|
||||
// Every folder's number, and no two folders sharing one.
|
||||
var cands []*candidate
|
||||
|
||||
@@ -55,11 +55,12 @@ func oneOf(field, v string, allowed []string) (string, error) {
|
||||
return "", fmt.Errorf("%s %q is not one of %s", field, v, strings.Join(allowed, ", "))
|
||||
}
|
||||
|
||||
// Unproven is who acted, as recorded, while the runtime does not tell a module who called it: a tool call
|
||||
// reaches a bundle as a name and arguments only (mesh-sdk Tool.Run), and the tool runner is not told who
|
||||
// called it either (hq issue 340). The caller is never taken from an argument, so this is recorded rather
|
||||
// than a name anybody could type (novox/hq ADR 0275 §5).
|
||||
const Unproven = "unproven"
|
||||
// Unproven is who acted, as recorded, where the runtime proves no caller: a call on the subject that names none,
|
||||
// a runtime from before hq issue 365, or a public method called at the terminal. Otherwise who acted is the bus
|
||||
// user the runtime took from the subject the call arrived on (stdio.CallerOf). The caller is never taken from an
|
||||
// argument, so neither is a name anybody could type (novox/hq ADR 0275 §5). Mesh-sdk says the same word
|
||||
// (stdio.Unproven).
|
||||
const Unproven = stdio.Unproven
|
||||
|
||||
// Issue is one issue as the tracker keeps it.
|
||||
type Issue struct {
|
||||
@@ -122,7 +123,7 @@ const ImportedBy = "import"
|
||||
// Import is how an issue, or its status, came from hq.
|
||||
type Import struct {
|
||||
At time.Time `json:"at"`
|
||||
// By is who ran the import, as the runtime proves it (Unproven until it does).
|
||||
// By is who ran the import, as the runtime proves it (Unproven where it proves none).
|
||||
By string `json:"by"`
|
||||
Commit string `json:"commit"`
|
||||
// From is the document read, empty for a status filled without one.
|
||||
|
||||
@@ -87,6 +87,8 @@ func main() {
|
||||
Logf: logf,
|
||||
Sleep: time.Sleep,
|
||||
Judge: busJudgement(os.Getenv("MESH_NODE")),
|
||||
// Who called, from the runtime (hq issue 365): never from the arguments.
|
||||
CallerOf: stdio.CallerOf,
|
||||
}
|
||||
t.Notify = busNotifier()
|
||||
t.Deliveries = func(repository, number string) (Delivery, error) {
|
||||
@@ -340,7 +342,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if ask.Needs, err = a.str("needs"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Open(ask)
|
||||
return t.open(t.callerOf(m), ask)
|
||||
}},
|
||||
{Name: Seat + ".get",
|
||||
Description: "One issue whole, by its number: title, status, severity, what it needs, report, who opened it " +
|
||||
@@ -463,7 +465,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t.Move(ask)
|
||||
return t.move(t.callerOf(m), ask)
|
||||
}},
|
||||
{Name: Seat + ".note",
|
||||
Description: "Add a note to an issue: what was found, tried or decided, in plain words. A note is kept as " +
|
||||
@@ -486,7 +488,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Note(n, s)
|
||||
return t.note(t.callerOf(m), n, s)
|
||||
}},
|
||||
{Name: Seat + ".link",
|
||||
Description: "Link an issue to something, or with remove, take a link away. Kinds: duplicates or relates-to " +
|
||||
@@ -518,7 +520,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if ask.Remove, err = a.flag("remove"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Link(ask)
|
||||
return t.link(t.callerOf(m), ask)
|
||||
}},
|
||||
{Name: Seat + ".import",
|
||||
Description: "Import hq's issue folders (04-ISSUES/NNN-slug/00-report.md), read through the records module at " +
|
||||
@@ -548,7 +550,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if records == nil {
|
||||
records = busRecords{}
|
||||
}
|
||||
return t.Import(ask, records)
|
||||
return t.importAs(t.callerOf(m), ask, records)
|
||||
}},
|
||||
{Name: Seat + ".groom",
|
||||
Description: "The triage over the backlog: every issue not resolved and not yet triaged is judged, " +
|
||||
@@ -606,7 +608,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if ask.SuggestedOwner, err = a.str("suggested-owner"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Set(ask)
|
||||
return t.set(t.callerOf(m), ask, false)
|
||||
}},
|
||||
{Name: Seat + ".tracking",
|
||||
Description: "Which issues track which conditions: for every condition key an issue links (link kind condition), " +
|
||||
@@ -654,7 +656,7 @@ func Tools(t *Tracker) []stdio.Tool {
|
||||
if ask.For, err = a.str("for"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.Reserve(ask)
|
||||
return t.reserve(t.callerOf(m), ask)
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,10 @@ type Reserved struct {
|
||||
|
||||
// Reserve hands out the next number of a sequence and keeps who reserved it, when and for what. A refused
|
||||
// reserve spends no number; a number handed out is spent forever.
|
||||
func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) {
|
||||
func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) { return t.reserve(t.by(), a) }
|
||||
|
||||
// reserve is Reserve as the one who acts.
|
||||
func (t *Tracker) reserve(by string, a ReserveAsk) (*Reserved, error) {
|
||||
kind := a.Kind
|
||||
seq, ok := sequences[kind]
|
||||
if !ok {
|
||||
@@ -121,7 +124,7 @@ func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := Reservation{Kind: kind, Number: n, For: purpose, By: t.by(), At: t.now(), ID: newID()}
|
||||
r := Reservation{Kind: kind, Number: n, For: purpose, By: by, At: t.now(), ID: newID()}
|
||||
if err := t.keep(&r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ type Tracker struct {
|
||||
Deliveries DeliveryState
|
||||
// Notify tells the operator's channel of a stuck fix (ADR 0281); nil says nothing there.
|
||||
Notify Notifier
|
||||
groom grooming
|
||||
// CallerOf is who called, for the arguments one call was handed (stdio.CallerOf); nil proves nobody.
|
||||
CallerOf func(map[string]any) string
|
||||
groom grooming
|
||||
}
|
||||
|
||||
// The events this module says, one per kind of change.
|
||||
@@ -57,10 +59,22 @@ const (
|
||||
createAttempts = 3
|
||||
)
|
||||
|
||||
// by is who acted on this call. The runtime does not hand a module its caller, so it is Unproven; it is
|
||||
// never read from the call's arguments, so no caller can name another (ADR 0275 §5).
|
||||
// by is who acted where no call says it — a public method called at the terminal or in a test: Unproven.
|
||||
func (t *Tracker) by() string { return Unproven }
|
||||
|
||||
// callerOf is who acted on one call, as the runtime proves it: the bus user that published the call, which the
|
||||
// runtime takes from the subject it arrived on (hq issue 365, mesh-sdk stdio.CallerOf), or Unproven where the
|
||||
// runtime proves none. It is never read from the call's arguments, so no caller can name another (ADR 0275 §5).
|
||||
func (t *Tracker) callerOf(m map[string]any) string {
|
||||
if t.CallerOf == nil {
|
||||
return Unproven
|
||||
}
|
||||
if c := t.CallerOf(m); c != "" {
|
||||
return c
|
||||
}
|
||||
return Unproven
|
||||
}
|
||||
|
||||
func (t *Tracker) now() time.Time { return t.Now().UTC().Truncate(time.Second) }
|
||||
|
||||
func (t *Tracker) owners() (Owners, error) {
|
||||
@@ -108,7 +122,10 @@ type Opened struct {
|
||||
// Open checks an issue, then takes the next number and writes the issue under it (to-be 53 §4). A refused
|
||||
// issue takes no number. A number taken and then not written — the bus failing between the two — stays
|
||||
// unused, and the answer says which: numbers may skip, never repeat.
|
||||
func (t *Tracker) Open(a OpenAsk) (*Opened, error) {
|
||||
func (t *Tracker) Open(a OpenAsk) (*Opened, error) { return t.open(t.by(), a) }
|
||||
|
||||
// open is Open as the one who acts.
|
||||
func (t *Tracker) open(by string, a OpenAsk) (*Opened, error) {
|
||||
title, err := singleLine("title", a.Title, MostTitle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -143,7 +160,7 @@ func (t *Tracker) Open(a OpenAsk) (*Opened, error) {
|
||||
}
|
||||
}
|
||||
at := t.now()
|
||||
is := Issue{Title: title, Status: StatusOpen, Report: report, Opened: at, OpenedBy: t.by(), LocatedIn: located, Opening: newID(),
|
||||
is := Issue{Title: title, Status: StatusOpen, Report: report, Opened: at, OpenedBy: by, LocatedIn: located, Opening: newID(),
|
||||
Severity: severity, Needs: needs}
|
||||
|
||||
n, err := t.next()
|
||||
|
||||
@@ -7,9 +7,9 @@ package main
|
||||
// from a caller for `worker-stale-after` (24 hours) is stale: `list` says so, and it may be taken over without a
|
||||
// reason.
|
||||
//
|
||||
// `by` is what the worker says it is, not who called: the runtime does not yet hand a module its caller (hq
|
||||
// issue 340), so who acted stays recorded as the runtime proves it (Unproven), and the worker is a way for
|
||||
// agents and people to see each other, not a lock against someone giving a false name.
|
||||
// `by` is what the worker says it is, not who called: who acted is recorded beside it as the runtime proves it
|
||||
// (the caller's bus user, hq issue 365, or Unproven), and the worker is a way for agents and people to see each
|
||||
// other, not a lock against someone giving a false name.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
Reference in New Issue
Block a user