# Agent Test Framework

A comprehensive testing framework for Yao AI agents with support for standard testing, dynamic (simulator-driven) testing, agent-driven assertions, and CI integration.

## Quick Start

### Standard Tests

```bash
# Test with direct message (auto-detect agent from current directory)
cd assistants/keyword
yao agent test -i "Extract keywords from: AI and machine learning"

# Test with direct message (specify agent explicitly)
yao agent test -i "Hello world" -n workers.system.keyword

# Test with JSONL file (auto-detect agent from path)
yao agent test -i assistants/keyword/tests/inputs.jsonl

# Generate HTML report
yao agent test -i tests/inputs.jsonl -o report.html

# Stability analysis (run each test 5 times)
yao agent test -i tests/inputs.jsonl --runs 5
```

### Agent-Driven Input

```bash
# Generate test cases using an agent
yao agent test -i "agents:tests.generator-agent?count=10" -n assistants.expense

# Preview generated tests without running (dry-run)
yao agent test -i "agents:tests.generator-agent?count=5" -n assistants.expense --dry-run
```

### Dynamic Mode (Simulator)

```bash
# Run dynamic tests with simulator
yao agent test -i tests/dynamic.jsonl --simulator tests.simulator-agent

# See detailed turn-by-turn output
yao agent test -i tests/dynamic.jsonl -v
```

### Script Tests

```bash
# Test agent handler scripts (hooks, tools, setup functions)
yao agent test -i scripts.expense.setup -v

# Run specific tests with regex filter
yao agent test -i scripts.expense.setup --run "TestSystemReady" -v

# Run with custom context (authorization, metadata)
yao agent test -i scripts.expense.setup --ctx tests/context.json -v
```

## Input Modes

The `-i` flag supports multiple input modes:

### 1. JSONL File Mode

Load test cases from a file:

```bash
yao agent test -i tests/inputs.jsonl
```

Agent is auto-detected by traversing up from the input file to find `package.yao`.

### 2. Direct Message Mode

Test with a single message:

```bash
# Auto-detect agent from current working directory
cd assistants/keyword
yao agent test -i "Extract keywords from this text"

# Or specify agent explicitly
yao agent test -i "Hello" -n workers.system.keyword
```

### 3. Agent-Driven Input Mode

Generate test cases using a generator agent:

```bash
# Basic usage (-n specifies the target agent to test)
yao agent test -i "agents:tests.generator-agent" -n assistants.expense

# With parameters
yao agent test -i "agents:tests.generator-agent?count=10&focus=edge-cases" -n assistants.expense

# Dry-run to preview generated tests
yao agent test -i "agents:tests.generator-agent?count=5" -n assistants.expense --dry-run
```

**Note**: The `-n` flag is **required** for agent-driven input mode to specify which agent to test. The generator agent creates test cases for the target agent.

### 4. Script Test Mode

Test agent handler scripts:

```bash
yao agent test -i scripts.expense.setup -v
```

Script test input format: `scripts.<assistant>.<module>` (e.g., `scripts.expense.setup` → `assistants/expense/src/setup_test.ts`).

### 5. Script-Generated Input Mode

Generate test cases using a script:

```bash
yao agent test -i "scripts:tests.gen.Generate" -n assistants.expense
```

**Note**: `scripts.xxx` (with dot) runs script tests, while `scripts:xxx` (with colon) generates test cases from a script.

## Test Modes

### Standard Mode

Single call to agent with optional message history. Each test is independent and stateless.

```jsonl
{
  "id": "T001",
  "input": "Hello",
  "assert": {
    "type": "contains",
    "value": "Hi"
  }
}
```

### Dynamic Mode

Simulator-driven testing with checkpoint validation. A simulator agent generates user messages while checkpoints verify agent behavior.

```jsonl
{
  "id": "T001",
  "input": "I want to order coffee",
  "simulator": {
    "use": "tests.simulator-agent",
    "options": {
      "metadata": {
        "persona": "Customer",
        "goal": "Order a latte"
      }
    }
  },
  "checkpoints": [
    {
      "id": "greeting",
      "assert": {
        "type": "regex",
        "value": "(?i)hello"
      }
    },
    {
      "id": "ask_size",
      "after": [
        "greeting"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)size"
      }
    }
  ],
  "max_turns": 10
}
```

## Command Line Options

| Flag          | Description                                              | Default                    |
| ------------- | -------------------------------------------------------- | -------------------------- |
| `-i`          | Input: JSONL file, message, `agents:xxx`, or `scripts:x` | (required)                 |
| `-o`          | Output file path                                         | `output-{timestamp}.jsonl` |
| `-n`          | Agent ID (optional, auto-detected)                       | auto-detect                |
| `-a`          | Application directory                                    | auto-detect                |
| `-e`          | Environment file                                         | -                          |
| `-c`          | Override connector                                       | agent default              |
| `-u`          | Test user ID                                             | `test-user`                |
| `-t`          | Test team ID                                             | `test-team`                |
| `-r`          | Reporter agent ID for custom report                      | built-in                   |
| `-v`          | Verbose output                                           | false                      |
| `--ctx`       | Path to context JSON file for custom authorization       | -                          |
| `--simulator` | Default simulator agent ID for dynamic mode              | -                          |
| `--before`    | Global BeforeAll hook (e.g., `env_test.BeforeAll`)       | -                          |
| `--after`     | Global AfterAll hook (e.g., `env_test.AfterAll`)         | -                          |
| `--runs`      | Runs per test (stability analysis)                       | 1                          |
| `--run`       | Regex pattern to filter which tests to run               | -                          |
| `--timeout`   | Timeout per test                                         | 2m                         |
| `--parallel`  | Parallel test cases                                      | 1                          |
| `--fail-fast` | Stop on first failure                                    | false                      |
| `--dry-run`   | Generate test cases without running them                 | false                      |

## Custom Context File

Create a JSON file for custom authorization:

```json
{
  "chat_id": "test-chat-001",
  "authorized": {
    "user_id": "test-user-123",
    "team_id": "test-team-456",
    "constraints": {
      "owner_only": true,
      "extra": { "department": "engineering" }
    }
  },
  "metadata": {
    "mode": "test"
  }
}
```

Use with `--ctx`:

```bash
yao agent test -i scripts.expense.setup --ctx tests/context.json -v
```

## Input Format (JSONL)

Each line is a JSON object. Below are examples organized by scenario.

### Scenario 1: Simple Text Input

Basic test with string input:

```jsonl
{"id": "greeting-basic", "input": "Hello, how are you?"}
{"id": "greeting-chinese", "input": "你好，请问有什么可以帮助你的？"}
```

### Scenario 2: With Assertions

Validate response content:

```jsonl
{"id": "keyword-extract", "input": "Extract keywords from: AI and machine learning", "assert": {"type": "contains", "value": "AI"}}
{"id": "json-response", "input": "What's the weather?", "assert": {"type": "json_path", "path": "need_search", "value": true}}
{"id": "no-error", "input": "Help me", "assert": {"type": "not_contains", "value": "error"}}
```

### Scenario 3: Multiple Assertions

All assertions must pass:

```jsonl
{
  "id": "expense-submit",
  "input": "Submit $500 travel expense",
  "assert": [
    {
      "type": "contains",
      "value": "expense"
    },
    {
      "type": "not_contains",
      "value": "error"
    },
    {
      "type": "regex",
      "value": "(?i)(submitted|created|confirmed)"
    }
  ]
}
```

### Scenario 4: Conversation History

Test with multi-turn context:

```jsonl
{
  "id": "expense-confirm",
  "input": [
    {
      "role": "user",
      "content": "Submit an expense"
    },
    {
      "role": "assistant",
      "content": "What type of expense?"
    },
    {
      "role": "user",
      "content": "Travel, $500"
    },
    {
      "role": "assistant",
      "content": "Please confirm: $500 travel expense"
    },
    {
      "role": "user",
      "content": "Yes, confirm"
    }
  ],
  "assert": {
    "type": "regex",
    "value": "(?i)(submitted|created)"
  }
}
```

### Scenario 5: With File Attachments

Test with images or documents:

```jsonl
{
  "id": "receipt-analyze",
  "input": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "Analyze this receipt"
      },
      {
        "type": "image",
        "source": "file://fixtures/receipt.jpg"
      }
    ]
  },
  "assert": {
    "type": "contains",
    "value": "amount"
  }
}
```

### Scenario 6: Agent-Driven Assertion

Use LLM to validate response semantics:

```jsonl
{
  "id": "helpful-response",
  "input": "How do I reset my password?",
  "assert": {
    "type": "agent",
    "use": "agents:tests.validator-agent",
    "value": "Response should provide clear step-by-step instructions"
  }
}
```

### Scenario 7: With Options

Override connector or skip features:

```jsonl
{"id": "fast-model", "input": "Quick question", "options": {"connector": "deepseek.v3", "skip": {"history": true, "trace": true}}}
{"id": "scenario-test", "input": "Query users", "options": {"metadata": {"scenario": "filter"}}, "assert": {"type": "json_path", "path": "from", "value": "users"}}
```

### Scenario 8: With Before/After Hooks

Setup and teardown for each test:

```jsonl
{
  "id": "with-user-data",
  "input": "Show my expenses",
  "before": "env_test.Before",
  "after": "env_test.After",
  "assert": {
    "type": "contains",
    "value": "expense"
  }
}
```

### Scenario 9: Skip Test

Temporarily disable a test:

```jsonl
{
  "id": "wip-feature",
  "input": "New feature test",
  "skip": true
}
```

### Scenario 10: Dynamic Mode (Simulator)

Multi-turn testing with user simulator:

```jsonl
{
  "id": "coffee-order",
  "input": "I want to order coffee",
  "simulator": {
    "use": "tests.simulator-agent",
    "options": {
      "metadata": {
        "persona": "Regular customer",
        "goal": "Order a medium latte"
      }
    }
  },
  "checkpoints": [
    {
      "id": "greeting",
      "assert": {
        "type": "regex",
        "value": "(?i)(hello|hi|help)"
      }
    },
    {
      "id": "ask-size",
      "after": [
        "greeting"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)size"
      }
    },
    {
      "id": "confirm",
      "after": [
        "ask-size"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)confirm"
      }
    }
  ],
  "max_turns": 10
}
```

### Scenario 11: Dynamic Mode with Optional Checkpoint

Some checkpoints are optional:

```jsonl
{
  "id": "expense-flow",
  "input": "Submit expense",
  "simulator": {
    "use": "tests.simulator-agent",
    "options": {
      "metadata": {
        "persona": "New employee",
        "goal": "Submit $500 travel expense"
      }
    }
  },
  "checkpoints": [
    {
      "id": "ask-type",
      "assert": {
        "type": "regex",
        "value": "(?i)type"
      }
    },
    {
      "id": "suggest-category",
      "required": false,
      "assert": {
        "type": "contains",
        "value": "category"
      }
    },
    {
      "id": "confirm",
      "after": [
        "ask-type"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)confirm"
      }
    }
  ],
  "max_turns": 15
}
```

### Standard Mode Fields

| Field      | Type                           | Required | Description                           |
| ---------- | ------------------------------ | -------- | ------------------------------------- |
| `id`       | string                         | Yes      | Test case ID                          |
| `input`    | string \| Message \| []Message | Yes      | Test input                            |
| `assert`   | Assertion \| []Assertion       | No       | Assertion rules                       |
| `expected` | any                            | No       | Expected output (exact match)         |
| `user`     | string                         | No       | Override user ID for this test        |
| `team`     | string                         | No       | Override team ID for this test        |
| `metadata` | map                            | No       | Additional metadata for hooks         |
| `options`  | Options                        | No       | Context options                       |
| `timeout`  | string                         | No       | Override timeout (e.g., "30s")        |
| `skip`     | bool                           | No       | Skip this test                        |
| `before`   | string                         | No       | Before hook (e.g., `env_test.Before`) |
| `after`    | string                         | No       | After hook (e.g., `env_test.After`)   |

### Dynamic Mode Fields

| Field                         | Type   | Required | Description                            |
| ----------------------------- | ------ | -------- | -------------------------------------- |
| `id`                          | string | Yes      | Test case ID                           |
| `input`                       | string | Yes      | Initial user message                   |
| `simulator`                   | object | Yes      | Simulator configuration                |
| `simulator.use`               | string | Yes      | Simulator agent ID (no prefix)         |
| `simulator.options`           | object | No       | Simulator options                      |
| `simulator.options.metadata`  | map    | No       | Metadata (persona, goal, etc.)         |
| `simulator.options.connector` | string | No       | Override simulator connector           |
| `checkpoints`                 | array  | Yes      | Checkpoints to verify                  |
| `checkpoints[].id`            | string | Yes      | Checkpoint identifier                  |
| `checkpoints[].description`   | string | No       | Human-readable description             |
| `checkpoints[].assert`        | object | Yes      | Assertion to validate                  |
| `checkpoints[].after`         | array  | No       | Checkpoint IDs that must occur first   |
| `checkpoints[].required`      | bool   | No       | Is checkpoint required (default: true) |
| `max_turns`                   | int    | No       | Maximum turns (default: 20)            |
| `timeout`                     | string | No       | Override timeout (e.g., "2m")          |

### Options

The `options` field allows per-test-case configuration:

| Field                    | Type   | Description                                |
| ------------------------ | ------ | ------------------------------------------ |
| `connector`              | string | Override connector (e.g., `"deepseek.v3"`) |
| `mode`                   | string | Agent mode (default: `"chat"`)             |
| `search`                 | bool   | Enable/disable search mode                 |
| `disable_global_prompts` | bool   | Temporarily disable global prompts         |
| `metadata`               | map    | Custom data passed to hooks                |
| `skip`                   | object | Skip configuration (see below)             |

### Options.skip

| Field     | Type | Description             |
| --------- | ---- | ----------------------- |
| `history` | bool | Skip history loading    |
| `trace`   | bool | Skip trace logging      |
| `output`  | bool | Skip output to client   |
| `keyword` | bool | Skip keyword extraction |
| `search`  | bool | Skip auto search        |

### Input Types

| Type        | Description          | Example                                               |
| ----------- | -------------------- | ----------------------------------------------------- |
| `string`    | Simple text          | `"Hello world"`                                       |
| `Message`   | Single message       | `{"role": "user", "content": "..."}`                  |
| `[]Message` | Conversation history | `[{"role": "user", ...}, {"role": "assistant", ...}]` |

## Assertions

Use `assert` for flexible validation. If `assert` is defined, it takes precedence over `expected`.

### Static Assertions

| Type           | Description                   | Example                                                   |
| -------------- | ----------------------------- | --------------------------------------------------------- |
| `equals`       | Exact match                   | `{"type": "equals", "value": {"key": "val"}}`             |
| `contains`     | Output contains value         | `{"type": "contains", "value": "keyword"}`                |
| `not_contains` | Output does not contain value | `{"type": "not_contains", "value": "error"}`              |
| `json_path`    | Extract JSON path and compare | `{"type": "json_path", "path": "$.field", "value": true}` |
| `regex`        | Match regex pattern           | `{"type": "regex", "value": "\\d+"}`                      |
| `type`         | Check output type             | `{"type": "type", "value": "object"}`                     |
| `tool_called`  | Check if a tool was called    | `{"type": "tool_called", "value": "setup"}`               |
| `tool_result`  | Check tool execution result   | `{"type": "tool_result", "value": {"tool": "setup", "result": {"success": true}}}` |

### Assertion Fields

| Field     | Type   | Description                                              |
| --------- | ------ | -------------------------------------------------------- |
| `type`    | string | Assertion type (required)                                |
| `value`   | any    | Expected value or pattern                                |
| `path`    | string | JSON path for `json_path` type                           |
| `script`  | string | Script name for `script` type                            |
| `use`     | string | Agent/script ID for `agent` type (with `agents:` prefix) |
| `options` | object | Options for agent assertions                             |
| `message` | string | Custom failure message                                   |
| `negate`  | bool   | Invert the assertion result                              |

### Agent-Driven Assertions

For semantic or fuzzy validation using an LLM:

```jsonl
{
  "id": "T001",
  "input": "Hello",
  "assert": {
    "type": "agent",
    "use": "agents:tests.validator-agent",
    "value": "Response should be friendly and helpful"
  }
}
```

The validator agent receives the output and criteria, then returns `{"passed": true/false, "reason": "..."}`.

**How it works:**

1. The framework builds a validation request with the agent's response (including tool result messages)
2. The validator agent evaluates the response against the criteria
3. The validator returns a JSON response with `passed` and `reason`

**Output in test report (for checkpoints):**

```json
{
  "agent_validation": {
    "passed": true,
    "reason": "Response explicitly confirms setup completion",
    "criteria": "Response should be friendly and helpful",
    "input": "Hello! How can I help you today?",
    "response": {
      "passed": true,
      "reason": "Response explicitly confirms setup completion"
    }
  }
}
```

- `input`: The content sent to the validator (agent response + tool result messages)
- `response`: The raw JSON response from the validator agent
- `criteria`: The validation criteria from the test case

### Tool Assertions

For validating that specific tools were called and their results:

#### tool_called

Check if a specific tool was called:

```jsonl
{
  "id": "T001",
  "input": "Set up my expense system",
  "assert": {
    "type": "tool_called",
    "value": "setup"
  }
}
```

**Value formats:**

- **String**: Tool name (supports suffix matching, e.g., `"setup"` matches `"agents_expense_tools__setup"`)
- **Array**: Any of the specified tools must be called
- **Object**: Match tool name and optionally arguments

```jsonl
// Match any of these tools
{"type": "tool_called", "value": ["setup", "init"]}

// Match tool with specific arguments
{"type": "tool_called", "value": {"name": "setup", "arguments": {"action": "init"}}}
```

#### tool_result

Check the result of a tool execution:

```jsonl
{
  "id": "T001",
  "input": "Set up my expense system",
  "assert": {
    "type": "tool_result",
    "value": {
      "tool": "setup",
      "result": {
        "success": true
      }
    }
  }
}
```

**Result matching:**

- If `result` is omitted, only checks that the tool executed without error
- Supports partial matching (only specified fields are checked)
- Supports regex patterns with `regex:` prefix for string values

```jsonl
// Just check tool executed without error
{"type": "tool_result", "value": {"tool": "setup"}}

// Check specific result fields
{"type": "tool_result", "value": {"tool": "setup", "result": {"success": true}}}

// Use regex for message matching
{"type": "tool_result", "value": {"tool": "setup", "result": {"message": "regex:(?i)setup.*complete"}}}
```

### Script Assertions

For custom validation logic:

```jsonl
{
  "id": "T001",
  "input": "Test",
  "assert": {
    "type": "script",
    "script": "scripts.test.Validate"
  }
}
```

### Multiple Assertions

All assertions must pass:

```jsonl
{
  "id": "T001",
  "input": "Hello",
  "assert": [
    {
      "type": "contains",
      "value": "Hi"
    },
    {
      "type": "not_contains",
      "value": "error"
    },
    {
      "type": "json_path",
      "path": "status",
      "value": "ok"
    }
  ]
}
```

## File Attachments

Test inputs support file attachments using the `file://` protocol:

```jsonl
{
  "id": "T001",
  "input": {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "Analyze this image"
      },
      {
        "type": "image",
        "source": "file://fixtures/receipt.jpg"
      }
    ]
  }
}
```

Supported types: images (jpg, png, gif, webp), audio (wav, mp3), documents (pdf, doc, txt).

## Before/After Hooks

Hooks allow you to run setup and teardown code before and after tests. Hook scripts must be placed in the agent's `src/` directory with `_test.ts` suffix.

### Hook Types

| Hook        | Scope    | When Called           | Use Case                        |
| ----------- | -------- | --------------------- | ------------------------------- |
| `Before`    | Per-test | Before each test case | Create test data, setup context |
| `After`     | Per-test | After each test case  | Cleanup test data, log results  |
| `BeforeAll` | Global   | Once before all tests | Database migration, init        |
| `AfterAll`  | Global   | Once after all tests  | Global cleanup, report          |

### Execution Order

```
BeforeAll (global)
  ├─ Before (test 1)
  │    └─ Test 1 execution
  │    └─ After (test 1)
  ├─ Before (test 2)
  │    └─ Test 2 execution
  │    └─ After (test 2)
  └─ ...
AfterAll (global)
```

### Per-Test Hooks

Defined in JSONL, scripts located in agent's `src/` directory:

```jsonl
{
  "id": "T001",
  "input": "Test",
  "before": "env_test.Before",
  "after": "env_test.After"
}
```

### Global Hooks

Via CLI flags:

```bash
yao agent test -i tests/inputs.jsonl --before env_test.BeforeAll --after env_test.AfterAll
```

### Hook Function Signatures

```typescript
// assistants/expense/src/env_test.ts

/**
 * Before - Called before each test case
 * @param ctx - Agent context with user/team info
 * @param testCase - The test case about to run
 * @returns any - Data passed to After hook (optional)
 */
export function Before(ctx: Context, testCase: TestCase): any {
  const userId = Process("models.user.Create", { name: "Test User" });
  return { userId }; // This data is passed to After
}

/**
 * After - Called after each test case (pass or fail)
 * @param ctx - Agent context
 * @param testCase - The test case that ran
 * @param result - Test result with status, output, duration
 * @param beforeData - Data returned from Before hook
 */
export function After(
  ctx: Context,
  testCase: TestCase,
  result: TestResult,
  beforeData: any
) {
  if (beforeData?.userId) {
    Process("models.user.Delete", beforeData.userId);
  }
  if (result.status === "failed") {
    console.log(`Test ${testCase.id} failed: ${result.error}`);
  }
}

/**
 * BeforeAll - Called once before all tests
 * @param ctx - Agent context
 * @param testCases - Array of all test cases
 * @returns any - Data passed to AfterAll hook (optional)
 */
export function BeforeAll(ctx: Context, testCases: TestCase[]): any {
  Process("models.migrate");
  return { initialized: true, count: testCases.length };
}

/**
 * AfterAll - Called once after all tests complete
 * @param ctx - Agent context
 * @param results - Array of all test results
 * @param beforeData - Data returned from BeforeAll hook
 */
export function AfterAll(ctx: Context, results: TestResult[], beforeData: any) {
  const passed = results.filter((r) => r.status === "passed").length;
  console.log(`Tests completed: ${passed}/${results.length} passed`);
  Process("models.cleanup");
}
```

### Hook Parameters

**Context** - Agent execution context:

```typescript
interface Context {
  locale: string; // Locale (e.g., "en-us")
  authorized: {
    user_id: string; // Test user ID
    team_id: string; // Test team ID
    constraints?: object; // Access constraints
  };
  metadata: object; // Custom metadata from test case
}
```

**TestCase** - Test case definition:

```typescript
interface TestCase {
  id: string; // Test case ID
  input: any; // Test input (string, Message, or Message[])
  assert?: object; // Assertion rules
  expected?: any; // Expected output
  user?: string; // Override user ID
  team?: string; // Override team ID
  metadata?: object; // Custom metadata
  options?: object; // Context options
  timeout?: string; // Timeout (e.g., "30s")
  skip?: boolean; // Skip flag
  before?: string; // Before hook reference
  after?: string; // After hook reference
}
```

**TestResult** - Test execution result:

```typescript
interface TestResult {
  id: string; // Test case ID
  status: string; // "passed" | "failed" | "error" | "skipped" | "timeout"
  input: any; // Actual input sent
  output: any; // Agent response
  expected?: any; // Expected output (if defined)
  error?: string; // Error message (if failed)
  duration_ms: number; // Execution time in milliseconds
  assertions?: object[]; // Assertion results
}
```

### Common Use Cases

**Database Setup/Teardown**:

```typescript
export function Before(ctx: Context, testCase: TestCase): any {
  // Create test records
  const user = Process("models.user.Create", {
    name: "Test",
    email: "test@example.com",
  });
  const expense = Process("models.expense.Create", {
    user_id: user.id,
    amount: 100,
  });
  return { user, expense };
}

export function After(
  ctx: Context,
  testCase: TestCase,
  result: TestResult,
  data: any
) {
  // Clean up in reverse order
  if (data?.expense) Process("models.expense.Delete", data.expense.id);
  if (data?.user) Process("models.user.Delete", data.user.id);
}
```

**Conditional Setup Based on Metadata**:

```typescript
export function Before(ctx: Context, testCase: TestCase): any {
  const scenario = testCase.metadata?.scenario || "default";

  if (scenario === "empty_db") {
    Process("models.expense.DeleteAll");
  } else if (scenario === "with_data") {
    Process("scripts.tests.seed.LoadTestData");
  }

  return { scenario };
}
```

**Logging and Debugging**:

```typescript
export function After(
  ctx: Context,
  testCase: TestCase,
  result: TestResult,
  data: any
) {
  if (result.status === "failed") {
    console.log("=== Test Failed ===");
    console.log("Test ID:", testCase.id);
    console.log("Input:", JSON.stringify(testCase.input));
    console.log("Output:", JSON.stringify(result.output));
    console.log("Error:", result.error);
  }
}
```

## Script Testing

Test agent handler scripts with the `t.assert` API:

```typescript
// assistants/expense/src/setup_test.ts
import { SystemReady } from "./setup";

export function TestSystemReady(t: TestingT, ctx: Context) {
  const result = SystemReady(ctx);

  t.assert.True(result.success, "Should succeed");
  t.assert.Equal(result.status, "ready", "Status should be ready");
  t.assert.NotNil(result.data, "Data should not be nil");
}

export function TestWithAgentAssertion(t: TestingT, ctx: Context) {
  const response = Process("agents.expense.Stream", ctx, messages);

  // Static assertion
  t.assert.Contains(response.content, "confirm");

  // Agent-driven assertion
  t.assert.Agent(response.content, "tests.validator-agent", {
    criteria: "Response should ask for confirmation",
  });
}
```

### Available Assertions

| Method                           | Description                    |
| -------------------------------- | ------------------------------ |
| `t.assert.True(value, msg)`      | Assert value is true           |
| `t.assert.False(value, msg)`     | Assert value is false          |
| `t.assert.Equal(a, b, msg)`      | Assert a equals b              |
| `t.assert.NotEqual(a, b, msg)`   | Assert a not equals b          |
| `t.assert.Nil(value, msg)`       | Assert value is null/undefined |
| `t.assert.NotNil(value, msg)`    | Assert value is not nil        |
| `t.assert.Contains(s, sub, msg)` | Assert string contains substr  |
| `t.assert.Len(arr, n, msg)`      | Assert array/string length     |
| `t.assert.Agent(resp, id, opts)` | Agent-driven assertion         |

## Dynamic Mode

For testing complex conversation flows where the path is unpredictable:

```jsonl
{
  "id": "coffee-order",
  "input": "I want to order coffee",
  "simulator": {
    "use": "tests.simulator-agent",
    "options": {
      "metadata": {
        "persona": "Customer ordering a latte",
        "goal": "Complete the coffee order"
      }
    }
  },
  "checkpoints": [
    {
      "id": "greeting",
      "description": "Agent greets customer",
      "assert": {
        "type": "regex",
        "value": "(?i)(hello|hi|help)"
      }
    },
    {
      "id": "ask_size",
      "description": "Agent asks for size",
      "after": [
        "greeting"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)size"
      }
    },
    {
      "id": "confirm",
      "description": "Agent confirms order",
      "after": [
        "ask_size"
      ],
      "assert": {
        "type": "regex",
        "value": "(?i)confirm"
      }
    }
  ],
  "max_turns": 10
}
```

### Console Output (Dynamic Mode)

```
► [coffee-order] (dynamic, 3 checkpoints)
ℹ Dynamic test: coffee-order (max 10 turns)
ℹ   Turn 1: User: I want to order coffee
ℹ   Turn 1: Agent: Hello! What can I get for you?
ℹ     ✓ checkpoint: greeting
ℹ   Turn 2: User: A medium latte please
ℹ   Turn 2: Agent: What size would you like?
ℹ     ✓ checkpoint: ask_size
ℹ   Turn 3: User: Medium
ℹ   Turn 3: Agent: Let me confirm: Medium latte. Correct?
ℹ     ✓ checkpoint: confirm
  └─ PASSED (3 turns, 3 checkpoints, 8.5s)
```

### Dynamic Mode Output Structure

Each turn in the output includes:

```typescript
interface TurnResult {
  turn: number; // Turn number (1-based)
  input: string; // User message
  output: any; // Agent response summary (for display)
  response: {
    // Full agent response (for detailed analysis)
    content: string; // LLM text content
    tool_calls: [
      {
        // Tool calls made
        tool: string; // Tool name
        arguments: any; // Call arguments
        result: any; // Execution result
      }
    ];
    next: any; // Next hook data
  };
  checkpoints_reached: string[]; // Checkpoint IDs reached
  duration_ms: number; // Execution time
}
```

### Checkpoint Result Structure

Each checkpoint in the output includes:

```typescript
interface CheckpointResult {
  id: string; // Checkpoint identifier
  reached: boolean; // Whether checkpoint was reached
  reached_at_turn?: number; // Turn number when reached (if reached)
  required: boolean; // Whether checkpoint is required
  passed: boolean; // Whether assertion passed
  message?: string; // Assertion result message
  agent_validation?: {
    // Agent assertion details (for type: "agent")
    passed: boolean; // Validator's determination
    reason: string; // Explanation from validator
    criteria: string; // Validation criteria checked
    input: any; // Content sent to validator
    response: {
      // Raw validator response
      passed: boolean;
      reason: string;
    };
  };
}
```

**Note**: For agent-based assertions (`type: "agent"`), the `agent_validation` field provides full transparency into the validation process. The `input` field contains the combined output (agent text response + tool result messages) that was validated.

## Output Formats

Determined by `-o` file extension:

| Extension | Format   | Description            |
| --------- | -------- | ---------------------- |
| `.jsonl`  | JSONL    | Streaming (default)    |
| `.json`   | JSON     | Complete structured    |
| `.md`     | Markdown | Human-readable         |
| `.html`   | HTML     | Interactive web report |

## Stability Analysis

Run each test multiple times to measure consistency:

```bash
yao agent test -i tests/inputs.jsonl --runs 5 -o stability.json
```

| Pass Rate | Classification  |
| --------- | --------------- |
| 100%      | Stable          |
| 80-99%    | Mostly Stable   |
| 50-79%    | Unstable        |
| < 50%     | Highly Unstable |

## CI Integration

```bash
# Exit code: 0 = all passed, 1 = failures
yao agent test -i tests/inputs.jsonl --fail-fast

# Run with parallel execution
yao agent test -i tests/inputs.jsonl --parallel 4
```

### GitHub Actions Example

```yaml
- name: Run Agent Tests
  run: |
    yao agent test -i assistants/expense/tests/inputs.jsonl \
      -u ci-user -t ci-team \
      --runs 3 \
      -o report.json

- name: Run Dynamic Tests
  run: |
    yao agent test -i assistants/expense/tests/dynamic.jsonl \
      --simulator tests.simulator-agent \
      -v

- name: Run Script Tests
  run: |
    yao agent test -i scripts.expense.setup -v
```

## Format Rules Reference

| Context                | Format                   | Example                                   |
| ---------------------- | ------------------------ | ----------------------------------------- |
| `-i agents:xxx` (CLI)  | Colon prefix             | `agents:tests.generator`                  |
| `-i scripts:xxx` (CLI) | Colon prefix             | `scripts:tests.gen.Generate`              |
| `-i scripts.xxx` (CLI) | Dot prefix (test mode)   | `scripts.expense.setup`                   |
| JSONL assertion `use`  | Prefix required          | `"use": "agents:tests.validator"`         |
| JSONL `simulator.use`  | No prefix (agent only)   | `"use": "tests.simulator-agent"`          |
| `--simulator` flag     | No prefix (agent only)   | `--simulator tests.simulator-agent`       |
| `t.assert.Agent()`     | No prefix (method-bound) | `t.assert.Agent(resp, "tests.validator")` |
| JSONL `before/after`   | No prefix (in src/)      | `"before": "env_test.Before"`             |
| `--before/--after`     | No prefix (in src/)      | `--before env_test.BeforeAll`             |

**Script input modes**:

- `scripts.xxx` (dot) - Run script tests (`*_test.ts` functions)
- `scripts:xxx` (colon) - Generate test cases from a script

## Built-in Test Agents

The framework provides three specialized agents for testing:

### Generator Agent (`tests.generator-agent`)

Generates test cases based on target agent description.

**package.yao**:

```json
{
  "name": "Test Case Generator",
  "connector": "gpt-4o",
  "description": "Generates test cases for agent testing",
  "options": { "temperature": 0.7 },
  "automated": true
}
```

**prompts.yml**:

```yaml
- role: system
  content: |
    You are a test case generator. Generate test cases based on the target agent.

    ## Input Format
    - `target_agent`: Agent info (id, description, tools)
    - `count`: Number of test cases (default: 5)
    - `focus`: Focus area (e.g., "edge-cases", "happy-path")

    ## Output Format
    JSON array of test cases:
    [
      {
        "id": "test-id",
        "input": "User message",
        "assert": [{"type": "contains", "value": "expected"}]
      }
    ]
```

**Usage**:

```bash
yao agent test -i "agents:tests.generator-agent?count=10" -n assistants.expense
```

### Validator Agent (`tests.validator-agent`)

Validates agent responses for agent-driven assertions.

**package.yao**:

```json
{
  "name": "Response Validator",
  "connector": "gpt-4o",
  "description": "Validates responses against criteria",
  "options": { "temperature": 0 },
  "automated": true
}
```

**prompts.yml**:

```yaml
- role: system
  content: |
    You are a response validator. Evaluate whether the response meets the criteria.

    ## Input Format
    - `output`: The response to validate
    - `criteria`: The validation rules
    - `input`: Original input (optional)

    ## Output Format
    JSON object (no markdown):
    {"passed": true/false, "reason": "explanation"}

    ## Examples
    Input: {"output": "Paris is the capital", "criteria": "factually accurate"}
    Output: {"passed": true, "reason": "Statement is correct"}
```

**Usage in JSONL**:

```jsonl
{
  "id": "T001",
  "input": "Hello",
  "assert": {
    "type": "agent",
    "use": "agents:tests.validator-agent",
    "value": "Response should be friendly"
  }
}
```

**Usage in script tests**:

```typescript
t.assert.Agent(response, "tests.validator-agent", {
  criteria: "Response should be helpful",
});
```

### Simulator Agent (`tests.simulator-agent`)

Simulates user behavior for dynamic mode testing.

**package.yao**:

```json
{
  "name": "User Simulator",
  "connector": "gpt-4o",
  "description": "Simulates user behavior for dynamic testing",
  "options": { "temperature": 0.7 },
  "automated": true
}
```

**prompts.yml**:

```yaml
- role: system
  content: |
    You are a user simulator. Generate realistic user messages based on persona and goal.

    ## Input Format
    - `persona`: User description (e.g., "New employee")
    - `goal`: What user wants to achieve
    - `conversation`: Previous messages
    - `turn_number`: Current turn
    - `max_turns`: Maximum turns

    ## Output Format
    JSON object:
    {
      "message": "User response",
      "goal_achieved": false,
      "reasoning": "Strategy explanation"
    }

    ## Guidelines
    1. Stay in character
    2. Work toward the goal
    3. Be realistic (include natural variations)
    4. Set goal_achieved: true when done
```

**Usage in JSONL**:

```jsonl
{
  "id": "dynamic-test",
  "input": "I need help",
  "simulator": {
    "use": "tests.simulator-agent",
    "options": {
      "metadata": {
        "persona": "New employee",
        "goal": "Submit expense report"
      }
    }
  },
  "checkpoints": [
    {
      "id": "greeting",
      "assert": {
        "type": "regex",
        "value": "(?i)hello"
      }
    }
  ],
  "max_turns": 10
}
```

**Usage via CLI**:

```bash
yao agent test -i tests/dynamic.jsonl --simulator tests.simulator-agent
```

## Extract Command

Extract test results from output JSONL file to individual Markdown or JSON files for human review:

```bash
# Extract to Markdown files (default)
yao agent extract output-20260127104118.jsonl

# Specify output directory
yao agent extract output.jsonl -o ./reports/

# Extract to JSON format
yao agent extract output.jsonl --format json
```

### Extract Command Options

| Flag       | Description                              | Default    |
| ---------- | ---------------------------------------- | ---------- |
| `-o`       | Output directory                         | same as input |
| `--format` | Output format: `markdown`, `json`        | `markdown` |

### Output Format (Markdown)

Each test result is extracted to a separate file:

```markdown
# T001-销售分析师-月末周五

**Status**: ✅ PASSED

**Duration**: 16743ms

## Input
(Full input content in markdown code block)

## Output
(Agent's response content)
```

This is useful for:
- Human review of agent outputs
- Comparing results across test runs
- Documentation and reporting

## Exit Codes

| Code | Description                                         |
| ---- | --------------------------------------------------- |
| 0    | All tests passed                                    |
| 1    | Tests failed, configuration error, or runtime error |
