#!/usr/bin/env node /** * PoC H1: Windows fs.renameSync 跨目录原子性验证 * * 验证 4 种场景: * A. 同分区同目录 rename (期望: 原子) * B. 同分区跨目录 rename (期望: 原子) * C. 跨分区 rename (期望: Node 自动 copy+unlink, 非原子) * D. 目标文件存在时 rename (期望: 覆盖, Windows >=10 支持) * * 红队 red-team-logic A1 [CRITICAL]: 跨卷 rename 非原子 → 本测试量化验证 * 产出: ai-delivery-pipeline/_poc-sandbox/h1-report.json */ 'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const SANDBOX = path.join(__dirname, '..', '..', 'ai-delivery-pipeline', '_poc-sandbox'); const REPORT = path.join(SANDBOX, 'h1-report.json'); function mk(subdir) { const d = path.join(SANDBOX, subdir); fs.mkdirSync(d, { recursive: true }); return d; } function scenarioA_sameDir() { const d = mk('h1-a'); const a = path.join(d, 'src.txt'); const b = path.join(d, 'dst.txt'); fs.writeFileSync(a, 'A-content', 'utf8'); const t0 = process.hrtime.bigint(); fs.renameSync(a, b); const dt = Number(process.hrtime.bigint() - t0) / 1e6; const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'A-content'; return { name: 'A-sameDir', ok, ms: dt.toFixed(3) }; } function scenarioB_crossDir() { const d1 = mk('h1-b/src'); const d2 = mk('h1-b/dst'); const a = path.join(d1, 'foo.txt'); const b = path.join(d2, 'foo.txt'); fs.writeFileSync(a, 'B-content', 'utf8'); const t0 = process.hrtime.bigint(); fs.renameSync(a, b); const dt = Number(process.hrtime.bigint() - t0) / 1e6; const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'B-content'; return { name: 'B-crossDir-sameVolume', ok, ms: dt.toFixed(3) }; } function scenarioC_crossVolume() { // 探测系统是否有第二卷; 无则跳过 const drives = []; for (const d of ['D:', 'E:', 'F:']) { try { fs.accessSync(d + '\\', fs.constants.W_OK); drives.push(d); } catch {} } if (drives.length === 0) { return { name: 'C-crossVolume', ok: null, skip: '未检测到第二可写卷' }; } const otherVol = drives[0]; const tmpOther = path.join(otherVol + '\\', 'bookworm-poc-' + Date.now()); try { fs.mkdirSync(tmpOther, { recursive: true }); const a = path.join(SANDBOX, 'h1-c-src.txt'); const b = path.join(tmpOther, 'dst.txt'); fs.writeFileSync(a, 'C-content', 'utf8'); const t0 = process.hrtime.bigint(); fs.renameSync(a, b); const dt = Number(process.hrtime.bigint() - t0) / 1e6; const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'C-content'; fs.unlinkSync(b); fs.rmdirSync(tmpOther); return { name: 'C-crossVolume', ok, ms: dt.toFixed(3), targetVol: otherVol }; } catch (e) { return { name: 'C-crossVolume', ok: false, error: e.message, targetVol: otherVol }; } } function scenarioD_overwrite() { const d = mk('h1-d'); const a = path.join(d, 'src.txt'); const b = path.join(d, 'dst.txt'); fs.writeFileSync(a, 'D-new', 'utf8'); fs.writeFileSync(b, 'D-old', 'utf8'); try { fs.renameSync(a, b); const content = fs.readFileSync(b, 'utf8'); const ok = !fs.existsSync(a) && content === 'D-new'; return { name: 'D-overwrite', ok, note: 'overwrote existing' }; } catch (e) { return { name: 'D-overwrite', ok: false, error: e.code + ': ' + e.message }; } } function main() { fs.mkdirSync(SANDBOX, { recursive: true }); const results = [ scenarioA_sameDir(), scenarioB_crossDir(), scenarioC_crossVolume(), scenarioD_overwrite(), ]; const report = { hypothesis: 'H1 · Windows fs.renameSync 跨目录原子性', platform: os.platform() + ' ' + os.release(), nodeVersion: process.version, timestamp: new Date().toISOString(), results, verdict: (() => { const a = results[0].ok === true; const b = results[1].ok === true; const d = results[3].ok === true; const c = results[2]; if (a && b && d) { const cState = c.ok === null ? '无法测试(无第二卷)' : c.ok === true ? '跨卷也可 rename(Node 自动 copy)' : '跨卷 rename 失败: ' + (c.error || 'unknown'); return { pass: true, summary: '同卷原子 ✅, 覆盖 ✅, ' + cState }; } return { pass: false, summary: 'A/B/D 有失败场景, 不可用于生产' }; })(), recommendation: '', }; report.recommendation = report.verdict.pass ? '✅ 采用同卷 rename 作为 staging→delivery 主路径; 跨卷场景须 fallback 到 copy+fsync+unlink(非原子, 需上层补偿)' : '❌ 方案不可行, 需重新设计'; fs.writeFileSync(REPORT, JSON.stringify(report, null, 2), 'utf8'); console.log(JSON.stringify(report, null, 2)); } main();