This commit is contained in:
2026-09-12 13:55:57 +08:00
commit 30f1cedd39
95 changed files with 42057 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
// 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);
}