mirror of
https://github.com/hanzoai/team-go.git
synced 2026-10-30 20:22:42 +00:00
The model was compiled in with go:embed, so every clone and every binary carried a 1.9MB artifact this repository does not own, under a licence that needed its own carve-out in LICENSE, its own section in NOTICE, and a LICENSE-EPL beside them. It is now read at start from model.json in the data directory. A deployment that has the artifact supplies it; this repository redistributes nothing. Absent is not an error -- loadModel returns nil for a file that is not there and buildHierarchy already answered an empty hierarchy for input it cannot parse -- so a server without one starts and serves an empty model rather than refusing to boot over a file it cannot carry. The model moves onto the server, which is where a value read at Register belongs; sessions reach it through the server they already hold. Tests carry their own small model, the ten classes they exercise, hand-written: they never depended on its contents, only that the model served is the model held. LICENSE is MIT OR Apache-2.0 with no exception. NOTICE lists dependencies only.
160 lines
5.4 KiB
Go
160 lines
5.4 KiB
Go
package transactor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/team/pkg/model"
|
|
)
|
|
|
|
func testSession() *session {
|
|
dir, _ := os.MkdirTemp("", "team-test")
|
|
return &session{
|
|
server: &server{hub: newHub(), model: testModel, modelHash: hashModel(testModel)},
|
|
store: newStore(dir),
|
|
hier: buildHierarchy(testModel),
|
|
account: "2d4d67ab-30f1-474e-b81f-f60461852259",
|
|
workspace: "e48f81fd-12be-4bcd-aecb-3eaa9a9b5b18",
|
|
}
|
|
}
|
|
|
|
// TestHelloNegotiatesJSON is the load-bearing no-msgpack assertion: the server
|
|
// answers hello with binary=false, so the client serializes JSON for the rest
|
|
// of the session and msgpack is never used.
|
|
func TestHelloNegotiatesJSON(t *testing.T) {
|
|
out := testSession().handle([]byte(`{"id":-1,"method":"hello","params":[]}`))
|
|
var r map[string]any
|
|
if err := json.Unmarshal(out, &r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r["result"] != "hello" {
|
|
t.Fatalf("result = %v", r["result"])
|
|
}
|
|
if r["binary"] != false {
|
|
t.Fatalf("binary = %v (msgpack must be off)", r["binary"])
|
|
}
|
|
if r["useCompression"] != false {
|
|
t.Fatalf("useCompression = %v", r["useCompression"])
|
|
}
|
|
if r["lastHash"] != hashModel(testModel) {
|
|
t.Fatalf("lastHash = %v", r["lastHash"])
|
|
}
|
|
acc, ok := r["account"].(map[string]any)
|
|
if !ok || acc["uuid"] != "2d4d67ab-30f1-474e-b81f-f60461852259" {
|
|
t.Fatalf("account = %v", r["account"])
|
|
}
|
|
}
|
|
|
|
// TestHelloServesModelVersion is the decouple guard: the front's version
|
|
// handshake reads serverVersion, which MUST be the MODEL version
|
|
// (model.Version) — NOT TEAM_VERSION. Default is the front's 0.6.0.
|
|
func TestHelloServesModelVersion(t *testing.T) {
|
|
out := testSession().handle([]byte(`{"id":-1,"method":"hello","params":[]}`))
|
|
var r map[string]any
|
|
if err := json.Unmarshal(out, &r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r["serverVersion"] != model.Version() {
|
|
t.Fatalf("serverVersion = %v, want model.Version() = %v", r["serverVersion"], model.Version())
|
|
}
|
|
if r["serverVersion"] != model.DefaultVersion {
|
|
t.Fatalf("serverVersion = %v, want default %v", r["serverVersion"], model.DefaultVersion)
|
|
}
|
|
}
|
|
|
|
// TestLoadModelServesFullModel proves the entire embedded model bundle is
|
|
// returned in full so the client can build its hierarchy.
|
|
func TestLoadModelServesFullModel(t *testing.T) {
|
|
out := testSession().handle([]byte(`{"id":1,"method":"loadModel","params":[0]}`))
|
|
var r struct {
|
|
ID int64 `json:"id"`
|
|
Result struct {
|
|
Full bool `json:"full"`
|
|
Hash string `json:"hash"`
|
|
Transactions []json.RawMessage `json:"transactions"`
|
|
} `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(out, &r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.ID != 1 || !r.Result.Full {
|
|
t.Fatalf("id=%d full=%v", r.ID, r.Result.Full)
|
|
}
|
|
// Assert against the embedded bundle's own element count so a model-bundle
|
|
// refresh (e.g. a new plugin/mixin) can never silently drift this test.
|
|
var embedded []json.RawMessage
|
|
if err := json.Unmarshal(testModel, &embedded); err != nil {
|
|
t.Fatalf("embedded model.json is not a Tx array: %v", err)
|
|
}
|
|
if len(r.Result.Transactions) != len(embedded) {
|
|
t.Fatalf("transactions = %d, want %d (full embedded model)",
|
|
len(r.Result.Transactions), len(embedded))
|
|
}
|
|
if r.Result.Hash != hashModel(testModel) {
|
|
t.Fatalf("hash mismatch")
|
|
}
|
|
}
|
|
|
|
func TestHandleMisc(t *testing.T) {
|
|
s := testSession()
|
|
if string(s.handle([]byte("ping"))) != "pong!" {
|
|
t.Fatal("ping must answer pong!")
|
|
}
|
|
// findAll over an empty workspace → an empty TotalArray (the wire shape the
|
|
// client's rpc reviver turns back into a FindResult).
|
|
var fa struct {
|
|
Result struct {
|
|
DataType string `json:"dataType"`
|
|
Total int `json:"total"`
|
|
Value []json.RawMessage `json:"value"`
|
|
} `json:"result"`
|
|
}
|
|
json.Unmarshal(s.handle([]byte(`{"id":2,"method":"findAll","params":["core:class:Doc",{}]}`)), &fa)
|
|
if fa.Result.DataType != "TotalArray" || fa.Result.Value == nil || len(fa.Result.Value) != 0 {
|
|
t.Fatalf("findAll result = %+v, want empty TotalArray", fa.Result)
|
|
}
|
|
// domainRequest(communication) → a well-formed DomainResult so .value is never null.
|
|
var dr struct {
|
|
Result struct {
|
|
Domain string `json:"domain"`
|
|
} `json:"result"`
|
|
}
|
|
json.Unmarshal(s.handle([]byte(`{"id":3,"method":"domainRequest","params":["communication",{"findLabels":{}}]}`)), &dr)
|
|
if dr.Result.Domain != "communication" {
|
|
t.Fatalf("domainRequest domain = %q", dr.Result.Domain)
|
|
}
|
|
// tx → object ack, correlated id
|
|
var tx struct {
|
|
ID int64 `json:"id"`
|
|
Result map[string]any `json:"result"`
|
|
}
|
|
json.Unmarshal(s.handle([]byte(`{"id":9,"method":"tx","params":[{}]}`)), &tx)
|
|
if tx.ID != 9 || tx.Result == nil {
|
|
t.Fatalf("tx reply = %+v", tx)
|
|
}
|
|
}
|
|
|
|
// TestEnvelopeWrapsRPC is the end-to-end transport check: a request wrapped in a
|
|
// ZAP envelope decodes, dispatches, and the reply re-wraps with the same id.
|
|
func TestEnvelopeWrapsRPC(t *testing.T) {
|
|
req := Encode(Envelope{ID: 42, Kind: KindRequest, Payload: []byte(`{"id":42,"method":"hello","params":[]}`)})
|
|
env, err := Decode(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reply := testSession().handle(env.Payload)
|
|
out := Encode(Envelope{ID: env.ID, Kind: KindResponse, Payload: reply})
|
|
back, err := Decode(out)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back.ID != 42 || back.Kind != KindResponse {
|
|
t.Fatalf("reply envelope id=%d kind=%d", back.ID, back.Kind)
|
|
}
|
|
var r map[string]any
|
|
if err := json.Unmarshal(back.Payload, &r); err != nil || r["result"] != "hello" {
|
|
t.Fatalf("reply payload = %s", back.Payload)
|
|
}
|
|
}
|