- src/test 迁移至根目录 tests/(16 测试 + fixtures/manual),导入改为 ../src/ - 新增 tsconfig.test.json 独立编译测试;.vscode-test.mjs / package.json / .gitignore 同步 - 新增 tests/test-execution-log.md、tests/test-cases.md(116 用例清单) - 新增 tests/measure/measure-review-time.mjs + performance-comparison.md(提效测量) - 重命名 3 个中文报告文件为 demo-*-coverage-report.md - git rm --cached 解除 vsix 跟踪;README 标注演示视频进行中
369 lines
16 KiB
TypeScript
369 lines
16 KiB
TypeScript
import * as assert from 'assert';
|
|
import { buildFinalYaml, parseImportableYaml } from '../src/rules/import-service';
|
|
import type { ImportableRule, PreviewDecision } from '../src/rules/import-types';
|
|
|
|
function makeRules(data: Array<Partial<ImportableRule>>): ImportableRule[] {
|
|
return data.map(d => ({
|
|
id: d.id ?? 'test-rule',
|
|
severity: d.severity ?? 'warning',
|
|
description: d.description ?? 'test description',
|
|
message: d.message ?? 'test message',
|
|
languages: d.languages,
|
|
excludeLanguages: d.excludeLanguages,
|
|
duplicateOf: d.duplicateOf,
|
|
duplicateLevel: d.duplicateLevel,
|
|
duplicateReason: d.duplicateReason,
|
|
}));
|
|
}
|
|
|
|
function defaultDecision(rules: ImportableRule[]): PreviewDecision {
|
|
const keepRule: Record<string, boolean> = {};
|
|
for (const rule of rules) {
|
|
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
|
|
}
|
|
return { keepRule, confirmed: true };
|
|
}
|
|
|
|
function makeYaml(rules: Array<{ id: string; fields: Record<string, string> }>): string {
|
|
return rules.map(r => {
|
|
const lines = [`- id: ${r.id}`];
|
|
for (const [key, value] of Object.entries(r.fields)) {
|
|
lines.push(` ${key}: ${value}`);
|
|
}
|
|
return lines.join('\n');
|
|
}).join('\n\n') + '\n';
|
|
}
|
|
|
|
suite('Import Dedup Tests', () => {
|
|
|
|
test('无重复规则→全部保留,无注释行', () => {
|
|
const rules = makeRules([
|
|
{ id: 'rule-a', duplicateLevel: 'none' },
|
|
{ id: 'rule-b' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateLevel: 'none' } },
|
|
{ id: 'rule-b', fields: { severity: 'info', description: 'desc b', message: 'msg b' } },
|
|
]);
|
|
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
|
assert.ok(!result.includes('# [DUPLICATE'), 'Should have no duplicate headers');
|
|
assert.ok(result.includes('id: rule-a'));
|
|
assert.ok(result.includes('id: rule-b'));
|
|
assert.ok(!result.includes('duplicateLevel:'), 'duplicateLevel should be stripped');
|
|
});
|
|
|
|
test('全部 exact→全部注释', () => {
|
|
const rules = makeRules([
|
|
{ id: 'rule-a', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
|
{ id: 'rule-b', duplicateOf: 'eslint/no-debugger', duplicateLevel: 'exact' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
|
{ id: 'rule-b', fields: { severity: 'error', description: 'desc b', message: 'msg b', duplicateOf: 'eslint/no-debugger', duplicateLevel: 'exact' } },
|
|
]);
|
|
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
|
assert.ok(!result.match(/^[^#]*- id:/m), 'No non-commented rule lines');
|
|
assert.ok(result.includes('[DUPLICATE: exact]'));
|
|
assert.ok(result.includes('# 如需启用'));
|
|
});
|
|
|
|
test('全部 overlap→全部保留,无注释行', () => {
|
|
const rules = makeRules([
|
|
{ id: 'rule-a', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求 logger' },
|
|
{ id: 'rule-b', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap', duplicateReason: '更窄范围' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求 logger' } },
|
|
{ id: 'rule-b', fields: { severity: 'warning', description: 'desc b', message: 'msg b', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap', duplicateReason: '更窄范围' } },
|
|
]);
|
|
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
|
assert.ok(!result.includes('# [DUPLICATE'), 'Overlap rules should not be commented by default');
|
|
assert.ok(result.includes('id: rule-a'));
|
|
assert.ok(result.includes('id: rule-b'));
|
|
});
|
|
|
|
test('混合三档→exact注释,overlap和none保留', () => {
|
|
const rules = makeRules([
|
|
{ id: 'exact-rule', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
|
{ id: 'overlap-rule', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap' },
|
|
{ id: 'none-rule', duplicateLevel: 'none' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'exact-rule', fields: { severity: 'warning', description: 'd1', message: 'm1', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
|
{ id: 'overlap-rule', fields: { severity: 'warning', description: 'd2', message: 'm2', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap' } },
|
|
{ id: 'none-rule', fields: { severity: 'info', description: 'd3', message: 'm3', duplicateLevel: 'none' } },
|
|
]);
|
|
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
|
assert.ok(result.includes('[DUPLICATE: exact]'));
|
|
assert.ok(!result.match(/^-\s+id:\s+exact-rule/m), 'exact rule should be commented');
|
|
assert.ok(result.match(/^-\s+id:\s+overlap-rule/m), 'overlap rule should be active');
|
|
assert.ok(result.match(/^-\s+id:\s+none-rule/m), 'none rule should be active');
|
|
});
|
|
|
|
test('用户恢复 exact 规则→取消注释', () => {
|
|
const rules = makeRules([
|
|
{ id: 'restored', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'restored', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { restored: true }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(!result.includes('[DUPLICATE'), 'Restored rule should have no duplicate annotation');
|
|
assert.ok(result.match(/^-\s+id:\s+restored/m), 'Restored rule should be active');
|
|
});
|
|
|
|
test('用户注释 overlap 规则→加 # 前缀和注释头', () => {
|
|
const rules = makeRules([
|
|
{ id: 'commented', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'commented', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { commented: false }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('[DUPLICATE: overlap]'));
|
|
assert.ok(result.includes('重叠原因:额外要求'));
|
|
assert.ok(!result.match(/^-\s+id:\s+commented/m), 'Commented rule should have # prefix');
|
|
});
|
|
|
|
test('用户注释 none 规则→加 # 前缀', () => {
|
|
const rules = makeRules([
|
|
{ id: 'comment-none', duplicateLevel: 'none' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'comment-none', fields: { severity: 'warning', description: 'd', message: 'm', duplicateLevel: 'none' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { 'comment-none': false }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(!result.match(/^-\s+id:\s+comment-none/m), 'Should be commented');
|
|
assert.ok(result.includes('# - id: comment-none'));
|
|
});
|
|
|
|
test('保留规则→duplicateLevel/duplicateOf/duplicateReason 行被移除', () => {
|
|
const rules = makeRules([
|
|
{ id: 'kept', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: 'reason' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'kept', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: 'reason' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { kept: true }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(!result.includes('duplicateOf:'));
|
|
assert.ok(!result.includes('duplicateLevel:'));
|
|
assert.ok(!result.includes('duplicateReason:'));
|
|
assert.ok(result.includes('id: kept'));
|
|
});
|
|
|
|
test('注释规则含 duplicateReason→注释头包含重叠原因', () => {
|
|
const rules = makeRules([
|
|
{ id: 'r1', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '检测目标相同但额外要求 logger' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'r1', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '检测目标相同但额外要求 logger' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { r1: false }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('重叠原因:检测目标相同但额外要求 logger'));
|
|
});
|
|
|
|
test('编辑 description 后确认→YAML 使用新值', () => {
|
|
const rules = makeRules([
|
|
{ id: 'edit-desc', duplicateLevel: 'none' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'edit-desc', fields: { severity: 'warning', description: '旧描述', message: '旧消息' } },
|
|
]);
|
|
const editedRules = makeRules([
|
|
{ id: 'edit-desc', severity: 'warning', description: '新描述', message: '新消息' },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { 'edit-desc': true }, confirmed: true, editedRules };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('description: 新描述'));
|
|
assert.ok(!result.includes('旧描述'));
|
|
assert.ok(result.includes('message: 新消息'));
|
|
});
|
|
|
|
test('编辑 severity 后确认→YAML 使用新 severity', () => {
|
|
const rules = makeRules([
|
|
{ id: 'edit-sev', duplicateLevel: 'none' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'edit-sev', fields: { severity: 'warning', description: 'd', message: 'm' } },
|
|
]);
|
|
const editedRules = makeRules([
|
|
{ id: 'edit-sev', severity: 'error', description: 'd', message: 'm' },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { 'edit-sev': true }, confirmed: true, editedRules };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('severity: error'));
|
|
assert.ok(!result.includes('severity: warning'));
|
|
});
|
|
|
|
test('编辑 languages 后确认→YAML 含新 languages', () => {
|
|
const rules = makeRules([
|
|
{ id: 'edit-lang', duplicateLevel: 'none', languages: ['java'] },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'edit-lang', fields: { severity: 'warning', description: 'd', message: 'm', languages: '[java]' } },
|
|
]);
|
|
const editedRules = makeRules([
|
|
{ id: 'edit-lang', severity: 'warning', description: 'd', message: 'm', languages: ['javascript', 'typescript'] },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { 'edit-lang': true }, confirmed: true, editedRules };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('languages: [javascript, typescript]'));
|
|
assert.ok(!result.includes('languages: [java]'), 'old java language should be gone');
|
|
assert.ok(!result.includes(' [java]'), 'standalone java tag should be gone');
|
|
});
|
|
|
|
test('编辑后切换为注释→注释内容为编辑后的值', () => {
|
|
const rules = makeRules([
|
|
{ id: 'edit-comment', duplicateLevel: 'none' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'edit-comment', fields: { severity: 'warning', description: '原描述', message: '原消息' } },
|
|
]);
|
|
const editedRules = makeRules([
|
|
{ id: 'edit-comment', severity: 'error', description: '新描述', message: '新消息' },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { 'edit-comment': false }, confirmed: true, editedRules };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('# severity: error'), 'should have commented severity: error');
|
|
assert.ok(result.includes('# description: 新描述'));
|
|
assert.ok(result.includes('# message: 新消息'));
|
|
assert.ok(result.includes('[手动注释]'));
|
|
});
|
|
|
|
test('无编辑场景→回退到原始 yamlContent 处理', () => {
|
|
const rules = makeRules([
|
|
{ id: 'fallback', duplicateLevel: 'exact', duplicateOf: 'eslint/no-console' },
|
|
]);
|
|
const yaml = makeYaml([
|
|
{ id: 'fallback', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
|
]);
|
|
const decision: PreviewDecision = { keepRule: { fallback: false }, confirmed: true };
|
|
const result = buildFinalYaml(yaml, rules, decision);
|
|
assert.ok(result.includes('[DUPLICATE: exact]'));
|
|
assert.ok(!result.match(/^-\s+id:\s+fallback/m));
|
|
assert.ok(result.includes('duplicateOf:'));
|
|
});
|
|
});
|
|
|
|
suite('parseImportableYaml Fallback Tests', () => {
|
|
|
|
test('severity 缺失→降级为 warning', () => {
|
|
const yaml = `- id: test-rule
|
|
description: test desc
|
|
message: test msg`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].severity, 'warning');
|
|
});
|
|
|
|
test('severity 非法值→降级为 warning', () => {
|
|
const yaml = `- id: test-rule
|
|
severity: critical
|
|
description: test desc
|
|
message: test msg`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].severity, 'warning');
|
|
});
|
|
|
|
test('id 缺失→生成 rule-N', () => {
|
|
const yaml = `- severity: warning
|
|
description: test desc
|
|
message: test msg`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].id, 'rule-1');
|
|
});
|
|
|
|
test('多条 id 缺失→rule-1, rule-2...', () => {
|
|
const yaml = `- severity: warning
|
|
description: desc a
|
|
message: msg a
|
|
- severity: info
|
|
description: desc b
|
|
message: msg b`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 2);
|
|
assert.strictEqual(rules[0].id, 'rule-1');
|
|
assert.strictEqual(rules[1].id, 'rule-2');
|
|
});
|
|
|
|
test('description 缺失、message 存在→互填', () => {
|
|
const yaml = `- id: test-rule
|
|
severity: error
|
|
message: test msg`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].description, 'test msg');
|
|
assert.strictEqual(rules[0].message, 'test msg');
|
|
});
|
|
|
|
test('message 缺失、description 存在→互填', () => {
|
|
const yaml = `- id: test-rule
|
|
severity: error
|
|
description: test desc`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].description, 'test desc');
|
|
assert.strictEqual(rules[0].message, 'test desc');
|
|
});
|
|
|
|
test('description 与 message 同时缺失→丢弃', () => {
|
|
const yaml = `- id: test-rule
|
|
severity: error
|
|
- id: test-rule2
|
|
severity: warning
|
|
description: test desc
|
|
message: test msg`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].id, 'test-rule2');
|
|
});
|
|
|
|
test('正常完整输入→无回归', () => {
|
|
const yaml = `- id: no-console-log
|
|
severity: error
|
|
description: 禁止使用 console.log
|
|
message: 请使用 logger 替代
|
|
- id: no-unused-vars
|
|
severity: warning
|
|
description: 禁止未使用变量
|
|
message: 删除或注释未使用变量
|
|
duplicateOf: eslint/no-unused-vars
|
|
duplicateLevel: exact`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 2);
|
|
assert.strictEqual(rules[0].severity, 'error');
|
|
assert.strictEqual(rules[0].description, '禁止使用 console.log');
|
|
assert.strictEqual(rules[1].duplicateLevel, 'exact');
|
|
});
|
|
|
|
test('id 带引号→剥离引号保留纯 id', () => {
|
|
const yaml = `- id: '123'
|
|
severity: warning
|
|
description: final 字段可改为 static
|
|
message: final 字段可改为 static`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].id, '123');
|
|
});
|
|
|
|
test('scalar 字段带引号→剥离引号', () => {
|
|
const yaml = `- id: "quoted-rule"
|
|
severity: "error"
|
|
description: "desc text"
|
|
message: 'msg text'`;
|
|
const rules = parseImportableYaml(yaml);
|
|
assert.strictEqual(rules.length, 1);
|
|
assert.strictEqual(rules[0].id, 'quoted-rule');
|
|
assert.strictEqual(rules[0].severity, 'error');
|
|
assert.strictEqual(rules[0].description, 'desc text');
|
|
assert.strictEqual(rules[0].message, 'msg text');
|
|
});
|
|
});
|