/** * Unit Tests — graph-service.js (P1) * * Uses GRAPH_DB_PATH env override (same pattern as graph-db.test.js / * graph-seed-service.test.js) so the real data/graph.sqlite is never touched. */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import fs from 'fs'; import path from 'path'; describe('graph-service', () => { let graphDb; let graphService; const TEST_DB_PATH = '/tmp/test-graph-service.sqlite'; beforeAll(() => { if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); process.env.GRAPH_DB_PATH = TEST_DB_PATH; delete require.cache[require.resolve('../../services/graph-db')]; delete require.cache[require.resolve('../../services/graph-service')]; graphDb = require('../../services/graph-db'); graphService = require('../../services/graph-service'); }); afterAll(() => { try { fs.unlinkSync(TEST_DB_PATH); } catch {} try { fs.unlinkSync(`${TEST_DB_PATH}-shm`); } catch {} try { fs.unlinkSync(`${TEST_DB_PATH}-wal`); } catch {} delete process.env.GRAPH_DB_PATH; }); beforeEach(() => { // Fresh tables between tests so assertions don't depend on ordering. const db = graphDb.getDb(); db.exec('DELETE FROM org_graph_nodes; DELETE FROM system_agents;'); }); function insertSystemAgent({ id, source_kind, legacy_path, scope = 'public', owner_company_id = null }) { const db = graphDb.getDb(); const now = new Date().toISOString(); db.prepare(` INSERT INTO system_agents (id, name, scope, owner_company_id, source_kind, legacy_path, status, benchmark_score, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'active', NULL, ?, ?) `).run(id, id, scope, owner_company_id, source_kind, legacy_path, now, now); } describe('lookupSharedAgentMd', () => { it('returns the resolved agent.md path when a template row exists', () => { insertSystemAgent({ id: 'qa-judge', source_kind: 'template', legacy_path: 'system/templates/qa-judge' }); const result = graphService.lookupSharedAgentMd('qa-judge'); expect(result).toBe(path.resolve(__dirname, '../../..', 'system/templates/qa-judge', 'agent.md')); }); it('returns null when the only matching row is source_kind=global_agent (naming-collision regression)', () => { // Regression case: evolver-youtube-crawler exists under BOTH system/agents/ and // system/templates/ but the template row got skipped by seedTemplates() because // seedGlobalAgents() claimed the id first. Only a global_agent row exists here — // lookupSharedAgentMd must NOT fall back to it, or idearun's already-hired copy // (which was physically copied from system/templates/) would silently start // reading a different file it was never hired with. insertSystemAgent({ id: 'evolver-youtube-crawler', source_kind: 'global_agent', legacy_path: 'system/agents/evolver-youtube-crawler', }); const result = graphService.lookupSharedAgentMd('evolver-youtube-crawler'); expect(result).toBeNull(); }); it('returns null when the id does not exist at all', () => { const result = graphService.lookupSharedAgentMd('does-not-exist'); expect(result).toBeNull(); }); }); describe('upsertOrgGraphNode', () => { it('creates an org_graph_nodes row when system_agents already has the id', () => { insertSystemAgent({ id: 'qa-judge', source_kind: 'template', legacy_path: 'system/templates/qa-judge' }); graphService.upsertOrgGraphNode('acme-co', 'qa-judge', 'qa-judge'); const db = graphDb.getDb(); const node = db.prepare('SELECT * FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get('acme-co', 'qa-judge'); expect(node).toBeTruthy(); expect(node.system_agent_id).toBe('qa-judge'); // Must not have touched/duplicated the existing system_agents row. const agentCount = db.prepare('SELECT COUNT(*) AS c FROM system_agents WHERE id = ?').get('qa-judge').c; expect(agentCount).toBe(1); }); it('inserts a fallback company_hired system_agents row when the id is missing', () => { graphService.upsertOrgGraphNode('acme-co', 'custom-role', 'custom-role'); const db = graphDb.getDb(); const agent = db.prepare('SELECT * FROM system_agents WHERE id = ?').get('custom-role'); expect(agent).toBeTruthy(); expect(agent.scope).toBe('company'); expect(agent.owner_company_id).toBe('acme-co'); expect(agent.source_kind).toBe('company_hired'); expect(agent.legacy_path).toBe('data/companies/acme-co/agents/custom-role'); expect(agent.status).toBe('active'); expect(agent.benchmark_score).toBeNull(); const node = db.prepare('SELECT * FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get('acme-co', 'custom-role'); expect(node).toBeTruthy(); expect(node.system_agent_id).toBe('custom-role'); }); it('is idempotent: calling twice for the same (companyId, roleName) does not duplicate org_graph_nodes rows', () => { insertSystemAgent({ id: 'qa-judge', source_kind: 'template', legacy_path: 'system/templates/qa-judge' }); graphService.upsertOrgGraphNode('acme-co', 'qa-judge', 'qa-judge'); graphService.upsertOrgGraphNode('acme-co', 'qa-judge', 'qa-judge'); const db = graphDb.getDb(); const count = db.prepare('SELECT COUNT(*) AS c FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get('acme-co', 'qa-judge').c; expect(count).toBe(1); }); }); });