/ __quarantined_tests__ / agents / security.test.js
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  });