okr-service.test.js 3.98 KB
/**
 * Unit Tests — okr-service.js + migrate-goals-to-okr.js
 *
 * 覆盖 27-NAMING §六 正名后的两件事:
 *   1. OkrService 用新命名工作(okr/ 目录、O- 前缀、objective.json、Objective/KeyResult 层级)
 *   2. 迁移脚本的纯函数把旧 ID 正确改写(含 nodes key、children 数组)
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';

const { OkrService } = require('../../services/okr-service');
const { reId, reObjective } = require('../../../scripts/migrate-goals-to-okr');

const companyDirOf = (cid) => path.resolve(__dirname, '../../../data/companies', cid);

describe('OkrService(27-NAMING §六 正名后)', () => {
  let tmpCompany;

  beforeEach(() => {
    // OkrService 按 companyId 落在 data/companies/{id}/okr/。id 固定 + 用例前后各清一次:
    // 随机后缀只是让「清理漏了那次」的垃圾换个名字继续留在盘上(用例是串行的,不会撞)。
    tmpCompany = '__test-okr';
    fs.rmSync(companyDirOf(tmpCompany), { recursive: true, force: true });
  });

  afterEach(() => {
    fs.rmSync(companyDirOf(tmpCompany), { recursive: true, force: true });
  });

  it('目录用 okr/ 而不是 goals/', () => {
    const svc = new OkrService(tmpCompany);
    expect(svc.okrDir.endsWith(path.join(tmpCompany, 'okr'))).toBe(true);
    expect(fs.existsSync(svc.okrDir)).toBe(true);
  });

  it('新建 Objective 用 O- 前缀,落盘为 objective.json', () => {
    const svc = new OkrService(tmpCompany);
    const obj = svc.create({ title: '测试目标', owner: 'ceo' });

    expect(obj.id).toBe('O-001');
    expect(fs.existsSync(path.join(svc.okrDir, 'O-001', 'objective.json'))).toBe(true);
    // 旧文件名不应再出现
    expect(fs.existsSync(path.join(svc.okrDir, 'O-001', 'goal.json'))).toBe(false);
  });

  it('层级名是 Objective / KeyResult / SubTask(不再有 Goal 这一层)', () => {
    const svc = new OkrService(tmpCompany);
    expect(svc.parseNodeId('O-001').levelName).toBe('Objective');
    expect(svc.parseNodeId('O-001.1').levelName).toBe('KeyResult');
    expect(svc.parseNodeId('O-001.1.1').levelName).toBe('SubTask');
    // 返回的是 objectiveId,不是 goalId
    expect(svc.parseNodeId('O-001.1.1').objectiveId).toBe('O-001');
  });

  it('addNode / updateNode / get 走通,ID 全程 O- 前缀', () => {
    const svc = new OkrService(tmpCompany);
    const obj = svc.create({ title: '目标', owner: 'ceo' });

    const { nodeId } = svc.addNode(obj.id, null, { title: 'KR1', owner: 'dev' });
    expect(nodeId).toBe('O-001.1');

    svc.updateNode(obj.id, nodeId, { status: 'done' });
    expect(svc.get(obj.id).nodes[nodeId].status).toBe('done');
    expect(svc.getProgress(obj.id).done).toBe(1);
  });

  it('updateObjective 存在(原 updateGoal 已改名)', () => {
    const svc = new OkrService(tmpCompany);
    const obj = svc.create({ title: '旧标题', owner: 'ceo' });
    expect(typeof svc.updateObjective).toBe('function');
    expect(svc.updateObjective(obj.id, { title: '新标题' }).title).toBe('新标题');
  });
});

describe('migrate-goals-to-okr 纯函数', () => {
  it('reId 只改 G- 前缀,其余原样', () => {
    expect(reId('G-001')).toBe('O-001');
    expect(reId('G-001.1.1')).toBe('O-001.1.1');
    expect(reId('O-001')).toBe('O-001');       // 幂等
    expect(reId('T-abc')).toBe('T-abc');       // 不误伤 taskId
    expect(reId(null)).toBe(null);
  });

  it('reObjective 改写 id、nodes 的 key、以及 children 数组', () => {
    const out = reObjective({
      id: 'G-001',
      title: 'x',
      nodes: {
        'G-001.1': { title: 'O1', children: ['G-001.1.1'] },
        'G-001.1.1': { title: 'KR1', children: [] },
      },
    });

    expect(out.id).toBe('O-001');
    expect(Object.keys(out.nodes).sort()).toEqual(['O-001.1', 'O-001.1.1']);
    expect(out.nodes['O-001.1'].children).toEqual(['O-001.1.1']);
    expect(out.title).toBe('x');  // 非 ID 字段不动
  });
});