- Added support for tool call responses in the agent's response structure, allowing for better handling of tool execution results. - Updated the `TurnResult` and `TurnResponse` types to include full agent responses, including tool call details and next hook data. - Improved dynamic test execution by ensuring consistent chat session state across turns, enhancing the overall testing framework's capabilities. - Enhanced documentation in README.md to reflect changes in response structure and dynamic testing output format.
724 lines
19 KiB
Go
724 lines
19 KiB
Go
package test
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/yaoapp/gou/application"
|
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
|
"github.com/yaoapp/yao/agent/context"
|
|
"rogchap.com/v8go"
|
|
)
|
|
|
|
// HookExecutor executes before/after scripts from *_test.ts files
|
|
// Scripts are loaded via V8 and executed directly, not via Process()
|
|
type HookExecutor struct {
|
|
verbose bool
|
|
output *OutputWriter
|
|
loadedDirs map[string]bool // Track which directories have been loaded
|
|
agentContext *context.Context
|
|
opts *Options // Test options (includes ContextData from --ctx)
|
|
}
|
|
|
|
// NewHookExecutor creates a new hook executor
|
|
func NewHookExecutor(verbose bool) *HookExecutor {
|
|
return &HookExecutor{
|
|
verbose: verbose,
|
|
output: NewOutputWriter(verbose),
|
|
loadedDirs: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// SetAgentContext sets the agent context for script execution
|
|
func (h *HookExecutor) SetAgentContext(ctx *context.Context) {
|
|
h.agentContext = ctx
|
|
}
|
|
|
|
// SetOptions sets the test options for hook execution
|
|
func (h *HookExecutor) SetOptions(opts *Options) {
|
|
h.opts = opts
|
|
}
|
|
|
|
// HookRef represents a parsed hook reference
|
|
// Format: "src/env_test.ts:Before" or just "Before" (uses default test file)
|
|
type HookRef struct {
|
|
ScriptFile string // e.g., "env_test.ts"
|
|
Function string // e.g., "Before"
|
|
}
|
|
|
|
// ParseHookRef parses a hook reference string
|
|
// Formats:
|
|
// - "Before" -> uses first *_test.ts file found
|
|
// - "env_test.Before" -> uses src/env_test.ts
|
|
// - "src/env_test.Before" -> uses src/env_test.ts
|
|
func ParseHookRef(ref string) (*HookRef, error) {
|
|
if ref == "" {
|
|
return nil, fmt.Errorf("empty hook reference")
|
|
}
|
|
|
|
// Split by last dot to get function name
|
|
lastDot := strings.LastIndex(ref, ".")
|
|
if lastDot == -1 {
|
|
// Just function name, will use default test file
|
|
return &HookRef{
|
|
ScriptFile: "", // Will be resolved later
|
|
Function: ref,
|
|
}, nil
|
|
}
|
|
|
|
scriptPart := ref[:lastDot]
|
|
funcName := ref[lastDot+1:]
|
|
|
|
// Normalize script file name
|
|
scriptFile := scriptPart
|
|
if !strings.HasSuffix(scriptFile, "_test") {
|
|
scriptFile += "_test"
|
|
}
|
|
scriptFile += ".ts"
|
|
|
|
// Remove "src/" prefix if present
|
|
scriptFile = strings.TrimPrefix(scriptFile, "src/")
|
|
|
|
return &HookRef{
|
|
ScriptFile: scriptFile,
|
|
Function: funcName,
|
|
}, nil
|
|
}
|
|
|
|
// LoadTestScripts loads all *_test.ts scripts from the agent's src directory
|
|
// Returns the script IDs that were loaded
|
|
func (h *HookExecutor) LoadTestScripts(agentPath string) ([]string, error) {
|
|
srcDir := filepath.Join(agentPath, "src")
|
|
|
|
// Convert to relative path for application.App
|
|
// application.App expects paths relative to YAO_ROOT
|
|
relSrcDir := srcDir
|
|
if application.App != nil {
|
|
if rel, err := filepath.Rel(application.App.Root(), srcDir); err == nil {
|
|
relSrcDir = rel
|
|
}
|
|
}
|
|
|
|
// Check if already loaded (use absolute path as key)
|
|
// No logging for already loaded - this is normal and happens frequently
|
|
if h.loadedDirs[srcDir] {
|
|
return nil, nil
|
|
}
|
|
|
|
// Check if src directory exists
|
|
exists, err := application.App.Exists(relSrcDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, nil // No src directory, not an error
|
|
}
|
|
|
|
var loadedScripts []string
|
|
exts := []string{"*_test.ts", "*_test.js"}
|
|
|
|
err = application.App.Walk(relSrcDir, func(root, file string, isdir bool) error {
|
|
if isdir {
|
|
return nil
|
|
}
|
|
|
|
// Only load *_test.ts/js files
|
|
base := filepath.Base(file)
|
|
if !strings.HasSuffix(base, "_test.ts") && !strings.HasSuffix(base, "_test.js") {
|
|
return nil
|
|
}
|
|
|
|
// Generate script ID (use relative path for consistency)
|
|
scriptID := generateHookScriptID(file, relSrcDir)
|
|
|
|
// Load the script (file path from Walk is relative to App root)
|
|
_, err := v8.Load(file, scriptID)
|
|
if err != nil {
|
|
if h.verbose {
|
|
h.output.Warning("Failed to load hook script %s: %v", base, err)
|
|
}
|
|
return nil // Continue loading other scripts
|
|
}
|
|
|
|
loadedScripts = append(loadedScripts, scriptID)
|
|
return nil
|
|
}, exts...)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to walk src directory: %w", err)
|
|
}
|
|
|
|
h.loadedDirs[srcDir] = true
|
|
|
|
// Log summary only once when scripts are first loaded
|
|
if h.verbose && len(loadedScripts) > 0 {
|
|
h.output.Verbose("Loaded %d hook scripts from %s", len(loadedScripts), relSrcDir)
|
|
}
|
|
|
|
return loadedScripts, nil
|
|
}
|
|
|
|
// generateHookScriptID generates a script ID for hook scripts
|
|
// Example: assistants/test/src/env_test.ts -> hook.env_test
|
|
func generateHookScriptID(filePath string, srcDir string) string {
|
|
filePath = filepath.ToSlash(filePath)
|
|
srcDir = filepath.ToSlash(srcDir)
|
|
|
|
relPath := strings.TrimPrefix(filePath, srcDir+"/")
|
|
relPath = strings.TrimPrefix(relPath, "/")
|
|
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
|
|
|
return "hook." + strings.ReplaceAll(relPath, "/", ".")
|
|
}
|
|
|
|
// FindTestScript finds a loaded test script by pattern
|
|
// If scriptFile is empty, returns the first *_test script found
|
|
func (h *HookExecutor) FindTestScript(scriptFile string) (*v8.Script, string, error) {
|
|
if scriptFile != "" {
|
|
// Look for specific script
|
|
scriptID := "hook." + strings.TrimSuffix(scriptFile, ".ts")
|
|
scriptID = strings.TrimSuffix(scriptID, ".js")
|
|
|
|
if script, ok := v8.Scripts[scriptID]; ok {
|
|
return script, scriptID, nil
|
|
}
|
|
return nil, "", fmt.Errorf("hook script not found: %s (id: %s)", scriptFile, scriptID)
|
|
}
|
|
|
|
// Find first *_test script
|
|
for id, script := range v8.Scripts {
|
|
if strings.HasPrefix(id, "hook.") && strings.Contains(id, "_test") {
|
|
return script, id, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("no hook test script found")
|
|
}
|
|
|
|
// ExecuteBefore executes a Before function from a test script
|
|
func (h *HookExecutor) ExecuteBefore(ref string, testCase *Case, agentPath string) (interface{}, error) {
|
|
hookRef, err := ParseHookRef(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure scripts are loaded
|
|
if _, err := h.LoadTestScripts(agentPath); err != nil {
|
|
return nil, fmt.Errorf("failed to load test scripts: %w", err)
|
|
}
|
|
|
|
// Find the script
|
|
script, scriptID, err := h.FindTestScript(hookRef.ScriptFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if h.verbose {
|
|
h.output.Verbose("Executing %s from %s", hookRef.Function, scriptID)
|
|
}
|
|
|
|
// Execute the function
|
|
return h.executeHookFunction(script, hookRef.Function, testCase, nil, nil)
|
|
}
|
|
|
|
// ExecuteAfter executes an After function from a test script
|
|
func (h *HookExecutor) ExecuteAfter(ref string, testCase *Case, result *Result, beforeData interface{}, agentPath string) error {
|
|
hookRef, err := ParseHookRef(ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure scripts are loaded
|
|
if _, err := h.LoadTestScripts(agentPath); err != nil {
|
|
return fmt.Errorf("failed to load test scripts: %w", err)
|
|
}
|
|
|
|
// Find the script
|
|
script, scriptID, err := h.FindTestScript(hookRef.ScriptFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if h.verbose {
|
|
h.output.Verbose("Executing %s from %s", hookRef.Function, scriptID)
|
|
}
|
|
|
|
// Execute the function
|
|
_, err = h.executeHookFunction(script, hookRef.Function, testCase, result, beforeData)
|
|
return err
|
|
}
|
|
|
|
// ExecuteBeforeAll executes a BeforeAll function
|
|
func (h *HookExecutor) ExecuteBeforeAll(ref string, testCases []*Case, agentPath string) (interface{}, error) {
|
|
hookRef, err := ParseHookRef(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure scripts are loaded
|
|
if _, err := h.LoadTestScripts(agentPath); err != nil {
|
|
return nil, fmt.Errorf("failed to load test scripts: %w", err)
|
|
}
|
|
|
|
// Find the script
|
|
script, scriptID, err := h.FindTestScript(hookRef.ScriptFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if h.verbose {
|
|
h.output.Verbose("Executing %s from %s", hookRef.Function, scriptID)
|
|
}
|
|
|
|
// Execute with test cases array
|
|
return h.executeHookFunctionWithCases(script, hookRef.Function, testCases)
|
|
}
|
|
|
|
// ExecuteAfterAll executes an AfterAll function
|
|
func (h *HookExecutor) ExecuteAfterAll(ref string, results []*Result, beforeData interface{}, agentPath string) error {
|
|
hookRef, err := ParseHookRef(ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure scripts are loaded
|
|
if _, err := h.LoadTestScripts(agentPath); err != nil {
|
|
return fmt.Errorf("failed to load test scripts: %w", err)
|
|
}
|
|
|
|
// Find the script
|
|
script, scriptID, err := h.FindTestScript(hookRef.ScriptFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if h.verbose {
|
|
h.output.Verbose("Executing %s from %s", hookRef.Function, scriptID)
|
|
}
|
|
|
|
// Execute with results array
|
|
_, err = h.executeHookFunctionWithResults(script, hookRef.Function, results, beforeData)
|
|
return err
|
|
}
|
|
|
|
// executeHookFunction executes a hook function with test case context
|
|
func (h *HookExecutor) executeHookFunction(script *v8.Script, funcName string, testCase *Case, result *Result, beforeData interface{}) (interface{}, error) {
|
|
// Create script context
|
|
scriptCtx, err := script.NewContext("", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create script context: %w", err)
|
|
}
|
|
defer scriptCtx.Close()
|
|
|
|
v8ctx := scriptCtx.Context
|
|
|
|
// Set share data
|
|
if err := h.setShareData(v8ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the function
|
|
global := v8ctx.Global()
|
|
fnValue, err := global.Get(funcName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get function %s: %w", funcName, err)
|
|
}
|
|
|
|
if fnValue.IsUndefined() || fnValue.IsNull() {
|
|
return nil, fmt.Errorf("function %s not defined", funcName)
|
|
}
|
|
|
|
if !fnValue.IsFunction() {
|
|
return nil, fmt.Errorf("%s is not a function", funcName)
|
|
}
|
|
|
|
fn, err := fnValue.AsFunction()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert to function: %w", err)
|
|
}
|
|
|
|
// Build arguments
|
|
args, err := h.buildHookArgs(v8ctx, testCase, result, beforeData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to v8go.Valuer slice for Call
|
|
valuerArgs := make([]v8go.Valuer, len(args))
|
|
for i, arg := range args {
|
|
valuerArgs[i] = arg
|
|
}
|
|
|
|
// Call the function
|
|
jsResult, err := fn.Call(global, valuerArgs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hook function %s failed: %w", funcName, err)
|
|
}
|
|
|
|
// Convert result to Go value
|
|
if jsResult == nil || jsResult.IsUndefined() || jsResult.IsNull() {
|
|
return nil, nil
|
|
}
|
|
|
|
goResult, err := bridge.GoValue(jsResult, v8ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert result: %w", err)
|
|
}
|
|
|
|
// Extract data field if present
|
|
if resultMap, ok := goResult.(map[string]interface{}); ok {
|
|
if data, exists := resultMap["data"]; exists {
|
|
return data, nil
|
|
}
|
|
}
|
|
|
|
return goResult, nil
|
|
}
|
|
|
|
// executeHookFunctionWithCases executes BeforeAll with test cases array
|
|
func (h *HookExecutor) executeHookFunctionWithCases(script *v8.Script, funcName string, testCases []*Case) (interface{}, error) {
|
|
scriptCtx, err := script.NewContext("", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create script context: %w", err)
|
|
}
|
|
defer scriptCtx.Close()
|
|
|
|
v8ctx := scriptCtx.Context
|
|
|
|
if err := h.setShareData(v8ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
global := v8ctx.Global()
|
|
fnValue, err := global.Get(funcName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get function %s: %w", funcName, err)
|
|
}
|
|
|
|
if fnValue.IsUndefined() || fnValue.IsNull() {
|
|
return nil, fmt.Errorf("function %s not defined", funcName)
|
|
}
|
|
|
|
if !fnValue.IsFunction() {
|
|
return nil, fmt.Errorf("%s is not a function", funcName)
|
|
}
|
|
|
|
fn, err := fnValue.AsFunction()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert to function: %w", err)
|
|
}
|
|
|
|
// Build ctx argument
|
|
ctxJS, err := h.buildCtxArg(v8ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert test cases to JS array
|
|
casesJS, err := h.testCasesToJS(v8ctx, testCases)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jsResult, err := fn.Call(global, ctxJS, casesJS)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hook function %s failed: %w", funcName, err)
|
|
}
|
|
|
|
if jsResult == nil || jsResult.IsUndefined() || jsResult.IsNull() {
|
|
return nil, nil
|
|
}
|
|
|
|
goResult, err := bridge.GoValue(jsResult, v8ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert result: %w", err)
|
|
}
|
|
|
|
if resultMap, ok := goResult.(map[string]interface{}); ok {
|
|
if data, exists := resultMap["data"]; exists {
|
|
return data, nil
|
|
}
|
|
}
|
|
|
|
return goResult, nil
|
|
}
|
|
|
|
// executeHookFunctionWithResults executes AfterAll with results array
|
|
func (h *HookExecutor) executeHookFunctionWithResults(script *v8.Script, funcName string, results []*Result, beforeData interface{}) (interface{}, error) {
|
|
scriptCtx, err := script.NewContext("", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create script context: %w", err)
|
|
}
|
|
defer scriptCtx.Close()
|
|
|
|
v8ctx := scriptCtx.Context
|
|
|
|
if err := h.setShareData(v8ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
global := v8ctx.Global()
|
|
fnValue, err := global.Get(funcName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get function %s: %w", funcName, err)
|
|
}
|
|
|
|
if fnValue.IsUndefined() || fnValue.IsNull() {
|
|
return nil, fmt.Errorf("function %s not defined", funcName)
|
|
}
|
|
|
|
if !fnValue.IsFunction() {
|
|
return nil, fmt.Errorf("%s is not a function", funcName)
|
|
}
|
|
|
|
fn, err := fnValue.AsFunction()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert to function: %w", err)
|
|
}
|
|
|
|
// Build ctx argument
|
|
ctxJS, err := h.buildCtxArg(v8ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert results to JS array
|
|
resultsJS, err := h.resultsToJS(v8ctx, results)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert beforeData to JS
|
|
beforeDataJS, err := bridge.JsValue(v8ctx, beforeData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert beforeData: %w", err)
|
|
}
|
|
|
|
jsResult, err := fn.Call(global, ctxJS, resultsJS, beforeDataJS)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hook function %s failed: %w", funcName, err)
|
|
}
|
|
|
|
if jsResult == nil || jsResult.IsUndefined() || jsResult.IsNull() {
|
|
return nil, nil
|
|
}
|
|
|
|
goResult, err := bridge.GoValue(jsResult, v8ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert result: %w", err)
|
|
}
|
|
|
|
return goResult, nil
|
|
}
|
|
|
|
// setShareData sets the share data for script execution
|
|
func (h *HookExecutor) setShareData(v8ctx *v8go.Context) error {
|
|
var authorized map[string]interface{}
|
|
if h.agentContext != nil && h.agentContext.Authorized != nil {
|
|
authorized = h.agentContext.Authorized.AuthorizedToMap()
|
|
}
|
|
|
|
return bridge.SetShareData(v8ctx, v8ctx.Global(), &bridge.Share{
|
|
Sid: "",
|
|
Root: false,
|
|
Global: nil,
|
|
Authorized: authorized,
|
|
})
|
|
}
|
|
|
|
// buildCtxArg builds the context argument for hook functions
|
|
func (h *HookExecutor) buildCtxArg(v8ctx *v8go.Context) (*v8go.Value, error) {
|
|
ctxMap := map[string]interface{}{
|
|
"locale": "en",
|
|
}
|
|
|
|
// Use ContextData from --ctx flag if available
|
|
if h.opts != nil && h.opts.ContextData != nil {
|
|
cfg := h.opts.ContextData
|
|
if cfg.Locale != "" {
|
|
ctxMap["locale"] = cfg.Locale
|
|
}
|
|
if cfg.Authorized != nil {
|
|
authorized := map[string]interface{}{}
|
|
if cfg.Authorized.UserID != "" {
|
|
authorized["user_id"] = cfg.Authorized.UserID
|
|
}
|
|
if cfg.Authorized.TeamID != "" {
|
|
authorized["team_id"] = cfg.Authorized.TeamID
|
|
}
|
|
if cfg.Authorized.TenantID != "" {
|
|
authorized["tenant_id"] = cfg.Authorized.TenantID
|
|
}
|
|
if cfg.Authorized.Sub != "" {
|
|
authorized["sub"] = cfg.Authorized.Sub
|
|
}
|
|
ctxMap["authorized"] = authorized
|
|
}
|
|
if cfg.Metadata != nil {
|
|
ctxMap["metadata"] = cfg.Metadata
|
|
}
|
|
}
|
|
|
|
return bridge.JsValue(v8ctx, ctxMap)
|
|
}
|
|
|
|
// buildHookArgs builds the arguments for a hook function call
|
|
// Arguments order: ctx, testCase, result (for After), beforeData (for After)
|
|
func (h *HookExecutor) buildHookArgs(v8ctx *v8go.Context, testCase *Case, result *Result, beforeData interface{}) ([]*v8go.Value, error) {
|
|
var args []*v8go.Value
|
|
|
|
// Arg 1: ctx (context) - build from opts.ContextData if available
|
|
ctxMap := map[string]interface{}{
|
|
"locale": "en",
|
|
}
|
|
|
|
// Use ContextData from --ctx flag if available
|
|
if h.opts != nil && h.opts.ContextData != nil {
|
|
cfg := h.opts.ContextData
|
|
if cfg.Locale != "" {
|
|
ctxMap["locale"] = cfg.Locale
|
|
}
|
|
if cfg.Authorized != nil {
|
|
authorized := map[string]interface{}{}
|
|
if cfg.Authorized.UserID != "" {
|
|
authorized["user_id"] = cfg.Authorized.UserID
|
|
}
|
|
if cfg.Authorized.TeamID != "" {
|
|
authorized["team_id"] = cfg.Authorized.TeamID
|
|
}
|
|
if cfg.Authorized.TenantID != "" {
|
|
authorized["tenant_id"] = cfg.Authorized.TenantID
|
|
}
|
|
if cfg.Authorized.Sub != "" {
|
|
authorized["sub"] = cfg.Authorized.Sub
|
|
}
|
|
ctxMap["authorized"] = authorized
|
|
}
|
|
if cfg.Metadata != nil {
|
|
ctxMap["metadata"] = cfg.Metadata
|
|
}
|
|
} else if testCase != nil {
|
|
// Fallback to test case fields
|
|
if testCase.UserID != "" {
|
|
ctxMap["user_id"] = testCase.UserID
|
|
}
|
|
if testCase.TeamID != "" {
|
|
ctxMap["team_id"] = testCase.TeamID
|
|
}
|
|
// Build authorized info
|
|
authorized := map[string]interface{}{}
|
|
if testCase.UserID != "" {
|
|
authorized["user_id"] = testCase.UserID
|
|
}
|
|
if testCase.TeamID != "" {
|
|
authorized["team_id"] = testCase.TeamID
|
|
}
|
|
if len(authorized) > 0 {
|
|
ctxMap["authorized"] = authorized
|
|
}
|
|
}
|
|
|
|
ctxJS, err := bridge.JsValue(v8ctx, ctxMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert ctx: %w", err)
|
|
}
|
|
args = append(args, ctxJS)
|
|
|
|
// Arg 2: testCase
|
|
if testCase != nil {
|
|
tcMap := map[string]interface{}{
|
|
"id": testCase.ID,
|
|
"input": testCase.Input,
|
|
}
|
|
if testCase.Metadata != nil {
|
|
tcMap["metadata"] = testCase.Metadata
|
|
}
|
|
if testCase.Assert != nil {
|
|
tcMap["assert"] = testCase.Assert
|
|
}
|
|
// Include simulator options for dynamic tests
|
|
if testCase.Simulator != nil {
|
|
tcMap["simulator"] = testCase.Simulator
|
|
}
|
|
|
|
tcJS, err := bridge.JsValue(v8ctx, tcMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert testCase: %w", err)
|
|
}
|
|
args = append(args, tcJS)
|
|
} else {
|
|
// Pass empty object if no testCase
|
|
emptyJS, _ := bridge.JsValue(v8ctx, map[string]interface{}{})
|
|
args = append(args, emptyJS)
|
|
}
|
|
|
|
// Arg 2: result (for After)
|
|
if result != nil {
|
|
resultMap := map[string]interface{}{
|
|
"id": result.ID,
|
|
"status": string(result.Status),
|
|
"duration_ms": result.DurationMs,
|
|
}
|
|
if result.Output != nil {
|
|
resultMap["output"] = result.Output
|
|
}
|
|
if result.Error != "" {
|
|
resultMap["error"] = result.Error
|
|
}
|
|
|
|
resultJS, err := bridge.JsValue(v8ctx, resultMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert result: %w", err)
|
|
}
|
|
args = append(args, resultJS)
|
|
}
|
|
|
|
// Arg 3: beforeData (for After)
|
|
if beforeData != nil {
|
|
beforeDataJS, err := bridge.JsValue(v8ctx, beforeData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert beforeData: %w", err)
|
|
}
|
|
args = append(args, beforeDataJS)
|
|
}
|
|
|
|
return args, nil
|
|
}
|
|
|
|
// testCasesToJS converts test cases to a JS array
|
|
func (h *HookExecutor) testCasesToJS(v8ctx *v8go.Context, testCases []*Case) (*v8go.Value, error) {
|
|
cases := make([]map[string]interface{}, len(testCases))
|
|
for i, tc := range testCases {
|
|
cases[i] = map[string]interface{}{
|
|
"id": tc.ID,
|
|
"input": tc.Input,
|
|
}
|
|
if tc.Metadata != nil {
|
|
cases[i]["metadata"] = tc.Metadata
|
|
}
|
|
}
|
|
|
|
return bridge.JsValue(v8ctx, cases)
|
|
}
|
|
|
|
// resultsToJS converts results to a JS array
|
|
func (h *HookExecutor) resultsToJS(v8ctx *v8go.Context, results []*Result) (*v8go.Value, error) {
|
|
resultMaps := make([]map[string]interface{}, len(results))
|
|
for i, r := range results {
|
|
resultMaps[i] = map[string]interface{}{
|
|
"id": r.ID,
|
|
"status": string(r.Status),
|
|
"duration_ms": r.DurationMs,
|
|
}
|
|
if r.Output != nil {
|
|
resultMaps[i]["output"] = r.Output
|
|
}
|
|
if r.Error != "" {
|
|
resultMaps[i]["error"] = r.Error
|
|
}
|
|
}
|
|
|
|
return bridge.JsValue(v8ctx, resultMaps)
|
|
}
|