Files
blog/editor-api/test/save-roundtrip.mjs
T

106 lines
4.1 KiB
JavaScript
Raw Normal View History

/**
* 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);