mesh/delivery delivered
mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery-group group adr-0307-state-history delivered: every member is delivered
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package stdio
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// historying serves a bundle whose one tool, "history", runs run, answering each mesh/state ask with
|
|
// answer beside the loop, as the runtime does; it returns what the bundle asked.
|
|
func historying(t *testing.T, answer func(message) map[string]any, run func()) []message {
|
|
t.Helper()
|
|
t.Setenv(ServedAs, "issues")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
go func() {
|
|
_ = ServeOn("", []Tool{{Name: "history", Run: func(map[string]any) (any, error) { run(); return "done", nil }}}, inR, outW)
|
|
}()
|
|
var wmu, amu sync.Mutex
|
|
var asked []message
|
|
send := func(m map[string]any) {
|
|
m["jsonrpc"] = "2.0"
|
|
b, _ := json.Marshal(m)
|
|
wmu.Lock()
|
|
defer wmu.Unlock()
|
|
_, _ = inW.Write(append(b, '\n'))
|
|
}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
lines := bufio.NewScanner(outR)
|
|
for lines.Scan() {
|
|
var m message
|
|
if json.Unmarshal(lines.Bytes(), &m) != nil {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(m.Method, "mesh/state.") {
|
|
amu.Lock()
|
|
asked = append(asked, m)
|
|
amu.Unlock()
|
|
a := answer(m)
|
|
a["id"] = m.ID
|
|
send(a)
|
|
continue
|
|
}
|
|
if m.Method == "" {
|
|
close(done)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
send(map[string]any{"id": 1, "method": "tools/call", "params": map[string]any{"name": "history"}})
|
|
t.Cleanup(func() { _ = inW.Close() })
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("the bundle did not answer")
|
|
}
|
|
amu.Lock()
|
|
defer amu.Unlock()
|
|
return asked
|
|
}
|
|
|
|
// History asks mesh/state.history with the state and the key alone, and hands back every kept value oldest
|
|
// first, a delete with its null value; nothing kept is an empty history; a refusal is the runtime's error
|
|
// (novox/hq ADR 0307).
|
|
func TestHistoryIsEveryKeptValueOldestFirst(t *testing.T) {
|
|
kept := []map[string]any{
|
|
{"key": "401", "value": map[string]any{"status": "open"}, "revision": 4, "op": "put", "at": "2026-10-10T19:46:00Z"},
|
|
{"key": "401", "value": map[string]any{"status": "fixing"}, "revision": 9, "op": "put", "at": "2026-10-11T08:01:02Z"},
|
|
{"key": "401", "value": nil, "revision": 12, "op": "delete", "at": "2026-10-11T09:30:00+02:00"},
|
|
}
|
|
var got, none, null []StateRevision
|
|
var refused error
|
|
asked := historying(t, func(m message) map[string]any {
|
|
var p map[string]any
|
|
_ = json.Unmarshal(m.Params, &p)
|
|
switch {
|
|
case p["state"] != "issues":
|
|
return map[string]any{"error": map[string]any{"code": -32000, "message": "issues was issued no state " + p["state"].(string)}}
|
|
case p["key"] == "401":
|
|
return map[string]any{"result": kept}
|
|
case p["key"] == "402":
|
|
return map[string]any{"result": nil}
|
|
}
|
|
return map[string]any{"result": []any{}}
|
|
}, func() {
|
|
var err error
|
|
if got, err = State("issues").History("401"); err != nil {
|
|
t.Errorf("history of 401: %v", err)
|
|
}
|
|
if none, err = State("issues").History("999"); err != nil {
|
|
t.Errorf("history of 999: %v", err)
|
|
}
|
|
if null, err = State("issues").History("402"); err != nil {
|
|
t.Errorf("history of 402: %v", err)
|
|
}
|
|
var h []StateRevision
|
|
h, refused = State("elsewhere.issues").History("401")
|
|
if h != nil {
|
|
t.Errorf("a refused history handed back %v", h)
|
|
}
|
|
})
|
|
|
|
if len(asked) != 4 {
|
|
t.Fatalf("asked %d times, not 4", len(asked))
|
|
}
|
|
var p map[string]any
|
|
_ = json.Unmarshal(asked[0].Params, &p)
|
|
if asked[0].Method != "mesh/state.history" || p["state"] != "issues" || p["key"] != "401" || len(p) != 2 {
|
|
t.Fatalf("asked %s %v", asked[0].Method, p)
|
|
}
|
|
|
|
if len(got) != 3 {
|
|
t.Fatalf("history of 401 is %d values, not 3: %+v", len(got), got)
|
|
}
|
|
want := []struct {
|
|
rev uint64
|
|
op string
|
|
value string
|
|
at time.Time
|
|
}{
|
|
{4, "put", `{"status":"open"}`, time.Date(2026, 10, 10, 19, 46, 0, 0, time.UTC)},
|
|
{9, "put", `{"status":"fixing"}`, time.Date(2026, 10, 11, 8, 1, 2, 0, time.UTC)},
|
|
{12, "delete", `null`, time.Date(2026, 10, 11, 7, 30, 0, 0, time.UTC)},
|
|
}
|
|
for i, w := range want {
|
|
r := got[i]
|
|
if r.Key != "401" || r.Revision != w.rev || r.Op != w.op || string(r.Value) != w.value || !r.At.Equal(w.at) {
|
|
t.Fatalf("value %d is %+v, not %+v", i, r, w)
|
|
}
|
|
}
|
|
if none == nil || len(none) != 0 || null == nil || len(null) != 0 {
|
|
t.Fatalf("nothing kept read as %v and %v, not an empty history", none, null)
|
|
}
|
|
if refused == nil || !strings.Contains(refused.Error(), "issued no state elsewhere.issues") {
|
|
t.Fatalf("a state not issued answered %v", refused)
|
|
}
|
|
}
|