package bus_test import ( "encoding/base64" "encoding/json" "testing" "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-sdk/go/buscalls" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // Design 47 G A-249, ยง681: the runner asks a chunked verb with a call token of its own, receives the chunks the answer counts // (each acknowledged before the next) and answers the verb's own answer with the whole file, so the asker receives none. func TestAChunkedSeatAnswerIsReceivedByTheRunnerAndHandedWhole(t *testing.T) { url := mt.URL(t) holder := dial(t, url, "mesh-delivery-indexer", "anchor", "") asker := dial(t, url, "mesh-build-go", "anchor", "") raw, err := nats.Connect(url) if err != nil { t.Fatal(err) } t.Cleanup(raw.Close) file := make([]byte, buscalls.SeatChunkBytes*2+5) for i := range file { file[i] = byte(i) } // the holder's process: answers the count, then sends each chunk and waits for its acknowledgement stop, err := holder.Handle("seat:mesh-catalog.input", func(body json.RawMessage) (any, error) { var in struct{ Call, Digest string } if err := json.Unmarshal(body, &in); err != nil || in.Call == "" { t.Errorf("the verb was asked with no call token: %s (%v)", body, err) return nil, err } send := buscalls.SeatChunks{Seat: "mesh-catalog", Verb: "input", Call: in.Call} go func() { for n := int64(1); n <= 3; n++ { end := min(int64(len(file)), n*buscalls.SeatChunkBytes) data, _ := json.Marshal(buscalls.SeatChunk{N: n, Of: 3, Data: file[(n-1)*buscalls.SeatChunkBytes : end]}) ackSub, _ := raw.SubscribeSync(send.Ack(n)) _ = raw.Flush() if err := raw.Publish(send.Chunk(n), data); err != nil { return } m, err := ackSub.NextMsg(buscalls.SeatAckWithin) if err != nil { t.Errorf("chunk %d not acknowledged: %v", n, err) return } _ = m.Respond([]byte("{}")) } }() return map[string]any{"digest": in.Digest, "chunks": 3}, nil }) if err != nil { t.Fatal(err) } defer stop() holder.Flush() // the bus holds the verb's subscription before it is asked: the asker is another connection got, err := asker.AskAsChunked("mesh-build-go", "seat:mesh-catalog.input", map[string]any{"digest": "d1"}) if err != nil { t.Fatal(err) } var ans struct { Digest string `json:"digest"` Chunks int `json:"chunks"` File string `json:"file"` } if err := json.Unmarshal(got.Result, &ans); err != nil || ans.Digest != "d1" || ans.Chunks != 3 { t.Fatalf("the answer %s: %v", got.Result, err) } if body, err := base64.StdEncoding.DecodeString(ans.File); err != nil || string(body) != string(file) { t.Errorf("the file handed is %d bytes, want the %d sent: %v", len(body), len(file), err) } // Design 47 G A-257, A-254: a bundle answers a chunked verb with the whole file in the member `file`; the runner that serves the // verb sends the file in chunks (each acknowledged before the next) and answers the count, so the bundle sends none, and the asker // of one node's verb receives it whole. t.Run("ABundlesChunkedAnswerIsSentByTheRunner", func(t *testing.T) { url := mt.URL(t) holder := dial(t, url, "mesh-build-go", "anchor", "") asker := dial(t, url, "mesh-delivery-indexer", "anchor", "") file := make([]byte, buscalls.SeatChunkBytes*2+7) for i := range file { file[i] = byte(i * 7) } stop, err := holder.Handle("seat:node-build-go.output@anchor", func(body json.RawMessage) (any, error) { answer := map[string]any{"ask": "a1", "file": base64.StdEncoding.EncodeToString(file)} return holder.AnswerChunked("node-build-go", "output", body, answer) }) if err != nil { t.Fatal(err) } defer stop() holder.Flush() // the bus holds the verb's subscription before it is asked: the asker is another connection got, err := asker.AskAsChunked("mesh-delivery-indexer", "seat:node-build-go.output@anchor", map[string]any{"ask": "a1"}) if err != nil { t.Fatal(err) } var ans struct { Ask string `json:"ask"` Chunks int `json:"chunks"` File string `json:"file"` } if err := json.Unmarshal(got.Result, &ans); err != nil || ans.Ask != "a1" || ans.Chunks != 3 { t.Fatalf("the answer %s: %v", got.Result, err) } if body, err := base64.StdEncoding.DecodeString(ans.File); err != nil || string(body) != string(file) { t.Errorf("the file handed is %d bytes, want the %d sent: %v", len(body), len(file), err) } // an answer with no file is the verb's own: no chunks are sent plain, err := holder.AnswerChunked("node-build-go", "output", json.RawMessage(`{"call":"c"}`), map[string]any{"ask": "x", "chunks": 0}) if m, _ := plain.(map[string]any); err != nil || m["chunks"] != 0 { t.Fatalf("an answer with no file was %v, %v", plain, err) } }) }