test-innovator-pipeline.js 9.5 KB
#!/usr/bin/env node

/**
 * 集成测试: Innovator 8-Skill Pipeline — 创建"一品布衣"小说写作 Skill
 *
 * 测试场景:
 *   用户消息: "帮我写一部历史穿越小说《一品布衣》"
 *   预期流程: dispatch → hunt → benchmark → digest → skill-creator → evolving
 *   预期产出: skills/yipin_buyi/ 下的完整 skill 目录
 *
 * 用法:
 *   node scripts/test-innovator-pipeline.js          # 完整 pipeline 测试
 *   node scripts/test-innovator-pipeline.js --dry-run # 仅测试配置和 skill 加载
 *
 * 注意: 完整测试需要 CLI agent 可用 (kimi/claude),可能耗时较长
 */

const path = require('path');
const fs = require('fs');

const ROOT = path.resolve(__dirname, '..');
const SKILLS_DIR = path.join(ROOT, 'skills');

// ─── 测试配置 ───

const TEST_MESSAGE = `帮我写一部历史穿越小说,小说名称:一品布衣,类型:历史古代、穿越、逆袭、家国情怀。\n小说概要:"牧哥儿,要老婆不?开金口就送来。" 现代工程师徐牧穿成市井无赖,遇逃难富家女。乱世之中,他刀弓仗马,誓要护家人活下去。`;

const EXPECTED_SKILL_NAME = 'yipin_buyi'; // dispatch 应该推断出的 skill name

const DRY_RUN = process.argv.includes('--dry-run');

// ─── Helpers ───

function log(step, msg) {
    const ts = new Date().toISOString().substring(11, 19);
    console.log(`[${ts}] [${step}] ${msg}`);
}

function checkFile(filePath, description) {
    if (fs.existsSync(filePath)) {
        const size = fs.statSync(filePath).size;
        log('CHECK', `✓ ${description} (${size} bytes)`);
        return true;
    } else {
        log('CHECK', `✗ ${description} — 不存在`);
        return false;
    }
}

// ─── Dry Run: 仅验证配置 ───

function dryRunTest() {
    log('DRY-RUN', '=== 配置验证模式 ===\n');

    // 1. 检查 8 个 system skill
    const SYSTEM_SKILLS = [
        'dispatch', 'deep-research', 'hunt', 'benchmark',
        'digest', 'skill-creator', 'benchmark-evaluator', 'evolving',
    ];

    let allOk = true;
    log('DRY-RUN', '--- System Skills ---');
    for (const s of SYSTEM_SKILLS) {
        const ok = checkFile(path.join(SKILLS_DIR, s, 'SKILL.md'), `skills/${s}/SKILL.md`);
        if (!ok) allOk = false;
    }

    // 2. 检查 InnovatorService 能否实例化
    log('DRY-RUN', '\n--- InnovatorService ---');
    try {
        const { InnovatorService } = require(path.join(ROOT, 'server/services/innovator-service'));
        const service = new InnovatorService({
            skillsDir: SKILLS_DIR,
            agentManagerFactory: () => ({ callAgent: async () => ({ output: '{}' }) }),
        });
        log('DRY-RUN', `✓ InnovatorService 实例化成功`);
        log('DRY-RUN', `  skillsDir: ${service.skillsDir}`);
        log('DRY-RUN', `  conversationsDir: ${service.conversationsDir}`);
        log('DRY-RUN', `  registry skills: ${service.registry.listSkills().length}`);
    } catch (err) {
        log('DRY-RUN', `✗ InnovatorService 实例化失败: ${err.message}`);
        allOk = false;
    }

    // 3. 检查 agents.json
    log('DRY-RUN', '\n--- Agent Config ---');
    try {
        const agentConfig = JSON.parse(fs.readFileSync(
            path.join(ROOT, 'server/config/agents.json'), 'utf-8'
        ));
        const writerAgent = agentConfig.agents?.writer;
        if (writerAgent) {
            log('DRY-RUN', `✓ writer agent: ${writerAgent.launcher} (${writerAgent.protocol})`);
        } else {
            log('DRY-RUN', `✗ writer agent 未配置`);
            allOk = false;
        }
    } catch (err) {
        log('DRY-RUN', `✗ agents.json 读取失败: ${err.message}`);
        allOk = false;
    }

    console.log('\n' + (allOk ? '✅ Dry run 全部通过' : '❌ Dry run 有问题'));
    return allOk;
}

// ─── Full Pipeline Test ───

async function fullPipelineTest() {
    log('TEST', '=== Innovator 8-Skill Pipeline 完整测试 ===');
    log('TEST', `用户消息: ${TEST_MESSAGE.substring(0, 60)}...`);
    log('TEST', '');

    // 先做 dry run
    if (!dryRunTest()) {
        log('TEST', '\n❌ Dry run 失败,跳过完整测试');
        process.exit(1);
    }

    console.log('\n' + '═'.repeat(60));
    log('TEST', '开始完整 pipeline 测试...\n');

    // 实例化 InnovatorService
    const { InnovatorService } = require(path.join(ROOT, 'server/services/innovator-service'));
    const AgentManager = require(path.join(ROOT, 'server/src/core/agent-manager'));

    function createAgentManager(workDir) {
        // 合并 agents.json + backends.json
        const agentsConfig = JSON.parse(fs.readFileSync(
            path.join(ROOT, 'server/config/agents.json'), 'utf-8'
        ));
        const backendsConfig = JSON.parse(fs.readFileSync(
            path.join(ROOT, 'server/config/backends.json'), 'utf-8'
        ));

        // AgentManager.getAgentConfig 需要 agent.client → config.clients[xxx]
        // 但静态配置用的是 agent.launcher → backends.launchers[xxx]
        // 将 launchers 转化为 clients 格式
        const clients = {};
        for (const [name, launcher] of Object.entries(backendsConfig.launchers || {})) {
            clients[name] = {
                name,
                command: launcher.command,
                acpArgs: launcher.args || [],
                acpProtocol: name.includes('claude') ? 'claude-stream' : 'acp',
                deploy: 'local',
            };
        }

        // 将 agents 的 launcher 映射为 client
        const agents = {};
        for (const [name, agent] of Object.entries(agentsConfig.agents || {})) {
            agents[name] = {
                ...agent,
                client: agent.launcher || agent.client, // launcher → client
                deploy: 'local',
            };
        }

        const mergedConfig = {
            ...agentsConfig,
            agents,
            clients,
            ...backendsConfig,
        };

        return new AgentManager(mergedConfig, workDir);
    }

    const innovator = new InnovatorService({
        skillsDir: SKILLS_DIR,
        agentManagerFactory: createAgentManager,
    });

    // 监听进度事件
    innovator.on('progress', (event) => {
        log(event.step.toUpperCase(), event.message);
    });

    // 运行 dispatch
    const startTime = Date.now();
    try {
        log('TEST', '>>> 调用 innovator.dispatch()...');
        const result = await innovator.dispatch(TEST_MESSAGE);

        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
        log('TEST', `\n>>> 完成!耗时 ${elapsed}s`);
        log('TEST', `action: ${result.action}`);
        log('TEST', `skillName: ${result.skillName}`);
        log('TEST', `runId: ${result.runId}`);

        if (result.output) {
            log('TEST', `output (前 200 字): ${result.output.substring(0, 200)}...`);
        }

        // 验证产物
        console.log('\n' + '═'.repeat(60));
        log('VERIFY', '=== 验证产物 ===\n');

        const skillName = result.skillName;
        const skillDir = path.join(SKILLS_DIR, skillName);

        if (result.action === 'created') {
            // 验证新创建的 skill 目录
            checkFile(path.join(skillDir, 'SKILL.md'), `${skillName}/SKILL.md`);
            checkFile(path.join(skillDir, 'benchmark.md'), `${skillName}/benchmark.md`);
            checkFile(path.join(skillDir, 'hunter', 'knowhow.md'), `${skillName}/hunter/knowhow.md`);
            checkFile(path.join(skillDir, 'hunter', 'toplist.md'), `${skillName}/hunter/toplist.md`);
            checkFile(path.join(skillDir, 'digest', 'knowhow.md'), `${skillName}/digest/knowhow.md`);
            checkFile(path.join(skillDir, 'evolve', 'evolve_log.md'), `${skillName}/evolve/evolve_log.md`);

            // 验证 conversation 日志
            const convDir = path.join(ROOT, 'conversations', result.runId);
            checkFile(path.join(convDir, 'messages.log'), `conversations/${result.runId}/messages.log`);
            checkFile(path.join(convDir, 'dispatchResult.md'), `conversations/${result.runId}/dispatchResult.md`);
            checkFile(path.join(convDir, 'dispatch.log'), `conversations/${result.runId}/dispatch.log`);
            checkFile(path.join(convDir, 'hunt.log'), `conversations/${result.runId}/hunt.log`);

            // 验证 CLI 同步
            checkFile(path.join(ROOT, '.claude', 'skills', skillName, 'SKILL.md'), `.claude/skills/${skillName}/SKILL.md`);

            // 检查 skill registry
            const registeredSkill = innovator.registry.getSkill(skillName);
            if (registeredSkill) {
                log('VERIFY', `\n✓ Skill 已注册:`);
                log('VERIFY', `  status: ${registeredSkill.status}`);
                log('VERIFY', `  version: ${registeredSkill.version}`);
                log('VERIFY', `  score: ${registeredSkill.currentScore}`);
                log('VERIFY', `  domain: ${registeredSkill.domain}`);
            }
        } else {
            log('VERIFY', `action=${result.action}, 使用了已有 Skill`);
        }

        console.log('\n✅ 完整 pipeline 测试完成');

    } catch (err) {
        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
        log('ERROR', `\n❌ Pipeline 测试失败 (耗时 ${elapsed}s)`);
        log('ERROR', err.message);
        log('ERROR', err.stack);
        process.exit(1);
    }
}

// ─── Main ───

if (DRY_RUN) {
    const ok = dryRunTest();
    process.exit(ok ? 0 : 1);
} else {
    fullPipelineTest().catch(err => {
        console.error(err);
        process.exit(1);
    });
}