graph-seed-meta-bootstrap.test.js 4.49 KB
/**
 * Unit Tests — verifyCounts 必须镜像 seedCompanyAgents 的 meta-bootstrap 过滤
 *
 * `seedCompanyAgents` 对 `type: 'meta-bootstrap'` 的公司角色是 `continue`:
 * 既不写 org_graph_nodes 也不写 system_agents。而 `verifyCounts` 复算期望值时
 * 原先把 `company.json` 的 `agents[]` **全部**算进去 —— 于是任何一家公司的
 * agents[] 里出现一个 meta-bootstrap 角色,对账就确定性变红。
 *
 * 不是假设:用户手工走查建的 `mycompany` 有 10 个角色,其中 `orggraph-planning`
 * 是 meta-bootstrap,对账报 `expected 10 / actual 9`。也就是说这条缺陷是被
 * **正常使用**触发的 —— 界面上点一遍新建公司就会踩到。
 *
 * 这个文件单独存在(不并进 graph-seed-service.test.js)的原因:那边 hermetic 块的
 * 断言钉着精确的行数/节点数,往它的夹具里加一个角色会连带改掉既有断言。
 * 这里用 `GRAPH_SEED_ROOT` 自建一个只有两个角色的最小夹具,互不干扰。
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';

const MODULES = ['../../services/graph-db', '../../services/graph-seed-service'];

describe('verifyCounts × meta-bootstrap 过滤', () => {
  let seed;
  let root;
  let dbPath;

  const COMPANY = 'co-mb';
  const NORMAL_ROLE = 'org-planner';
  const BOOTSTRAP_ROLE = 'orggraph-planning';

  beforeAll(() => {
    root = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-mb-root-'));
    dbPath = path.join(root, 'graph.sqlite');

    // 最小夹具:system/agents/<id>/{agent.md, config.json} + system/agents/_registry.json
    // + data/companies/<cid>/company.json
    //
    // config.json 放在**本体**里(28 批次 4):公司不再持有 agent 实体,
    // isMetaBootstrap() 读的就是本体那一份。放回公司侧的话它读不到、恒返回 false,
    // 症状正是本文件要防的那个 —— meta-bootstrap 角色被算进期望值。
    for (const [id, type] of [[NORMAL_ROLE, 'executor'], [BOOTSTRAP_ROLE, 'meta-bootstrap']]) {
      const d = path.join(root, 'system', 'agents', id);
      fs.mkdirSync(d, { recursive: true });
      fs.writeFileSync(path.join(d, 'agent.md'), `# ${id}\n`);
      fs.writeFileSync(path.join(d, 'config.json'), JSON.stringify({ type }));
    }
    fs.writeFileSync(path.join(root, 'system', 'agents', '_registry.json'), JSON.stringify({}));

    const companyDir = path.join(root, 'data', 'companies', COMPANY);
    fs.mkdirSync(companyDir, { recursive: true });
    fs.writeFileSync(
      path.join(companyDir, 'company.json'),
      JSON.stringify({ id: COMPANY, name: COMPANY, agents: [NORMAL_ROLE, BOOTSTRAP_ROLE] })
    );

    process.env.GRAPH_SEED_ROOT = root;
    process.env.GRAPH_DB_PATH = dbPath;
    for (const m of MODULES) delete require.cache[require.resolve(m)];
    seed = require('../../services/graph-seed-service');
  });

  afterAll(() => {
    delete process.env.GRAPH_SEED_ROOT;
    delete process.env.GRAPH_DB_PATH;
    try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
    for (const m of MODULES) delete require.cache[require.resolve(m)];
  });

  it('种完之后对账干净:meta-bootstrap 角色不算进期望值', () => {
    // runStartupSeed 只做 reconcile + seedAgents,**不含** seedCompanyAgents
    // (两个 server 入口启动时也不种公司角色)—— 所以公司那部分要显式调。
    // 少了这一行的话对账会报 expected 1 / actual 0,看着像本条要防的那个 bug,
    // 其实是夹具没种东西:两种原因症状相同,所以下面第二条用例专门把它们分开。
    seed.runStartupSeed();
    seed.seedCompanyAgents();
    const v = seed.verifyCounts();
    // 期望值把 meta-bootstrap 算进去时,这里会是
    // [{check: "org_graph_nodes COUNT(*) WHERE company_id='co-mb'", expected: 2, actual: 1}]
    expect(v.mismatches).toEqual([]);
    expect(v.ok).toBe(true);
  });

  it('这个夹具确实覆盖到了那条分支(否则上一条是空跑)', () => {
    const db = require('../../services/graph-db').getDb();
    const rows = db
      .prepare(`SELECT role_name FROM org_graph_nodes WHERE company_id = ? ORDER BY role_name`)
      .all(COMPANY)
      .map((r) => r.role_name);
    // 普通角色种进去了、meta-bootstrap 角色没有 —— 两侧都断言,
    // 只断言「对账干净」的话,seed 一个都没种时它也会“通过”
    expect(rows).toEqual([NORMAL_ROLE]);
  });
});