bookworm-smart-assistant/tests/v59-regression.test.js

237 lines
10 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* v5.9 回归测试
*
* 覆盖 v5.9 修改的 5 个核心模块:
* 1. sanitize.js 脱敏规则
* 2. route-ab-test.js Beta NaN 防护
* 3. intent-classifier.js 相邻意图对 + simple 修饰符优先级
* 4. context-tracker.js 反粘滞机制
* 5. route-feedback.js validateHoldout 空数组防御
* 6. route-analyzer.js normalizeScores 空数组防御
* 7. compliance-gate.js buildAllowedSet 逻辑
*
* 运行: node tests/v59-regression.test.js
*/
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
let passed = 0;
let failed = 0;
const failures = [];
function assert(condition, name) {
if (condition) {
passed++;
} else {
failed++;
failures.push(name);
console.log(` FAIL: ${name}`);
}
}
function assertEqual(actual, expected, name) {
if (actual === expected) {
passed++;
} else {
failed++;
failures.push(name);
console.log(` FAIL: ${name} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}
// ============================================================
// 1. sanitize.js
// ============================================================
console.log('\n=== 1. sanitize.js ===');
const { sanitize } = require(path.join(ROOT, 'scripts', 'sanitize.js'));
// 基础类型安全
assertEqual(sanitize(null), '', 'sanitize null → empty');
assertEqual(sanitize(undefined), '', 'sanitize undefined → empty');
assertEqual(sanitize(''), '', 'sanitize empty → empty');
assertEqual(sanitize(123), 123, 'sanitize number → passthrough');
// 正常文本不变
assertEqual(sanitize('hello world'), 'hello world', 'sanitize normal text unchanged');
assertEqual(sanitize('AKIA short'), 'AKIA short', 'sanitize short AKIA unchanged');
// 键值对脱敏
assert(sanitize('api_key= sk-abc123456789').includes('[REDACTED]'), 'sanitize key=value');
assert(sanitize('password: mysecretpassword').includes('[REDACTED]'), 'sanitize password:value');
assert(sanitize('passwd= supersecret123').includes('[REDACTED]'), 'sanitize passwd=value');
assert(sanitize('token: abcdef123456').includes('[REDACTED]'), 'sanitize token:value');
assert(sanitize('secret=verylongsecretvalue').includes('[REDACTED]'), 'sanitize secret=value');
// Token 前缀脱敏
assert(sanitize('sk-1234567890abcdefghij').includes('[REDACTED_TOKEN]'), 'sanitize sk- prefix');
assert(sanitize('ghp_ABCDEFghij1234567890').includes('[REDACTED_TOKEN]'), 'sanitize ghp_ prefix');
assert(sanitize('Bearer eyJhbGciOiJIUzI1NiJ9.payload').includes('[REDACTED_TOKEN]'), 'sanitize Bearer token');
// AWS AKIA
assert(sanitize('AKIA1234567890ABCDEF').includes('[REDACTED_TOKEN]'), 'sanitize AWS AKIA 20-char');
assertEqual(sanitize('AKIA short'), 'AKIA short', 'sanitize AKIA too short → unchanged');
// JWT
assert(sanitize('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0').includes('[REDACTED_TOKEN]'), 'sanitize JWT token');
// Base64 长字符串
assert(sanitize('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop').includes('[REDACTED_B64]'), 'sanitize long base64');
// 短 base64 不脱敏
assertEqual(sanitize('abc123'), 'abc123', 'sanitize short b64 unchanged');
// ============================================================
// 2. route-ab-test.js — Beta NaN 防护
// ============================================================
console.log('\n=== 2. route-ab-test.js (Beta NaN) ===');
const abTest = require(path.join(ROOT, 'scripts', 'route-ab-test.js'));
// 验证模块加载
assert(typeof abTest.selectVariant === 'function', 'selectVariant exists');
assert(typeof abTest.shouldExperiment === 'function', 'shouldExperiment exists');
// shouldExperiment 阈值测试
assertEqual(abTest.shouldExperiment([
{ name: 'a', confidence: 0.9 },
{ name: 'b', confidence: 0.8 },
]), true, 'shouldExperiment close scores → true');
assertEqual(abTest.shouldExperiment([
{ name: 'a', confidence: 0.9 },
{ name: 'b', confidence: 0.3 },
]), false, 'shouldExperiment distant scores → false');
// ============================================================
// 3. intent-classifier.js
// ============================================================
console.log('\n=== 3. intent-classifier.js ===');
const { classifyIntent, scoreComplexity, ADJACENT_INTENT_PAIRS } = require(path.join(ROOT, 'scripts', 'intent-classifier.js'));
// 相邻意图对数量
assert(ADJACENT_INTENT_PAIRS.size >= 18, `ADJACENT_INTENT_PAIRS has ${ADJACENT_INTENT_PAIRS.size} pairs (>= 18)`);
// explain 相邻对存在
assert(ADJACENT_INTENT_PAIRS.has('debug:explain'), 'debug:explain is adjacent');
assert(ADJACENT_INTENT_PAIRS.has('explain:performance'), 'explain:performance is adjacent');
assert(ADJACENT_INTENT_PAIRS.has('architecture:explain'), 'architecture:explain is adjacent');
// scoreComplexity: simple 修饰符覆盖
assertEqual(scoreComplexity(['debug', 'performance'], ['simple'], ['React']), 'simple', 'simple modifier overrides 2 intents');
assertEqual(scoreComplexity(['debug', 'performance', 'security'], ['simple'], []), 'simple', 'simple modifier overrides 3 intents');
// scoreComplexity: complex 修饰符优先
assertEqual(scoreComplexity(['explain'], ['complex'], []), 'complex', 'complex modifier forces complex');
// scoreComplexity: 相邻意图对 → medium
assertEqual(scoreComplexity(['debug', 'performance'], [], ['React']), 'medium', 'adjacent pair → medium');
assertEqual(scoreComplexity(['debug', 'explain'], [], []), 'medium', 'debug+explain → medium');
// scoreComplexity: 非相邻 2 意图 → complex
assertEqual(scoreComplexity(['create', 'security'], [], ['React']), 'complex', 'non-adjacent 2 intents → complex');
// scoreComplexity: 3+ 意图 → complex
assertEqual(scoreComplexity(['debug', 'performance', 'security'], [], []), 'complex', '3 intents → complex');
// scoreComplexity: simple 意图
assertEqual(scoreComplexity(['explain'], [], []), 'simple', 'explain only → simple');
assertEqual(scoreComplexity(['general'], [], []), 'simple', 'general only → simple');
// classifyIntent 基础
const r1 = classifyIntent('');
assertEqual(r1.complexity, 'simple', 'empty input → simple');
const r2 = classifyIntent('调试 React bug');
assert(r2.intents.includes('debug'), 'debug intent detected for "调试 React bug"');
// ============================================================
// 4. context-tracker.js — 反粘滞
// ============================================================
console.log('\n=== 4. context-tracker.js ===');
const { computeContextScore, DECAY_FACTOR, ANTI_STICKY_THRESHOLD } = require(path.join(ROOT, 'scripts', 'context-tracker.js'));
// 常量验证
assertEqual(DECAY_FACTOR, 0.70, 'DECAY_FACTOR is 0.70');
assertEqual(ANTI_STICKY_THRESHOLD, 3, 'ANTI_STICKY_THRESHOLD is 3');
// 空历史 → 0 分
assertEqual(computeContextScore('any-skill', {}, { recentSkills: [], updatedAt: '' }), 0, 'empty history → 0');
// 反粘滞: 连续 3+ 次同技能 → 降低加成
const state3 = { recentSkills: ['a', 'a', 'a'], updatedAt: '' };
const state2 = { recentSkills: ['b', 'a', 'a'], updatedAt: '' };
const score3 = computeContextScore('a', {}, state3);
const score2 = computeContextScore('a', {}, state2);
assert(score3 < score2, `anti-sticky: 3x same (${score3}) < 2x same (${score2})`);
// composable enhances 加成
const compIndex = { 'skill-a': { enhances: ['skill-b'] } };
const stateComp = { recentSkills: ['skill-a'], updatedAt: '' };
const compScore = computeContextScore('skill-b', compIndex, stateComp);
assert(compScore > 0, `composable enhances gives positive score: ${compScore}`);
// ============================================================
// 5. route-analyzer.js — normalizeScores 防御
// ============================================================
console.log('\n=== 5. route-analyzer.js ===');
const { normalizeScores } = require(path.join(ROOT, 'scripts', 'route-analyzer.js'));
// 空数组
const emptyResult = normalizeScores([]);
assert(Array.isArray(emptyResult), 'normalizeScores([]) returns array');
assertEqual(emptyResult.length, 0, 'normalizeScores([]) returns empty array');
// null 防御
const nullResult = normalizeScores(null);
assert(Array.isArray(nullResult), 'normalizeScores(null) returns array');
// 正常情况
const normalResult = normalizeScores([{ name: 'a', score: 10 }, { name: 'b', score: 5 }]);
assertEqual(normalResult.length, 2, 'normalizeScores normal length');
assertEqual(normalResult[0].confidence, 1.0, 'normalizeScores top confidence = 1.0');
assertEqual(normalResult[1].confidence, 0.5, 'normalizeScores second confidence = 0.5');
// ============================================================
// 6. compliance-gate.js — buildAllowedSet
// ============================================================
console.log('\n=== 6. compliance-gate.js ===');
const { buildAllowedSet } = require(path.join(ROOT, 'hooks', 'route-compliance-gate.js'));
// 基础路由
const state1 = {
routing: {
primary: 'developer-expert',
candidates: [
{ name: 'debugger', confidence: 0.5 },
{ name: 'low-conf', confidence: 0.1 },
],
chain: ['chain-skill'],
},
};
const allowed1 = buildAllowedSet(state1, '');
assert(allowed1.has('developer-expert'), 'primary in allowed');
assert(allowed1.has('debugger'), 'high-conf candidate in allowed');
assert(!allowed1.has('low-conf'), 'low-conf candidate NOT in allowed');
assert(allowed1.has('chain-skill'), 'chain skill in allowed');
// 显式 /skill-name 放行
const allowed2 = buildAllowedSet(state1, '/custom-skill some args');
assert(allowed2.has('custom-skill'), 'explicit /skill-name in allowed');
// developer-expert 始终允许
const allowed3 = buildAllowedSet({ routing: {} }, '');
assert(allowed3.has('developer-expert'), 'developer-expert always allowed');
// ============================================================
// 结果汇总
// ============================================================
console.log('\n' + '='.repeat(50));
console.log(`v5.9 回归测试: ${passed} passed, ${failed} failed`);
if (failures.length > 0) {
console.log('\nFailed tests:');
for (const f of failures) console.log(` - ${f}`);
}
console.log('='.repeat(50));
process.exit(failed > 0 ? 1 : 0);