update
This commit is contained in:
156
scripts/check-snapshot-creation.mjs
Normal file
156
scripts/check-snapshot-creation.mjs
Normal file
@@ -0,0 +1,156 @@
|
||||
// file:create-snapshot IPC 的契约级测试
|
||||
//
|
||||
// 主进程的 file:create-snapshot handler 用了 ipcMain / Electron 上下文,
|
||||
// 无法在 Node 里直接调用。这个脚本用「静态分析 + 真实文件操作」两步验证:
|
||||
// 1) 静态扫描 main.js 验证:
|
||||
// - VALID_SNAPSHOT_LABELS 包含全部预期 label(discard / 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);
|
||||
}
|
||||
Reference in New Issue
Block a user