guanghulab/glada/tests/glada-smoke.test.js
2026-05-10 13:12:44 +08:00

939 lines
40 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* GLADA · 冒烟测试 · glada-smoke.test.js
*
* 验证所有模块可以正常加载和基本功能可用。
*
* 用法: node glada/tests/glada-smoke.test.js
*
* 版权:国作登字-2026-A-00037559
*/
'use strict';
const path = require('path');
const fs = require('fs');
const ROOT = path.resolve(__dirname, '../..');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed++;
} catch (err) {
console.log(`${name}: ${err.message}`);
failed++;
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
console.log('🧪 GLADA 冒烟测试\n');
// ── 预加载模块E2E测试需要──
const taskReceiver = require('../task-receiver');
const contextBuilder = require('../context-builder');
const stepExecutor = require('../step-executor');
const notifier = require('../notifier');
// ── 1. 模块加载测试 ──
console.log('📦 模块加载:');
test('task-receiver 加载', () => {
const mod = require('../task-receiver');
assert(typeof mod.scanPendingTasks === 'function', 'scanPendingTasks 不是函数');
assert(typeof mod.validateTaskSpec === 'function', 'validateTaskSpec 不是函数');
assert(typeof mod.convertToGladaTask === 'function', 'convertToGladaTask 不是函数');
assert(typeof mod.receiveNextTask === 'function', 'receiveNextTask 不是函数');
assert(typeof mod.updateTask === 'function', 'updateTask 不是函数');
assert(typeof mod.archiveTask === 'function', 'archiveTask 不是函数');
});
test('context-builder 加载', () => {
const mod = require('../context-builder');
assert(typeof mod.buildContext === 'function', 'buildContext 不是函数');
assert(typeof mod.contextToSystemPrompt === 'function', 'contextToSystemPrompt 不是函数');
assert(typeof mod.scanTargetFiles === 'function', 'scanTargetFiles 不是函数');
assert(typeof mod.scanDependencies === 'function', 'scanDependencies 不是函数');
});
test('step-executor 加载', () => {
const mod = require('../step-executor');
assert(typeof mod.executeStep === 'function', 'executeStep 不是函数');
assert(typeof mod.parseFileChanges === 'function', 'parseFileChanges 不是函数');
assert(typeof mod.snapshotFiles === 'function', 'snapshotFiles 不是函数');
assert(typeof mod.applyFileChanges === 'function', 'applyFileChanges 不是函数');
});
test('code-generator 加载', () => {
const mod = require('../code-generator');
assert(typeof mod.findDependents === 'function', 'findDependents 不是函数');
assert(typeof mod.regressionCheck === 'function', 'regressionCheck 不是函数');
assert(typeof mod.executeWithRegressionGuard === 'function', 'executeWithRegressionGuard 不是函数');
});
test('git-operator 加载', () => {
const mod = require('../git-operator');
assert(typeof mod.getCurrentBranch === 'function', 'getCurrentBranch 不是函数');
assert(typeof mod.createTaskBranch === 'function', 'createTaskBranch 不是函数');
assert(typeof mod.commitStep === 'function', 'commitStep 不是函数');
});
test('notifier 加载', () => {
const mod = require('../notifier');
assert(typeof mod.notify === 'function', 'notify 不是函数');
assert(typeof mod.buildNotification === 'function', 'buildNotification 不是函数');
assert(typeof mod.sendEmail === 'function', 'sendEmail 不是函数');
assert(typeof mod.sendWeCom === 'function', 'sendWeCom 不是函数');
});
test('execution-loop 加载', () => {
const mod = require('../execution-loop');
assert(typeof mod.executeTask === 'function', 'executeTask 不是函数');
assert(typeof mod.startLoop === 'function', 'startLoop 不是函数');
});
// ── 2. 功能测试 ──
console.log('\n⚙ 功能测试:');
test('任务规格验证 - 合法', () => {
const { validateTaskSpec } = require('../task-receiver');
const result = validateTaskSpec({
task_id: 'CAB-20260417-001',
authorization: { sovereign: '冰朔 · TCS-0002∞' },
development_plan: {
title: '测试任务',
steps: ['步骤1', '步骤2']
}
});
assert(result.valid === true, `应该是合法的,但返回: ${JSON.stringify(result)}`);
});
test('任务规格验证 - 非法(无授权)', () => {
const { validateTaskSpec } = require('../task-receiver');
const result = validateTaskSpec({
task_id: 'CAB-20260417-001',
authorization: { sovereign: '其他人' },
development_plan: { title: '测试', steps: ['步骤1'] }
});
assert(result.valid === false, '应该是非法的');
assert(result.errors.length > 0, '应该有错误信息');
});
test('任务规格验证 - 非法(无步骤)', () => {
const { validateTaskSpec } = require('../task-receiver');
const result = validateTaskSpec({
task_id: 'CAB-20260417-001',
authorization: { sovereign: '冰朔 · TCS-0002∞' },
development_plan: { title: '测试', steps: [] }
});
assert(result.valid === false, '应该是非法的');
});
test('CAB → GLADA 任务转换', () => {
const { convertToGladaTask } = require('../task-receiver');
const spec = {
task_id: 'CAB-20260417-001',
authorization: { sovereign: '冰朔 · TCS-0002∞' },
development_plan: {
title: '测试项目',
description: '这是一个测试',
steps: ['创建文件', '修改配置'],
priority: 'high'
},
architecture: { summary: '架构描述' },
constraints: { no_touch_files: ['.github/'], required_tests: true }
};
const task = convertToGladaTask(spec);
assert(task.glada_task_id === 'GLADA-CAB-20260417-001', `任务ID错误: ${task.glada_task_id}`);
assert(task.plan.steps.length === 2, `步骤数错误: ${task.plan.steps.length}`);
assert(task.plan.steps[0].step_id === 1, '步骤1 ID错误');
assert(task.plan.steps[0].description === '创建文件', '步骤1 描述错误');
assert(task.plan.priority === 'high', '优先级错误');
assert(task.status === 'pending', '状态错误');
assert(Array.isArray(task.execution_log), '缺少 execution_log');
});
test('LLM 输出解析 - JSON 代码块', () => {
const { parseFileChanges } = require('../step-executor');
const output = '这是一些解释\n```json\n{"reasoning":"测试","files":[{"path":"test.txt","action":"create","content":"hello"}],"summary":"创建测试文件"}\n```\n更多解释';
const result = parseFileChanges(output);
assert(result !== null, '解析失败');
assert(result.files.length === 1, `文件数错误: ${result.files.length}`);
assert(result.files[0].path === 'test.txt', '路径错误');
assert(result.reasoning === '测试', '推理错误');
});
test('LLM 输出解析 - 纯 JSON', () => {
const { parseFileChanges } = require('../step-executor');
const output = '{"reasoning":"纯JSON","files":[{"path":"a.js","action":"modify","content":"code"}],"summary":"修改"}';
const result = parseFileChanges(output);
assert(result !== null, '解析失败');
assert(result.files[0].action === 'modify', '操作错误');
});
test('LLM 输出解析 - 无效格式', () => {
const { parseFileChanges } = require('../step-executor');
const result = parseFileChanges('这是一段普通文本');
assert(result === null, '应该返回 null');
});
test('文件快照和恢复', () => {
const { snapshotFiles, restoreFromSnapshot } = require('../step-executor');
// 创建测试文件
const testFile = '/tmp/glada-test-snapshot.txt';
fs.writeFileSync(testFile, 'original content', 'utf-8');
// 拍摄快照(使用相对路径模拟)
const snapshot = new Map();
snapshot.set(testFile, 'original content');
// 修改文件
fs.writeFileSync(testFile, 'modified content', 'utf-8');
assert(fs.readFileSync(testFile, 'utf-8') === 'modified content', '文件没有被修改');
// 清理
fs.unlinkSync(testFile);
});
test('上下文构建', () => {
const { buildContext, contextToSystemPrompt } = require('../context-builder');
const mockTask = {
glada_task_id: 'GLADA-TEST-001',
plan: {
title: '测试任务',
description: '测试描述',
steps: [{ step_id: 1, description: '步骤1', status: 'pending' }]
},
architecture: { summary: '测试架构', target_files: [] },
constraints: {},
reasoning_context: {},
execution_log: []
};
const context = buildContext(mockTask);
assert(context.task_id === 'GLADA-TEST-001', 'task_id 不匹配');
assert(context.sections.identity, '缺少 identity section');
assert(context.sections.task, '缺少 task section');
const prompt = contextToSystemPrompt(context);
assert(typeof prompt === 'string', 'prompt 不是字符串');
assert(prompt.includes('铸渊'), 'prompt 缺少铸渊身份');
});
test('通知内容构建', () => {
const { buildNotification } = require('../notifier');
const mockTask = {
glada_task_id: 'GLADA-TEST-001',
plan: {
title: '测试任务',
steps: [
{ step_id: 1, description: '步骤1', status: 'completed' },
{ step_id: 2, description: '步骤2', status: 'pending' }
]
},
completion: {
total_files_changed: ['a.js', 'b.js'],
git_branch: 'glada/test',
git_commits: ['abc1234: 步骤1']
}
};
const notif = buildNotification(mockTask, 'completed');
assert(notif.subject.includes('GLADA-TEST-001'), '主题缺少任务ID');
assert(notif.body.includes('测试任务'), '正文缺少任务标题');
assert(notif.body.includes('a.js'), '正文缺少变更文件');
assert(notif.html, '缺少 HTML 邮件内容');
assert(notif.html.includes('GLADA-TEST-001'), 'HTML 缺少任务ID');
assert(notif.html.includes('a.js'), 'HTML 缺少变更文件');
assert(notif.html.includes('铸渊'), 'HTML 缺少铸渊签名');
});
test('文件依赖扫描', () => {
const { scanDependencies } = require('../context-builder');
// 扫描一个已知文件的依赖
const serverPath = path.join(ROOT, 'server.js');
if (fs.existsSync(serverPath)) {
const deps = scanDependencies(serverPath);
assert(Array.isArray(deps), 'deps 不是数组');
}
});
test('中央任务队列 glada-task 类型已注册', () => {
const taskQueuePath = path.join(ROOT, 'core', 'task-queue', 'index.js');
assert(fs.existsSync(taskQueuePath), 'core/task-queue/index.js 不存在');
const content = fs.readFileSync(taskQueuePath, 'utf-8');
assert(content.includes("'glada-task'"), 'glada-task 类型未注册到中央任务队列');
});
test('CAB 任务模板包含 execution_plan 字段', () => {
const templatePath = path.join(ROOT, 'bridge', 'chat-to-agent', 'task-template.json');
assert(fs.existsSync(templatePath), 'task-template.json 不存在');
const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
assert(template.execution_plan, '缺少 execution_plan 字段');
assert(template.execution_plan.executor === 'glada', 'executor 应为 glada');
assert(template.execution_plan.rollback_on_failure === true, 'rollback_on_failure 应为 true');
});
test('ecosystem.config.js 包含 env 加载逻辑', () => {
const ecosystemPath = path.join(ROOT, 'glada', 'ecosystem.config.js');
const content = fs.readFileSync(ecosystemPath, 'utf-8');
assert(content.includes('loadEnvFile'), 'ecosystem.config.js 缺少 loadEnvFile 函数');
assert(content.includes('.env.glada'), '缺少 .env.glada 加载');
});
test('execution-loop 集成中央任务队列', () => {
const loopPath = path.join(ROOT, 'glada', 'execution-loop.js');
const content = fs.readFileSync(loopPath, 'utf-8');
assert(content.includes('core/task-queue'), '缺少 core/task-queue 集成');
assert(content.includes('dequeueFromCentralQueue'), '缺少 dequeueFromCentralQueue 函数');
assert(content.includes('glada-task'), '缺少 glada-task 类型检查');
});
// ── E2E 集成测试 ──
test('E2E: CAB任务提交→转换→入队→状态查询完整链路', () => {
const cabSpec = {
cab_version: '1.0',
task_id: 'CAB-20260417-999',
created_at: new Date().toISOString(),
created_by: 'TCS-0002∞',
status: 'pending',
authorization: {
sovereign: '冰朔 · TCS-0002∞',
authorized_agent: 'copilot-agent',
scope: 'full-auto-development'
},
architecture: {
summary: 'E2E 测试任务',
target_files: ['glada/README.md'],
target_modules: ['glada']
},
development_plan: {
title: 'E2E 集成测试任务',
description: '验证完整链路',
steps: ['步骤1: 检查文件', '步骤2: 修改文件'],
priority: 'normal'
},
execution_plan: {
executor: 'glada',
model_preference: 'deepseek-chat',
retry_policy: { max_retries: 2, backoff_ms: 1000 }
},
constraints: {
no_touch_files: ['.github/brain/'],
required_tests: false,
max_files_changed: 5
}
};
// 1. 验证任务规格
const validation = taskReceiver.validateTaskSpec(cabSpec);
assert(validation.valid, `验证失败: ${validation.errors.join(', ')}`);
// 2. 转换为 GLADA 任务
const gladaTask = taskReceiver.convertToGladaTask(cabSpec);
assert(gladaTask.glada_task_id === 'GLADA-CAB-20260417-999', 'ID 转换错误');
assert(gladaTask.plan.steps.length === 2, '步骤数应为 2');
assert(gladaTask.plan.steps[0].status === 'pending', '步骤状态应为 pending');
assert(gladaTask.constraints.no_touch_files.length === 1, '约束丢失');
// 3. 保存到临时队列并读回
const tmpQueueDir = '/tmp/glada-e2e-test-queue';
fs.mkdirSync(tmpQueueDir, { recursive: true });
const queueFile = path.join(tmpQueueDir, `${gladaTask.glada_task_id}.json`);
fs.writeFileSync(queueFile, JSON.stringify(gladaTask, null, 2), 'utf-8');
const readBack = JSON.parse(fs.readFileSync(queueFile, 'utf-8'));
assert(readBack.glada_task_id === gladaTask.glada_task_id, '队列读回不一致');
assert(readBack.plan.title === 'E2E 集成测试任务', '标题不一致');
// 4. 构建上下文
const context = contextBuilder.buildContext(gladaTask);
assert(context.sections.identity, '上下文缺少 identity');
assert(context.sections.task, '上下文缺少 task');
const systemPrompt = contextBuilder.contextToSystemPrompt(context);
assert(systemPrompt.length > 100, '系统提示词过短');
assert(systemPrompt.includes('铸渊'), '提示词应包含铸渊身份');
// 5. 构建步骤提示词
const stepPrompt = stepExecutor.buildStepPrompt(gladaTask.plan.steps[0], gladaTask);
assert(stepPrompt.includes('步骤 1'), '步骤提示词应包含步骤编号');
assert(stepPrompt.includes('JSON'), '步骤提示词应要求 JSON 输出');
// 6. 验证通知构建
gladaTask.status = 'completed';
gladaTask.completion = {
total_files_changed: ['test.js'],
git_branch: 'glada/test',
git_commits: ['abc123']
};
const notification = notifier.buildNotification(gladaTask, 'completed');
assert(notification.subject.includes('GLADA'), '通知标题应包含 GLADA');
assert(notification.body.includes('test.js'), '通知正文应包含变更文件');
// 清理
fs.unlinkSync(queueFile);
try { fs.rmdirSync(tmpQueueDir); } catch { /* ok */ }
});
test('step-executor 支持 LLM 重试配置', () => {
const content = fs.readFileSync(path.join(ROOT, 'glada', 'step-executor.js'), 'utf-8');
assert(content.includes('maxRetries'), '缺少 maxRetries 参数');
assert(content.includes('backoffMs'), '缺少 backoffMs 参数');
assert(content.includes('execution_plan'), '缺少 execution_plan 集成');
assert(content.includes('retry_policy'), '缺少 retry_policy 读取');
});
test('service.js 包含速率限制', () => {
const content = fs.readFileSync(path.join(ROOT, 'glada', 'service.js'), 'utf-8');
assert(content.includes('rateLimit'), '缺少速率限制函数');
assert(content.includes('429'), '缺少 429 状态码');
});
test('package.json 包含 express 依赖', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'glada', 'package.json'), 'utf-8'));
assert(pkg.dependencies && pkg.dependencies.express, '缺少 express 依赖');
});
test('package.json 包含 nodemailer 依赖', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'glada', 'package.json'), 'utf-8'));
assert(pkg.dependencies && pkg.dependencies.nodemailer, '缺少 nodemailer 依赖');
});
test('notifier.js 已替换钉钉为邮箱+企业微信', () => {
const content = fs.readFileSync(path.join(ROOT, 'glada', 'notifier.js'), 'utf-8');
// 应包含邮箱通道
assert(content.includes('sendEmail'), '缺少 sendEmail 函数');
assert(content.includes('nodemailer'), '缺少 nodemailer 引用');
assert(content.includes('smtp.qq.com'), '缺少 QQ 邮箱 SMTP 配置');
assert(content.includes('ZY_SMTP_USER'), '缺少 ZY_SMTP_USER 环境变量');
// 应包含企业微信预留通道
assert(content.includes('sendWeCom'), '缺少 sendWeCom 函数');
assert(content.includes('WECOM_WEBHOOK'), '缺少 WECOM_WEBHOOK 环境变量');
// 不应包含钉钉
assert(!content.includes('sendDingTalk'), '仍包含已废弃的 sendDingTalk');
assert(!content.includes('DINGTALK_WEBHOOK'), '仍包含已废弃的 DINGTALK_WEBHOOK');
});
test('通知 HTML 邮件模板包含光湖视觉风格', () => {
const { buildNotification } = require('../notifier');
const mockTask = {
glada_task_id: 'GLADA-TEST-HTML',
plan: {
title: 'HTML模板测试',
steps: [
{ step_id: 1, description: '步骤A', status: 'completed' },
{ step_id: 2, description: '步骤B', status: 'failed', error: '测试错误' }
]
},
completion: { total_files_changed: ['test.js'], git_branch: 'glada/html-test', git_commits: ['hash: msg'] }
};
// 测试 completed
const completed = buildNotification(mockTask, 'completed');
assert(completed.html.includes('<!DOCTYPE html>'), 'HTML 缺少 DOCTYPE');
assert(completed.html.includes('渊'), 'HTML 缺少铸渊标识');
assert(completed.html.includes('#050810'), 'HTML 缺少光湖暗色背景');
// 测试 failed
const failed = buildNotification(mockTask, 'failed');
assert(failed.html.includes('f87171'), 'failed HTML 应包含红色');
assert(failed.subject.includes('❌'), 'failed 主题应包含 ❌');
// 测试 started
const started = buildNotification(mockTask, 'started');
assert(started.html.includes('🚀'), 'started HTML 应包含 🚀');
});
test('deploy workflow 注入 SMTP 到 .env.glada', () => {
const content = fs.readFileSync(path.join(ROOT, '.github', 'workflows', 'deploy-to-zhuyuan-server.yml'), 'utf-8');
assert(content.includes('ZY_SMTP_USER') && content.includes('.env.glada'), '部署 workflow 缺少 SMTP 注入到 .env.glada');
});
test('install-check.js 预飞检查工具存在', () => {
const checkPath = path.join(ROOT, 'glada', 'install-check.js');
assert(fs.existsSync(checkPath), 'install-check.js 不存在');
const content = fs.readFileSync(checkPath, 'utf-8');
assert(content.includes('ZY_LLM_API_KEY'), '预飞检查应检查 API Key');
assert(content.includes('ZY_LLM_BASE_URL'), '预飞检查应检查 Base URL');
assert(content.includes('testLLMConnection'), '预飞检查应测试 LLM 连通性');
assert(content.includes('ZY_SMTP_USER'), '预飞检查应检查 SMTP');
assert(content.includes('WECOM_WEBHOOK'), '预飞检查应检查企业微信');
});
// ── 3. Skill 蒸馏测试Hermes-inspired · Phase 1 ──
console.log('\n🧪 Skill 蒸馏测试:');
test('skill-distiller 模块加载', () => {
const mod = require('../skill-distiller');
assert(typeof mod.distillSkill === 'function', 'distillSkill 不是函数');
assert(typeof mod.saveSkill === 'function', 'saveSkill 不是函数');
assert(typeof mod.loadAllSkills === 'function', 'loadAllSkills 不是函数');
assert(typeof mod.findRelevantSkills === 'function', 'findRelevantSkills 不是函数');
assert(typeof mod.skillsToContext === 'function', 'skillsToContext 不是函数');
assert(typeof mod.distillAndSave === 'function', 'distillAndSave 不是函数');
assert(typeof mod.extractTags === 'function', 'extractTags 不是函数');
});
test('Skill 蒸馏 - 成功任务生成 HNL 格式 skill', () => {
const { distillSkill } = require('../skill-distiller');
const mockTask = {
glada_task_id: 'GLADA-CAB-20260418-001',
status: 'completed',
plan: {
title: '添加用户认证 API',
description: '实现 login 和 register 接口',
steps: [
{ step_id: 1, description: '创建 schema', status: 'completed', files_changed: ['src/schemas/hli/auth/login.schema.json'], reasoning: 'HLI 协议要求 schema 优先' },
{ step_id: 2, description: '创建路由', status: 'completed', files_changed: ['src/routes/hli/auth/login.js'], reasoning: '路由遵循 HLI 约定' }
]
},
execution_log: [
{ step_id: 1, action: '创建 schema', status: 'completed', reasoning: 'HLI 协议要求', files_changed: ['src/schemas/hli/auth/login.schema.json'], duration_ms: 5000 },
{ step_id: 2, action: '创建路由', status: 'completed', reasoning: '遵循约定', files_changed: ['src/routes/hli/auth/login.js'], duration_ms: 8000 }
],
architecture: { target_files: ['src/routes/hli/auth/'], target_modules: ['auth'] },
constraints: { no_touch_files: ['.github/'], required_tests: true }
};
const skill = distillSkill(mockTask);
assert(skill !== null, 'skill 不应为 null');
assert(skill.hnl_v === '1.0', 'HNL 版本应为 1.0');
assert(skill.type === 'SKILL', '类型应为 SKILL');
assert(skill.from === 'YM001/ZY001', '发送者应为铸渊');
assert(skill.skill.name === '添加用户认证 API', 'skill 名称错误');
assert(skill.skill.step_patterns.length === 2, '步骤模式数量错误');
assert(skill.skill.success_rate === 1, '成功率应为 1');
assert(skill.skill.file_domain.length === 2, '文件域数量错误');
assert(skill.memory_sovereignty.can_forget === true, '记忆主权:应可遗忘');
assert(skill.op.includes('GROW'), '操作应为 GROW');
});
test('Skill 蒸馏 - 未完成任务返回 null', () => {
const { distillSkill } = require('../skill-distiller');
const result = distillSkill({ status: 'failed', plan: { steps: [] }, execution_log: [] });
assert(result === null, '未完成任务应返回 null');
});
test('Skill 蒸馏 - 部分失败任务包含教训', () => {
const { distillSkill } = require('../skill-distiller');
const mockTask = {
glada_task_id: 'GLADA-CAB-20260418-002',
status: 'completed',
plan: {
title: '数据库迁移',
steps: [
{ step_id: 1, description: '备份数据', status: 'completed', files_changed: ['scripts/backup.sh'] },
{ step_id: 2, description: '迁移表', status: 'failed', error: 'SQL 语法错误' },
{ step_id: 3, description: '验证迁移', status: 'completed', files_changed: ['tests/migration.test.js'] }
]
},
execution_log: [],
architecture: {},
constraints: {}
};
const skill = distillSkill(mockTask);
assert(skill !== null, 'skill 不应为 null');
assert(skill.skill.lessons_learned.length === 1, '应有 1 条教训');
assert(skill.skill.lessons_learned[0].error === 'SQL 语法错误', '教训应包含错误信息');
assert(skill.skill.success_rate < 1, '成功率应小于 1');
});
test('Skill 标签提取', () => {
const { extractTags } = require('../skill-distiller');
const tags = extractTags('添加 HLI API 认证中间件', [
{ description: '创建 auth middleware', files_changed: ['src/middleware/auth.js'] }
]);
assert(Array.isArray(tags), 'tags 应为数组');
assert(tags.length > 0, 'tags 不应为空');
assert(tags.includes('middleware'), '应包含 middleware 标签');
assert(tags.includes('src'), '应包含 src 模块标签');
});
test('Skill 查找 - 按文件域匹配', () => {
const { findRelevantSkills, saveSkill, SKILLS_DIR } = require('../skill-distiller');
// 创建临时 skill
const tmpSkill = {
type: 'SKILL',
id: 'SKILL-TEST-MATCH',
skill: {
name: '测试 Skill',
tags: ['glada', 'test'],
success_rate: 1,
step_patterns: [],
file_domain: ['glada/notifier.js', 'glada/service.js'],
lessons_learned: [],
architecture_context: { target_files: ['glada/'], target_modules: ['glada'] }
}
};
// 保存测试 skill
fs.mkdirSync(SKILLS_DIR, { recursive: true });
const testPath = path.join(SKILLS_DIR, 'SKILL-TEST-MATCH.json');
fs.writeFileSync(testPath, JSON.stringify(tmpSkill, null, 2), 'utf-8');
// 查找匹配
const results = findRelevantSkills({
architecture: { target_files: ['glada/notifier.js'], target_modules: ['glada'] },
plan: { title: 'GLADA 通知改进' }
});
// 清理
try { fs.unlinkSync(testPath); } catch { /* ok */ }
assert(results.length > 0, '应找到匹配的 skill');
assert(results[0].skill.name === '测试 Skill', '匹配的 skill 名称错误');
});
test('Skill 格式化为上下文', () => {
const { skillsToContext } = require('../skill-distiller');
const mockSkills = [{
skill: {
name: '测试上下文 Skill',
source_task: 'GLADA-TEST-001',
success_rate: 0.85,
step_patterns: [
{ description: '创建文件', reasoning: '规范要求', files_changed: ['test.js'] }
],
lessons_learned: [
{ step_description: '编译', error: '类型错误' }
],
file_domain: ['test.js']
}
}];
const context = skillsToContext(mockSkills);
assert(context.includes('测试上下文 Skill'), '上下文应包含 skill 名称');
assert(context.includes('85%'), '上下文应包含成功率');
assert(context.includes('教训'), '上下文应包含教训');
assert(context.includes('test.js'), '上下文应包含文件');
});
test('skillsToContext 空 skills 返回空字符串', () => {
const { skillsToContext } = require('../skill-distiller');
assert(skillsToContext([]) === '', '空 skills 应返回空字符串');
assert(skillsToContext(null) === '', 'null 应返回空字符串');
});
// ── 4. 自我反思测试Hermes-inspired · Phase 2 ──
console.log('\n🪞 自我反思测试:');
test('reflector 模块加载', () => {
const mod = require('../reflector');
assert(typeof mod.buildReflectionPrompt === 'function', 'buildReflectionPrompt 不是函数');
assert(typeof mod.parseReflection === 'function', 'parseReflection 不是函数');
assert(typeof mod.wrapAsHNL === 'function', 'wrapAsHNL 不是函数');
assert(typeof mod.reflectOffline === 'function', 'reflectOffline 不是函数');
assert(typeof mod.reflectWithLLM === 'function', 'reflectWithLLM 不是函数');
assert(typeof mod.loadRecentReflections === 'function', 'loadRecentReflections 不是函数');
});
test('反思提示词构建', () => {
const { buildReflectionPrompt } = require('../reflector');
const mockTask = {
glada_task_id: 'GLADA-TEST-REFLECT',
status: 'completed',
plan: {
title: '反思测试任务',
description: '用于测试反思功能',
steps: [
{ step_id: 1, description: '步骤1', status: 'completed', reasoning: '推理1', files_changed: ['a.js'] },
{ step_id: 2, description: '步骤2', status: 'failed', error: '测试错误' }
]
},
execution_log: [
{ step_id: 1, action: '步骤1', status: 'completed', reasoning: '推理1', files_changed: ['a.js'], duration_ms: 3000 },
{ step_id: 2, action: '步骤2', status: 'failed', error: '测试错误', duration_ms: 2000 }
],
constraints: { no_touch_files: ['.github/'] }
};
const { systemPrompt, userMessage } = buildReflectionPrompt(mockTask);
assert(systemPrompt.includes('铸渊'), '系统提示词应包含铸渊身份');
assert(systemPrompt.includes('JSON'), '系统提示词应要求 JSON 输出');
assert(userMessage.includes('GLADA-TEST-REFLECT'), '用户消息应包含任务ID');
assert(userMessage.includes('反思测试任务'), '用户消息应包含任务标题');
assert(userMessage.includes('测试错误'), '用户消息应包含失败错误');
});
test('反思 JSON 解析 - 标准格式', () => {
const { parseReflection } = require('../reflector');
const output = '```json\n{"what_went_well":["步骤1成功"],"what_went_wrong":["步骤2失败"],"improvements":["增加检查"],"patterns_discovered":["模式A"],"confidence_score":0.75}\n```';
const result = parseReflection(output);
assert(result !== null, '解析不应为 null');
assert(result.what_went_well.length === 1, 'what_went_well 长度错误');
assert(result.what_went_wrong.length === 1, 'what_went_wrong 长度错误');
assert(result.improvements.length === 1, 'improvements 长度错误');
assert(result.confidence_score === 0.75, '信心评分错误');
});
test('反思 JSON 解析 - 异常值安全处理', () => {
const { parseReflection } = require('../reflector');
const output = '{"what_went_well":"not_array","confidence_score":999}';
const result = parseReflection(output);
assert(result !== null, '解析不应为 null');
assert(Array.isArray(result.what_went_well), 'what_went_well 应降级为数组');
assert(result.what_went_well.length === 0, '非数组应降级为空数组');
assert(result.confidence_score === 1, '超出范围的分数应 clamp 到 1');
});
test('反思 JSON 解析 - 无效输入返回 null', () => {
const { parseReflection } = require('../reflector');
assert(parseReflection('这不是 JSON') === null, '纯文本应返回 null');
});
test('反思 HNL 包装', () => {
const { wrapAsHNL } = require('../reflector');
const mockTask = {
glada_task_id: 'GLADA-TEST-HNL',
status: 'completed',
plan: { title: 'HNL 包装测试' }
};
const reflectionData = {
what_went_well: ['很好'],
what_went_wrong: [],
improvements: ['可以更好'],
patterns_discovered: ['模式1'],
confidence_score: 0.9
};
const doc = wrapAsHNL(mockTask, reflectionData);
assert(doc.hnl_v === '1.0', 'HNL 版本应为 1.0');
assert(doc.type === 'GROW', '类型应为 GROW');
assert(doc.from === 'YM001/ZY001', '发送者应为铸渊');
assert(doc.to === 'YM001/ZY001', '接收者应为铸渊(自我反思)');
assert(doc.op.includes('GROW'), '操作应为 GROW');
assert(doc.payload.intent === 'task_reflection', '意图应为 task_reflection');
assert(doc.payload.data.confidence_score === 0.9, '信心评分应传递');
assert(doc.memory_sovereignty.can_forget === true, '记忆主权:应可遗忘');
});
test('离线反思 - 全部成功的任务', () => {
const { reflectOffline, REFLECTIONS_DIR } = require('../reflector');
const mockTask = {
glada_task_id: 'GLADA-TEST-OFFLINE-OK',
status: 'completed',
plan: {
title: '离线反思成功测试',
steps: [
{ step_id: 1, description: '步骤1', status: 'completed', files_changed: ['a.js'], reasoning: '推理1' },
{ step_id: 2, description: '步骤2', status: 'completed', files_changed: ['b.js'] }
]
},
execution_log: [
{ step_id: 1, action: '步骤1', status: 'completed', reasoning: '推理1', files_changed: ['a.js'], duration_ms: 1000 },
{ step_id: 2, action: '步骤2', status: 'completed', files_changed: ['b.js'], duration_ms: 2000 }
],
constraints: {}
};
const result = reflectOffline(mockTask);
assert(result.saved === true, '应保存成功');
assert(result.reflection.confidence_score === 1, '全部成功时信心应为 1');
assert(result.reflection.what_went_well.length === 2, '应有 2 条好的');
assert(result.reflection.what_went_wrong.length === 0, '不应有差的');
assert(result.hnlDoc.hnl_v === '1.0', 'HNL 文档版本应为 1.0');
// 清理
try { fs.unlinkSync(result.path); } catch { /* ok */ }
});
test('离线反思 - 有失败步骤的任务', () => {
const { reflectOffline } = require('../reflector');
const mockTask = {
glada_task_id: 'GLADA-TEST-OFFLINE-FAIL',
status: 'failed',
plan: {
title: '离线反思失败测试',
steps: [
{ step_id: 1, description: '成功步骤', status: 'completed', files_changed: ['ok.js'] },
{ step_id: 2, description: '失败步骤', status: 'failed', error: 'SQL 错误' },
{ step_id: 3, description: '回滚步骤', status: 'rolled_back', error: '测试失败' }
]
},
execution_log: [
{ step_id: 1, action: '成功步骤', status: 'completed', files_changed: ['ok.js'] },
{ step_id: 2, action: '失败步骤', status: 'failed', error: 'SQL 错误' },
{ step_id: 3, action: '回滚步骤', status: 'rolled_back', error: '测试失败' }
],
constraints: {}
};
const result = reflectOffline(mockTask);
assert(result.saved === true, '应保存成功');
assert(result.reflection.confidence_score < 1, '有失败时信心应小于 1');
assert(result.reflection.what_went_wrong.length === 2, '应有 2 条差的');
assert(result.reflection.improvements.length >= 2, '应有改进建议');
// 清理
try { fs.unlinkSync(result.path); } catch { /* ok */ }
});
test('execution-loop 集成 skill-distiller 和 reflector', () => {
const loopContent = fs.readFileSync(path.join(ROOT, 'glada', 'execution-loop.js'), 'utf-8');
assert(loopContent.includes("require('./skill-distiller')"), 'execution-loop 应引用 skill-distiller');
assert(loopContent.includes("require('./reflector')"), 'execution-loop 应引用 reflector');
assert(loopContent.includes('distillAndSave'), 'execution-loop 应调用 distillAndSave');
assert(loopContent.includes('reflectOffline'), 'execution-loop 应调用 reflectOffline');
});
test('context-builder 集成 skills 加载', () => {
const ctxContent = fs.readFileSync(path.join(ROOT, 'glada', 'context-builder.js'), 'utf-8');
assert(ctxContent.includes('skill-distiller'), 'context-builder 应引用 skill-distiller');
assert(ctxContent.includes('findRelevantSkills'), 'context-builder 应调用 findRelevantSkills');
assert(ctxContent.includes('skillsToContext'), 'context-builder 应调用 skillsToContext');
assert(ctxContent.includes('sections.skills'), 'context-builder 应有 skills section');
});
test('context-builder 将 skills 加入系统提示词', () => {
const ctxContent = fs.readFileSync(path.join(ROOT, 'glada', 'context-builder.js'), 'utf-8');
assert(ctxContent.includes('context.sections.skills'), 'contextToSystemPrompt 应包含 skills 输出');
});
// ── 5. 模型自动路由测试(第三方代理多模型支持) ──
console.log('\n🔀 模型路由测试:');
test('model-router 模块加载', () => {
const router = require('../model-router');
assert(typeof router.initialize === 'function', '应有 initialize 方法');
assert(typeof router.selectModel === 'function', '应有 selectModel 方法');
assert(typeof router.detectTaskType === 'function', '应有 detectTaskType 方法');
assert(typeof router.classifyModels === 'function', '应有 classifyModels 方法');
assert(typeof router.getStatus === 'function', '应有 getStatus 方法');
assert(typeof router.shutdown === 'function', '应有 shutdown 方法');
});
test('模型分类 - 推理型模型识别', () => {
const router = require('../model-router');
const models = ['deepseek-r1', 'claude-3-opus', 'gpt-4o', 'qwen-turbo'];
const classified = router.classifyModels(models);
assert(classified.reasoning.includes('deepseek-r1'), 'deepseek-r1 应被分为推理型');
assert(classified.reasoning.length > 0, '应有推理型模型');
});
test('模型分类 - 代码型模型识别', () => {
const router = require('../model-router');
const models = ['deepseek-coder-v2', 'codestral-latest', 'gpt-4o', 'llama-3'];
const classified = router.classifyModels(models);
assert(classified.coding.includes('deepseek-coder-v2'), 'deepseek-coder-v2 应被分为代码型');
assert(classified.coding.includes('codestral-latest'), 'codestral-latest 应被分为代码型');
});
test('模型分类 - 经济型模型识别', () => {
const router = require('../model-router');
const models = ['gpt-4o-mini', 'gemini-2-flash', 'claude-3-haiku', 'qwen-turbo'];
const classified = router.classifyModels(models);
assert(classified.economy.includes('gpt-4o-mini'), 'gpt-4o-mini 应被分为经济型');
assert(classified.economy.length >= 3, '应有多个经济型模型');
});
test('模型分类 - 通用型模型识别', () => {
const router = require('../model-router');
const models = ['gpt-4o', 'claude-3-sonnet', 'deepseek-chat', 'qwen-max', 'gemini-pro'];
const classified = router.classifyModels(models);
assert(classified.general.length > 0, '应有通用型模型');
assert(classified.general.includes('gpt-4o'), 'gpt-4o 应被分为通用型');
});
test('任务类型检测 - 代码任务', () => {
const router = require('../model-router');
assert(router.detectTaskType('创建新的路由文件') === 'coding', '创建路由应为 coding');
assert(router.detectTaskType('修改数据库模块') === 'coding', '修改模块应为 coding');
assert(router.detectTaskType('implement the API endpoint') === 'coding', 'implement 应为 coding');
});
test('任务类型检测 - 推理任务', () => {
const router = require('../model-router');
assert(router.detectTaskType('分析系统架构设计') === 'reasoning', '架构分析应为 reasoning');
assert(router.detectTaskType('安全审核代码') === 'reasoning', '安全审核应为 reasoning');
});
test('任务类型检测 - 经济型任务', () => {
const router = require('../model-router');
assert(router.detectTaskType('更新 README 文档') === 'economy', 'README 应为 economy');
assert(router.detectTaskType('添加配置文件注释') === 'economy', '配置应为 economy');
});
test('任务类型检测 - 默认为 coding', () => {
const router = require('../model-router');
assert(router.detectTaskType('some unknown task') === 'coding', '未知任务应默认为 coding');
assert(router.detectTaskType('') === 'general', '空描述应为 general');
});
test('getStatus 未初始化时返回正确状态', () => {
const router = require('../model-router');
const status = router.getStatus();
assert(typeof status === 'object', 'status 应为对象');
assert('initialized' in status, 'status 应有 initialized 字段');
assert('models' in status || 'message' in status, 'status 应有 models 或 message 字段');
});
test('step-executor 集成 model-router', () => {
const seContent = fs.readFileSync(path.join(ROOT, 'glada', 'step-executor.js'), 'utf-8');
assert(seContent.includes("require('./model-router')"), 'step-executor 应引用 model-router');
assert(seContent.includes('modelRouter.selectModel'), 'step-executor 应调用 selectModel');
});
test('reflector 集成 model-router', () => {
const rfContent = fs.readFileSync(path.join(ROOT, 'glada', 'reflector.js'), 'utf-8');
assert(rfContent.includes("require('./model-router')"), 'reflector 应引用 model-router');
assert(rfContent.includes('modelRouter.selectModel'), 'reflector 应调用 selectModel');
});
test('service.js 集成 model-router', () => {
const svcContent = fs.readFileSync(path.join(ROOT, 'glada', 'service.js'), 'utf-8');
assert(svcContent.includes("require('./model-router')"), 'service.js 应引用 model-router');
assert(svcContent.includes('/api/glada/models'), 'service.js 应有模型状态端点');
assert(svcContent.includes('modelRouter.initialize'), 'service.js 应初始化 model-router');
assert(svcContent.includes('modelRouter.shutdown'), 'service.js 应关闭 model-router');
});
test('ecosystem.config.js 包含模型路由环境变量', () => {
const ecoContent = fs.readFileSync(path.join(ROOT, 'glada', 'ecosystem.config.js'), 'utf-8');
assert(ecoContent.includes('GLADA_MODEL_PREFERENCE'), 'ecosystem 应有 GLADA_MODEL_PREFERENCE');
assert(ecoContent.includes('GLADA_MODEL_REFRESH_MS'), 'ecosystem 应有 GLADA_MODEL_REFRESH_MS');
});
test('install-check.js 包含模型发现检查', () => {
const icContent = fs.readFileSync(path.join(ROOT, 'glada', 'install-check.js'), 'utf-8');
assert(icContent.includes('model-router'), 'install-check 应引用 model-router');
assert(icContent.includes('discoverModels'), 'install-check 应调用 discoverModels');
assert(icContent.includes('classifyModels'), 'install-check 应调用 classifyModels');
});
test('deploy workflow 包含模型路由配置', () => {
const wfContent = fs.readFileSync(path.join(ROOT, '.github', 'workflows', 'deploy-to-zhuyuan-server.yml'), 'utf-8');
assert(wfContent.includes('GLADA_MODEL_PREFERENCE'), 'workflow 应写入 GLADA_MODEL_PREFERENCE');
assert(wfContent.includes('GLADA_MODEL_REFRESH_MS'), 'workflow 应写入 GLADA_MODEL_REFRESH_MS');
});
// ── 结果 ──
console.log(`\n${'═'.repeat(40)}`);
console.log(`📊 测试结果: ${passed} 通过, ${failed} 失败`);
console.log(`${'═'.repeat(40)}`);
if (failed > 0) {
process.exit(1);
} else {
console.log('✅ 全部通过!\n');
}