// test-runner.js — Simple test runner that doesn't require Jest const fs = require('fs'); const path = require('path'); // Mock chrome APIs global.chrome = { storage: { local: { get: async (keys) => { const store = {}; if (typeof keys === 'string') keys = [keys]; for (const key of keys) { store[key] = global.__chromeStore?.[key] ?? undefined; } return store; }, set: async (items) => { global.__chromeStore = { ...global.__chromeStore, ...items }; }, remove: async (keys) => { if (typeof keys === 'string') keys = [keys]; for (const key of keys) delete global.__chromeStore?.[key]; }, clear: async () => { global.__chromeStore = {}; } } }, runtime: { getManifest: () => ({ version: '1.0.0' }) }, action: { setBadgeText: async () => {}, setBadgeBackgroundColor: async () => {} } }; global.__chromeStore = {}; let testsPassed = 0; let testsFailed = 0; const failures = []; function test(name, fn) { global.__chromeStore = {}; try { fn(); testsPassed++; console.log(` ✓ ${name}`); } catch (err) { testsFailed++; failures.push({ name, error: err.message }); console.log(` ✗ ${name}`); console.log(` ${err.message}`); } } function expect(actual) { return { toBe(expected) { if (actual !== expected) { throw new Error(`Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); } }, toEqual(expected) { const a = JSON.stringify(actual); const e = JSON.stringify(expected); if (a !== e) { throw new Error(`Expected ${e} but got ${a}`); } }, toBeDefined() { if (actual === undefined) { throw new Error(`Expected value to be defined but got undefined`); } }, toBeUndefined() { if (actual !== undefined) { throw new Error(`Expected value to be undefined but got ${JSON.stringify(actual)}`); } }, toHaveLength(expected) { if (actual.length !== expected) { throw new Error(`Expected length ${expected} but got ${actual.length}`); } }, toContain(expected) { if (!actual.includes(expected)) { throw new Error(`Expected array to contain ${JSON.stringify(expected)}`); } }, toMatch(pattern) { if (!pattern.test(actual)) { throw new Error(`Expected ${JSON.stringify(actual)} to match ${pattern}`); } }, toBeGreaterThan(expected) { if (actual <= expected) { throw new Error(`Expected ${JSON.stringify(actual)} to be greater than ${JSON.stringify(expected)}`); } }, toBeLessThan(expected) { if (actual >= expected) { throw new Error(`Expected ${JSON.stringify(actual)} to be less than ${JSON.stringify(expected)}`); } }, toBeLessThanOrEqual(expected) { if (actual > expected) { throw new Error(`Expected ${JSON.stringify(actual)} to be less than or equal to ${JSON.stringify(expected)}`); } }, not: { toBe(expected) { if (actual === expected) { throw new Error(`Expected values to differ but both were ${JSON.stringify(expected)}`); } } } }; } async function runAsyncTest(name, fn) { global.__chromeStore = {}; try { await fn(); testsPassed++; console.log(` ✓ ${name}`); } catch (err) { testsFailed++; failures.push({ name, error: err.message }); console.log(` ✗ ${name}`); console.log(` ${err.message}`); } } // Module cache for loaded modules const moduleCache = {}; // Helper to load ES modules by converting them to CommonJS function loadModule(filePath) { const absPath = path.resolve(filePath); if (moduleCache[absPath]) return moduleCache[absPath]; const content = fs.readFileSync(absPath, 'utf-8'); const dir = path.dirname(absPath); // Track exported names const exportedNames = []; // Convert ES module imports to requires let modified = content; // Replace import { a, b } from './file.js' with const { a, b } = require('./file.js') modified = modified.replace( /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"];?/g, (match, imports, source) => { const cleanSource = source.replace(/\.js$/, ''); return `const { ${imports} } = require('${cleanSource}');`; } ); // Replace import * as name from './file.js' with const name = require('./file.js') modified = modified.replace( /import\s*\*\s*as\s+(\w+)\s+from\s*['"]([^'"]+)['"];?/g, (match, name, source) => { const cleanSource = source.replace(/\.js$/, ''); return `const ${name} = require('${cleanSource}');`; } ); // Replace import name from './file.js' with const name = require('./file.js') modified = modified.replace( /import\s+(\w+)\s+from\s*['"]([^'"]+)['"];?/g, (match, name, source) => { const cleanSource = source.replace(/\.js$/, ''); return `const ${name} = require('${cleanSource}');`; } ); // Convert export function to function + track export modified = modified.replace(/export\s+async\s+function\s+(\w+)/g, (match, name) => { exportedNames.push(name); return `async function ${name}`; }); modified = modified.replace(/export\s+function\s+(\w+)/g, (match, name) => { exportedNames.push(name); return `function ${name}`; }); // Convert export const to const + track export modified = modified.replace(/export\s+const\s+(\w+)/g, (match, name) => { exportedNames.push(name); return `const ${name}`; }); // Convert export { a, b } to module.exports = { a, b } modified = modified.replace( /export\s*\{([^}]+)\};?/g, (match, names) => { const assignments = names.split(',').map(n => n.trim()).filter(Boolean).map(n => { const [orig, alias] = n.split(' as ').map(s => s.trim()); exportedNames.push(alias || orig); return `${alias || orig}: ${orig}`; }); return `module.exports = { ${assignments.join(', ')} };`; } ); // Add module.exports for tracked exports at the end if not already present if (!modified.includes('module.exports') && exportedNames.length > 0) { const uniqueExports = [...new Set(exportedNames)]; const exportsStr = uniqueExports.map(n => `${n}: ${n}`).join(', '); modified += `\nmodule.exports = { ${exportsStr} };\n`; } // Create a custom require function that resolves relative paths const customRequire = (id) => { if (id.startsWith('.')) { const base = id.endsWith('.js') ? id : id + '.js'; const resolved = path.resolve(dir, base); return loadModule(resolved); } return require(id); }; const moduleObj = { exports: {} }; const fn = new Function('module', 'exports', 'require', 'console', 'crypto', 'global', modified); fn(moduleObj, moduleObj.exports, customRequire, console, globalThis.crypto, global); moduleCache[absPath] = moduleObj.exports; return moduleObj.exports; } async function main() { console.log('Agent Police Department Test Runner\n'); const buildDir = __dirname; // Load modules in dependency order const hashMod = loadModule(path.join(buildDir, 'lib/hash.js')); const { hashEvent, hashChain, sha256Async } = hashMod; const policyMod = loadModule(path.join(buildDir, 'lib/policy.js')); const { loadPolicy, savePolicy, checkPolicy } = policyMod; const auditMod = loadModule(path.join(buildDir, 'lib/audit.js')); const { addEvent, addViolation, getStats, getAuditLog, getViolations, exportAuditLog, searchAuditLog, verifyIntegrity } = auditMod; // Hash tests console.log('Hash Tests'); test('hashEvent returns consistent hash', () => { const event = { id: 'e1', type: 'tool_use' }; const h1 = hashEvent(event); const h2 = hashEvent(event); expect(h1).toBe(h2); expect(h1.length).toBeGreaterThan(0); }); test('hashEvent returns different hash for different events', () => { const h1 = hashEvent({ id: 'e1', type: 'tool_use' }); const h2 = hashEvent({ id: 'e2', type: 'api_call' }); expect(h1).not.toBe(h2); }); test('hashChain returns empty for empty array', () => { expect(hashChain([])).toBe(''); }); test('hashChain returns hash for events', () => { const chain = hashChain([{ id: 'e1' }, { id: 'e2' }]); expect(chain.length).toBeGreaterThan(0); }); test('hashChain is order-sensitive', () => { const chain1 = hashChain([{ id: 'e1' }, { id: 'e2' }]); const chain2 = hashChain([{ id: 'e2' }, { id: 'e1' }]); expect(chain1).not.toBe(chain2); }); await runAsyncTest('sha256Async returns hex string', async () => { const hash = await sha256Async('hello'); expect(hash).toMatch(/^[a-f0-9]{64}$/); }); // Policy tests console.log('\nPolicy Tests'); await runAsyncTest('loadPolicy returns default policy when none stored', async () => { const policy = await loadPolicy(); expect(policy).toBeDefined(); expect(policy.version).toBe(1); expect(policy.rules).toHaveLength(6); }); await runAsyncTest('savePolicy increments version', async () => { const policy = await loadPolicy(); const saved = await savePolicy(policy); expect(saved.version).toBe(2); }); test('checkPolicy allows permitted domains', () => { const event = { type: 'api_call', tool: 'fetch', target: 'https://github.com/api', url: 'https://claude.ai/chat' }; const policy = { rules: [ { id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } } ] }; const result = checkPolicy(event, policy); expect(result.outcome).toBe('allowed'); expect(result.severity).toBe('info'); }); test('checkPolicy flags disallowed domains', () => { const event = { type: 'api_call', tool: 'fetch', target: 'https://evil.com/api', url: 'https://claude.ai/chat' }; const policy = { rules: [ { id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } } ] }; const result = checkPolicy(event, policy); expect(result.outcome).toBe('violation'); expect(result.severity).toBe('high'); expect(result.ruleId).toBe('rule1'); }); test('checkPolicy flags disallowed tools', () => { const event = { type: 'tool_use', tool: 'eval', target: 'script.js', url: 'https://claude.ai/chat' }; const policy = { rules: [ { id: 'rule1', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } } ] }; const result = checkPolicy(event, policy); expect(result.outcome).toBe('violation'); expect(result.severity).toBe('critical'); }); test('disabled rules are ignored', () => { const event = { type: 'tool_use', tool: 'eval', target: 'script.js', url: 'https://claude.ai/chat' }; const policy = { rules: [ { id: 'rule1', type: 'disallowed_tools', enabled: false, params: { tools: ['eval'] } } ] }; const result = checkPolicy(event, policy); expect(result.outcome).toBe('allowed'); }); // Audit tests console.log('\nAudit Tests'); await runAsyncTest('addEvent stores event', async () => { const event = { id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() }; await addEvent(event); const log = await getAuditLog(10); expect(log).toHaveLength(1); expect(log[0].id).toBe('e1'); }); await runAsyncTest('addViolation stores violation', async () => { const violation = { id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'critical', timestamp: new Date().toISOString() }; await addViolation(violation); const list = await getViolations(10); expect(list).toHaveLength(1); expect(list[0].severity).toBe('critical'); }); await runAsyncTest('getStats returns correct counts', async () => { await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() }); await addEvent({ id: 'e2', type: 'api_call', timestamp: new Date().toISOString() }); await addViolation({ id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'high', timestamp: new Date().toISOString() }); const stats = await getStats(); expect(stats.totalEvents).toBe(2); expect(stats.totalViolations).toBe(1); expect(stats.severityCounts.high).toBe(1); }); await runAsyncTest('exportAuditLog includes integrity hash', async () => { await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() }); const exported = await exportAuditLog(); expect(exported.totalEvents).toBe(1); expect(exported.integrity).toBeDefined(); expect(exported.integrity.length).toBeGreaterThan(0); }); await runAsyncTest('searchAuditLog filters by query', async () => { await addEvent({ id: 'e1', type: 'tool_use', tool: 'read_file', target: 'app.js', timestamp: new Date().toISOString() }); await addEvent({ id: 'e2', type: 'tool_use', tool: 'edit_file', target: 'config.js', timestamp: new Date().toISOString() }); const results = await searchAuditLog('config'); expect(results).toHaveLength(1); expect(results[0].target).toBe('config.js'); }); await runAsyncTest('verifyIntegrity returns valid for empty log', async () => { const result = await verifyIntegrity(); expect(result.valid).toBe(true); expect(result.eventsChecked).toBe(0); }); await runAsyncTest('max events limit enforced', async () => { for (let i = 0; i < 1100; i++) { await addEvent({ id: `e${i}`, type: 'tool_use', timestamp: new Date().toISOString() }); } const log = await getAuditLog(2000); expect(log.length).toBeLessThanOrEqual(1000); }); // Detectors tests console.log('\nDetectors Tests'); const detectorsMod = loadModule(path.join(buildDir, 'lib/detectors.js')); const { detectViolations, DETECTOR_NAMES } = detectorsMod; test('DETECTOR_NAMES has 6 detectors', () => { expect(DETECTOR_NAMES).toHaveLength(6); expect(DETECTOR_NAMES).toContain('permission_bypass'); expect(DETECTOR_NAMES).toContain('out_of_scope'); expect(DETECTOR_NAMES).toContain('self_permissioning'); expect(DETECTOR_NAMES).toContain('disallowed_tools'); expect(DETECTOR_NAMES).toContain('redundant_operations'); expect(DETECTOR_NAMES).toContain('off_task'); }); await runAsyncTest('detects permission bypass for disallowed domain', async () => { const event = { id: 'evt-1', type: 'api_call', tool: 'fetch', target: 'https://malicious.com/api', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com', 'claude.ai'] } } ] }; const violations = await detectViolations(event, policy); const bypass = violations.find(v => v.detector === 'permission_bypass'); expect(bypass).toBeDefined(); expect(bypass.severity).toBe('high'); expect(bypass.message).toContain('malicious.com'); }); await runAsyncTest('allows permitted domain', async () => { const event = { id: 'evt-2', type: 'api_call', tool: 'fetch', target: 'https://github.com/api', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } } ] }; const violations = await detectViolations(event, policy); const bypass = violations.find(v => v.detector === 'permission_bypass'); expect(bypass).toBeUndefined(); }); await runAsyncTest('detects disallowed tools', async () => { const event = { id: 'evt-3', type: 'tool_use', tool: 'eval', target: 'some-script.js', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval', 'exec'] } } ] }; const violations = await detectViolations(event, policy); const disallowed = violations.find(v => v.detector === 'disallowed_tools'); expect(disallowed).toBeDefined(); expect(disallowed.severity).toBe('critical'); expect(disallowed.message).toContain('eval'); }); await runAsyncTest('detects self-permissioning', async () => { const event = { id: 'evt-4', type: 'tool_use', tool: 'edit_file', target: 'permissions.json', context: 'grant admin access control role privilege', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_permission_change', type: 'permission_change', enabled: true, params: {} } ] }; const violations = await detectViolations(event, policy); const perm = violations.find(v => v.detector === 'self_permissioning'); expect(perm).toBeDefined(); expect(perm.severity).toBe('critical'); }); await runAsyncTest('detects out-of-scope operations', async () => { const event = { id: 'evt-5', type: 'api_call', tool: 'delete', target: 'https://api.example.com/resource', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_allowed_operations', type: 'allowed_operations', enabled: true, params: { operations: ['read', 'chat'] } } ] }; const violations = await detectViolations(event, policy); const scope = violations.find(v => v.detector === 'out_of_scope'); expect(scope).toBeDefined(); expect(scope.severity).toBe('medium'); }); await runAsyncTest('no violations for normal activity', async () => { const event = { id: 'evt-6', type: 'chat', tool: 'chat', target: 'conversation', url: 'https://claude.ai/chat', timestamp: new Date().toISOString() }; const policy = { rules: [ { id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['claude.ai'] } }, { id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } } ] }; const violations = await detectViolations(event, policy); expect(violations).toHaveLength(0); }); // Summary console.log('\n' + '='.repeat(50)); console.log(`Results: ${testsPassed} passed, ${testsFailed} failed`); console.log('='.repeat(50)); if (testsFailed > 0) { console.log('\nFailures:'); for (const f of failures) { console.log(` - ${f.name}: ${f.error}`); } process.exit(1); } else { console.log('\n✓ All tests passed'); process.exit(0); } } main().catch(err => { console.error('Test runner failed:', err); process.exit(1); });