import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { PatternBlockerPlugin } from '../../../src/guardrails/plugins/pattern-blocker';
import { createGuardrailContext } from '../../../src/guardrails/context';
// Mock logger to avoid console noise during tests
jest.mock('../../../src/utils/logger');
describe('PatternBlockerPlugin', () => {
let plugin: PatternBlockerPlugin;
beforeEach(async () => {
plugin = new PatternBlockerPlugin();
});
describe('initialization', () => {
it('should initialize with default values', async () => {
await plugin.initialize({ enabled: true });
expect(plugin.enabled).toBe(true);
expect(plugin.name).toBe('pattern_blocker');
expect(plugin.phases).toContain('pre_request');
expect(plugin.phases).toContain('pre_tool_input');
});
it('should initialize with custom patterns', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password', 'secret'],
blocked_patterns_regex: ['api[_-]?key'],
});
const patterns = plugin.getPatterns();
expect(patterns.simple).toEqual(['password', 'secret']);
expect(patterns.regex).toEqual(['api[_-]?key']);
});
});
describe('simple pattern matching', () => {
it('should block prompts containing blocked patterns', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password', 'secret'],
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'My password is hunter2',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('block');
expect(result.blockedBy).toBe('pattern_blocker');
});
it('should allow prompts without blocked patterns', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password', 'secret'],
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'Hello world, how are you?',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('allow');
});
it('should be case insensitive by default', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password'],
case_sensitive: false,
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'My PASSWORD is hunter2',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('block');
});
it('should respect case sensitivity when configured', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password'],
case_sensitive: true,
action_on_match: 'block',
});
// Lowercase should match
const context1 = createGuardrailContext({
prompt: 'My password is hunter2',
});
const result1 = await plugin.execute('pre_request', context1);
expect(result1.action).toBe('block');
// Uppercase should NOT match
const context2 = createGuardrailContext({
prompt: 'My PASSWORD is hunter2',
});
const result2 = await plugin.execute('pre_request', context2);
expect(result2.action).toBe('allow');
});
});
describe('regex pattern matching', () => {
it('should match regex patterns', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns_regex: ['api[_-]?key[_-]?[a-z0-9]{8,}'],
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'My api_key_abc12345678 is sensitive',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('block');
});
it('should handle invalid regex gracefully', async () => {
// Invalid regex should be skipped during initialization
await plugin.initialize({
enabled: true,
blocked_patterns_regex: ['[invalid(regex'],
action_on_match: 'block',
});
const patterns = plugin.getPatterns();
expect(patterns.regex).toEqual([]); // Invalid regex should be skipped
const context = createGuardrailContext({
prompt: 'This should be allowed',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('allow');
});
});
describe('action modes', () => {
it('should block when action is block', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'This is a secret message',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('block');
expect(context.violations.some((v) => v.severity === 'error')).toBe(true);
});
it('should warn but allow when action is warn', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'warn',
});
const context = createGuardrailContext({
prompt: 'This is a secret message',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('allow');
expect(context.violations.some((v) => v.severity === 'warning')).toBe(true);
});
it('should redact when action is redact', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'redact',
});
const context = createGuardrailContext({
prompt: 'This is a secret message',
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('modify');
expect(context.prompt).toContain('[REDACTED]');
expect(context.prompt).not.toContain('secret');
});
});
describe('tool input checking', () => {
it('should check tool arguments', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password'],
action_on_match: 'block',
});
const context = createGuardrailContext({
toolName: 'some_tool',
toolArgs: { user: 'admin', password: 'hunter2' },
});
const result = await plugin.execute('pre_tool_input', context);
expect(result.action).toBe('block');
});
});
describe('phase handling', () => {
it('should skip phases not in its list (post_response)', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'block',
});
// post_response is not in pattern_blocker's phases
const context = createGuardrailContext({
response: 'This is a secret response',
});
const result = await plugin.execute('post_response', context);
// Should allow since phase is not handled
expect(result.action).toBe('allow');
});
it('should skip post_tool_output phase', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'block',
});
const context = createGuardrailContext({
toolResult: { data: 'secret data' },
});
const result = await plugin.execute('post_tool_output', context);
expect(result.action).toBe('allow');
});
});
describe('redact mode with messages', () => {
it('should update last message when redacting in pre_request phase', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['secret'],
action_on_match: 'redact',
});
const context = createGuardrailContext({
prompt: 'Tell me the secret code',
messages: [
{ role: 'user', content: 'Tell me the secret code', timestamp: new Date() },
],
});
const result = await plugin.execute('pre_request', context);
expect(result.action).toBe('modify');
expect(context.prompt).toContain('[REDACTED]');
// Last message should also be updated
expect(context.messages[0].content).toContain('[REDACTED]');
expect(context.messages[0].content).not.toContain('secret');
});
it('should redact in tool args during pre_tool_input phase', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password'],
action_on_match: 'redact',
});
const context = createGuardrailContext({
toolName: 'login',
toolArgs: { user: 'admin', pass: 'password123' },
});
const result = await plugin.execute('pre_tool_input', context);
expect(result.action).toBe('modify');
// toolArgs should be updated (as string since JSON parse might fail)
const toolArgsStr = JSON.stringify(context.toolArgs);
expect(toolArgsStr).toContain('[REDACTED]');
});
});
describe('violations', () => {
it('should add violation with pattern details', async () => {
await plugin.initialize({
enabled: true,
blocked_patterns: ['password'],
action_on_match: 'block',
});
const context = createGuardrailContext({
prompt: 'My password is secret',
});
await plugin.execute('pre_request', context);
expect(context.violations.length).toBeGreaterThan(0);
expect(context.violations[0].rule).toBe('blocked_pattern');
expect(context.violations[0].details?.matches).toBeDefined();
});
});
});