security.test.js
1 /** 2 * Security Agent Tests - Comprehensive Coverage 3 * 4 * Covers: processTask (all branches), auditCode, checkSqlInjection, 5 * checkSecrets, checkCommandInjection, getJsFiles, scanDependencies, 6 * verifyCompliance, checkTcpaCompliance, checkCanSpamCompliance, 7 * checkGdprCompliance, scanSecrets, threatModel, calculateDreadScore, 8 * getRiskLevel, getSecurityContext, fixSecurityIssue, 9 * reviewDependencyUpdate, generateSbom 10 */ 11 12 import { test, mock, before, after, beforeEach, afterEach } from 'node:test'; 13 import assert from 'node:assert/strict'; 14 import Database from 'better-sqlite3'; 15 import fs from 'fs/promises'; 16 import path from 'path'; 17 import os from 'os'; 18 19 const TEST_DB_PATH = path.join(os.tmpdir(), `security-agent-test-${process.pid}.db`); 20 process.env.DATABASE_PATH = TEST_DB_PATH; 21 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 22 process.env.OPENROUTER_API_KEY = 'test-key'; 23 mock.module('../../src/agents/utils/agent-claude-api.js', { 24 namedExports: { 25 simpleLLMCall: async () => 'NO', 26 analyzeCode: async () => ({ findings: [], suggestions: [] }), 27 generateCode: async () => ({ code: '// generated' }), 28 generateTests: async () => ({ tests: [] }), 29 reviewArchitecture: async () => ({ approved: true, concerns: [] }), 30 suggestRefactoring: async () => ({ suggestions: [] }), 31 classifyIssue: async () => ({ category: 'other' }), 32 resetDb: () => {}, 33 selectModel: () => 'anthropic/claude-3.5-haiku', 34 getTodaySpending: () => 0, 35 getHourlySpending: () => 0, 36 getUsageStats: () => ({ total: 0 }), 37 performThreatModeling: async (_a, _t, _o) => ({ 38 threats: [ 39 { 40 title: 'SQL Injection', 41 description: 'Attacker injects SQL', 42 attack_scenario: 'User input', 43 mitigation: 'Use params', 44 risk_level: 'critical', 45 stride_category: 'Tampering', 46 cwe_id: 'CWE-89', 47 dread: { 48 damage: 9, 49 reproducibility: 9, 50 exploitability: 8, 51 affected_users: 10, 52 discoverability: 9, 53 }, 54 }, 55 { 56 title: 'XSS', 57 description: 'Cross-site scripting', 58 attack_scenario: 'Script injection', 59 mitigation: 'Sanitize', 60 risk_level: 'high', 61 stride_category: 'Spoofing', 62 cwe_id: 'CWE-79', 63 dread: { 64 damage: 7, 65 reproducibility: 8, 66 exploitability: 7, 67 affected_users: 7, 68 discoverability: 7, 69 }, 70 }, 71 { 72 title: 'CSRF', 73 description: 'Forged requests', 74 attack_scenario: 'CSRF attack', 75 mitigation: 'CSRF tokens', 76 risk_level: 'medium', 77 stride_category: 'Spoofing', 78 cwe_id: 'CWE-352', 79 dread: { 80 damage: 5, 81 reproducibility: 5, 82 exploitability: 5, 83 affected_users: 5, 84 discoverability: 5, 85 }, 86 }, 87 { 88 title: 'Info Disclosure', 89 description: 'Verbose errors', 90 attack_scenario: 'Stack traces', 91 mitigation: 'Suppress errors', 92 risk_level: 'low', 93 stride_category: 'Info Disc', 94 cwe_id: 'CWE-209', 95 dread: { 96 damage: 2, 97 reproducibility: 9, 98 exploitability: 8, 99 affected_users: 3, 100 discoverability: 9, 101 }, 102 }, 103 ], 104 priority_threats: ['SQL Injection', 'XSS'], 105 }), 106 analyzeCodeSecurity: async () => ({ findings: [] }), 107 generateSecureFix: async () => ({ 108 old_string: 'old code', 109 new_string: 'new code', 110 explanation: 'Fixed', 111 testing_notes: 'Test it', 112 }), 113 }, 114 }); 115 116 mock.module('../../src/utils/human-review-queue.js', { 117 namedExports: { 118 addReviewItem: () => {}, 119 initializeQueue: () => {}, 120 getReviewItems: () => [], 121 updateReviewItem: () => {}, 122 }, 123 }); 124 125 mock.module('../../src/agents/utils/file-operations.js', { 126 namedExports: { 127 readFile: async fp => ({ content: 'const x = 1;', path: fp }), 128 editFile: async () => ({ backupPath: '/tmp/bak.js', diff: 'diff' }), 129 }, 130 }); 131 132 mock.module('../../src/agents/utils/context-loader.js', { 133 namedExports: { 134 loadContextWithMetadata: async () => ({ 135 content: '# Security context', 136 metadata: { files: [] }, 137 }), 138 }, 139 }); 140 141 mock.module('../../src/agents/utils/context-builder.js', { 142 namedExports: { 143 buildAgentContext: async () => ({ context: 'ctx', tokens: 100 }), 144 }, 145 }); 146 147 mock.module('../../src/agents/utils/structured-logger.js', { 148 defaultExport: class MockSL { 149 constructor() {} 150 log() {} 151 info() {} 152 warn() {} 153 error() {} 154 }, 155 }); 156 157 mock.module('../../src/agents/utils/agent-tools.js', { 158 namedExports: { getAgentTools: () => [], getToolByName: () => null }, 159 }); 160 161 mock.module('../../src/agents/utils/message-manager.js', { 162 namedExports: { 163 sendAgentMessage: async () => 1, 164 getUnreadMessages: async () => [], 165 markMessageRead: async () => {}, 166 sendQuestion: async () => 1, 167 sendAnswer: async () => {}, 168 sendHandoff: async () => {}, 169 sendNotification: async () => {}, 170 hasPendingQuestions: async () => false, 171 }, 172 }); 173 174 // Dynamic imports (must come after mock.module() calls for mocks to take effect) 175 const { SecurityAgent } = await import('../../src/agents/security.js'); 176 const { resetDb } = await import('../../src/agents/base-agent.js'); 177 const { resetDbConnection: resetTaskManagerDb } = 178 await import('../../src/agents/utils/task-manager.js'); 179 180 function createTestDb() { 181 const db = new Database(TEST_DB_PATH); 182 db.exec(` 183 CREATE TABLE IF NOT EXISTS agent_tasks ( 184 id INTEGER PRIMARY KEY AUTOINCREMENT, 185 task_type TEXT NOT NULL, assigned_to TEXT NOT NULL, created_by TEXT, 186 status TEXT DEFAULT 'pending', priority INTEGER DEFAULT 5, 187 context_json TEXT, result_json TEXT, parent_task_id INTEGER, 188 error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 189 started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0, 190 reviewed_by TEXT, approval_json TEXT 191 ); 192 CREATE TABLE IF NOT EXISTS agent_logs ( 193 id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, 194 agent_name TEXT NOT NULL, 195 log_level TEXT NOT NULL CHECK(log_level IN ('debug','info','warn','error')), 196 message TEXT NOT NULL, data_json TEXT, 197 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 198 ); 199 CREATE TABLE IF NOT EXISTS agent_state ( 200 agent_name TEXT PRIMARY KEY, status TEXT NOT NULL, 201 current_task_id INTEGER, last_active DATETIME DEFAULT CURRENT_TIMESTAMP 202 ); 203 CREATE TABLE IF NOT EXISTS human_review_queue ( 204 id INTEGER PRIMARY KEY AUTOINCREMENT, file TEXT, reason TEXT, 205 type TEXT, priority TEXT DEFAULT 'medium', 206 status TEXT DEFAULT 'pending', 207 created_at TEXT DEFAULT (datetime('now')), 208 reviewed_at TEXT, reviewed_by TEXT, notes TEXT 209 ); 210 CREATE TABLE IF NOT EXISTS agent_llm_usage ( 211 id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER, 212 model TEXT, prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, 213 total_tokens INTEGER DEFAULT 0, cost_usd REAL DEFAULT 0, 214 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 215 ); 216 CREATE TABLE IF NOT EXISTS agent_messages ( 217 id INTEGER PRIMARY KEY AUTOINCREMENT, from_agent TEXT NOT NULL, 218 to_agent TEXT, message_type TEXT NOT NULL, content_json TEXT, 219 is_read INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP 220 ); 221 `); 222 db.close(); 223 } 224 225 function resetAllDbs() { 226 resetDb(); 227 resetTaskManagerDb(); 228 } 229 230 function insertTask(taskType, contextObj, priority) { 231 const db = new Database(TEST_DB_PATH); 232 const ctx = contextObj !== undefined ? contextObj : {}; 233 const pri = priority !== undefined ? priority : 5; 234 const result = db 235 .prepare( 236 ` 237 INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) 238 VALUES (?, ?, ?, ?) 239 ` 240 ) 241 .run(taskType, 'security', pri, JSON.stringify(ctx)); 242 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(result.lastInsertRowid); 243 db.close(); 244 task.context_json = JSON.parse(task.context_json || '{}'); 245 return task; 246 } 247 248 function getTask(id) { 249 const db = new Database(TEST_DB_PATH); 250 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(id); 251 db.close(); 252 if (task && task.result_json) task.result_json = JSON.parse(task.result_json); 253 return task; 254 } 255 256 const tmpFiles = []; 257 async function writeTmp(name, content) { 258 const filePath = path.join(os.tmpdir(), name); 259 await fs.writeFile(filePath, content, 'utf8'); 260 tmpFiles.push(filePath); 261 return filePath; 262 } 263 async function cleanupTmpFiles() { 264 for (const f of tmpFiles) { 265 try { 266 await fs.unlink(f); 267 } catch (_e) { 268 /* ignore */ 269 } 270 } 271 tmpFiles.length = 0; 272 } 273 274 before(async () => { 275 createTestDb(); 276 // Create temp files needed by security check tests (they use hardcoded /tmp/ paths) 277 await writeTmp('sqli-exec.js', 'db.exec(`SELECT * FROM users WHERE id = ${userId}`);\n'); 278 await writeTmp('sqli-prepare.js', 'db.prepare(`SELECT * FROM t WHERE x = ${val}`);\n'); 279 await writeTmp('sqli-safe.js', "db.prepare('SELECT * FROM t WHERE x = ?').run(val);\n"); 280 await writeTmp('sqli-multi.js', 'db.exec(`SELECT ${a}`);\ndb.prepare(`SELECT ${b}`);\n'); 281 await writeTmp('cmd-execsync.js', 'execSync(`ls ${userDir}`);\n'); 282 await writeTmp('cmd-exec.js', 'exec(`rm -f ${file}`);\n'); 283 await writeTmp('cmd-spawn.js', 'spawn(`bash -c ${cmd}`);\n'); 284 await writeTmp('tcpa-nostop.js', "sendSms({ body: 'Hello there, check out our service!' });\n"); 285 await writeTmp('tcpa-optout.js', "sendSms({ body: 'Hello! Reply STOP to opt-out.' });\n"); 286 await writeTmp('tcpa-nohours.js', "sendSms({ body: 'Hello! Reply STOP to opt-out.' });\n"); 287 await writeTmp( 288 'canspam-compliant.js', 289 "sendEmail({ body: 'Click to unsubscribe', footer: 'SENDER_ADDRESS: 123 Main St' });\n" 290 ); 291 await writeTmp( 292 'canspam-nounsub.js', 293 "sendEmail({ body: 'Hello!', footer: 'No opt-out here.' });\n" 294 ); 295 await writeTmp( 296 'canspam-noaddr.js', 297 "sendEmail({ body: 'Click here to unsubscribe', footer: 'info@example.com' });\n" 298 ); 299 await writeTmp('gdpr-compliant.js', 'if (EU_COUNTRIES.includes(country)) { return; }\n'); 300 await writeTmp('gdpr-none.js', '// regular scoring\nfunction score(site) { return 50; }\n'); 301 await writeTmp( 302 'all-vuln.js', 303 `${[ 304 'db.exec(`SELECT * FROM users WHERE id = ${userId}`);', 305 'execSync(`ls ${userDir}`);', 306 'const api_key = "sk_live_abcd1234567890efghijklmnopqrstuvwxyz";', 307 ].join('\n')}\n` 308 ); 309 }); 310 after(async () => { 311 resetAllDbs(); 312 await cleanupTmpFiles(); 313 try { 314 await fs.unlink(TEST_DB_PATH); 315 } catch (_e) { 316 /* ignore */ 317 } 318 }); 319 beforeEach(() => { 320 resetAllDbs(); 321 createTestDb(); 322 }); 323 afterEach(() => { 324 resetAllDbs(); 325 }); 326 327 async function makeAgent() { 328 const agent = new SecurityAgent(); 329 await agent.initialize(); 330 return agent; 331 } 332 333 // ===== SECTION 1: calculateDreadScore ===== 334 335 test('calculateDreadScore - correct average', () => { 336 const a = new SecurityAgent(); 337 assert.equal( 338 a.calculateDreadScore({ 339 dread: { 340 damage: 9, 341 reproducibility: 10, 342 exploitability: 8, 343 affected_users: 10, 344 discoverability: 9, 345 }, 346 }), 347 9.2 348 ); 349 }); 350 351 test('calculateDreadScore - returns 0 when fields missing', () => { 352 const a = new SecurityAgent(); 353 assert.equal(a.calculateDreadScore({ dread: {} }), 0); 354 assert.equal(a.calculateDreadScore({}), 0); 355 assert.equal(a.calculateDreadScore({ dread: { damage: 5 } }), 0); 356 }); 357 358 test('calculateDreadScore - all equal scores of 5', () => { 359 assert.equal( 360 new SecurityAgent().calculateDreadScore({ 361 dread: { 362 damage: 5, 363 reproducibility: 5, 364 exploitability: 5, 365 affected_users: 5, 366 discoverability: 5, 367 }, 368 }), 369 5 370 ); 371 }); 372 373 test('calculateDreadScore - max scores return 10', () => { 374 assert.equal( 375 new SecurityAgent().calculateDreadScore({ 376 dread: { 377 damage: 10, 378 reproducibility: 10, 379 exploitability: 10, 380 affected_users: 10, 381 discoverability: 10, 382 }, 383 }), 384 10 385 ); 386 }); 387 388 test('calculateDreadScore - min scores return 1', () => { 389 assert.equal( 390 new SecurityAgent().calculateDreadScore({ 391 dread: { 392 damage: 1, 393 reproducibility: 1, 394 exploitability: 1, 395 affected_users: 1, 396 discoverability: 1, 397 }, 398 }), 399 1 400 ); 401 }); 402 403 // ===== SECTION 2: getRiskLevel ===== 404 405 test('getRiskLevel - critical for score 9.5', () => { 406 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 9.5 } }), 'critical'); 407 }); 408 test('getRiskLevel - critical for score 8.5', () => { 409 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 8.5 } }), 'critical'); 410 }); 411 test('getRiskLevel - high for score 7.5', () => { 412 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 7.5 } }), 'high'); 413 }); 414 test('getRiskLevel - high for score exactly 7.0', () => { 415 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 7.0 } }), 'high'); 416 }); 417 test('getRiskLevel - medium for score 5.0', () => { 418 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 5.0 } }), 'medium'); 419 }); 420 test('getRiskLevel - medium for score exactly 4.0', () => { 421 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 4.0 } }), 'medium'); 422 }); 423 test('getRiskLevel - low for score 3.9', () => { 424 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 3.9 } }), 'low'); 425 }); 426 test('getRiskLevel - low for score 0', () => { 427 assert.equal(new SecurityAgent().getRiskLevel({ dread: { average: 0 } }), 'low'); 428 }); 429 test('getRiskLevel - uses calculateDreadScore when no average', () => { 430 assert.equal( 431 new SecurityAgent().getRiskLevel({ 432 dread: { 433 damage: 9, 434 reproducibility: 9, 435 exploitability: 9, 436 affected_users: 9, 437 discoverability: 9, 438 }, 439 }), 440 'critical' 441 ); 442 }); 443 444 // ===== SECTION 3: getSecurityContext ===== 445 446 test('getSecurityContext - sql_injection critical severity', () => { 447 const ctx = new SecurityAgent().getSecurityContext('sql_injection'); 448 assert.ok(ctx.patterns.length > 0); 449 assert.ok(ctx.fix_template.length > 0); 450 assert.ok(ctx.test_guidance.length > 0); 451 assert.equal(ctx.severity, 'critical'); 452 }); 453 454 test('getSecurityContext - xss high severity', () => { 455 const ctx = new SecurityAgent().getSecurityContext('xss'); 456 assert.ok(ctx.patterns.length > 0); 457 assert.equal(ctx.severity, 'high'); 458 assert.match(ctx.fix_template, /sanitize|DOMPurify|textContent/i); 459 }); 460 461 test('getSecurityContext - command_injection recommends spawn', () => { 462 const ctx = new SecurityAgent().getSecurityContext('command_injection'); 463 assert.ok(ctx.patterns.length > 0); 464 assert.match(ctx.fix_template, /spawn/i); 465 assert.equal(ctx.severity, 'high'); 466 }); 467 468 test('getSecurityContext - secrets recommends process.env', () => { 469 const ctx = new SecurityAgent().getSecurityContext('secrets'); 470 assert.ok(ctx.patterns.length > 0); 471 assert.equal(ctx.severity, 'critical'); 472 }); 473 474 test('getSecurityContext - path_traversal high severity', () => { 475 const ctx = new SecurityAgent().getSecurityContext('path_traversal'); 476 assert.ok(ctx.patterns.length > 0); 477 assert.equal(ctx.severity, 'high'); 478 }); 479 480 test('getSecurityContext - unknown type returns defaults', () => { 481 const ctx = new SecurityAgent().getSecurityContext('totally_unknown'); 482 assert.equal(ctx.patterns.length, 0); 483 assert.ok(ctx.fix_template.length > 0); 484 assert.equal(ctx.severity, 'medium'); 485 }); 486 487 // ===== SECTION 4: checkSqlInjection ===== 488 489 test('checkSqlInjection - detects interpolation in db.exec', async () => { 490 const agent = await makeAgent(); 491 const findings = await agent.checkSqlInjection(['/tmp/sqli-exec.js']); 492 assert.ok(findings.length > 0); 493 assert.equal(findings[0].type, 'sql_injection'); 494 assert.equal(findings[0].severity, 'critical'); 495 assert.ok(findings[0].line > 0); 496 assert.ok(findings[0].description); 497 assert.ok(findings[0].recommendation); 498 }); 499 500 test('checkSqlInjection - detects interpolation in db.prepare', async () => { 501 const agent = await makeAgent(); 502 assert.ok((await agent.checkSqlInjection(['/tmp/sqli-prepare.js'])).length > 0); 503 }); 504 505 test('checkSqlInjection - no findings for safe parameterized queries', async () => { 506 const agent = await makeAgent(); 507 assert.equal((await agent.checkSqlInjection(['/tmp/sqli-safe.js'])).length, 0); 508 }); 509 510 test('checkSqlInjection - handles unreadable file gracefully', async () => { 511 const agent = await makeAgent(); 512 assert.equal((await agent.checkSqlInjection(['/nonexistent/file.js'])).length, 0); 513 }); 514 515 test('checkSqlInjection - multiple findings across lines', async () => { 516 const agent = await makeAgent(); 517 assert.equal((await agent.checkSqlInjection(['/tmp/sqli-multi.js'])).length, 2); 518 }); 519 520 test('checkSqlInjection - null files uses getJsFiles fallback', async () => { 521 const agent = await makeAgent(); 522 assert.ok(Array.isArray(await agent.checkSqlInjection(null))); 523 }); 524 525 // ===== SECTION 5: checkSecrets ===== 526 527 test('checkSecrets - detects stripe live key', async () => { 528 const agent = await makeAgent(); 529 const file = await writeTmp('sec-stripe.js', 'const k = "sk_live_abcd1234567890efghijklmnop";'); 530 const findings = await agent.checkSecrets([file]); 531 assert.ok(findings.length > 0); 532 assert.equal(findings[0].type, 'hardcoded_secret'); 533 assert.equal(findings[0].severity, 'critical'); 534 }); 535 536 test('checkSecrets - detects api_key assignment', async () => { 537 const agent = await makeAgent(); 538 const file = await writeTmp('sec-apikey.js', 'const api_key = "abcdefghijklmnopqrstuvwxyz";'); 539 assert.ok((await agent.checkSecrets([file])).length > 0); 540 }); 541 542 test('checkSecrets - detects password assignment', async () => { 543 const agent = await makeAgent(); 544 const file = await writeTmp('sec-pwd.js', 'const password = "mysupersecretpass123";'); 545 assert.ok((await agent.checkSecrets([file])).length > 0); 546 }); 547 548 test('checkSecrets - detects token assignment', async () => { 549 const agent = await makeAgent(); 550 const file = await writeTmp('sec-token.js', 'const token = "abcdefghijklmnopqrstuvwxyz";'); 551 assert.ok((await agent.checkSecrets([file])).length > 0); 552 }); 553 554 test('checkSecrets - detects secret assignment', async () => { 555 const agent = await makeAgent(); 556 const file = await writeTmp('sec-secret.js', 'const secret = "abcdefghijklmnopqrstuvwxyz1234";'); 557 assert.ok((await agent.checkSecrets([file])).length > 0); 558 }); 559 560 test('checkSecrets - ignores process.env usage', async () => { 561 const agent = await makeAgent(); 562 const file = await writeTmp('sec-env.js', 'const k = process.env.API_KEY;'); 563 assert.equal((await agent.checkSecrets([file])).length, 0); 564 }); 565 566 test('checkSecrets - ignores comment lines', async () => { 567 const agent = await makeAgent(); 568 const lines = [ 569 '// api_key = "sk_live_abcd1234567890efghijklmnop"', 570 '/* secret = "abcdefghijklmnopqrstuvwxyz" */', 571 ].join('\n'); 572 const file = await writeTmp('sec-comments.js', lines); 573 assert.equal((await agent.checkSecrets([file])).length, 0); 574 }); 575 576 test('checkSecrets - handles unreadable file gracefully', async () => { 577 const agent = await makeAgent(); 578 assert.equal((await agent.checkSecrets(['/nonexistent/file.js'])).length, 0); 579 }); 580 581 test('checkSecrets - null files uses getJsFiles fallback', async () => { 582 const agent = await makeAgent(); 583 assert.ok(Array.isArray(await agent.checkSecrets(null))); 584 }); 585 586 // ===== SECTION 6: checkCommandInjection ===== 587 588 test('checkCommandInjection - detects execSync with interpolation', async () => { 589 const agent = await makeAgent(); 590 const findings = await agent.checkCommandInjection(['/tmp/cmd-execsync.js']); 591 assert.ok(findings.length > 0); 592 assert.equal(findings[0].type, 'command_injection'); 593 assert.equal(findings[0].severity, 'high'); 594 assert.ok(findings[0].line > 0); 595 }); 596 597 test('checkCommandInjection - detects exec with interpolation', async () => { 598 const agent = await makeAgent(); 599 assert.ok((await agent.checkCommandInjection(['/tmp/cmd-exec.js'])).length > 0); 600 }); 601 602 test('checkCommandInjection - detects spawn with interpolation', async () => { 603 const agent = await makeAgent(); 604 assert.ok((await agent.checkCommandInjection(['/tmp/cmd-spawn.js'])).length > 0); 605 }); 606 607 test('checkCommandInjection - no findings for static command', async () => { 608 const agent = await makeAgent(); 609 const file = await writeTmp('cmd-safe.js', 'const out = execSync("npm audit --json");'); 610 assert.equal((await agent.checkCommandInjection([file])).length, 0); 611 }); 612 613 test('checkCommandInjection - handles unreadable file gracefully', async () => { 614 const agent = await makeAgent(); 615 assert.equal((await agent.checkCommandInjection(['/nonexistent/file.js'])).length, 0); 616 }); 617 618 test('checkCommandInjection - null files uses getJsFiles fallback', async () => { 619 const agent = await makeAgent(); 620 assert.ok(Array.isArray(await agent.checkCommandInjection(null))); 621 }); 622 623 // ===== SECTION 7: checkTcpaCompliance ===== 624 // ===== SECTION 7: checkTcpaCompliance ===== 625 626 test('checkTcpaCompliance - no violations when STOP and hours present', async () => { 627 const agent = await makeAgent(); 628 const findings = await agent.checkTcpaCompliance(['/tmp/tcpa-compliant.js']); 629 assert.equal(findings.length, 0); 630 }); 631 632 test('checkTcpaCompliance - opt_out violation when STOP missing', async () => { 633 const agent = await makeAgent(); 634 const v = await agent.checkTcpaCompliance(['/tmp/tcpa-nostop.js']); 635 const sv = v.find(x => x.type === 'tcpa_opt_out'); 636 assert.ok(sv); 637 assert.equal(sv.severity, 'high'); 638 }); 639 640 test('checkTcpaCompliance - no opt_out violation when opt-out present', async () => { 641 const agent = await makeAgent(); 642 const v = await agent.checkTcpaCompliance(['/tmp/tcpa-optout.js']); 643 assert.ok(!v.find(x => x.type === 'tcpa_opt_out')); 644 }); 645 646 test('checkTcpaCompliance - hours violation when no business hours check', async () => { 647 const agent = await makeAgent(); 648 const v = await agent.checkTcpaCompliance(['/tmp/tcpa-nohours.js']); 649 const hv = v.find(x => x.type === 'tcpa_business_hours'); 650 assert.ok(hv); 651 assert.equal(hv.severity, 'medium'); 652 }); 653 654 test('checkTcpaCompliance - handles missing file gracefully', async () => { 655 const agent = await makeAgent(); 656 assert.ok(Array.isArray(await agent.checkTcpaCompliance(['/nonexistent/sms.js']))); 657 }); 658 659 // ===== SECTION 8: checkCanSpamCompliance ===== 660 661 test('checkCanSpamCompliance - no violations when compliant', async () => { 662 const agent = await makeAgent(); 663 assert.equal((await agent.checkCanSpamCompliance(['/tmp/canspam-compliant.js'])).length, 0); 664 }); 665 666 test('checkCanSpamCompliance - violation when unsubscribe missing', async () => { 667 const agent = await makeAgent(); 668 const v = await agent.checkCanSpamCompliance(['/tmp/canspam-nounsub.js']); 669 const uv = v.find(x => x.type === 'can_spam_unsubscribe'); 670 assert.ok(uv); 671 assert.equal(uv.severity, 'critical'); 672 }); 673 674 test('checkCanSpamCompliance - violation when physical address missing', async () => { 675 const agent = await makeAgent(); 676 const v = await agent.checkCanSpamCompliance(['/tmp/canspam-noaddr.js']); 677 const av = v.find(x => x.type === 'can_spam_address'); 678 assert.ok(av); 679 assert.equal(av.severity, 'high'); 680 }); 681 682 test('checkCanSpamCompliance - handles missing file gracefully', async () => { 683 const agent = await makeAgent(); 684 assert.ok(Array.isArray(await agent.checkCanSpamCompliance(['/nonexistent/email.js']))); 685 }); 686 687 // ===== SECTION 9: checkGdprCompliance ===== 688 689 test('checkGdprCompliance - no violations when EU_COUNTRIES present', async () => { 690 const agent = await makeAgent(); 691 assert.equal((await agent.checkGdprCompliance(['/tmp/gdpr-compliant.js'])).length, 0); 692 }); 693 694 test('checkGdprCompliance - violation when no EU handling', async () => { 695 const agent = await makeAgent(); 696 const v = await agent.checkGdprCompliance(['/tmp/gdpr-none.js']); 697 assert.ok(v.length > 0); 698 assert.equal(v[0].type, 'gdpr_eu_blocking'); 699 }); 700 701 test('checkGdprCompliance - handles missing file gracefully', async () => { 702 const agent = await makeAgent(); 703 assert.ok(Array.isArray(await agent.checkGdprCompliance(['/nonexistent/scoring.js']))); 704 }); 705 706 // ===== SECTION 10: auditCode ===== 707 708 test('auditCode - finds SQL injection with focus_areas filter', async () => { 709 const agent = await makeAgent(); 710 const task = insertTask('audit_code', { 711 files: ['/tmp/sqli-exec.js'], 712 focus_areas: ['sql_injection'], 713 }); 714 await agent.auditCode(task); 715 const updated = getTask(task.id); 716 assert.equal(updated.status, 'completed'); 717 assert.ok(updated.result_json.findings.length > 0); 718 assert.ok('summary' in updated.result_json); 719 assert.ok('by_severity' in updated.result_json.summary); 720 }); 721 722 test('auditCode - finds secrets with focus_areas filter', async () => { 723 const agent = await makeAgent(); 724 const task = insertTask('audit_code', { 725 files: ['/tmp/sec-stripe.js'], 726 focus_areas: ['secrets'], 727 }); 728 await agent.auditCode(task); 729 const updated = getTask(task.id); 730 assert.equal(updated.status, 'completed'); 731 assert.ok(updated.result_json.findings.length > 0); 732 }); 733 734 test('auditCode - finds command injection with focus_areas filter', async () => { 735 const agent = await makeAgent(); 736 const task = insertTask('audit_code', { 737 files: ['/tmp/cmd-execsync.js'], 738 focus_areas: ['command_injection'], 739 }); 740 await agent.auditCode(task); 741 assert.equal(getTask(task.id).status, 'completed'); 742 }); 743 744 test('auditCode - runs all checks when no focus_areas', async () => { 745 const agent = await makeAgent(); 746 const task = insertTask('audit_code', { files: ['/tmp/all-vuln.js'] }); 747 await agent.auditCode(task); 748 const updated = getTask(task.id); 749 assert.equal(updated.status, 'completed'); 750 assert.ok(updated.result_json.findings.length >= 3); 751 }); 752 753 test('auditCode - zero findings on clean code', async () => { 754 const agent = await makeAgent(); 755 const task = insertTask('audit_code', { files: ['/tmp/audit-clean.js'] }); 756 await agent.auditCode(task); 757 const updated = getTask(task.id); 758 assert.equal(updated.status, 'completed'); 759 assert.equal(updated.result_json.summary.total, 0); 760 }); 761 762 test('auditCode - handles null context_json gracefully', async () => { 763 const agent = await makeAgent(); 764 const db2 = new Database(TEST_DB_PATH); 765 const res = db2 766 .prepare( 767 'INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) VALUES (?, ?, ?, NULL)' 768 ) 769 .run('audit_code', 'security', 5); 770 const task = db2.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(res.lastInsertRowid); 771 db2.close(); 772 task.context_json = {}; 773 await agent.auditCode(task); 774 assert.equal(getTask(task.id).status, 'completed'); 775 }); 776 777 test('auditCode - summary has correct severity breakdown keys', async () => { 778 const agent = await makeAgent(); 779 const task = insertTask('audit_code', { files: ['/tmp/sqli-exec.js'] }); 780 await agent.auditCode(task); 781 const result = getTask(task.id).result_json; 782 assert.ok('critical' in result.summary.by_severity); 783 assert.ok('high' in result.summary.by_severity); 784 assert.ok('medium' in result.summary.by_severity); 785 assert.ok('low' in result.summary.by_severity); 786 }); 787 788 // ===== SECTION 11: scanSecrets ===== 789 790 test('scanSecrets - reports secrets found', async () => { 791 const agent = await makeAgent(); 792 const task = insertTask('scan_secrets', { files: ['/tmp/sec-stripe.js'] }); 793 await agent.scanSecrets(task); 794 const updated = getTask(task.id); 795 assert.equal(updated.status, 'completed'); 796 assert.ok(updated.result_json.secrets_found > 0); 797 assert.ok(updated.result_json.findings.length > 0); 798 }); 799 800 test('scanSecrets - zero secrets on clean file', async () => { 801 const agent = await makeAgent(); 802 const task = insertTask('scan_secrets', { files: ['/tmp/sec-env.js'] }); 803 await agent.scanSecrets(task); 804 const updated = getTask(task.id); 805 assert.equal(updated.status, 'completed'); 806 assert.equal(updated.result_json.secrets_found, 0); 807 }); 808 809 test('scanSecrets - handles missing files gracefully', async () => { 810 const agent = await makeAgent(); 811 const task = insertTask('scan_secrets', { files: ['/nonexistent/file.js'] }); 812 await agent.scanSecrets(task); 813 assert.equal(getTask(task.id).status, 'completed'); 814 }); 815 816 // ===== SECTION 12: verifyCompliance ===== 817 818 test('verifyCompliance - tcpa compliance check', async () => { 819 const agent = await makeAgent(); 820 const task = insertTask('verify_compliance', { 821 compliance_type: 'tcpa', 822 files: ['/tmp/tcpa-compliant.js'], 823 }); 824 await agent.verifyCompliance(task); 825 const updated = getTask(task.id); 826 assert.equal(updated.status, 'completed'); 827 assert.ok('violations' in updated.result_json); 828 assert.ok('compliant' in updated.result_json); 829 }); 830 831 test('verifyCompliance - can-spam compliance check', async () => { 832 const agent = await makeAgent(); 833 const task = insertTask('verify_compliance', { 834 compliance_type: 'can-spam', 835 files: ['/tmp/canspam-compliant.js'], 836 }); 837 await agent.verifyCompliance(task); 838 const updated = getTask(task.id); 839 assert.equal(updated.status, 'completed'); 840 assert.ok(Array.isArray(updated.result_json.violations)); 841 }); 842 843 test('verifyCompliance - gdpr compliance check', async () => { 844 const agent = await makeAgent(); 845 const task = insertTask('verify_compliance', { 846 compliance_type: 'gdpr', 847 files: ['/tmp/gdpr-compliant.js'], 848 }); 849 await agent.verifyCompliance(task); 850 const updated = getTask(task.id); 851 assert.equal(updated.status, 'completed'); 852 assert.equal(updated.result_json.compliance_type, 'gdpr'); 853 }); 854 855 test('verifyCompliance - all compliance types', async () => { 856 const agent = await makeAgent(); 857 const task = insertTask('verify_compliance', { 858 compliance_type: 'all', 859 files: ['/tmp/vc-all-tcpa.js', '/tmp/vc-all-email.js', '/tmp/vc-all-gdpr.js'], 860 }); 861 await agent.verifyCompliance(task); 862 const updated = getTask(task.id); 863 assert.equal(updated.status, 'completed'); 864 assert.ok(Array.isArray(updated.result_json.violations)); 865 assert.ok('compliant' in updated.result_json); 866 }); 867 868 // ===== SECTION 13: threatModel ===== 869 870 test('threatModel - completes with component string', async () => { 871 const agent = await makeAgent(); 872 const task = insertTask('threat_model', { 873 component: 'function x(input) { return db.query(input); }', 874 component_type: 'database', 875 data_flow: 'user->API->DB', 876 }); 877 await agent.threatModel(task); 878 const updated = getTask(task.id); 879 assert.equal(updated.status, 'completed'); 880 assert.ok('threat_model' in updated.result_json); 881 assert.ok('summary' in updated.result_json); 882 assert.ok('total_threats' in updated.result_json.summary); 883 assert.ok('by_risk' in updated.result_json.summary); 884 assert.ok('critical' in updated.result_json.summary.by_risk); 885 assert.ok('high' in updated.result_json.summary.by_risk); 886 assert.ok('medium' in updated.result_json.summary.by_risk); 887 assert.ok('low' in updated.result_json.summary.by_risk); 888 }); 889 890 test('threatModel - creates fix tasks for critical and high threats', async () => { 891 const agent = await makeAgent(); 892 const task = insertTask('threat_model', { component: 'vulnerable code', component_type: 'api' }); 893 await agent.threatModel(task); 894 const db2 = new Database(TEST_DB_PATH); 895 const fixTasks = db2 896 .prepare('SELECT * FROM agent_tasks WHERE task_type = ?') 897 .all('fix_security_issue'); 898 db2.close(); 899 assert.ok(fixTasks.length >= 2); 900 }); 901 902 test('threatModel - with files reads content via readFile mock', async () => { 903 const agent = await makeAgent(); 904 const file = await writeTmp('tm-file.js', 'function pay(n) { return charge(n); }'); 905 const task = insertTask('threat_model', { files: [file], component_type: 'payment' }); 906 await agent.threatModel(task); 907 assert.equal(getTask(task.id).status, 'completed'); 908 }); 909 910 test('threatModel - fails when no component and no files', async () => { 911 const agent = await makeAgent(); 912 const task = insertTask('threat_model', {}); 913 await agent.threatModel(task); 914 const updated = getTask(task.id); 915 assert.equal(updated.status, 'failed'); 916 assert.ok(updated.error_message.includes('Missing required field')); 917 }); 918 919 test('threatModel - fails when context has only data_flow', async () => { 920 const agent = await makeAgent(); 921 const task = insertTask('threat_model', { data_flow: 'x->y' }); 922 await agent.threatModel(task); 923 assert.equal(getTask(task.id).status, 'failed'); 924 }); 925 926 test('threatModel - includes priority_threats in result', async () => { 927 const agent = await makeAgent(); 928 const task = insertTask('threat_model', { component: 'code', component_type: 'general' }); 929 await agent.threatModel(task); 930 const updated = getTask(task.id); 931 assert.equal(updated.status, 'completed'); 932 assert.ok(Array.isArray(updated.result_json.summary.priority_threats)); 933 }); 934 935 // ===== SECTION 14: fixSecurityIssue ===== 936 937 test('fixSecurityIssue - succeeds with file specified', async () => { 938 const agent = await makeAgent(); 939 const task = insertTask('fix_security_issue', { 940 file: '/tmp/sqli-exec.js', 941 vulnerability: 'SQL Injection', 942 description: 'String interpolation in SQL query', 943 type: 'sql_injection', 944 risk_level: 'critical', 945 line: 1, 946 recommendation: 'Use parameterized queries', 947 }); 948 await agent.fixSecurityIssue(task); 949 const updated = getTask(task.id); 950 assert.equal(updated.status, 'completed'); 951 assert.ok(updated.result_json.fixed === true || 'developer_task_id' in updated.result_json); 952 }); 953 954 test('fixSecurityIssue - escalates to developer when no file', async () => { 955 const agent = await makeAgent(); 956 const task = insertTask('fix_security_issue', { 957 vulnerability: 'SQL Injection', 958 description: 'SQL injection found', 959 risk_level: 'critical', 960 }); 961 await agent.fixSecurityIssue(task); 962 const updated = getTask(task.id); 963 assert.equal(updated.status, 'completed'); 964 assert.ok('developer_task_id' in updated.result_json); 965 assert.match(updated.result_json.note, /Escalated/i); 966 }); 967 968 test('fixSecurityIssue - fails when no vulnerability or description', async () => { 969 const agent = await makeAgent(); 970 const task = insertTask('fix_security_issue', {}); 971 await agent.fixSecurityIssue(task); 972 const updated = getTask(task.id); 973 assert.equal(updated.status, 'failed'); 974 assert.ok(updated.error_message.includes('Missing required field')); 975 }); 976 977 test('fixSecurityIssue - creates QA task after successful fix', async () => { 978 const agent = await makeAgent(); 979 const task = insertTask('fix_security_issue', { 980 file: '/tmp/sqli-exec.js', 981 vulnerability: 'SQL Injection', 982 description: 'SQL injection', 983 type: 'sql_injection', 984 risk_level: 'high', 985 }); 986 await agent.fixSecurityIssue(task); 987 const db2 = new Database(TEST_DB_PATH); 988 const qaTasks = db2 989 .prepare('SELECT * FROM agent_tasks WHERE task_type = ? AND assigned_to = ?') 990 .all('run_tests', 'qa'); 991 db2.close(); 992 assert.ok(qaTasks.length > 0); 993 }); 994 995 test('fixSecurityIssue - high priority developer task for critical risk', async () => { 996 const agent = await makeAgent(); 997 const task = insertTask('fix_security_issue', { 998 vulnerability: 'RCE', 999 description: 'Remote code execution', 1000 risk_level: 'critical', 1001 }); 1002 await agent.fixSecurityIssue(task); 1003 const db2 = new Database(TEST_DB_PATH); 1004 const devTasks = db2 1005 .prepare('SELECT * FROM agent_tasks WHERE task_type = ? AND assigned_to = ? AND priority = 10') 1006 .all('fix_bug', 'developer'); 1007 db2.close(); 1008 assert.ok(devTasks.length > 0); 1009 }); 1010 1011 test('fixSecurityIssue - uses source field from threat_model', async () => { 1012 const agent = await makeAgent(); 1013 const task = insertTask('fix_security_issue', { 1014 file: '/tmp/sqli-exec.js', 1015 vulnerability: 'SQL Injection', 1016 description: 'From threat model analysis', 1017 type: 'sql_injection', 1018 risk_level: 'critical', 1019 source: 'threat_model', 1020 parent_threat_model_task_id: 999, 1021 }); 1022 await agent.fixSecurityIssue(task); 1023 assert.equal(getTask(task.id).status, 'completed'); 1024 }); 1025 1026 // ===== SECTION 15: generateSbom ===== 1027 1028 test('generateSbom - handles cyclonedx format gracefully', async () => { 1029 const agent = await makeAgent(); 1030 const task = insertTask('generate_sbom', {}); 1031 await agent.generateSbom(task); 1032 const updated = getTask(task.id); 1033 assert.ok(['completed', 'failed'].includes(updated.status)); 1034 if (updated.status === 'completed') { 1035 assert.ok( 1036 'format' in updated.result_json || 1037 'fallback' in updated.result_json || 1038 'dependency_count' in updated.result_json 1039 ); 1040 } 1041 }); 1042 1043 test('generateSbom - handles spdx format gracefully', async () => { 1044 const agent = await makeAgent(); 1045 const task = insertTask('generate_sbom', { format: 'spdx' }); 1046 await agent.generateSbom(task); 1047 assert.ok(['completed', 'failed'].includes(getTask(task.id).status)); 1048 }); 1049 1050 test('generateSbom - unsupported format uses fallback', async () => { 1051 const agent = await makeAgent(); 1052 const task = insertTask('generate_sbom', { format: 'unsupported-xyz' }); 1053 await agent.generateSbom(task); 1054 assert.ok(['completed', 'failed'].includes(getTask(task.id).status)); 1055 }); 1056 1057 // ===== SECTION 16: reviewDependencyUpdate ===== 1058 1059 test('reviewDependencyUpdate - fails when package_name missing', async () => { 1060 const agent = await makeAgent(); 1061 const task = insertTask('review_dependency_update', { 1062 old_version: '1.0.0', 1063 new_version: '2.0.0', 1064 }); 1065 await agent.reviewDependencyUpdate(task); 1066 const updated = getTask(task.id); 1067 assert.equal(updated.status, 'failed'); 1068 assert.match(updated.error_message, /package_name/); 1069 }); 1070 1071 test('reviewDependencyUpdate - completes with package_name provided', async () => { 1072 const agent = await makeAgent(); 1073 const task = insertTask('review_dependency_update', { 1074 package_name: 'express', 1075 old_version: '4.18.0', 1076 new_version: '4.19.0', 1077 }); 1078 await agent.reviewDependencyUpdate(task); 1079 const updated = getTask(task.id); 1080 assert.ok(['completed', 'failed'].includes(updated.status)); 1081 if (updated.status === 'completed') { 1082 assert.ok('approved' in updated.result_json); 1083 } 1084 }); 1085 1086 // ===== SECTION 17: processTask - all routing branches ===== 1087 1088 test('processTask - routes audit_code', async () => { 1089 const agent = await makeAgent(); 1090 const task = insertTask('audit_code', { 1091 files: ['/tmp/sqli-safe.js'], 1092 focus_areas: ['sql_injection'], 1093 }); 1094 await agent.processTask(task); 1095 assert.equal(getTask(task.id).status, 'completed'); 1096 }); 1097 1098 test('processTask - routes verify_compliance', async () => { 1099 const agent = await makeAgent(); 1100 const file = await writeTmp('pt-vc.js', '// STOP\nif (hour >= 8 && hour <= 21) sendSms();'); 1101 const task = insertTask('verify_compliance', { compliance_type: 'tcpa', files: [file] }); 1102 await agent.processTask(task); 1103 assert.equal(getTask(task.id).status, 'completed'); 1104 }); 1105 1106 test('processTask - routes scan_secrets', async () => { 1107 const agent = await makeAgent(); 1108 const file = await writeTmp('pt-ss.js', 'const k = process.env.KEY;'); 1109 const task = insertTask('scan_secrets', { files: [file] }); 1110 await agent.processTask(task); 1111 assert.equal(getTask(task.id).status, 'completed'); 1112 }); 1113 1114 test('processTask - routes threat_model', async () => { 1115 const agent = await makeAgent(); 1116 const task = insertTask('threat_model', { 1117 component: 'function x(i) { return db.query(i); }', 1118 component_type: 'api', 1119 }); 1120 await agent.processTask(task); 1121 assert.equal(getTask(task.id).status, 'completed'); 1122 }); 1123 1124 test('processTask - routes fix_security_issue (no file, escalates)', async () => { 1125 const agent = await makeAgent(); 1126 const task = insertTask('fix_security_issue', { 1127 vulnerability: 'SQL Injection', 1128 description: 'SQL injection in query builder', 1129 risk_level: 'high', 1130 }); 1131 await agent.processTask(task); 1132 const updated = getTask(task.id); 1133 assert.equal(updated.status, 'completed'); 1134 assert.ok('developer_task_id' in updated.result_json); 1135 }); 1136 1137 test('processTask - routes generate_sbom', async () => { 1138 const agent = await makeAgent(); 1139 const task = insertTask('generate_sbom', {}); 1140 await agent.processTask(task); 1141 assert.ok(['completed', 'failed'].includes(getTask(task.id).status)); 1142 }); 1143 1144 test('processTask - routes review_dependency_update to scan_dependencies', async () => { 1145 const agent = await makeAgent(); 1146 const task = insertTask('review_dependency_update', {}); 1147 try { 1148 await agent.processTask(task); 1149 } catch (_e) { 1150 /* ignore */ 1151 } 1152 assert.ok(['completed', 'failed', 'pending'].includes(getTask(task.id).status)); 1153 }); 1154 1155 test('processTask - routes implement_feature to delegateToCorrectAgent', async () => { 1156 const agent = await makeAgent(); 1157 const task = insertTask('implement_feature', { 1158 feature_name: 'OAuth2', 1159 description: 'Add OAuth2', 1160 }); 1161 await agent.processTask(task); 1162 assert.equal(getTask(task.id).status, 'completed'); 1163 }); 1164 1165 test('processTask - routes fix_bug to delegateToCorrectAgent', async () => { 1166 const agent = await makeAgent(); 1167 const task = insertTask('fix_bug', { error: 'TypeError: cannot read foo' }); 1168 await agent.processTask(task); 1169 assert.equal(getTask(task.id).status, 'completed'); 1170 }); 1171 1172 test('processTask - routes unknown task type to delegateToCorrectAgent', async () => { 1173 const agent = await makeAgent(); 1174 const task = insertTask('totally_unknown_task_type', {}); 1175 await agent.processTask(task); 1176 assert.equal(getTask(task.id).status, 'completed'); 1177 }); 1178 1179 test('processTask - handles null context_json', async () => { 1180 const agent = await makeAgent(); 1181 const db2 = new Database(TEST_DB_PATH); 1182 const res = db2 1183 .prepare( 1184 'INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) VALUES (?, ?, ?, NULL)' 1185 ) 1186 .run('scan_secrets', 'security', 5); 1187 const task = db2.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(res.lastInsertRowid); 1188 db2.close(); 1189 task.context_json = null; 1190 await agent.processTask(task); 1191 assert.equal(getTask(task.id).status, 'completed'); 1192 }); 1193 1194 test('processTask - handles string context_json (auto-parses)', async () => { 1195 const agent = await makeAgent(); 1196 const file = await writeTmp('pt-str.js', 'const k = process.env.KEY;'); 1197 const db2 = new Database(TEST_DB_PATH); 1198 const res = db2 1199 .prepare( 1200 'INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) VALUES (?, ?, ?, ?)' 1201 ) 1202 .run('scan_secrets', 'security', 5, JSON.stringify({ files: [file] })); 1203 const task = db2.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(res.lastInsertRowid); 1204 db2.close(); 1205 await agent.processTask(task); 1206 assert.equal(getTask(task.id).status, 'completed'); 1207 }); 1208 1209 test('processTask - threat_model with empty context fails gracefully', async () => { 1210 const agent = await makeAgent(); 1211 const task = insertTask('threat_model', {}); 1212 await agent.processTask(task); 1213 assert.equal(getTask(task.id).status, 'failed'); 1214 }); 1215 1216 // ===== SECTION 18: Constructor and accessors ===== 1217 1218 test('SecurityAgent - constructor sets agent name to security', () => { 1219 assert.equal(new SecurityAgent().agentName, 'security'); 1220 }); 1221 1222 test('SecurityAgent - all public methods exist', () => { 1223 const agent = new SecurityAgent(); 1224 const methods = [ 1225 'processTask', 1226 'auditCode', 1227 'checkSqlInjection', 1228 'checkSecrets', 1229 'checkCommandInjection', 1230 'getJsFiles', 1231 'scanDependencies', 1232 'verifyCompliance', 1233 'checkTcpaCompliance', 1234 'checkCanSpamCompliance', 1235 'checkGdprCompliance', 1236 'scanSecrets', 1237 'threatModel', 1238 'calculateDreadScore', 1239 'getRiskLevel', 1240 'getSecurityContext', 1241 'fixSecurityIssue', 1242 'reviewDependencyUpdate', 1243 'generateSbom', 1244 ]; 1245 for (const m of methods) { 1246 assert.equal(typeof agent[m], 'function', `${m} should be a function`); 1247 } 1248 });