#!/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);