PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
源码索引 / 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
	}
}