Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
package busstate
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
)
|
|
|
|
// serveRuntime runs a bundle whose tool `go` runs fn in a goroutine and answers every call the bundle makes through handle,
|
|
// until done is closed by fn returning.
|
|
func serveRuntime(t *testing.T, fn func(), handle func(method string, params map[string]any) any) {
|
|
t.Helper()
|
|
t.Setenv(stdio.ServedAs, "distribution")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = stdio.ServeOn("", []stdio.Tool{{Name: "go", Run: func(map[string]any) (any, error) {
|
|
go func() { fn(); close(done) }()
|
|
return "started", nil
|
|
}}}, inR, outW)
|
|
}()
|
|
t.Cleanup(func() { _ = inW.Close() })
|
|
lines := bufio.NewScanner(outR)
|
|
lines.Buffer(make([]byte, 0, 1<<20), 64<<20)
|
|
send := func(m map[string]any) { b, _ := json.Marshal(m); _, _ = inW.Write(append(b, '\n')) }
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "go"}})
|
|
deadline := time.After(10 * time.Second)
|
|
for {
|
|
got := make(chan map[string]any, 1)
|
|
go func() {
|
|
if lines.Scan() {
|
|
var m map[string]any
|
|
_ = json.Unmarshal(lines.Bytes(), &m)
|
|
got <- m
|
|
}
|
|
}()
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-deadline:
|
|
t.Fatal("the bundle did not finish")
|
|
case m := <-got:
|
|
method, _ := m["method"].(string)
|
|
if method == "" {
|
|
continue
|
|
}
|
|
p, _ := m["params"].(map[string]any)
|
|
send(map[string]any{"jsonrpc": "2.0", "id": m["id"], "result": handle(method, p)})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Design 47 G §518, §345, §490 (A-21): a value larger than one chunk is put as chunks in order under one call id, each
|
|
// answered before the next, with the store named in each; a get the runtime answers as a session is read chunk by chunk and
|
|
// checked against its digest.
|
|
func subAValueLargerThanOneChunkIsPutAndGotBySession(t *testing.T) {
|
|
value := bytes.Repeat([]byte("0123456789abcdef"), (2*buscalls.ChunkBytes+1000)/16)
|
|
sum := sha256.Sum256(value)
|
|
digest := "sha256:" + hex.EncodeToString(sum[:])
|
|
var stored []byte
|
|
var seqs []uint32
|
|
calls := map[string]bool{}
|
|
var gotValue []byte
|
|
var putErr, getErr, tamperErr error
|
|
var tampered bool
|
|
serveRuntime(t, func() {
|
|
o := ObjectsOf("incoming")
|
|
_, putErr = o.Put("big", value, "a big one")
|
|
d, err := o.Get("big")
|
|
gotValue, getErr = d.Data, err
|
|
tampered = true
|
|
_, tamperErr = o.Get("big")
|
|
}, func(method string, p map[string]any) any {
|
|
switch method {
|
|
case stdio.CapabilitiesCall:
|
|
return map[string]any{"words": []string{stdio.CapPutSessions}}
|
|
case buscalls.ObjectsPutChunk:
|
|
calls[p["call"].(string)] = true
|
|
if p["objects"] != "incoming" || p["name"] != "big" {
|
|
t.Errorf("a chunk named %v in %v", p["name"], p["objects"])
|
|
}
|
|
raw, _ := json.Marshal(p["data"])
|
|
var b []byte
|
|
_ = json.Unmarshal(raw, &b)
|
|
if (p["description"] == "a big one") != (p["seq"].(float64) == 1) {
|
|
t.Errorf("chunk %v carries the description %v: the first chunk alone does", p["seq"], p["description"])
|
|
}
|
|
stored = append(stored, b...)
|
|
seqs = append(seqs, uint32(p["seq"].(float64)))
|
|
if p["last"] == true {
|
|
return buscalls.ObjectInfo{Name: "big", Size: uint64(len(stored))}
|
|
}
|
|
return buscalls.ObjectsChunkTaken{Seq: uint32(p["seq"].(float64))}
|
|
case buscalls.ObjectsGet:
|
|
d := digest
|
|
if tampered {
|
|
d = "sha256:" + hex.EncodeToString(make([]byte, 32))
|
|
}
|
|
return map[string]any{"chunked": 3, "size": len(stored), "digest": d, "call": "g1"}
|
|
case buscalls.ObjectsGetChunk:
|
|
seq := int(p["seq"].(float64))
|
|
at := (seq - 1) * buscalls.ChunkBytes
|
|
return buscalls.ObjectsChunk{Seq: uint32(seq), Last: seq == 3, Data: stored[at:min(at+buscalls.ChunkBytes, len(stored))]}
|
|
}
|
|
t.Errorf("unexpected call %s", method)
|
|
return nil
|
|
})
|
|
if putErr != nil || getErr != nil {
|
|
t.Fatalf("put %v, get %v", putErr, getErr)
|
|
}
|
|
if len(seqs) != 3 || seqs[0] != 1 || seqs[1] != 2 || seqs[2] != 3 || len(calls) != 1 || !bytes.Equal(stored, value) || !bytes.Equal(gotValue, value) {
|
|
t.Errorf("chunks %v under %d call ids, stored %d bytes, got %d", seqs, len(calls), len(stored), len(gotValue))
|
|
}
|
|
if tamperErr == nil {
|
|
t.Error("a value that is not the one the runtime named by its digest was accepted")
|
|
}
|
|
}
|