Skip to main content
Test your Veto policies before deploying to production. Veto provides the veto guard check command for testing individual tool calls against your rules without running a full agent.

Why Test Policies?

Catch Errors Early

Find rule syntax errors and logic bugs before agents hit them.

Validate Coverage

Ensure every tool has appropriate rules.

Prevent Regressions

Detect unintended changes when updating policies.

Document Intent

Test cases serve as living documentation of policy behavior.

Using veto guard check

The veto guard check command validates a tool call against your rules without executing the tool.

Basic Usage

veto guard check --tool transfer_funds --args '{"amount": 500}'
Output:
✓ ALLOWED: transfer_funds
  Arguments: {"amount":500}
  No blocking rules matched.

JSON Output

For programmatic testing, use --json:
veto guard check \
  --tool transfer_funds \
  --args '{"amount": 15000}' \
  --json
Output:
{
  "decision": "deny",
  "ruleId": "block-large-transfers",
  "ruleName": "Block Large Transfers",
  "reason": "Transfer amount exceeds $10,000 threshold",
  "severity": "high",
  "matchedConditions": [
    {
      "field": "arguments.amount",
      "operator": "greater_than",
      "value": 10000,
      "matched": true
    }
  ]
}

With Session Context

veto guard check \
  --tool transfer_funds \
  --args '{"amount": 5000}' \
  --session-id "test-session" \
  --agent-id "financial-agent" \
  --user-id "user-123" \
  --json

Writing Test Cases

Test Script (Bash)

Create a test-policies.sh script:
test-policies.sh
#!/bin/bash

set -e

echo "Testing financial transfer rules..."

# Test 1: Small transfer should be allowed
RESULT=$(veto guard check --tool transfer_funds --args '{"amount": 500}' --json)
if [[ $(echo $RESULT | jq -r '.decision') != "allow" ]]; then
  echo "❌ Test 1 failed: Small transfer should be allowed"
  exit 1
fi
echo "✓ Test 1 passed: Small transfer allowed"

# Test 2: Large transfer should be blocked
RESULT=$(veto guard check --tool transfer_funds --args '{"amount": 15000}' --json)
if [[ $(echo $RESULT | jq -r '.decision') != "deny" ]]; then
  echo "❌ Test 2 failed: Large transfer should be blocked"
  exit 1
fi
echo "✓ Test 2 passed: Large transfer blocked"

# Test 3: External transfer should be blocked
RESULT=$(veto guard check --tool transfer_funds --args '{"to_account": "EXT-999"}' --json)
if [[ $(echo $RESULT | jq -r '.decision') != "deny" ]]; then
  echo "❌ Test 3 failed: External transfer should be blocked"
  exit 1
fi
echo "✓ Test 3 passed: External transfer blocked"

echo "All tests passed!"
chmod +x test-policies.sh
./test-policies.sh

Test Suite (Node.js)

Create a test-policies.test.ts file:
test-policies.test.ts
import { execSync } from 'node:child_process';
import { describe, it, expect } from 'vitest';

function guardCheck(tool: string, args: Record<string, unknown>) {
  const result = execSync(
    `veto guard check --tool ${tool} --args '${JSON.stringify(args)}' --json`,
    { encoding: 'utf-8' }
  );
  return JSON.parse(result);
}

describe('Financial Transfer Rules', () => {
  it('should allow small transfers', () => {
    const result = guardCheck('transfer_funds', { amount: 500 });
    expect(result.decision).toBe('allow');
  });

  it('should block transfers over $10,000', () => {
    const result = guardCheck('transfer_funds', { amount: 15000 });
    expect(result.decision).toBe('deny');
    expect(result.ruleId).toBe('block-large-transfers');
  });

  it('should block external transfers', () => {
    const result = guardCheck('transfer_funds', { to_account: 'EXT-999' });
    expect(result.decision).toBe('deny');
    expect(result.ruleId).toBe('block-external-transfers');
  });

  it('should allow internal transfers under limit', () => {
    const result = guardCheck('transfer_funds', {
      amount: 5000,
      to_account: 'ACC-001',
    });
    expect(result.decision).toBe('allow');
  });
});

describe('Deployment Rules', () => {
  it('should block production deployments', () => {
    const result = guardCheck('deploy', { env: 'production' });
    expect(result.decision).toBe('deny');
  });

  it('should allow staging deployments', () => {
    const result = guardCheck('deploy', { env: 'staging' });
    expect(result.decision).toBe('allow');
  });
});
npm install -D vitest
npx vitest test-policies.test.ts

Test Suite (Python)

Create a test_policies.py file:
test_policies.py
import json
import subprocess
import pytest

def guard_check(tool: str, args: dict) -> dict:
    result = subprocess.run(
        ['veto', 'guard', 'check', '--tool', tool, '--args', json.dumps(args), '--json'],
        capture_output=True,
        text=True,
    )
    return json.loads(result.stdout)

class TestFinancialRules:
    def test_allow_small_transfers(self):
        result = guard_check('transfer_funds', {'amount': 500})
        assert result['decision'] == 'allow'

    def test_block_large_transfers(self):
        result = guard_check('transfer_funds', {'amount': 15000})
        assert result['decision'] == 'deny'
        assert result['ruleId'] == 'block-large-transfers'

    def test_block_external_transfers(self):
        result = guard_check('transfer_funds', {'to_account': 'EXT-999'})
        assert result['decision'] == 'deny'
        assert result['ruleId'] == 'block-external-transfers'

class TestDeploymentRules:
    def test_block_production_deploys(self):
        result = guard_check('deploy', {'env': 'production'})
        assert result['decision'] == 'deny'

    def test_allow_staging_deploys(self):
        result = guard_check('deploy', {'env': 'staging'})
        assert result['decision'] == 'allow'
pip install pytest
pytest test_policies.py

Testing Approval Rules

For rules with action: require_approval, the CLI returns decision: "require_approval":
veto guard check \
  --tool transfer_funds \
  --args '{"amount": 12000}' \
  --json
Output:
{
  "decision": "require_approval",
  "ruleId": "require-large-transfer-approval",
  "approvalId": "approval_abc123",
  "reason": "Large transfer requires human review"
}
Test in your suite:
it('should require approval for large transfers', () => {
  const result = guardCheck('transfer_funds', { amount: 12000 });
  expect(result.decision).toBe('require_approval');
  expect(result.approvalId).toBeDefined();
});

Testing with Mock Data

Create a test-fixtures directory with sample inputs:
mkdir -p test-fixtures
test-fixtures/small-transfer.json
{
  "tool": "transfer_funds",
  "args": {
    "amount": 500,
    "from_account": "ACC-001",
    "to_account": "ACC-002"
  },
  "expected": "allow"
}
test-fixtures/large-transfer.json
{
  "tool": "transfer_funds",
  "args": {
    "amount": 50000,
    "from_account": "ACC-001",
    "to_account": "ACC-002"
  },
  "expected": "deny"
}
Test runner:
test-runner.ts
import { execSync } from 'node:child_process';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

const fixturesDir = './test-fixtures';
const fixtures = readdirSync(fixturesDir).filter(f => f.endsWith('.json'));

for (const fixture of fixtures) {
  const path = join(fixturesDir, fixture);
  const { tool, args, expected } = JSON.parse(readFileSync(path, 'utf-8'));

  const result = JSON.parse(
    execSync(
      `veto guard check --tool ${tool} --args '${JSON.stringify(args)}' --json`,
      { encoding: 'utf-8' }
    )
  );

  if (result.decision === expected) {
    console.log(`✓ ${fixture} passed`);
  } else {
    console.error(`❌ ${fixture} failed: expected ${expected}, got ${result.decision}`);
    process.exit(1);
  }
}

console.log('All tests passed!');
node test-runner.ts

Testing Time-Based Rules

For rules with within_hours or outside_hours, mock the current time:
# Test during business hours (should block)
TZ="America/New_York" faketime '2026-03-04 10:00:00' \
  veto guard check --tool deploy --args '{"env":"production"}' --json

# Test outside business hours (should allow)
TZ="America/New_York" faketime '2026-03-04 22:00:00' \
  veto guard check --tool deploy --args '{"env":"production"}' --json
faketime is available via libfaketime on Linux/macOS. Install with apt install faketime or brew install libfaketime.

Coverage Analysis

Use veto scan to ensure all tools have rules:
veto scan --fail-uncovered
Output:
Veto Scan Coverage Audit
========================

Project directory: /home/user/my-agent
Rules loaded: 5 (global: 0)
Framework hints: langchain
Coverage: 4/5 (80.0%)

Discovered tools:
  [COVERED] transfer_funds(amount, from_account, to_account)
    sources: source-ts
  [COVERED] get_balance(account_id)
    sources: source-ts
  [COVERED] deploy(env, branch)
    sources: source-ts
  [UNCOVERED] send_email(to, subject, body)
    sources: source-ts
  [COVERED] read_file(path)
    sources: source-ts
Add --fail-uncovered to exit with code 1 if any tools lack rules (useful for CI).

Adversarial Testing

Veto includes a built-in adversarial analyzer:
veto test --policy ./veto/rules
Output:
Veto Policy Gap Analysis
========================

Policy directory: /home/user/my-agent/veto/rules
Rules loaded: 5
Tools covered: transfer_funds, get_balance, deploy

Found 3 gap(s):
  Critical: 1
  Warning:  2
  Info:     0

--- CRITICAL ---

  [CRITICAL] Numeric limit on "amount" can be split across calls
  Rule "block-large-transfers" (block-large-transfers) limits amount with
  greater_than 10000 for transfer_funds. An agent could bypass this by
  splitting into multiple calls (e.g., 2x5000 instead of 1x10000).
  Fix: Add session-level aggregate constraints or rate limits for amount.
  Consider tracking cumulative totals across calls.
  Rule: block-large-transfers

--- WARNING ---

  [WARNING] Tool "send_email" has no blocking rules
  The tool "send_email" is referenced in your policies but has no rules
  with action "block". An agent could call this tool without any guardrail
  constraints.
  Fix: Add a rule with action "block" targeting the "send_email" tool with
  appropriate conditions.
  Tool: send_email
Use this to find:
  • Splitting attacks: Numeric limits that can be bypassed with multiple calls
  • Unchecked fields: Sensitive parameters without constraints
  • Regex bypasses: Pattern bugs (missing anchors, subdomain tricks)
  • Cross-tool exploits: Dangerous tool combinations (read_file + http_request)
  • Type coercion issues: Mismatched types in conditions

CI Integration

Add policy tests to your CI pipeline:

GitHub Actions

.github/workflows/test-policies.yml
name: Test Veto Policies

on:
  pull_request:
    paths:
      - 'veto/**'
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Veto CLI
        run: npm install -g veto-cli

      - name: Run policy tests
        run: ./test-policies.sh

      - name: Check coverage
        run: veto scan --fail-uncovered

      - name: Adversarial analysis
        run: veto test --policy ./veto/rules

GitLab CI

.gitlab-ci.yml
test-policies:
  stage: test
  image: node:20
  script:
    - npm install -g veto-cli
    - ./test-policies.sh
    - veto scan --fail-uncovered
    - veto test --policy ./veto/rules
  only:
    changes:
      - veto/**
See CI/CD Integration for more examples.

Best Practices

Test Each Rule

Write at least one test case for every rule ID.
describe('Rule: block-large-transfers', () => {
  it('blocks amounts > $10,000', () => { ... });
  it('allows amounts <= $10,000', () => { ... });
});

Test Edge Cases

Test boundary values, empty inputs, and unexpected types.
it('handles zero amount', () => {
  const result = guardCheck('transfer_funds', { amount: 0 });
  expect(result.decision).toBe('allow');
});

Test Condition Groups

If using condition_groups (OR logic), test each group.
it('blocks on high amount', () => { ... });
it('blocks on external recipient', () => { ... });
it('allows when neither condition matches', () => { ... });

Run in CI

Make policy tests part of your CI pipeline.
- name: Test Policies
  run: npm test

Next Steps

CI/CD Integration

Integrate policy tests into GitHub Actions, GitLab CI, etc.

Writing Rules

Learn rule syntax to write testable policies

Audit Trail

Use audit logs to validate rule behavior

Approval Workflows

Test approval rules with mock callbacks

Build docs developers (and LLMs) love