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:
#!/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:
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:
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:
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:
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!' );
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
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