Refactor Before/After Script Integration in Agent Test Framework
- Updated DESIGN_V2.md to reflect changes in the handling of before/after scripts, removing the `scripts:` prefix and clarifying their usage in JSONL test cases. - Enhanced runner.go to integrate global before/after hooks, ensuring they execute correctly before and after test cases. - Revised types.go to include new fields for before/after scripts in test case and options structures. - Improved TODO_V2.md to track the implementation progress of before/after script functionality and related tasks. - Added utility function LoadAgentTestScripts to facilitate loading of test scripts from the agent's src directory.
This commit is contained in:
parent
45c01f9c04
commit
5351128f89
8 changed files with 1026 additions and 48 deletions
|
|
@ -17,8 +17,8 @@ This document describes the design for Agent Test Framework V2, which extends th
|
|||
| JSONL `simulator.use` | No prefix (agent only) | `"use": "workers.test.user-simulator"` |
|
||||
| `--simulator` flag | No prefix (agent only) | `--simulator workers.test.user-simulator` |
|
||||
| `t.assert.Agent()` | No prefix (method-bound) | `t.assert.Agent(resp, "workers.test.validator", {...})` |
|
||||
| JSONL `before/after` | `scripts:` prefix | `"before": "scripts:tests.env.Before"` |
|
||||
| `--before/--after` | `scripts:` prefix | `--before scripts:tests.env.BeforeAll` |
|
||||
| JSONL `before/after` | No prefix (in src/) | `"before": "env_test.Before"` |
|
||||
| `--before/--after` | No prefix (in src/) | `--before env_test.BeforeAll` |
|
||||
|
||||
## Design Goals
|
||||
|
||||
|
|
@ -294,8 +294,8 @@ For coverage testing where conversation flow is unpredictable:
|
|||
| `input` | string \| Message \| Message[] | Yes | Input: text, single message, or message array |
|
||||
| `assertions` | array | No | Assertions to validate response (alias: `assert`) |
|
||||
| `options` | object | No | `context.Options` passed to agent |
|
||||
| `before` | string | No | Before script (e.g., `scripts:tests.env.Before`) |
|
||||
| `after` | string | No | After script (e.g., `scripts:tests.env.After`) |
|
||||
| `before` | string | No | Before script (e.g., `env_test.Before`) |
|
||||
| `after` | string | No | After script (e.g., `env_test.After`) |
|
||||
|
||||
**Note**: The `input` field supports three formats:
|
||||
|
||||
|
|
@ -329,15 +329,17 @@ JSONL test cases can reference `*_test.ts` scripts for environment preparation:
|
|||
|
||||
### Script Location
|
||||
|
||||
Scripts are located in the agent's `tests/` directory:
|
||||
Scripts are located in the agent's `src/` directory (as `*_test.ts` files):
|
||||
|
||||
```
|
||||
assistants/expense/
|
||||
├── agent.yml
|
||||
├── package.yao
|
||||
├── prompts.yml
|
||||
├── src/
|
||||
│ ├── index.ts # Main agent script
|
||||
│ └── env_test.ts # Before/after functions
|
||||
└── tests/
|
||||
├── inputs.jsonl # Test cases
|
||||
├── env_test.ts # Before/after functions
|
||||
└── fixtures/
|
||||
└── receipt.jpg
|
||||
```
|
||||
|
|
@ -345,7 +347,7 @@ assistants/expense/
|
|||
### Script Interface
|
||||
|
||||
```typescript
|
||||
// tests/env_test.ts
|
||||
// src/env_test.ts
|
||||
|
||||
// Before function - called before test case runs
|
||||
// Returns context data that will be passed to After
|
||||
|
|
@ -402,8 +404,8 @@ export function AfterAll(ctx: Context, results: TestResult[], beforeData: any) {
|
|||
{
|
||||
"id": "T001",
|
||||
"name": "Submit expense with user context",
|
||||
"before": "scripts:tests.env.Before",
|
||||
"after": "scripts:tests.env.After",
|
||||
"before": "env_test.Before",
|
||||
"after": "env_test.After",
|
||||
"input": "Submit a $500 travel expense",
|
||||
"assertions": [
|
||||
{
|
||||
|
|
@ -419,8 +421,8 @@ export function AfterAll(ctx: Context, results: TestResult[], beforeData: any) {
|
|||
```bash
|
||||
# Run with global before/after
|
||||
yao agent test -i ./tests/inputs.jsonl \
|
||||
--before scripts:tests.env.BeforeAll \
|
||||
--after scripts:tests.env.AfterAll
|
||||
--before env_test.BeforeAll \
|
||||
--after env_test.AfterAll
|
||||
```
|
||||
|
||||
### Execution Order
|
||||
|
|
@ -639,8 +641,8 @@ options := &context.Options{
|
|||
| `-c` | `--connector` | Override connector for the target agent |
|
||||
| `-v` | `--verbose` | Verbose output |
|
||||
| | `--simulator` | Default simulator agent ID |
|
||||
| | `--before` | Global before script (e.g., `scripts:tests.env.BeforeAll`) |
|
||||
| | `--after` | Global after script (e.g., `scripts:tests.env.AfterAll`) |
|
||||
| | `--before` | Global before script (e.g., `env_test.BeforeAll`) |
|
||||
| | `--after` | Global after script (e.g., `env_test.AfterAll`) |
|
||||
| | `--timeout` | Timeout per test case (default: 5m) |
|
||||
| | `--parallel` | Number of parallel test cases |
|
||||
| | `--fail-fast` | Stop on first failure |
|
||||
|
|
|
|||
|
|
@ -11,21 +11,23 @@
|
|||
| JSONL `simulator.use` | No prefix (agent only) | `"use": "workers.test.user-simulator"` |
|
||||
| `--simulator` flag | No prefix (agent only) | `--simulator workers.test.user-simulator` |
|
||||
| `t.assert.Agent()` | No prefix (method-bound) | `t.assert.Agent(resp, "workers.test.val")` |
|
||||
| JSONL `before/after` | `scripts:` prefix | `"before": "scripts:tests.env.Before"` |
|
||||
| `--before/--after` | `scripts:` prefix | `--before scripts:tests.env.BeforeAll` |
|
||||
| JSONL `before/after` | No prefix (in src/) | `"before": "env_test.Before"` |
|
||||
| `--before/--after` | No prefix (in src/) | `--before env_test.BeforeAll` |
|
||||
|
||||
## Phase 1: Before/After Scripts
|
||||
## Phase 1: Before/After Scripts ✅
|
||||
|
||||
**新增文件**: `script_hooks.go`
|
||||
|
||||
- [ ] `types.go`: 添加 `Before`, `After` 字段到 `Case`
|
||||
- [ ] `types.go`: 添加 `BeforeAll`, `AfterAll` 字段到 `Options`
|
||||
- [ ] `script_hooks.go`: 实现 `HookExecutor`
|
||||
- [ ] `script_hooks.go`: 解析 `scripts:` 前缀
|
||||
- [ ] `runner.go`: 集成 before/after 到 `runSingleTest`
|
||||
- [ ] `runner.go`: 集成 beforeAll/afterAll 到 `RunTests`
|
||||
- [ ] `cmd/agent/agent.go`: 添加 `--before`, `--after` flags
|
||||
- [ ] 创建示例脚本 `tests/env_test.ts`
|
||||
- [x] `types.go`: 添加 `Before`, `After` 字段到 `Case`
|
||||
- [x] `types.go`: 添加 `BeforeAll`, `AfterAll` 字段到 `Options`
|
||||
- [x] `script_hooks.go`: 实现 `HookExecutor`
|
||||
- [x] `script_hooks.go`: 通过 V8 直接执行 `*_test.ts` 脚本
|
||||
- [x] `runner.go`: 集成 before/after 到 `runSingleTest`
|
||||
- [x] `runner.go`: 集成 beforeAll/afterAll 到 `RunTests`
|
||||
- [x] `cmd/agent/test.go`: 添加 `--before`, `--after` flags
|
||||
- [x] `test/utils.go`: 添加 `LoadAgentTestScripts()` 通用函数
|
||||
- [x] 创建示例脚本 `assistants/tests/hooks-test/src/env_test.ts`
|
||||
- [x] 创建单元测试 `script_hooks_test.go` (黑盒测试)
|
||||
|
||||
## Phase 2: Agent-Driven Assertions
|
||||
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
- [x] `--fail-fast` flag
|
||||
- [x] `-v` verbose mode
|
||||
- [x] Script testing (`*_test.ts`)
|
||||
- [x] Before/After hooks (Phase 1)
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
stdContext "context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -16,19 +15,22 @@ import (
|
|||
|
||||
// Executor executes test cases against an agent
|
||||
type Executor struct {
|
||||
opts *Options
|
||||
output *OutputWriter
|
||||
resolver Resolver
|
||||
loader Loader
|
||||
opts *Options
|
||||
output *OutputWriter
|
||||
resolver Resolver
|
||||
loader Loader
|
||||
hookExecutor *HookExecutor
|
||||
agentPath string // Path to the agent being tested
|
||||
}
|
||||
|
||||
// NewRunner creates a new test runner
|
||||
func NewRunner(opts *Options) *Executor {
|
||||
return &Executor{
|
||||
opts: opts,
|
||||
output: NewOutputWriter(opts.Verbose),
|
||||
resolver: NewResolver(),
|
||||
loader: NewLoader(),
|
||||
opts: opts,
|
||||
output: NewOutputWriter(opts.Verbose),
|
||||
resolver: NewResolver(),
|
||||
loader: NewLoader(),
|
||||
hookExecutor: NewHookExecutor(opts.Verbose),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +163,7 @@ func (r *Executor) RunTests() (*Report, error) {
|
|||
}
|
||||
|
||||
r.output.Info("Agent: %s", agentInfo.ID)
|
||||
r.agentPath = agentInfo.Path // Store agent path for hook execution
|
||||
if r.opts.Connector != "" {
|
||||
r.output.Info("Connector: %s (override)", r.opts.Connector)
|
||||
} else if agentInfo.Connector != "" {
|
||||
|
|
@ -222,6 +225,27 @@ func (r *Executor) RunTests() (*Report, error) {
|
|||
},
|
||||
}
|
||||
|
||||
// Execute global BeforeAll if specified
|
||||
var globalBeforeData interface{}
|
||||
if r.opts.BeforeAll != "" {
|
||||
r.output.Info("BeforeAll: %s", r.opts.BeforeAll)
|
||||
var err error
|
||||
globalBeforeData, err = r.hookExecutor.ExecuteBeforeAll(r.opts.BeforeAll, activeTests, agentInfo.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("beforeAll script failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure AfterAll runs even if tests fail
|
||||
defer func() {
|
||||
if r.opts.AfterAll != "" {
|
||||
r.output.Info("AfterAll: %s", r.opts.AfterAll)
|
||||
if err := r.hookExecutor.ExecuteAfterAll(r.opts.AfterAll, report.Results, globalBeforeData, agentInfo.Path); err != nil {
|
||||
r.output.Warning("afterAll script failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run tests
|
||||
r.output.SubHeader("Running Tests")
|
||||
|
||||
|
|
@ -322,6 +346,31 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
|
|||
Options: tc.Options,
|
||||
}
|
||||
|
||||
// Execute before script if specified
|
||||
var beforeData interface{}
|
||||
if tc.Before != "" {
|
||||
var err error
|
||||
beforeData, err = r.hookExecutor.ExecuteBefore(tc.Before, tc, r.agentPath)
|
||||
if err != nil {
|
||||
result.Status = StatusError
|
||||
result.Error = fmt.Sprintf("before script failed: %s", err.Error())
|
||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||
r.output.TestResult(result.Status, time.Since(startTime))
|
||||
r.output.TestError(result.Error)
|
||||
// Note: after script is NOT called when before fails
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure after script runs even if test fails (but only if before succeeded)
|
||||
defer func() {
|
||||
if tc.After != "" && (tc.Before == "" || beforeData != nil || result.Status != StatusError || !isBeforeError(result.Error)) {
|
||||
if err := r.hookExecutor.ExecuteAfter(tc.After, tc, result, beforeData, r.agentPath); err != nil {
|
||||
r.output.Warning("after script failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Parse input to messages with file loading support
|
||||
// BaseDir is derived from the input file directory
|
||||
inputOpts := r.getInputOptions()
|
||||
|
|
@ -395,6 +444,19 @@ func (r *Executor) runSingleTest(ast *assistant.Assistant, tc *Case, agentID str
|
|||
return result
|
||||
}
|
||||
|
||||
// isBeforeError checks if the error message indicates a before script failure
|
||||
func isBeforeError(errMsg string) bool {
|
||||
return len(errMsg) > 0 && errMsg[:min(len(errMsg), 20)] == "before script failed"
|
||||
}
|
||||
|
||||
// min returns the minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// runStabilityTests runs each test case multiple times for stability analysis
|
||||
func (r *Executor) runStabilityTests(ast *assistant.Assistant, testCases []*Case, agentID string) []*StabilityResult {
|
||||
results := make([]*StabilityResult, 0, len(testCases))
|
||||
|
|
@ -499,20 +561,6 @@ func (r *Executor) writeOutput(report *Report) error {
|
|||
return reporter.Write(report, file)
|
||||
}
|
||||
|
||||
// writeJSONLine writes a JSON line to the writer
|
||||
func writeJSONLine(writer *bufio.Writer, data interface{}) error {
|
||||
line, err := jsoniter.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.WriteString("\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// buildContextOptions builds context.Options from test case and runner options
|
||||
// Priority: test case options > runner options > defaults
|
||||
func buildContextOptions(tc *Case, runnerOpts *Options) *context.Options {
|
||||
|
|
|
|||
592
agent/test/script_hooks.go
Normal file
592
agent/test/script_hooks.go
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
// Check if already loaded
|
||||
if h.loadedDirs[srcDir] {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if src directory exists
|
||||
exists, err := application.App.Exists(srcDir)
|
||||
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(srcDir, 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
|
||||
scriptID := generateHookScriptID(file, srcDir)
|
||||
|
||||
// Load the script
|
||||
_, 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)
|
||||
if h.verbose {
|
||||
h.output.Verbose("Loaded hook script: %s (id: %s)", base, scriptID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, exts...)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to walk src directory: %w", err)
|
||||
}
|
||||
|
||||
h.loadedDirs[srcDir] = true
|
||||
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)
|
||||
}
|
||||
|
||||
// Convert test cases to JS array
|
||||
casesJS, err := h.testCasesToJS(v8ctx, testCases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsResult, err := fn.Call(global, 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)
|
||||
}
|
||||
|
||||
// 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, 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,
|
||||
})
|
||||
}
|
||||
|
||||
// buildHookArgs builds the arguments for a hook function call
|
||||
func (h *HookExecutor) buildHookArgs(v8ctx *v8go.Context, testCase *Case, result *Result, beforeData interface{}) ([]*v8go.Value, error) {
|
||||
var args []*v8go.Value
|
||||
|
||||
// Arg 1: 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
|
||||
}
|
||||
|
||||
tcJS, err := bridge.JsValue(v8ctx, tcMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert testCase: %w", err)
|
||||
}
|
||||
args = append(args, tcJS)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
244
agent/test/script_hooks_test.go
Normal file
244
agent/test/script_hooks_test.go
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
package test_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
agenttest "github.com/yaoapp/yao/agent/test"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
const hooksTestAgent = "assistants/tests/hooks-test"
|
||||
|
||||
func TestParseHookRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantFile string
|
||||
wantFunc string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "function only",
|
||||
input: "Before",
|
||||
wantFile: "",
|
||||
wantFunc: "Before",
|
||||
},
|
||||
{
|
||||
name: "with script file",
|
||||
input: "env_test.Before",
|
||||
wantFile: "env_test.ts",
|
||||
wantFunc: "Before",
|
||||
},
|
||||
{
|
||||
name: "with src prefix",
|
||||
input: "src/env_test.Before",
|
||||
wantFile: "env_test.ts",
|
||||
wantFunc: "Before",
|
||||
},
|
||||
{
|
||||
name: "nested path",
|
||||
input: "setup/db_test.Before",
|
||||
wantFile: "setup/db_test.ts",
|
||||
wantFunc: "Before",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ref, err := agenttest.ParseHookRef(tt.input)
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantFile, ref.ScriptFile)
|
||||
assert.Equal(t, tt.wantFunc, ref.Function)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookExecutorLoadTestScripts(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts using the utility function
|
||||
scripts := test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
assert.NotEmpty(t, scripts, "Should load at least one test script")
|
||||
|
||||
// Verify the script was loaded into V8
|
||||
found := false
|
||||
for _, scriptID := range scripts {
|
||||
if _, ok := v8.Scripts[scriptID]; ok {
|
||||
found = true
|
||||
t.Logf("Loaded script: %s", scriptID)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "At least one script should be loaded into V8")
|
||||
}
|
||||
|
||||
func TestHookExecutorExecuteBefore(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
testCase := &agenttest.Case{
|
||||
ID: "TEST001",
|
||||
Input: "Hello World",
|
||||
}
|
||||
|
||||
// Execute Before hook
|
||||
beforeData, err := executor.ExecuteBefore("env_test.Before", testCase, hooksTestAgent)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, beforeData)
|
||||
|
||||
// Verify returned data
|
||||
dataMap, ok := beforeData.(map[string]interface{})
|
||||
assert.True(t, ok, "beforeData should be a map")
|
||||
assert.Equal(t, "TEST001", dataMap["test_id"])
|
||||
assert.NotEmpty(t, dataMap["mock_user_id"])
|
||||
assert.NotEmpty(t, dataMap["mock_session_id"])
|
||||
}
|
||||
|
||||
func TestHookExecutorExecuteAfter(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
testCase := &agenttest.Case{
|
||||
ID: "TEST002",
|
||||
Input: "Test input",
|
||||
}
|
||||
|
||||
result := &agenttest.Result{
|
||||
ID: "TEST002",
|
||||
Status: agenttest.StatusPassed,
|
||||
DurationMs: 100,
|
||||
}
|
||||
|
||||
beforeData := map[string]interface{}{
|
||||
"test_id": "TEST002",
|
||||
"mock_user_id": "user_TEST002_12345",
|
||||
"mock_session_id": "session_12345",
|
||||
}
|
||||
|
||||
// Execute After hook
|
||||
err := executor.ExecuteAfter("env_test.After", testCase, result, beforeData, hooksTestAgent)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHookExecutorExecuteBeforeAll(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
testCases := []*agenttest.Case{
|
||||
{ID: "T001", Input: "Test 1"},
|
||||
{ID: "T002", Input: "Test 2"},
|
||||
{ID: "T003", Input: "Test 3"},
|
||||
}
|
||||
|
||||
// Execute BeforeAll hook
|
||||
globalData, err := executor.ExecuteBeforeAll("env_test.BeforeAll", testCases, hooksTestAgent)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, globalData)
|
||||
|
||||
// Verify returned data
|
||||
dataMap, ok := globalData.(map[string]interface{})
|
||||
assert.True(t, ok, "globalData should be a map")
|
||||
assert.NotEmpty(t, dataMap["suite_id"])
|
||||
assert.Equal(t, float64(3), dataMap["test_count"]) // JSON numbers are float64
|
||||
}
|
||||
|
||||
func TestHookExecutorExecuteAfterAll(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
results := []*agenttest.Result{
|
||||
{ID: "T001", Status: agenttest.StatusPassed, DurationMs: 100},
|
||||
{ID: "T002", Status: agenttest.StatusFailed, DurationMs: 200, Error: "assertion failed"},
|
||||
{ID: "T003", Status: agenttest.StatusPassed, DurationMs: 150},
|
||||
}
|
||||
|
||||
globalData := map[string]interface{}{
|
||||
"suite_id": "suite_12345",
|
||||
"test_count": 3,
|
||||
}
|
||||
|
||||
// Execute AfterAll hook
|
||||
err := executor.ExecuteAfterAll("env_test.AfterAll", results, globalData, hooksTestAgent)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHookExecutorFunctionNotFound(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
testCase := &agenttest.Case{
|
||||
ID: "TEST001",
|
||||
Input: "Hello",
|
||||
}
|
||||
|
||||
// Try to execute non-existent function
|
||||
_, err := executor.ExecuteBefore("env_test.NonExistent", testCase, hooksTestAgent)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not defined")
|
||||
}
|
||||
|
||||
func TestHookExecutorScriptNotFound(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Load agent test scripts
|
||||
test.LoadAgentTestScripts(t, hooksTestAgent)
|
||||
|
||||
executor := agenttest.NewHookExecutor(true)
|
||||
|
||||
testCase := &agenttest.Case{
|
||||
ID: "TEST001",
|
||||
Input: "Hello",
|
||||
}
|
||||
|
||||
// Try to execute from non-existent script
|
||||
_, err := executor.ExecuteBefore("nonexistent_test.Before", testCase, hooksTestAgent)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
|
@ -144,6 +144,14 @@ type Options struct {
|
|||
// Only tests matching the pattern will be executed
|
||||
// Example: "TestSystem" matches TestSystemReady, TestSystemError, etc.
|
||||
Run string `json:"run,omitempty"`
|
||||
|
||||
// BeforeAll is the global before script (e.g., "scripts:tests.env.BeforeAll")
|
||||
// Called once before all test cases
|
||||
BeforeAll string `json:"before_all,omitempty"`
|
||||
|
||||
// AfterAll is the global after script (e.g., "scripts:tests.env.AfterAll")
|
||||
// Called once after all test cases
|
||||
AfterAll string `json:"after_all,omitempty"`
|
||||
}
|
||||
|
||||
// ContextConfig represents custom context configuration from JSON file
|
||||
|
|
@ -375,6 +383,14 @@ type Case struct {
|
|||
// Timeout overrides the default timeout for this test case
|
||||
// Format: "30s", "1m", "2m30s"
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
|
||||
// Before script function (e.g., "scripts:tests.env.Before")
|
||||
// Called before the test case runs, returns data passed to After
|
||||
Before string `json:"before,omitempty"`
|
||||
|
||||
// After script function (e.g., "scripts:tests.env.After")
|
||||
// Called after the test case completes (pass or fail)
|
||||
After string `json:"after,omitempty"`
|
||||
}
|
||||
|
||||
// CaseOptions represents per-test-case context options
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ var (
|
|||
testParallel int
|
||||
testVerbose bool
|
||||
testFailFast bool
|
||||
testBefore string // --before flag for global BeforeAll hook
|
||||
testAfter string // --after flag for global AfterAll hook
|
||||
)
|
||||
|
||||
// TestCmd is the agent test command
|
||||
|
|
@ -157,6 +159,8 @@ var TestCmd = &cobra.Command{
|
|||
Parallel: testParallel,
|
||||
Verbose: testVerbose,
|
||||
FailFast: testFailFast,
|
||||
BeforeAll: testBefore,
|
||||
AfterAll: testAfter,
|
||||
}
|
||||
|
||||
// Merge with defaults
|
||||
|
|
@ -244,6 +248,8 @@ func init() {
|
|||
TestCmd.Flags().IntVar(&testParallel, "parallel", 1, L("Number of parallel test cases"))
|
||||
TestCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, L("Verbose output"))
|
||||
TestCmd.Flags().BoolVar(&testFailFast, "fail-fast", false, L("Stop on first failure"))
|
||||
TestCmd.Flags().StringVar(&testBefore, "before", "", L("Global BeforeAll hook (e.g., env_test.BeforeAll)"))
|
||||
TestCmd.Flags().StringVar(&testAfter, "after", "", L("Global AfterAll hook (e.g., env_test.AfterAll)"))
|
||||
|
||||
// Mark input as required
|
||||
TestCmd.MarkFlagRequired("input")
|
||||
|
|
|
|||
|
|
@ -800,3 +800,70 @@ func GuardBearerJWT(c *gin.Context) {
|
|||
claims := helper.JwtValidate(tokenString)
|
||||
c.Set("__sid", claims.SID)
|
||||
}
|
||||
|
||||
// LoadAgentTestScripts loads all *_test.ts/js scripts from an agent's src directory.
|
||||
// This is useful for testing agent hooks (before/after scripts) and other agent-specific test scripts.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// test.Prepare(t, config.Conf)
|
||||
// defer test.Clean()
|
||||
// scripts := test.LoadAgentTestScripts(t, "assistants/tests/hooks-test")
|
||||
//
|
||||
// Parameters:
|
||||
// - t: testing.T instance
|
||||
// - agentRelPath: relative path to agent directory from app root (e.g., "assistants/tests/hooks-test")
|
||||
//
|
||||
// Returns:
|
||||
// - []string: list of loaded script IDs (e.g., ["hook.env_test"])
|
||||
func LoadAgentTestScripts(t *testing.T, agentRelPath string) []string {
|
||||
srcDir := filepath.Join(agentRelPath, "src")
|
||||
|
||||
// Check if src directory exists
|
||||
exists, err := application.App.Exists(srcDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check src directory: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Logf("No src directory found at %s, skipping", srcDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
var loadedScripts []string
|
||||
exts := []string{"*_test.ts", "*_test.js"}
|
||||
|
||||
err = application.App.Walk(srcDir, 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: hook.{relative_path_without_ext}
|
||||
// e.g., assistants/tests/hooks-test/src/env_test.ts -> hook.env_test
|
||||
relPath := strings.TrimPrefix(file, srcDir+"/")
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||||
scriptID := "hook." + strings.ReplaceAll(relPath, "/", ".")
|
||||
|
||||
// Load the script
|
||||
_, err := v8.Load(file, scriptID)
|
||||
if err != nil {
|
||||
t.Logf("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 {
|
||||
t.Fatalf("Failed to walk src directory: %v", err)
|
||||
}
|
||||
|
||||
return loadedScripts
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue