Files
Todo-List/scripts/verify-todo-end-to-end.mjs
2026-09-12 13:55:57 +08:00

112 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
// 端到端验证:用真实 todo.md 验证往返后任务总数与分类结构守恒。
//
// v3.6 起 todo.md 不再含顶部「# 当前任务」/「# 重要任务」镜像段;智能视图在内存中
// 按 task.current / task.important 聚合,源任务行只活在「全部任务」容器的子分类下。
//
// 本脚本验证:
// 1. 加载当前 todo.md或内置样例缺文件时兜底
// 2. 解析 + 重新序列化 + 再次解析
// 3. 任务总数与每个分类的任务数完全一致(不丢、不翻倍)
//
// 用法node scripts/verify-todo-end-to-end.mjs
// 路径查找顺序(避免在别人机器 / CI 上硬挂):
// 1) --file <path> 命令行参数CI / 自定义)
// 2) TODO_LIST_FILE 环境变量
// 3) 当前用户的真实 todo.md~/TodoList/todo.md跨平台一致
// 4) 内置 SAMPLE_MD —— 跑端到端往返但**不**回写,避免污染未知位置的文件
// 脚本会备份原文件到 <todo.md>.e2e.bak再把重新序列化结果写回方便人工 diff。
import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
import { parseMarkdown } from '../src/markdown-parser.js';
import { writeMarkdown } from '../src/markdown-writer.js';
import { check, summary, printSummary } from './_lib/check.mjs';
// 内置样例 —— 文件不在 / 无法定位时兜底,确保脚本在任意机器上都能跑通
const SAMPLE_MD = `# 全部任务
## 工作
- [ ] 工作任务 A
- [ ] 工作任务 B
- [✓] 已完成的工作任务
## 学习
- [ ] 学习任务 A
- [ ] 学习任务 B
## 未分类
- [ ] 未分类任务
# 已完成任务
- [✓] 单独的已完成任务
`;
// 找用户的真实 todo.md —— 跨平台 + 避坑非 ASCII 路径
// 已抽到 scripts/_lib/find-todo-path.mjs这里 import 复用
import { findTodoFile } from './_lib/find-todo-path.mjs';
let todoPath, original, isSample;
const found = findTodoFile();
if (found) {
todoPath = found.path;
original = found.content;
isSample = false;
} else {
console.log('[verify-todo-end-to-end] 找不到用户 todo.md回退到内置样例不写回磁盘');
todoPath = null;
original = SAMPLE_MD;
isSample = true;
}
if (!isSample) {
const backupPath = todoPath + '.e2e.bak';
copyFileSync(todoPath, backupPath);
console.log(`已备份原文件到 ${backupPath}(共 ${original.length} 字符)`);
} else {
const sampleLineCount = SAMPLE_MD.split('\n').filter(Boolean).length;
console.log(`(样例模式:${sampleLineCount} 行内置内容,仅跑往返不落盘)`);
}
const cats1 = parseMarkdown(original);
const beforeTotal = cats1.reduce((n, c) => n + c.tasks.length, 0);
console.log(`\n解析结果:${cats1.length} 个分类,${beforeTotal} 条任务`);
for (const c of cats1) {
console.log(` · ${c.name}${c.tasks.length}`);
}
const md2 = writeMarkdown(cats1);
const cats2 = parseMarkdown(md2);
const afterTotal = cats2.reduce((n, c) => n + c.tasks.length, 0);
console.log(`\n重新序列化:${md2.length} 字符`);
console.log(`往返后任务总数:${beforeTotal}${afterTotal}(差 ${afterTotal - beforeTotal}`);
console.log('\n[断言]');
check('往返后任务总数不变', beforeTotal === afterTotal, `${beforeTotal}${afterTotal}`);
// 每个分类的任务数也应一一对应(按 name 匹配;不存在的分类允许新增空分类)
const before = new Map(cats1.map(c => [c.name, c.tasks.length]));
const after = new Map(cats2.map(c => [c.name, c.tasks.length]));
let mismatched = 0;
for (const [name, count] of before) {
const a = after.get(name);
if (a !== count) {
mismatched++;
console.log(` ✗ 分类「${name}」:${count}${a}`);
}
}
check('每个分类的任务数也守恒', mismatched === 0);
// 写回文件(让用户能看到效果);样例模式不落盘避免污染未知位置
if (!isSample && todoPath) {
writeFileSync(todoPath, md2, 'utf8');
console.log(`\n已写回文件 ${todoPath}(共 ${md2.length} 字符)`);
} else {
console.log(`\n(样例模式:往返结果未落盘)`);
}
console.log(`\n${'─'.repeat(48)}\n通过 ${summary.pass} / 失败 ${summary.fail}`);
process.exit(summary.fail === 0 ? 0 : 1);