/ __quarantined_tests__ / agents / developer-deep.test.js
developer-deep.test.js
  1  /**
  2   * Developer Agent Deep Coverage Tests
  3   *
  4   * Uses method-level mocking to test implementation paths inside
  5   * fixBug, implementFeature, refactorCode, applyFeedback, createCommit,
  6   * escalateCoverageToHuman, and getFileCoverage.
  7   */
  8  
  9  import { test, describe, beforeEach, afterEach } from 'node:test';
 10  import assert from 'node:assert/strict';
 11  import Database from 'better-sqlite3';
 12  import { DeveloperAgent } from '../../src/agents/developer.js';
 13  import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js';
 14  import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js';
 15  import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js';
 16  import fsPromises from 'fs/promises';
 17  
 18  const TEST_DB_PATH = './tests/agents/test-developer-deep.db';
 19  let db;
 20  let agent;
 21  
 22  beforeEach(async () => {
 23    try {
 24      await fsPromises.unlink(TEST_DB_PATH);
 25    } catch (_e) {
 26      /* ignore */
 27    }
 28    db = new Database(TEST_DB_PATH);
 29    process.env.DATABASE_PATH = TEST_DB_PATH;
 30    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 31    process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
 32    db.exec(`
 33      CREATE TABLE agent_tasks (
 34        id INTEGER PRIMARY KEY AUTOINCREMENT,
 35        task_type TEXT NOT NULL,
 36        assigned_to TEXT NOT NULL,
 37        created_by TEXT,
 38        status TEXT DEFAULT 'pending',
 39        priority INTEGER DEFAULT 5,
 40        context_json TEXT,
 41        result_json TEXT,
 42        parent_task_id INTEGER,
 43        error_message TEXT,
 44        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 45        started_at DATETIME,
 46        completed_at DATETIME,
 47        retry_count INTEGER DEFAULT 0
 48      );
 49      CREATE TABLE agent_messages (
 50        id INTEGER PRIMARY KEY AUTOINCREMENT,
 51        task_id INTEGER,
 52        from_agent TEXT NOT NULL,
 53        to_agent TEXT NOT NULL,
 54        message_type TEXT,
 55        content TEXT NOT NULL,
 56        metadata_json TEXT,
 57        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 58        read_at DATETIME
 59      );
 60      CREATE TABLE agent_logs (
 61        id INTEGER PRIMARY KEY AUTOINCREMENT,
 62        task_id INTEGER,
 63        agent_name TEXT NOT NULL,
 64        log_level TEXT,
 65        message TEXT,
 66        data_json TEXT,
 67        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 68      );
 69      CREATE TABLE agent_state (
 70        agent_name TEXT PRIMARY KEY,
 71        last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
 72        current_task_id INTEGER,
 73        status TEXT DEFAULT 'idle',
 74        metrics_json TEXT
 75      );
 76      CREATE TABLE agent_outcomes (
 77        id INTEGER PRIMARY KEY AUTOINCREMENT,
 78        task_id INTEGER NOT NULL,
 79        agent_name TEXT NOT NULL,
 80        task_type TEXT NOT NULL,
 81        outcome TEXT NOT NULL,
 82        context_json TEXT,
 83        result_json TEXT,
 84        duration_ms INTEGER,
 85        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 86      );
 87      CREATE TABLE agent_llm_usage (
 88        id INTEGER PRIMARY KEY AUTOINCREMENT,
 89        agent_name TEXT NOT NULL,
 90        task_id INTEGER,
 91        model TEXT NOT NULL,
 92        prompt_tokens INTEGER NOT NULL,
 93        completion_tokens INTEGER NOT NULL,
 94        cost_usd DECIMAL(10, 6) NOT NULL,
 95        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 96      );
 97      CREATE TABLE structured_logs (
 98        id INTEGER PRIMARY KEY AUTOINCREMENT,
 99        agent_name TEXT,
100        task_id INTEGER,
101        level TEXT,
102        message TEXT,
103        data_json TEXT,
104        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
105      );
106    `);
107    agent = new DeveloperAgent();
108    await agent.initialize();
109  });
110  
111  afterEach(async () => {
112    resetBaseDb();
113    resetTaskDb();
114    resetMessageDb();
115    if (db) db.close();
116    try {
117      await fsPromises.unlink(TEST_DB_PATH);
118    } catch (_e) {
119      /* ignore */
120    }
121  });
122  
123  describe('DeveloperAgent Deep - escalateCoverageToHuman real implementation', () => {
124    test('sends question to architect with file details', async () => {
125      const taskId = db
126        .prepare(
127          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
128        )
129        .run(
130          'fix_bug',
131          'developer',
132          'pending',
133          JSON.stringify({ error_message: 'test' })
134        ).lastInsertRowid;
135  
136      const belowThreshold = [
137        { file: 'src/score.js', coverage: 60, gap: 25 },
138        { file: 'src/enrich.js', coverage: 72, gap: 13 },
139      ];
140  
141      await agent.escalateCoverageToHuman(belowThreshold, taskId);
142  
143      const messages = db
144        .prepare(
145          "SELECT * FROM agent_messages WHERE from_agent = 'developer' AND to_agent = 'architect' AND message_type = 'question'"
146        )
147        .all();
148  
149      assert.ok(messages.length >= 1);
150      const { content } = messages[0];
151      assert.ok(content.includes('src/score.js'), 'should mention score.js');
152      assert.ok(content.includes('85%'), 'should mention 85% threshold');
153    });
154  
155    test('escalateCoverageToHuman includes all below-threshold files in message', async () => {
156      const taskId = db
157        .prepare(
158          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
159        )
160        .run(
161          'fix_bug',
162          'developer',
163          'pending',
164          JSON.stringify({ error_message: 'test' })
165        ).lastInsertRowid;
166  
167      const belowThreshold = [{ file: 'src/score.js', coverage: 60, gap: 25 }];
168  
169      await agent.escalateCoverageToHuman(belowThreshold, taskId);
170  
171      const messages = db
172        .prepare(
173          "SELECT * FROM agent_messages WHERE from_agent = 'developer' AND to_agent = 'architect'"
174        )
175        .all();
176      assert.ok(messages.length >= 1);
177      // Message should mention options a, b, c
178      assert.ok(
179        messages[0].content.includes('(a)') || messages[0].content.includes('Refactor'),
180        'message should include guidance options'
181      );
182    });
183  });
184  
185  describe('DeveloperAgent Deep - createCommit escalation path', () => {
186    test('createCommit calls attemptWriteTestsForCoverage when coverage fails', async () => {
187      const originalCheckCoverage = agent.checkCoverageBeforeCommit.bind(agent);
188      const originalAttemptWrite = agent.attemptWriteTestsForCoverage.bind(agent);
189      const originalEscalate = agent.escalateCoverageToHuman.bind(agent);
190  
191      let attemptWriteCalled = false;
192      let escalateCalled = false;
193  
194      agent.checkCoverageBeforeCommit = async function (files, taskId) {
195        return {
196          canCommit: false,
197          belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }],
198          coverage: {},
199        };
200      };
201  
202      agent.attemptWriteTestsForCoverage = async function (bt, tid) {
203        attemptWriteCalled = true;
204        return false;
205      };
206  
207      agent.escalateCoverageToHuman = async function (bt, tid) {
208        escalateCalled = true;
209        await this.log('warn', 'Escalating coverage', { task_id: tid });
210      };
211  
212      const taskId = db
213        .prepare(
214          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
215        )
216        .run(
217          'fix_bug',
218          'developer',
219          'pending',
220          JSON.stringify({ error_message: 'test' })
221        ).lastInsertRowid;
222  
223      await assert.rejects(
224        () => agent.createCommit('fix: something', ['src/score.js'], taskId),
225        /Coverage gate failed/
226      );
227  
228      assert.ok(attemptWriteCalled, 'attemptWriteTestsForCoverage should have been called');
229      assert.ok(escalateCalled, 'escalateCoverageToHuman should have been called');
230  
231      agent.checkCoverageBeforeCommit = originalCheckCoverage;
232      agent.attemptWriteTestsForCoverage = originalAttemptWrite;
233      agent.escalateCoverageToHuman = originalEscalate;
234    });
235  
236    test('createCommit re-checks coverage after attemptWriteTestsForCoverage returns true', async () => {
237      const originalCheckCoverage = agent.checkCoverageBeforeCommit.bind(agent);
238      const originalAttemptWrite = agent.attemptWriteTestsForCoverage.bind(agent);
239  
240      let checkCallCount = 0;
241  
242      agent.checkCoverageBeforeCommit = async function (files, taskId) {
243        checkCallCount++;
244        return {
245          canCommit: false,
246          belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }],
247          coverage: {},
248        };
249      };
250  
251      agent.attemptWriteTestsForCoverage = async function (bt, tid) {
252        return true; // Thinks it wrote tests
253      };
254  
255      const taskId = db
256        .prepare(
257          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
258        )
259        .run(
260          'fix_bug',
261          'developer',
262          'pending',
263          JSON.stringify({ error_message: 'test' })
264        ).lastInsertRowid;
265  
266      await assert.rejects(
267        () => agent.createCommit('fix: something', ['src/score.js'], taskId),
268        /Coverage still below 85%/
269      );
270  
271      assert.strictEqual(checkCallCount, 2, 'coverage should be checked twice');
272  
273      agent.checkCoverageBeforeCommit = originalCheckCoverage;
274      agent.attemptWriteTestsForCoverage = originalAttemptWrite;
275    });
276  });
277  
278  describe('DeveloperAgent Deep - getFileCoverage real error path', () => {
279    test('getFileCoverage returns 0 coverage when npm test fails (real method)', async () => {
280      // This exercises the actual error-handling path in getFileCoverage (lines 1585-1596)
281      // npm test will fail in this environment without proper setup
282      const result = await agent.getFileCoverage(['src/fake-module-xyz123.js']);
283  
284      assert.strictEqual(typeof result, 'object');
285      assert.ok('src/fake-module-xyz123.js' in result, 'should have entry for the file');
286      // In a test environment without proper npm test setup, returns 0
287      assert.strictEqual(result['src/fake-module-xyz123.js'], 0);
288    });
289  
290    test('getFileCoverage handles empty files array', async () => {
291      const result = await agent.getFileCoverage([]);
292      assert.deepStrictEqual(result, {});
293    });
294  });
295  
296  describe('DeveloperAgent Deep - checkCoverageBeforeCommit real implementation paths', () => {
297    test('calls getFileCoverage with source files and returns canCommit false when below 85', async () => {
298      const originalGetFileCoverage = agent.getFileCoverage.bind(agent);
299      let calledWithFiles = null;
300  
301      agent.getFileCoverage = async function (files) {
302        calledWithFiles = files;
303        const results = {};
304        for (const f of files) results[f] = 60;
305        return results;
306      };
307  
308      const taskId = db
309        .prepare(
310          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
311        )
312        .run(
313          'fix_bug',
314          'developer',
315          'pending',
316          JSON.stringify({ error_message: 'test' })
317        ).lastInsertRowid;
318  
319      const result = await agent.checkCoverageBeforeCommit(['src/score.js'], taskId);
320  
321      assert.strictEqual(result.canCommit, false);
322      assert.deepStrictEqual(calledWithFiles, ['src/score.js']);
323      assert.strictEqual(result.belowThreshold.length, 1);
324      assert.ok(result.reason.includes('85%'));
325  
326      agent.getFileCoverage = originalGetFileCoverage;
327    });
328  
329    test('calls getFileCoverage and returns canCommit true when all files pass', async () => {
330      const originalGetFileCoverage = agent.getFileCoverage.bind(agent);
331  
332      agent.getFileCoverage = async function (files) {
333        const results = {};
334        for (const f of files) results[f] = 90;
335        return results;
336      };
337  
338      const taskId = db
339        .prepare(
340          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
341        )
342        .run(
343          'fix_bug',
344          'developer',
345          'pending',
346          JSON.stringify({ error_message: 'test' })
347        ).lastInsertRowid;
348  
349      const result = await agent.checkCoverageBeforeCommit(['src/score.js', 'src/enrich.js'], taskId);
350  
351      assert.strictEqual(result.canCommit, true);
352      assert.ok('src/score.js' in result.coverage);
353  
354      agent.getFileCoverage = originalGetFileCoverage;
355    });
356  });
357  
358  describe('DeveloperAgent Deep - runTests real implementation', () => {
359    test('agent.runTests with no files builds npm test command', async () => {
360      // Test the agent instance runTests method (not the imported utility)
361      // This method uses execSync which will fail in test environment
362      // Testing that it returns success: false gracefully when tests fail
363      const result = await agent.runTests([]);
364      assert.strictEqual(typeof result, 'object');
365      assert.ok('success' in result);
366      assert.ok('output' in result);
367    });
368  
369    test('agent.runTests with specific files builds file-specific command', async () => {
370      const result = await agent.runTests(['src/fake-file.js']);
371      assert.strictEqual(typeof result, 'object');
372      assert.ok('success' in result);
373    });
374  });