106 lines
4.1 KiB
JavaScript
106 lines
4.1 KiB
JavaScript
/**
|
||||
|
|
* savePost 全量往返测试 —— 对仓库里**每一篇**真实文章跑一遍「读出来原样存回去」。
|
|||
|
|
*
|
|||
|
|
* 为什么要有这个:frontmatter-roundtrip 只测 split/join 这两个纯函数,
|
|||
|
|
* 但真正会写坏文章的是 getPost → savePost 这条链路(中间的 content 剥了前导换行、
|
|||
|
|
* 回写时又要还原,任何一处不对称都会在部分文章上产生脏 diff)。
|
|||
|
|
* 所以这里必须用真文件、真篇数跑,不能只挑一篇(19/130 的写法差异就藏在里面)。
|
|||
|
|
*
|
|||
|
|
* 测试期间会**真实改写文件**,但每篇测完都立刻按原字节还原;
|
|||
|
|
* 结尾再逐篇核对 sha256,任何一篇没还原都会让脚本失败并指名道姓。
|
|||
|
|
*
|
|||
|
|
* 跑法:node test/save-roundtrip.mjs
|
|||
|
|
*/
|
|||
|
|
import fs from 'node:fs';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
import crypto from 'node:crypto';
|
|||
|
|
import { makePosts } from '../src/posts.mjs';
|
|||
|
|
import { splitFrontMatter } from '../src/frontmatter.mjs';
|
|||
|
|
|
|||
|
|
const repoRoot = path.resolve(process.env.BLOG_ROOT || path.join(import.meta.dirname, '..', '..'));
|
|||
|
|
const contentDir = path.join(repoRoot, 'content', 'posts');
|
|||
|
|
const trashDir = path.join(repoRoot, '.editor-tmp', 'trash');
|
|||
|
|
|
|||
|
|
const posts = makePosts({ contentDir, trashDir, repoRoot, defaultAuthor: '' });
|
|||
|
|
|
|||
|
|
const sha = (p) => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|||
|
|
const fmBlockOf = (text) => {
|
|||
|
|
const { raw } = splitFrontMatter(text);
|
|||
|
|
if (raw == null) return null;
|
|||
|
|
return '---\n' + raw + '\n---';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const list = posts.listPosts({ perPage: 500 });
|
|||
|
|
console.log('待测文章: ' + list.total + ' 篇 (仓库 ' + repoRoot + ')\n');
|
|||
|
|
|
|||
|
|
let pass = 0;
|
|||
|
|
let fail = 0;
|
|||
|
|
const failures = [];
|
|||
|
|
|
|||
|
|
/** 记录每篇的原始字节,最后统一核对还原情况 */
|
|||
|
|
const originals = new Map();
|
|||
|
|
|
|||
|
|
for (const item of list.posts) {
|
|||
|
|
// 用 id(目录名)而不是 slug —— 仓库里有 slug 撞名,按 slug 会拿到 409 或别的文章
|
|||
|
|
const post = posts.getPost(item.id);
|
|||
|
|
if (!post || typeof post.filePath !== 'string') {
|
|||
|
|
fail++;
|
|||
|
|
failures.push(item.id + ' → getPost 拿不到 filePath');
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
const file = post.filePath;
|
|||
|
|
const before = fs.readFileSync(file);
|
|||
|
|
originals.set(file, before);
|
|||
|
|
const beforeHash = crypto.createHash('sha256').update(before).digest('hex');
|
|||
|
|
|
|||
|
|
let mark = '✓';
|
|||
|
|
|
|||
|
|
// ---- ① 原样回存:必须逐字节不变(这是最重要的一条)----
|
|||
|
|
posts.savePost(item.id, { content: post.content, frontMatter: post.frontMatter });
|
|||
|
|
const afterHash = sha(file);
|
|||
|
|
if (afterHash !== beforeHash) {
|
|||
|
|
mark = '✗';
|
|||
|
|
failures.push(item.id + ' → 「原样回存」字节变了 ' + beforeHash.slice(0, 12) + ' ≠ ' + afterHash.slice(0, 12));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- ② 只改正文:front matter 区块必须逐字节不变 ----
|
|||
|
|
const midText = fs.readFileSync(file, 'utf8');
|
|||
|
|
const fmBefore = fmBlockOf(midText);
|
|||
|
|
posts.savePost(item.id, { content: post.content + '\n\n<!-- probe -->\n' });
|
|||
|
|
const editedText = fs.readFileSync(file, 'utf8');
|
|||
|
|
if (fmBefore !== fmBlockOf(editedText)) {
|
|||
|
|
mark = '✗';
|
|||
|
|
failures.push(item.id + ' → 改正文时 front matter 被改动');
|
|||
|
|
}
|
|||
|
|
if (!editedText.includes('<!-- probe -->')) {
|
|||
|
|
mark = '✗';
|
|||
|
|
failures.push(item.id + ' → 正文没写进去');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- ③ 立刻还原成原字节 ----
|
|||
|
|
fs.writeFileSync(file, before);
|
|||
|
|
|
|||
|
|
if (mark === '✓') pass++;
|
|||
|
|
else fail++;
|
|||
|
|
if (mark === '✗') console.log(mark + ' ' + item.slug);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- ④ 总核对:全仓库不允许残留任何被改动的文章 ----
|
|||
|
|
console.log('\n--- 还原核对 ---');
|
|||
|
|
let notRestored = 0;
|
|||
|
|
for (const [file, buf] of originals) {
|
|||
|
|
const now = fs.readFileSync(file);
|
|||
|
|
if (!now.equals(buf)) {
|
|||
|
|
notRestored++;
|
|||
|
|
console.log(' ✗ 未还原: ' + file);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (notRestored === 0) console.log(' ✓ ' + originals.size + ' 篇全部还原为原始字节');
|
|||
|
|
|
|||
|
|
console.log('\n================================');
|
|||
|
|
console.log('通过 ' + pass + ' / 失败 ' + fail + ' 未还原 ' + notRestored);
|
|||
|
|
if (failures.length) {
|
|||
|
|
console.log('\n失败明细:');
|
|||
|
|
for (const f of failures) console.log(' · ' + f);
|
|||
|
|
}
|
|||
|
|
process.exit(fail || notRestored ? 1 : 0);
|