Testing
The TypeScript SDK ships with testing utilities at @pillar-ai/server/testing that make it easy to generate valid signatures and mock tool contexts. For Python, you can construct signatures manually using the standard library.
TypeScript
Setup
Import the testing helpers:
import { generateSignature, createToolContext } from '@pillar-ai/server/testing';
generateSignature
Generates a valid X-Pillar-Signature header for a request body:
const body = JSON.stringify({action: 'tool_call',tool_name: 'lookup_customer',arguments: { email: 'sarah@acme.com' },// ...});const signature = generateSignature(body, 'your_test_secret');// Returns: "t=1710000000,v1=abc123..."
Pass an optional third argument to set the timestamp (useful for testing expiration):
const signature = generateSignature(body, secret, 1710000000);
createToolContext
Builds a ToolContext object from a raw webhook payload, useful for unit-testing tool handlers directly:
const ctx = createToolContext({caller: { channel: 'web', email: 'sarah@acme.com' },conversation_id: 'conv_1',call_id: 'tc_test',});const result = await myTool.execute({ email: 'sarah@acme.com' }, ctx);
Full integration test
This example tests a tool end-to-end by calling pillar.handle() with a signed request:
import { describe, it, expect } from 'vitest';import { generateSignature, createToolContext } from '@pillar-ai/server/testing';import { pillar } from './pillar-tools';const SECRET = 'plr_test_secret';describe('lookup_customer', () => {it('returns customer data', async () => {const body = JSON.stringify({action: 'tool_call',call_id: 'tc_test_123',tool_name: 'lookup_customer',arguments: { email: 'sarah@acme.com' },caller: { channel: 'web', email: 'sarah@acme.com' },conversation_id: 'conv_1',});const signature = generateSignature(body, SECRET);const [result, status] = await pillar.handle(body, {'x-pillar-signature': signature,});expect(status).toBe(200);expect(result.success).toBe(true);expect(result.result.name).toBe('Sarah');});});
Python
The Python SDK doesn't ship separate testing utilities, but you can generate signatures with the standard library:
import jsonimport hmacimport hashlibimport timeimport pytestfrom myapp.pillar_tools import pillarSECRET = "plr_test_secret"def _generate_signature(body: bytes, secret: str) -> str:timestamp = str(int(time.time()))payload = f"{timestamp}.".encode() + bodydigest = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()return f"t={timestamp},v1={digest}"@pytest.mark.asyncioasync def test_lookup_customer():body = json.dumps({"action": "tool_call","call_id": "tc_test_123","tool_name": "lookup_customer","arguments": {"email": "sarah@acme.com"},"caller": {"channel": "web", "email": "sarah@acme.com"},"conversation_id": "conv_1",}).encode()signature = _generate_signature(body, SECRET)result, status = await pillar.handle(body, {"X-Pillar-Signature": signature,})assert status == 200assert result["success"] is Trueassert result["result"]["name"] == "Sarah"
Testing confirmation flows
To test a tool that uses confirmations, make two calls — one without confirmed and one with the tool_confirm action:
@pytest.mark.asyncioasync def test_confirmation_flow():# First call — should return confirmation_requiredbody = json.dumps({"action": "tool_call","call_id": "tc_1","tool_name": "delete_member","arguments": {"user_id": "usr_123"},"caller": {"channel": "web"},"conversation_id": "conv_1",}).encode()result, status = await pillar.handle(body, {"X-Pillar-Signature": _generate_signature(body, SECRET),})assert result["result"]["confirmation_required"] is True# Second call — user confirmedconfirm_body = json.dumps({"action": "tool_confirm","call_id": "tc_1","tool_name": "delete_member","confirm_payload": {"user_id": "usr_123"},"caller": {"channel": "web"},"conversation_id": "conv_1",}).encode()result, status = await pillar.handle(confirm_body, {"X-Pillar-Signature": _generate_signature(confirm_body, SECRET),})assert result["success"] is Trueassert result["result"]["removed"] is True
Tips
- Use a fixed test secret (e.g.,
plr_test_secret) instead of your production secret - For unit tests, call the tool's
executefunction directly with acreateToolContext(TypeScript) or a manually constructedToolContext(Python) - For integration tests, call
pillar.handle()with a signed request body to test the full pipeline including signature verification and tool dispatch
Next steps
- Defining Tools — Tool definition reference
- Webhook Security — How signature verification works
- Confirmation Flows — Test confirmation round-trips