- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
122 lines
4.4 KiB
JavaScript
122 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PoC H3: 从 staging snapshot 回滚的正确性
|
|
*
|
|
* 场景:
|
|
* 原 src/foo.ts 内容 = "version-1"
|
|
* Claude Edit 写入 "version-2-BAD" (模拟坏变更)
|
|
* PostToolUse hook 同步复制一份到 staging/<sessionId>/foo.ts (snapshot)
|
|
* validator 判定失败
|
|
* → 回滚: 从 file-history/ 找回 "version-1" 放回原路径; staging 版本 mv 到 quarantine
|
|
*
|
|
* 验证点:
|
|
* 1. 回滚后 src/foo.ts 完全等于原始内容 (字节级)
|
|
* 2. UTF-8 BOM 保留
|
|
* 3. 二进制文件 (\x00 \xff) 不损坏
|
|
* 4. quarantine 版本可审计读回
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const SANDBOX = path.join(__dirname, '..', '..', 'ai-delivery-pipeline', '_poc-sandbox');
|
|
|
|
function sha256(p) {
|
|
return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
}
|
|
|
|
function scenario(name, originalContent, badContent, isBinary) {
|
|
const workDir = path.join(SANDBOX, 'h3-' + name);
|
|
fs.mkdirSync(workDir, { recursive: true });
|
|
const srcDir = path.join(workDir, 'src');
|
|
const historyDir = path.join(workDir, 'file-history');
|
|
const stagingDir = path.join(workDir, 'staging', 'session-x');
|
|
const quarantineDir = path.join(workDir, 'quarantine');
|
|
[srcDir, historyDir, stagingDir, quarantineDir].forEach(d => fs.mkdirSync(d, { recursive: true }));
|
|
|
|
const srcFile = path.join(srcDir, 'foo.bin');
|
|
|
|
// Step 1: 原始写入 + file-history 快照 (模拟既有能力)
|
|
fs.writeFileSync(srcFile, originalContent);
|
|
const historySnap = path.join(historyDir, 'foo.bin.v1');
|
|
fs.copyFileSync(srcFile, historySnap);
|
|
const originalHash = sha256(srcFile);
|
|
|
|
// Step 2: Claude Edit 写入坏版本
|
|
fs.writeFileSync(srcFile, badContent);
|
|
|
|
// Step 3: PostToolUse hook 同步 snapshot 到 staging (模拟)
|
|
const stagingSnap = path.join(stagingDir, 'foo.bin');
|
|
fs.copyFileSync(srcFile, stagingSnap);
|
|
|
|
// Step 4: validator 失败 → 回滚
|
|
// 4a. 从 file-history 恢复原文件
|
|
fs.copyFileSync(historySnap, srcFile);
|
|
// 4b. staging 版本 mv 到 quarantine
|
|
const quarantined = path.join(quarantineDir, 'session-x_foo.bin');
|
|
fs.renameSync(stagingSnap, quarantined);
|
|
|
|
// 验证
|
|
const restoredHash = sha256(srcFile);
|
|
const restoredContent = fs.readFileSync(srcFile);
|
|
const quarantineContent = fs.readFileSync(quarantined);
|
|
|
|
const bytesEqual = Buffer.compare(restoredContent, Buffer.isBuffer(originalContent) ? originalContent : Buffer.from(originalContent)) === 0;
|
|
const quarantineEqual = Buffer.compare(quarantineContent, Buffer.isBuffer(badContent) ? badContent : Buffer.from(badContent)) === 0;
|
|
const hashMatch = restoredHash === originalHash;
|
|
|
|
return {
|
|
scenario: name,
|
|
isBinary,
|
|
originalHash,
|
|
restoredHash,
|
|
hashMatch,
|
|
bytesEqual,
|
|
quarantineEqual,
|
|
pass: hashMatch && bytesEqual && quarantineEqual,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
fs.mkdirSync(SANDBOX, { recursive: true });
|
|
const results = [
|
|
scenario('text-ascii', 'function foo() { return 1; }\n', 'MALICIOUS\n', false),
|
|
scenario('text-utf8-bom', '中文内容\n测试', 'BAD\n坏内容', false),
|
|
scenario('text-lf-crlf-mix',
|
|
'line1\r\nline2\nline3\r\n',
|
|
'line1\nline2\r\nline3',
|
|
false),
|
|
scenario('binary-null-bytes',
|
|
Buffer.from([0x00, 0xff, 0x7f, 0x80, 0x01, 0xde, 0xad, 0xbe, 0xef]),
|
|
Buffer.from([0xba, 0xd0, 0xc0, 0xde]),
|
|
true),
|
|
scenario('empty-file', '', 'not-empty', false),
|
|
scenario('large-1mb',
|
|
Buffer.alloc(1024 * 1024, 0xab),
|
|
Buffer.alloc(1024 * 1024, 0xcd),
|
|
true),
|
|
];
|
|
|
|
const pass = results.filter(r => r.pass).length;
|
|
const report = {
|
|
hypothesis: 'H3 · staging snapshot 回滚的正确性',
|
|
platform: os.platform() + ' ' + os.release(),
|
|
nodeVersion: process.version,
|
|
timestamp: new Date().toISOString(),
|
|
totalScenarios: results.length,
|
|
passedScenarios: pass,
|
|
results,
|
|
verdict: { pass: pass === results.length, passRate: (pass / results.length * 100).toFixed(1) + '%' },
|
|
recommendation: pass === results.length
|
|
? '✅ 回滚逻辑字节级正确, 覆盖 6 种文件类型; 可接入生产 (需加 fsync 保证掉电安全)'
|
|
: '❌ 存在回滚失败场景, 见 results[].pass',
|
|
};
|
|
fs.writeFileSync(path.join(SANDBOX, 'h3-report.json'), JSON.stringify(report, null, 2), 'utf8');
|
|
console.log(JSON.stringify(report, null, 2));
|
|
}
|
|
|
|
main();
|