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") } }