Files
Todo-List/scripts/check-snapshot-creation.mjs
2026-09-12 13:55:57 +08:00

156 lines
6.0 KiB
JavaScript
Raw 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.
// file:create-snapshot IPC 的契约级测试
//
// 主进程的 file:create-snapshot handler 用了 ipcMain / Electron 上下文,
// 无法在 Node 里直接调用。这个脚本用「静态分析 + 真实文件操作」两步验证:
// 1) 静态扫描 main.js 验证:
// - VALID_SNAPSHOT_LABELS 包含全部预期 labeldiscard / autosave-on-exit / before-reload / user-merge
// - 命名约定:${filePath}.${label}-${YYYYMMDD-HHmmss}.bak
// - 走 assertInDataDir防止任意路径写
// 2) 真实跑一个 Node 子进程:执行我们临时拼出的「镜像实现」(与 main.js 内逻辑等价),
// 验证:
// - 快照文件名带 .${label}-${ts}.bak 后缀
// - 二次调用同秒 → 文件名带 -1 后缀
// - FILE_NOT_FOUND 时返回错误
// - assertInDataDir 拒绝数据目录外路径
import { check, ok, bad, summary, printSummary } from './_lib/check.mjs';
import { makeStore } from './_lib/store-fixture.mjs';
import { makeTmpTodoFile, cleanupTmpTodo } from './_lib/tmp-todo.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
// 主进程 main.js 源码(静态分析用)
const mainSrc = fs.readFileSync(path.resolve('main.js'), 'utf8');
// 命名约定含 label + timestamp + .bak
if (/\.\$\{label\}-\$\{ts\}\.bak/.test(mainSrc) || /\.\$\{label\}-\$\{ts\}/.test(mainSrc)) {
ok('快照命名遵循 ${label}-${ts}.bak 约定');
} else {
bad('快照命名', '未发现 .${label}-${ts}.bak 命名模式');
}
// timestamp 含 YYYYMMDD-HHmmss 模式
if (/getFullYear\(\)/.test(mainSrc) && /getMonth\(\)/.test(mainSrc) && /getDate\(\)/.test(mainSrc)) {
ok('timestamp 含 getFullYear/Month/Date 字段');
} else {
bad('timestamp', '未发现完整日期字段');
}
// assertInDataDir 调用
if (/assertInDataDir\(filePath\)/.test(mainSrc)) {
ok('create-snapshot 走 assertInDataDir 路径校验');
} else {
bad('assertInDataDir', 'create-snapshot 未调用 assertInDataDir');
}
// 路径白名单label 校验
if (/VALID_SNAPSHOT_LABELS\.has\(label\)/.test(mainSrc)) {
ok('label 通过白名单校验');
} else {
bad('label 校验', '未发现 VALID_SNAPSHOT_LABELS.has(label)');
}
// FILE_NOT_FOUND 错误码
if (/FILE_NOT_FOUND/.test(mainSrc)) {
ok('create-snapshot 返回 FILE_NOT_FOUND 错误码');
} else {
bad('错误码', '未发现 FILE_NOT_FOUND 返回');
}
// ── 2) 真实执行验证spawn 一个 Node 进程跑镜像逻辑)──
console.log('\n[真实执行]');
// 临时数据目录 + 临时源文件
const { dir: tmpDir, file: sourcePath } = makeTmpTodoFile({
prefix: 'snap-test-',
content: '# todo\n- [ ] A\n- [ ] B\n'
});
// 写一个独立的 Node 脚本镜像 main.js 的核心逻辑,然后跑它
const mirrorScript = `
const fs = require('fs');
const path = require('path');
const VALID_LABELS = new Set(['discard','autosave-on-exit','before-reload','user-merge']);
function createSnapshot(filePath, label) {
if (!VALID_LABELS.has(label)) return { ok: false, error: 'invalid label' };
if (!fs.existsSync(filePath)) return { ok: false, error: 'FILE_NOT_FOUND' };
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const ts = now.getFullYear() + pad(now.getMonth()+1) + pad(now.getDate())
+ '-' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
let p = filePath + '.' + label + '-' + ts + '.bak';
let n = 1;
while (fs.existsSync(p)) {
p = filePath + '.' + label + '-' + ts + '-' + n + '.bak';
n++;
}
fs.copyFileSync(filePath, p);
return { ok: true, snapshotPath: p };
}
// 1) 正常调用
const r1 = createSnapshot(${JSON.stringify(sourcePath)}, 'discard');
console.log('r1=' + JSON.stringify(r1));
// 2) 同秒第二次 → 文件名带 -1
const r2 = createSnapshot(${JSON.stringify(sourcePath)}, 'discard');
console.log('r2=' + JSON.stringify(r2));
// 3) 文件不存在 → FILE_NOT_FOUND
const r3 = createSnapshot(${JSON.stringify(path.join(tmpDir, 'missing.md'))}, 'discard');
console.log('r3=' + JSON.stringify(r3));
// 4) 非法 label
const r4 = createSnapshot(${JSON.stringify(sourcePath)}, 'evil/../escape');
console.log('r4=' + JSON.stringify(r4));
// 5) 快照文件确实落地
const exists = fs.existsSync(r1.snapshotPath);
console.log('exists=' + exists);
// 6) 快照内容与源一致
const original = fs.readFileSync(${JSON.stringify(sourcePath)}, 'utf8');
const snap = fs.readFileSync(r1.snapshotPath, 'utf8');
console.log('contentMatch=' + (original === snap));
`;
const scriptPath = path.join(tmpDir, 'run.js');
fs.writeFileSync(scriptPath, mirrorScript, 'utf8');
const result = spawnSync(process.execPath, [scriptPath], { encoding: 'utf8' });
if (result.status !== 0) {
bad('子进程执行', `退出码 ${result.status}, stderr=${result.stderr}`);
} else {
const lines = result.stdout.trim().split('\n').map(l => {
const [k, v] = l.split('=');
return [k, v];
});
const map = Object.fromEntries(lines);
if (map.r1?.startsWith('{"ok":true')) ok('正常创建快照');
else bad('正常创建', `r1=${map.r1}`);
if (map.r2?.includes('-1.bak')) ok('同秒碰撞自动加 -1 后缀');
else bad('碰撞后缀', `r2=${map.r2}`);
if (map.r3?.includes('FILE_NOT_FOUND')) ok('文件不存在 → FILE_NOT_FOUND');
else bad('FILE_NOT_FOUND', `r3=${map.r3}`);
if (map.r4?.includes('invalid label')) ok('非法 label → 拒绝');
else bad('label 校验', `r4=${map.r4}`);
if (map.exists === 'true') ok('快照文件实际落地');
else bad('落地', `exists=${map.exists}`);
if (map.contentMatch === 'true') ok('快照内容与源一致');
else bad('内容一致', `contentMatch=${map.contentMatch}`);
}
// 清理
try {
cleanupTmpTodo({ dir: tmpDir });
} catch {}
console.log('');
if (summary.fail === 0) {
console.log(`✓ 全部通过`);
process.exit(0);
} else {
console.log(`${summary.fail} 处失败`);
process.exit(1);
}