Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 35 additions & 52 deletions go/contract_test.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,46 @@
package cstx

import (
"encoding/json"
"reflect"
"sort"
"strings"
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"testing"
)

// The transport contract is owned by cstx.core.elements (Node/Edge pydantic
// models) and mirrored by codegen/generate_transport_ts.py. These tests pin
// the Go structs to the same field sets so the three languages cannot drift.
func jsonFieldNames(t *testing.T, value any) []string {
t.Helper()
typ := reflect.TypeOf(value)
var names []string
for i := 0; i < typ.NumField(); i++ {
tag := typ.Field(i).Tag.Get("json")
if tag == "" || tag == "-" {
continue
}
names = append(names, strings.Split(tag, ",")[0])
}
sort.Strings(names)
return names
}

func TestNodeMatchesTransportContract(t *testing.T) {
got := jsonFieldNames(t, Node{})
want := []string{"extras", "id", "model", "sources", "type", "value"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("node fields drifted from transport contract: got %v want %v", got, want)
// This gate prevents the SDK from quietly growing another public graph or
// repository model beside the generated protobuf package.
func TestNoDuplicatedPublicModelTypes(t *testing.T) {
banned := map[string]bool{
"Node": true, "Edge": true, "Relationship": true, "GraphStats": true,
"Delta": true, "ChangeSet": true, "Commit": true, "GraphDiff": true,
"History": true, "HistoryEntry": true, "RepositorySync": true,
"MissingPlan": true, "NodeFilter": true, "EdgeFilter": true,
"RelationshipFilter": true, "QueryOptions": true, "Config": true,
}
}

func TestEdgeMatchesTransportContract(t *testing.T) {
got := jsonFieldNames(t, Edge{})
want := []string{"attrs", "id", "relation_type", "source_id", "sources", "target_id"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("edge fields drifted from transport contract: got %v want %v", got, want)
}
}

func TestQueryOptionsMatchesRustTransportContract(t *testing.T) {
got := jsonFieldNames(t, QueryOptions{})
want := []string{"collection", "exclude_mask", "include_mask"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("query option fields drifted from Rust contract: got %v want %v", got, want)
}

payload, err := json.Marshal(QueryOptions{ExcludeMask: 5, IncludeMask: 8})
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatalf("marshal query options: %v", err)
}
var wire map[string]any
if err := json.Unmarshal(payload, &wire); err != nil {
t.Fatalf("unmarshal query options: %v", err)
t.Fatal(err)
}
if wire["exclude_mask"] != float64(5) || wire["include_mask"] != float64(8) {
t.Fatalf("query option values drifted from Rust contract: %s", payload)
for _, file := range files {
if filepath.Ext(file) != ".go" || filepath.Base(file) == "contract_test.go" {
continue
}
parsed, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", file, err)
}
for _, declaration := range parsed.Decls {
general, ok := declaration.(*ast.GenDecl)
if !ok || general.Tok != token.TYPE {
continue
}
for _, spec := range general.Specs {
name := spec.(*ast.TypeSpec).Name.Name
if banned[name] {
t.Fatalf("%s declares duplicated public model %s; use cstxproto.%s", file, name, name)
}
}
}
}
}
57 changes: 30 additions & 27 deletions go/cstx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,32 @@ import (
"context"
"errors"
"sync"

"github.com/chainreactors/libcstx/go/proto/cstxproto"
)

// Config configures an in-memory CSTX runtime.
type Config struct {
// ProjectID namespaces the runtime; defaults to "default".
ProjectID string
// CursorPageSize bounds incremental cursor materialization; defaults to
// DefaultCursorPageSize.
CursorPageSize int
type runtimeConfig struct {
projectID string
cursorPageSize int
// Which spelling of a node payload reads return. A program holding no
// generated type for a node needs the value spelling, so this has to
// survive normalization rather than being dropped here.
payloadFormat cstxproto.PayloadFormat
}

func (c Config) normalize() Config {
if c.ProjectID == "" {
c.ProjectID = "default"
func normalizeRuntimeConfig(value *cstxproto.RuntimeConfig) runtimeConfig {
config := runtimeConfig{projectID: "default", cursorPageSize: DefaultCursorPageSize}
if value == nil {
return config
}
if value.ProjectId != "" {
config.projectID = value.ProjectId
}
if c.CursorPageSize <= 0 {
c.CursorPageSize = DefaultCursorPageSize
if value.CursorPageSize > 0 {
config.cursorPageSize = int(value.CursorPageSize)
}
return c
config.payloadFormat = value.PayloadFormat
return config
}

// CSTX is the single owner of shared schema, graph, and repository
Expand All @@ -31,38 +38,34 @@ type CSTX struct {
eng engine
projectID string

// Schemas, Graph, and Repo are lightweight namespaces sharing
// Extensions, Graph, and Repo are lightweight namespaces sharing
// this runtime's state.
Schemas *Schemas
Graph *Graph
Repo *Repository
Raw *Raw
Extensions *Extensions
Graph *Graph
Repo *Repository

mu sync.Mutex
closed bool
}

// Open creates an in-memory native runtime.
func Open(ctx context.Context, config Config) (*CSTX, error) {
func Open(ctx context.Context, value *cstxproto.RuntimeConfig) (*CSTX, error) {
if err := contextError(ctx); err != nil {
return nil, err
}
config = config.normalize()
config := normalizeRuntimeConfig(value)
eng, err := newEngine(config)
if err != nil {
return nil, err
}
return wrapRuntime(eng, config.ProjectID), nil
return wrapRuntime(eng, config.projectID), nil
}

func wrapRuntime(eng engine, projectID string) *CSTX {
rt := &CSTX{eng: eng, projectID: projectID}
rt.Schemas = &Schemas{eng: eng}
rt.Extensions = &Extensions{eng: eng}
rt.Graph = &Graph{eng: eng}
rt.Repo = &Repository{eng: eng}
if raw, ok := eng.(rawEngine); ok {
rt.Raw = &Raw{eng: raw}
}
return rt
}

Expand Down Expand Up @@ -96,9 +99,9 @@ func (c *CSTX) Closed() bool {
}

// LastChange returns the IDs changed by the most recent committed mutation.
func (c *CSTX) LastChange(ctx context.Context) (ChangeSet, error) {
func (c *CSTX) LastChange(ctx context.Context) (*cstxproto.GraphChangeSet, error) {
if err := contextError(ctx); err != nil {
return ChangeSet{}, err
return nil, err
}
return c.eng.lastChange(ctx)
}
Loading
Loading