源码索引 / CURRENT IMPLEMENTATION
cmd/trade-demo/main_test.go
191 行 · 点击行号可定位;函数链接随声明位置更新,不手填行号。
跳到函数 / 声明
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"strings"
"testing"
"time"
spine "pysolate-spine"
)
func testRunner(t *testing.T, m *market, manifest spine.Manifest) *spine.Runner {
t.Helper()
wasm, err := os.ReadFile("../../dist/spine.wasm")
if err != nil {
t.Fatal(err)
}
if manifest == nil {
manifest = m.tools()
}
r, err := spine.New(context.Background(), wasm, manifest)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := r.Close(context.Background()); err != nil {
t.Error(err)
}
})
return r
}
func inputs(limit, quantity int) map[string]any {
return map[string]any{"symbol": "ACME", "limit": limit, "quantity": quantity, "approved": true}
}
func TestTradingControlFlowAndHostGate(t *testing.T) {
code, err := os.ReadFile("../../examples/trading.py")
if err != nil {
t.Fatal(err)
}
m := newMarket("ACME", 100, 2000, false, 0, newTrace(io.Discard))
r := testRunner(t, m, nil)
cases := []struct {
name string
limit, cash, quantity int
allow bool
decision string
attempts, orders, remaining int
}{
{"price skip", 90, 2000, 10, true, "hold", 0, 0, 2000},
{"cash skip", 110, 500, 10, true, "hold", 0, 0, 500},
{"host denial despite approved input", 110, 2000, 10, false, "blocked", 1, 0, 2000},
{"approved simulation", 110, 2000, 10, true, "bought", 1, 1, 1000},
{"host validates bad quantity", 110, 2000, -1, true, "blocked", 1, 0, 2000},
}
for _, mode := range []string{"normal", "plm", "prefix"} {
for _, tc := range cases {
t.Run(mode+"/"+tc.name, func(t *testing.T) {
m.mu.Lock()
m.initialCash = tc.cash
m.remainingCash = tc.cash
m.allowOrder = tc.allow
m.attempts = 0
m.orders = nil
m.calls = map[string]int{}
m.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var out spine.Output
var err error
if mode == "prefix" {
ch := make(chan string, 1)
ch <- string(code)
close(ch)
out, err = r.RunPrefix(ctx, ch, inputs(tc.limit, tc.quantity))
} else if mode == "plm" {
out, err = r.RunPLM(ctx, string(code), inputs(tc.limit, tc.quantity))
} else {
out, err = r.Run(ctx, string(code), inputs(tc.limit, tc.quantity))
}
if err != nil {
t.Fatal(err)
}
var got struct {
Decision string `json:"decision"`
}
if err = json.Unmarshal(out.Value, &got); err != nil {
t.Fatal(err)
}
if got.Decision != tc.decision || m.attempts != tc.attempts || len(m.orders) != tc.orders || m.remainingCash != tc.remaining {
t.Fatalf("result=%s attempts=%d orders=%v remaining=%d", out.Value, m.attempts, m.orders, m.remainingCash)
}
if m.calls["quote"] != 1 || m.calls["cash"] != 1 {
t.Fatalf("duplicated snapshot reads: %v", m.calls)
}
if mode != "normal" && out.Transformed == "" {
t.Fatal("expected actual AST rewrite")
}
})
}
}
}
func TestTradingPrefixReadsBeforeEOFWithoutBuying(t *testing.T) {
code, err := os.ReadFile("../../examples/trading.py")
if err != nil {
t.Fatal(err)
}
m := newMarket("ACME", 100, 2000, true, 0, newTrace(io.Discard))
started := make(chan string, 2)
manifest := m.tools()
for _, name := range []string{"quote", "cash"} {
name, spec := name, manifest[name]
original := spec.Call
spec.Call = func(ctx context.Context, args json.RawMessage) (any, error) {
started <- name
return original(ctx, args)
}
manifest[name] = spec
}
r := testRunner(t, m, manifest)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ch := make(chan string, 1)
ch <- string(code)
done := make(chan error, 1)
go func() { _, err := r.RunPrefix(ctx, ch, inputs(110, 10)); done <- err }()
for i := 0; i < 2; i++ {
select {
case <-started:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
m.mu.Lock()
attempts, orders := m.attempts, len(m.orders)
m.mu.Unlock()
if attempts != 0 || orders != 0 {
t.Fatal("buy occurred before source EOF")
}
close(ch)
if err = <-done; err != nil {
t.Fatal(err)
}
if m.attempts != 1 || len(m.orders) != 1 {
t.Fatalf("expected one post-EOF order: %d %v", m.attempts, m.orders)
}
}
func TestTradingBuyCannotBePreparedOrSelfAuthorized(t *testing.T) {
m := newMarket("ACME", 100, 2000, true, 0, newTrace(io.Discard))
r := testRunner(t, m, nil)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
source := `import _spine, json
result = _spine.prepare(json.dumps({"tool":"buy","args":{"symbol":"ACME","quantity":1}}))`
out, err := r.RunPLM(ctx, source, nil)
if err != nil || string(out.Value) != "0" || m.attempts != 0 {
t.Fatalf("buy prepare: %s %v %d", out.Value, err, m.attempts)
}
m.allowOrder = false
_, err = r.RunPLM(ctx, `result=buy(symbol="ACME",quantity=1)`, map[string]any{"approved": true})
if err == nil || !strings.Contains(err.Error(), "Host -allow-order") || len(m.orders) != 0 {
t.Fatalf("unauthorized direct order: %v %v", err, m.orders)
}
}
func TestTraceRecordsRealToolBoundaries(t *testing.T) {
var logs bytes.Buffer
m := newMarket("ACME", 100, 2000, false, 0, newTrace(&logs))
_, err := m.tools()["buy"].Call(context.Background(), json.RawMessage(`{"symbol":"ACME","quantity":1}`))
if err == nil {
t.Fatal("expected denial")
}
dec := json.NewDecoder(&logs)
expected := []string{"tool.start", "order.denied", "tool.error"}
previous := 0.0
for i, kind := range expected {
var e event
if err := dec.Decode(&e); err != nil {
t.Fatal(err)
}
if e.Event != kind || e.Sequence != i+1 || e.ElapsedMS < previous {
t.Fatalf("event: %+v", e)
}
if _, err := time.Parse(time.RFC3339Nano, e.Time); err != nil {
t.Fatal(err)
}
previous = e.ElapsedMS
}
}