yao/sui/test/context.go
Max a1e745ec90 chore(tests): update model versions in Anthropic tests and add Test command to SUI
- Updated testConnectorID comment and model version in TestAnthropicStreamRetry to reflect the latest model (Claude Haiku 4.5).
- Added Test command to the SUI command set with associated flags for improved testing capabilities.
2026-04-24 08:45:19 +08:00

206 lines
5.9 KiB
Go

package test
import (
"fmt"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/sui/core"
"rogchap.com/v8go"
)
// SUITestContext provides the test execution context for SUI backend scripts.
// It wraps a loaded Script and a mock Request, exposing call/callWithRequest
// to JS test functions.
type SUITestContext struct {
Script *core.Script
Request *core.Request
Prefix string
Sid string
}
// NewSUITestContext creates a new SUI test context for a page
func NewSUITestContext(script *core.Script, prefix string, sid string) *SUITestContext {
return &SUITestContext{
Script: script,
Prefix: prefix,
Sid: sid,
Request: &core.Request{
Method: "GET",
Sid: sid,
Payload: map[string]interface{}{},
Params: map[string]string{},
},
}
}
// Call invokes an Api-prefixed method on the backend script (sui.Run path).
// The JS function is looked up as <Prefix><method> (e.g. "ApiGetDashboard").
func (ctx *SUITestContext) Call(method string, args ...interface{}) (interface{}, error) {
scriptCtx, err := ctx.Script.NewContext(ctx.Sid, nil)
if err != nil {
return nil, err
}
defer scriptCtx.Close()
if ctx.Request.Authorized != nil {
scriptCtx.WithAuthorized(ctx.Request.Authorized)
}
fnName := ctx.Prefix + method
global := scriptCtx.Global()
if !global.Has(fnName) {
return nil, fmt.Errorf("method %s not found (looked for %s)", method, fnName)
}
return scriptCtx.Call(fnName, args...)
}
// CallWithRequest invokes a method on the backend script, appending *Request
// as the last argument (page-render @Method path).
func (ctx *SUITestContext) CallWithRequest(method string, args ...interface{}) (interface{}, error) {
return ctx.Script.Call(ctx.Request, method, args...)
}
// NewSUITestContextObject creates a JavaScript object exposing the SUITestContext to V8
func NewSUITestContextObject(v8ctx *v8go.Context, ctx *SUITestContext) (*v8go.Value, error) {
iso := v8ctx.Isolate()
tmpl := v8go.NewObjectTemplate(iso)
tmpl.Set("call", ctx.callMethod(iso, v8ctx))
tmpl.Set("callWithRequest", ctx.callWithRequestMethod(iso, v8ctx))
tmpl.Set("setAuthorized", ctx.setAuthorizedMethod(iso, v8ctx))
tmpl.Set("reset", ctx.resetMethod(iso))
instance, err := tmpl.NewInstance(v8ctx)
if err != nil {
return nil, err
}
obj, err := instance.Value.AsObject()
if err != nil {
return nil, err
}
reqObj, err := ctx.buildRequestObject(v8ctx)
if err != nil {
return nil, err
}
obj.Set("request", reqObj)
return instance.Value, nil
}
// callMethod implements ctx.call(method, ...args) in JS
func (ctx *SUITestContext) callMethod(iso *v8go.Isolate, v8ctx *v8go.Context) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
jsArgs := info.Args()
if len(jsArgs) < 1 {
throwJSError(v8ctx, "call requires at least 1 argument (method name)")
return v8go.Undefined(iso)
}
method := jsArgs[0].String()
goArgs := make([]interface{}, 0, len(jsArgs)-1)
for _, arg := range jsArgs[1:] {
val, err := bridge.GoValue(arg, v8ctx)
if err != nil {
goArgs = append(goArgs, arg.String())
continue
}
goArgs = append(goArgs, val)
}
result, err := ctx.Call(method, goArgs...)
if err != nil {
return bridge.JsException(v8ctx, err)
}
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, err)
}
return jsVal
})
}
// callWithRequestMethod implements ctx.callWithRequest(method, ...args) in JS
func (ctx *SUITestContext) callWithRequestMethod(iso *v8go.Isolate, v8ctx *v8go.Context) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
jsArgs := info.Args()
if len(jsArgs) < 1 {
throwJSError(v8ctx, "callWithRequest requires at least 1 argument (method name)")
return v8go.Undefined(iso)
}
method := jsArgs[0].String()
goArgs := make([]interface{}, 0, len(jsArgs)-1)
for _, arg := range jsArgs[1:] {
val, err := bridge.GoValue(arg, v8ctx)
if err != nil {
goArgs = append(goArgs, arg.String())
continue
}
goArgs = append(goArgs, val)
}
result, err := ctx.CallWithRequest(method, goArgs...)
if err != nil {
return bridge.JsException(v8ctx, err)
}
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, err)
}
return jsVal
})
}
// setAuthorizedMethod implements ctx.setAuthorized(auth) in JS
func (ctx *SUITestContext) setAuthorizedMethod(iso *v8go.Isolate, v8ctx *v8go.Context) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
jsArgs := info.Args()
if len(jsArgs) < 1 {
return v8go.Undefined(iso)
}
val, err := bridge.GoValue(jsArgs[0], v8ctx)
if err != nil {
return v8go.Undefined(iso)
}
if authMap, ok := val.(map[string]interface{}); ok {
ctx.Request.Authorized = authMap
}
return v8go.Undefined(iso)
})
}
// resetMethod implements ctx.reset() in JS — clears authorized, payload, params
func (ctx *SUITestContext) resetMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx.Request.Authorized = nil
ctx.Request.Payload = map[string]interface{}{}
ctx.Request.Params = map[string]string{}
ctx.Request.Query = nil
ctx.Request.Headers = nil
ctx.Request.Body = nil
return v8go.Undefined(iso)
})
}
// buildRequestObject creates a JS object representing ctx.request
func (ctx *SUITestContext) buildRequestObject(v8ctx *v8go.Context) (*v8go.Value, error) {
reqMap := map[string]interface{}{
"sid": ctx.Request.Sid,
"method": ctx.Request.Method,
"payload": ctx.Request.Payload,
"params": ctx.Request.Params,
"authorized": ctx.Request.Authorized,
}
return bridge.JsValue(v8ctx, reqMap)
}
func throwJSError(v8ctx *v8go.Context, msg string) {
bridge.JsException(v8ctx, fmt.Errorf("%s", msg))
}