graph-agent-p1-hire.test.js
6.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* Integration Test — company-service.hire() P1 引用模式
*
* 覆盖 docs/todo/system-design/27-p1-virtual-reference.md §6 验收标准 1/3/5:
* 1. system_agents 里有 source_kind='template' 行的角色(qa-judge) → 引用模式:
* 私有目录没有 agent.md,有 company-context.md(含公司 ID),config.json 有
* systemAgentRef,org_graph_nodes 新增一行。
* 2. 不在 system_agents 里的角色 → 完全 legacy 行为(含 agent.md 被拷贝)。
* 3. 幂等性:同一 (companyId, role) 调用 hire() 两次,第二次 already_exists,
* org_graph_nodes 不重复。
*
* 用 GRAPH_DB_PATH 覆盖到临时 SQLite 文件(同 graph-service.test.js / graph-db.test.js
* 的模式),永不触碰真实 data/graph.sqlite;用临时公司 id,测试完清理 data/companies/
* 下对应目录,不污染真实数据。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
describe('company-service.hire() — P1 virtual reference mode', () => {
const TEST_DB_PATH = '/tmp/test-graph-agent-p1-hire.sqlite';
const REPO_ROOT = path.resolve(__dirname, '../../..');
const DATA_DIR = path.join(REPO_ROOT, 'data/companies');
const TEMPLATES_DIR = path.join(REPO_ROOT, 'system/templates');
const TEST_COMPANY_ID = `tmp-p1-hire-test-co-${Date.now()}`;
const LEGACY_TEMPLATE_ROLE = `tmp-p1-legacy-role-${Date.now()}`;
const legacyTemplateDir = path.join(TEMPLATES_DIR, LEGACY_TEMPLATE_ROLE);
let graphDb;
let CompanyService;
let companyService;
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')];
delete require.cache[require.resolve('../../services/company-service')];
graphDb = require('../../services/graph-db');
({ CompanyService } = require('../../services/company-service'));
companyService = new CompanyService();
// Seed a 'template' row for qa-judge pointing at the real template dir (already exists
// in the repo with a real agent.md) — this is exactly what npm run graph:seed produced
// in commit 8421f55, just re-created here against the isolated test DB.
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 (?, ?, 'public', NULL, 'template', ?, 'active', NULL, ?, ?)
`).run('qa-judge', 'qa-judge', 'system/templates/qa-judge', now, now);
// Fixture template dir for the "role not in system_agents" legacy-fallback case.
// Deliberately NOT inserted into system_agents, so lookupSharedAgentMd() returns null
// and hire() must fall back to the full legacy copy (including agent.md).
fs.mkdirSync(legacyTemplateDir, { recursive: true });
fs.writeFileSync(path.join(legacyTemplateDir, 'agent.md'), '# legacy fixture agent\n', 'utf-8');
fs.writeFileSync(path.join(legacyTemplateDir, 'config.json'), JSON.stringify({ type: 'skill', name: LEGACY_TEMPLATE_ROLE }), 'utf-8');
// Company fixture (CompanyService.hire() requires an existing company).
companyService.create({ id: TEST_COMPANY_ID, name: 'P1 Hire Test Co', description: 'test fixture' });
});
afterAll(() => {
try { fs.rmSync(path.join(DATA_DIR, TEST_COMPANY_ID), { recursive: true, force: true }); } catch {}
try { fs.rmSync(legacyTemplateDir, { recursive: true, force: true }); } catch {}
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;
});
it('reference mode: hiring a system_agents template role does not copy agent.md, writes company-context.md, sets systemAgentRef, records org_graph_nodes', () => {
const result = companyService.hire(TEST_COMPANY_ID, 'qa-judge', {});
expect(result.status).toBe('hired_from_template');
const agentDir = result.agentDir;
expect(fs.existsSync(path.join(agentDir, 'agent.md'))).toBe(false);
const contextPath = path.join(agentDir, 'company-context.md');
expect(fs.existsSync(contextPath)).toBe(true);
const contextContent = fs.readFileSync(contextPath, 'utf-8');
expect(contextContent).toContain(TEST_COMPANY_ID);
expect(contextContent).toContain('## 所属公司');
const config = JSON.parse(fs.readFileSync(path.join(agentDir, 'config.json'), 'utf-8'));
expect(config.systemAgentRef).toBe('qa-judge');
// Non-shared files must still be provisioned privately (workDir constraint, §3.3 of the design doc).
expect(fs.existsSync(path.join(agentDir, 'soul.md'))).toBe(true);
expect(fs.existsSync(path.join(agentDir, 'identity.md'))).toBe(true);
expect(fs.existsSync(path.join(agentDir, 'memory/memory.md'))).toBe(true);
const db = graphDb.getDb();
const node = db.prepare('SELECT * FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get(TEST_COMPANY_ID, 'qa-judge');
expect(node).toBeTruthy();
expect(node.system_agent_id).toBe('qa-judge');
});
it('legacy fallback: hiring a role absent from system_agents copies agent.md and writes no company-context.md / systemAgentRef', () => {
const result = companyService.hire(TEST_COMPANY_ID, LEGACY_TEMPLATE_ROLE, {});
expect(result.status).toBe('hired_from_template');
const agentDir = result.agentDir;
expect(fs.existsSync(path.join(agentDir, 'agent.md'))).toBe(true);
const agentMdContent = fs.readFileSync(path.join(agentDir, 'agent.md'), 'utf-8');
expect(agentMdContent).toContain('legacy fixture agent');
expect(agentMdContent).toContain('## 所属公司'); // still appended in-place, legacy behavior
expect(fs.existsSync(path.join(agentDir, 'company-context.md'))).toBe(false);
const config = JSON.parse(fs.readFileSync(path.join(agentDir, 'config.json'), 'utf-8'));
expect(config.systemAgentRef).toBeUndefined();
// Still gets an org_graph_nodes audit row even though it went through the legacy path.
const db = graphDb.getDb();
const node = db.prepare('SELECT * FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get(TEST_COMPANY_ID, LEGACY_TEMPLATE_ROLE);
expect(node).toBeTruthy();
});
it('idempotency: hiring the same (companyId, role) twice returns already_exists the second time and does not duplicate org_graph_nodes', () => {
const role = 'qa-judge'; // already hired by the first test above
const second = companyService.hire(TEST_COMPANY_ID, role, {});
expect(second.status).toBe('already_exists');
const db = graphDb.getDb();
const count = db.prepare('SELECT COUNT(*) AS c FROM org_graph_nodes WHERE company_id = ? AND role_name = ?').get(TEST_COMPANY_ID, role).c;
expect(count).toBe(1);
});
});