const assert = require('node:assert/strict'); const fs = require('node:fs'); const http = require('node:http'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const { UpgradeError, Updater, compareVersions, loadManifest, verifyPackage, ensureNewerVersion, backupInstall, restoreBackup, probeHealth, } = require('../updater.cjs'); const EOL = '\n'; /** 临时目录夹具:installDir(模拟安装目录)+ 升级包 + 清单 */ function fixture() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-updater-')); const installDir = path.join(root, 'install'); const backupDir = path.join(root, 'backup'); const packageDir = path.join(root, 'upgrade'); const apsHome = path.join(root, '.aps'); fs.mkdirSync(path.join(installDir, 'resources'), { recursive: true }); fs.mkdirSync(packageDir, { recursive: true }); fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'old-exe-v0.1.0'); fs.writeFileSync(path.join(installDir, 'resources', 'index.html'), 'old'); return { root, installDir, backupDir, packageDir, apsHome }; } /** 写一个升级包 + 对应清单;返回 { manifestPath, packagePath, manifest } */ function makeUpgrade({ packageDir, version = '0.2.0', payload = 'installer-payload-v0.2.0', size, sha256 }) { const fileName = 'aps-agent-desktop-' + version + '.exe'; const packagePath = path.join(packageDir, fileName); fs.writeFileSync(packagePath, payload); const crypto = require('node:crypto'); const manifest = { schemaVersion: 1, product: 'aps-agent-desktop', version, platform: process.platform, package: { fileName, sha256: sha256 || crypto.createHash('sha256').update(payload).digest('hex'), size: size ?? Buffer.byteLength(payload), }, releasedAt: new Date().toISOString(), }; const manifestPath = path.join(packageDir, 'upgrade-manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); return { manifestPath, packagePath, manifest }; } /** 403 健康服务(模拟 sidecar nonce 门禁未通过) */ function startHealthServer403() { return new Promise((resolve) => { const server = http.createServer((req, res) => { res.writeHead(403, { 'content-type': 'text/plain' }); res.end('Forbidden'); }); server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ server, base: 'http://127.0.0.1:' + port }); }); }); } /** 本地健康检查服务:/api/health 返回 200 + 可选 version */ function startHealthServer({ version } = {}) { return new Promise((resolve) => { const server = http.createServer((req, res) => { if (req.url === '/api/health') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: true, ...(version ? { version } : {}) })); return; } res.writeHead(404); res.end(); }); server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ server, base: 'http://127.0.0.1:' + port }); }); }); } test('compareVersions: 0.2.0 > 0.1.0, equal, downgrade', () => { assert.equal(compareVersions('0.2.0', '0.1.0'), 1); assert.equal(compareVersions('0.1.0', '0.1.0'), 0); assert.equal(compareVersions('0.1.0', '0.2.0'), -1); assert.equal(compareVersions('1.10.0', '1.9.9'), 1); }); test('ensureNewerVersion rejects downgrade and same version', () => { assert.throws( () => ensureNewerVersion({ currentVersion: '0.2.0', targetVersion: '0.1.0' }), (err) => err.code === 'VERSION_NOT_NEWER', ); assert.throws( () => ensureNewerVersion({ currentVersion: '0.2.0', targetVersion: '0.2.0' }), (err) => err.code === 'VERSION_NOT_NEWER', ); assert.equal(ensureNewerVersion({ currentVersion: '0.1.0', targetVersion: '0.2.0' }), true); }); test('loadManifest rejects malformed manifest', () => { const { root } = fixture(); try { const bad = path.join(root, 'bad.json'); fs.writeFileSync(bad, '{}'); assert.throws(() => loadManifest(bad), (err) => err.code === 'MANIFEST_INVALID'); fs.writeFileSync(bad, 'not json'); assert.throws(() => loadManifest(bad), (err) => err.code === 'MANIFEST_READ_FAILED'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('verifyPackage rejects checksum mismatch (tampered installer)', async () => { const { root, packageDir } = fixture(); try { const { manifestPath, packagePath, manifest } = makeUpgrade({ packageDir }); manifest.package.sha256 = '0'.repeat(64); // 篡改清单哈希 fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); await assert.rejects( verifyPackage({ manifest, packagePath }), (err) => err.code === 'CHECKSUM_MISMATCH', ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('verifyPackage rejects size mismatch', async () => { const { root, packageDir } = fixture(); try { const { manifestPath, packagePath, manifest } = makeUpgrade({ packageDir, payload: 'abc' }); manifest.package.size = 9999; fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); await assert.rejects( verifyPackage({ manifest, packagePath }), (err) => err.code === 'SIZE_MISMATCH', ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('verifyPackage rejects missing package and platform mismatch', async () => { const { root, packageDir } = fixture(); try { const { manifest, packagePath } = makeUpgrade({ packageDir }); await assert.rejects( verifyPackage({ manifest, packagePath: path.join(packageDir, 'nope.exe'), platform: process.platform }), (err) => err.code === 'PACKAGE_NOT_FOUND', ); await assert.rejects( verifyPackage({ manifest, packagePath, platform: 'darwin' }), (err) => err.code === 'PLATFORM_MISMATCH', ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('backup/restore roundtrip restores files (copy mode)', async () => { const { root, installDir, backupDir } = fixture(); try { const backup = await backupInstall({ installDir, backupDir, mode: 'copy' }); assert.ok(backup.fileCount >= 2); assert.ok(fs.existsSync(path.join(backupDir, 'backup-manifest.json'))); // 破坏安装目录 fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'corrupted'); fs.rmSync(path.join(installDir, 'resources'), { recursive: true, force: true }); const restored = await restoreBackup({ backupDir, installDir, expectedMode: 'copy' }); assert.equal(restored.restored, true); assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0'); assert.equal(fs.readFileSync(path.join(installDir, 'resources', 'index.html'), 'utf8'), 'old'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('listing mode backup cannot restore content', async () => { const { root, installDir, backupDir } = fixture(); try { const backup = await backupInstall({ installDir, backupDir, mode: 'listing' }); assert.ok(backup.fileCount >= 2); await assert.rejects( restoreBackup({ backupDir, installDir, expectedMode: 'listing' }), (err) => err.code === 'BACKUP_LISTING_ONLY', ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('applyUpdate happy path: verify → backup → install → health check → success audit', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); const { server, base } = await startHealthServer({ version: '0.2.0' }); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); // 注入 fake installer:模拟 NSIS 写入新版本文件 const install = async () => { fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'new-exe-v0.2.0'); }; const report = await updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', healthBase: base, expectedVersion: '0.2.0', install, }); assert.equal(report.ok, true); assert.equal(report.targetVersion, '0.2.0'); assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'new-exe-v0.2.0'); // 审计事件齐全 const audit = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); const events = audit.map((line) => JSON.parse(line).event); assert.ok(events.includes('upgrade.verified')); assert.ok(events.includes('upgrade.backup')); assert.ok(events.includes('upgrade.installed')); assert.ok(events.includes('upgrade.health-check')); assert.ok(events.includes('upgrade.succeeded')); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('applyUpdate failure auto-rolls back and records rollback audit', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); const { server, base } = await startHealthServer({ version: '0.2.0' }); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); // 注入 installer:写坏文件 → 健康检查失败(版本不匹配) const install = async () => { fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'broken-exe'); fs.writeFileSync(path.join(installDir, 'resources', 'index.html'), 'broken'); }; await assert.rejects( updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', healthBase: base, expectedVersion: '0.3.0', // 与健康检查返回的 0.2.0 不匹配 → 失败 install, }), (err) => err.code === 'HEALTH_VERSION_MISMATCH', ); // 自动回滚:文件应恢复为备份内容 assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0'); assert.equal(fs.readFileSync(path.join(installDir, 'resources', 'index.html'), 'utf8'), 'old'); // 审计:upgrade.failed + rollback.done const audit = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); const events = audit.map((line) => JSON.parse(line).event); assert.ok(events.includes('upgrade.failed')); assert.ok(events.includes('rollback.done')); const failed = audit.map((line) => JSON.parse(line)).find((e) => e.event === 'upgrade.failed'); assert.equal(failed.payload.reason, 'HEALTH_VERSION_MISMATCH'); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('applyUpdate rejects tampered installer before touching install dir', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); try { const { manifestPath, packagePath, manifest } = makeUpgrade({ packageDir }); manifest.package.sha256 = 'f'.repeat(64); // 篡改哈希 fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); await assert.rejects( updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', install: async () => { throw new Error('should not install'); }, }), (err) => err.code === 'CHECKSUM_MISMATCH', ); // 未备份、未安装 assert.equal(fs.existsSync(backupDir), false); assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('applyUpdate rejects version downgrade', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir, version: '0.0.9' }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); await assert.rejects( updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', install: async () => { throw new Error('should not install'); }, }), (err) => err.code === 'VERSION_NOT_NEWER', ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('checkForUpdate reports updateAvailable/verified', async () => { const { root, packageDir, apsHome } = fixture(); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir, version: '0.2.0' }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); const result = await updater.checkForUpdate({ manifestPath, packagePath, currentVersion: '0.1.0' }); assert.equal(result.updateAvailable, true); assert.equal(result.verified, true); assert.equal(result.targetVersion, '0.2.0'); const stale = await updater.checkForUpdate({ manifestPath, packagePath, currentVersion: '0.3.0' }); assert.equal(stale.updateAvailable, false); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('probeHealth succeeds and detects version mismatch', async () => { const { server, base } = await startHealthServer({ version: '0.2.0' }); try { const ok = await probeHealth({ base, expectedVersion: '0.2.0' }); assert.equal(ok.ok, true); assert.equal(ok.version, '0.2.0'); await assert.rejects( probeHealth({ base, expectedVersion: '9.9.9' }), (err) => err.code === 'HEALTH_VERSION_MISMATCH', ); } finally { server.close(); } }); test('UpgradeError carries code', () => { const err = new UpgradeError('SIZE_MISMATCH', 'size mismatch', { expected: 1, actual: 2 }); assert.equal(err.code, 'SIZE_MISMATCH'); assert.equal(err.details.expected, 1); }); /** 伪造审计上报端点:记录 POST /api/gov/audit 的 method/url/headers/body;可选返回状态码 */ function startAuditServer({ status = 200 } = {}) { return new Promise((resolve) => { const received = []; const server = http.createServer((req, res) => { let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { received.push({ method: req.method, url: req.url, headers: req.headers, body }); res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: status < 400 })); }); }); server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ server, received, base: 'http://127.0.0.1:' + port }); }); }); } test('probeHealth falls back to HTTP-200-only check when body has no version', async () => { const { server, base } = await startHealthServer({}); // 响应体无 version 字段 try { const ok = await probeHealth({ base, expectedVersion: '0.2.0' }); assert.equal(ok.ok, true); assert.equal(ok.version, null); const plain = await probeHealth({ base }); assert.equal(plain.ok, true); assert.equal(plain.version, null); } finally { server.close(); } }); test('writeAudit uploads audit with one-time bearer token and sidecar nonce', async () => { const { root, apsHome } = fixture(); const { server, received, base } = await startAuditServer(); try { const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} }, auditApiUrl: base, auditToken: 'audit-token-ff', sidecarNonce: 'n'.repeat(64), }); const entry = await updater.writeAudit({ event: 'upgrade.verified', payload: { fromVersion: '0.1.0', toVersion: '0.2.0' } }); assert.equal(entry.event, 'upgrade.verified'); assert.equal(received.length, 1); assert.equal(received[0].method, 'POST'); assert.equal(received[0].url, '/api/gov/audit/events'); assert.equal(received[0].headers.authorization, 'Bearer audit-token-ff'); assert.equal(received[0].headers['x-aps-sidecar-nonce'], 'n'.repeat(64)); const sent = JSON.parse(received[0].body); assert.equal(sent.action, 'upgrade.verified'); // 本地 event 字段映射为链上 action assert.equal(sent.category, 'UPGRADE'); assert.equal(sent.actor, 'desktop-updater'); assert.equal(sent.power, 'P0'); assert.equal(sent.result, 'SUCCESS'); assert.equal(sent.timestamp, entry.ts); assert.equal(typeof sent.idempotencyKey, 'string'); assert.equal(sent.idempotencyKey.length, 64); // sha256 确定性指纹 assert.deepEqual(sent.rationale.payload, { fromVersion: '0.1.0', toVersion: '0.2.0' }); // 本地 JSONL 仍为默认审计通道 const local = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); assert.equal(JSON.parse(local[0]).event, 'upgrade.verified'); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('writeAudit without token stays local-only (no upload)', async () => { const { root, apsHome } = fixture(); const { server, received, base } = await startAuditServer(); try { const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); await updater.writeAudit({ event: 'rollback.done' }); assert.equal(received.length, 0); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('audit upload failure is non-fatal and keeps local JSONL', async () => { const { root, apsHome } = fixture(); const { server, received, base } = await startAuditServer({ status: 500 }); const warnings = []; try { const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn: (message) => warnings.push(String(message)) }, auditApiUrl: base, auditToken: 'audit-token-ff', }); const entry = await updater.writeAudit({ event: 'upgrade.failed', result: 'FAILED' }); assert.equal(entry.event, 'upgrade.failed'); // 上传失败不抛错 assert.equal(received.length, 1); assert.ok(warnings.length >= 1); const local = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); assert.equal(JSON.parse(local[0]).event, 'upgrade.failed'); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); /** 本地健康检查服务:/api/health 要求 x-aps-sidecar-nonce 匹配 expectedNonce(否则 403)。 */ function startNonceHealthServer({ version, expectedNonce } = {}) { return new Promise((resolve) => { const seen = []; const server = http.createServer((req, res) => { seen.push({ url: req.url, nonce: req.headers['x-aps-sidecar-nonce'] || null }); if (req.headers['x-aps-sidecar-nonce'] !== expectedNonce) { res.writeHead(403); res.end(); return; } if (req.url === '/api/health') { res.writeHead(200, { 'content-type': 'application/json', 'x-aps-sidecar-nonce': expectedNonce }); res.end(JSON.stringify({ ok: true, ...(version ? { version } : {}) })); return; } res.writeHead(404); res.end(); }); server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ server, seen, base: 'http://127.0.0.1:' + port }); }); }); } test('probeHealth sends sidecar nonce and flags nonce-gated 403', async () => { const expectedNonce = 'e'.repeat(64); const { server, seen, base } = await startNonceHealthServer({ version: '0.2.0', expectedNonce }); try { const ok = await probeHealth({ base, expectedVersion: '0.2.0', sidecarNonce: expectedNonce }); assert.equal(ok.ok, true); assert.equal(ok.version, '0.2.0'); assert.equal(ok.nonce, expectedNonce); assert.equal(seen[0].nonce, expectedNonce); // 缺失 nonce → 新网关 nonce 门禁 403 await assert.rejects( probeHealth({ base }), (err) => err.code === 'HEALTH_HTTP_STATUS' && err.details.statusCode === 403, ); // 旧/错误 nonce → 403 且 nonceMismatch=true(升级后 nonce 已轮换的可诊断信号) await assert.rejects( probeHealth({ base, sidecarNonce: 'd'.repeat(64) }), (err) => err.code === 'HEALTH_HTTP_STATUS' && err.details.statusCode === 403 && err.details.nonceMismatch === true, ); } finally { server.close(); } }); test('applyUpdate post-install health check uses the new gateway nonce', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); const newNonce = 'f'.repeat(64); const { server, seen, base } = await startNonceHealthServer({ version: '0.2.0', expectedNonce: newNonce }); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir }); // 升级前构造时侧写的是旧 nonce;applyUpdate 显式传入升级后新 sidecar 的 nonce 续接健康检查 const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} }, auditApiUrl: base, auditToken: 'audit-token-ff', sidecarNonce: 'old-nonce-' + '0'.repeat(54), }); const install = async () => { fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'new-exe-v0.2.0'); }; const report = await updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', healthBase: base, expectedVersion: '0.2.0', install, sidecarNonce: newNonce, // 新网关 nonce(main 在 sidecar 重启后获取并传入) }); assert.equal(report.ok, true); assert.equal(report.nonce, newNonce); assert.equal(report.health.nonce, newNonce); // 健康检查请求必须携带新网关 nonce(seen[0] 是升级前的审计上报,用旧 nonce 属预期 fail-soft) const healthSeen = seen.find((entry) => entry.url === '/api/health'); assert.equal(healthSeen.nonce, newNonce); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('audit upload 404/403 (endpoint missing / nonce gate) stays local-only', async () => { for (const status of [404, 403]) { const { root, apsHome } = fixture(); const { server, received, base } = await startAuditServer({ status }); const warnings = []; try { const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn: (message) => warnings.push(String(message)) }, auditApiUrl: base, auditToken: 'audit-token-ff', sidecarNonce: 'n'.repeat(64), }); const entry = await updater.writeAudit({ event: 'upgrade.installed' }); assert.equal(entry.event, 'upgrade.installed'); // 上报失败不抛错(fail-soft) assert.equal(received.length, 1); assert.ok(warnings.length >= 1); const local = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); assert.equal(JSON.parse(local[0]).event, 'upgrade.installed'); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } } }); test('applyUpdate health 403 with nonce defers verification (no rollback, round-46)', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); const { server, base } = await startHealthServer403(); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); const install = async () => { fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'new-exe-v0.2.0'); fs.writeFileSync(path.join(installDir, 'resources', 'index.html'), 'new'); }; const report = await updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', healthBase: base, install, sidecarNonce: 'deadbeef' + '0'.repeat(56), }); assert.equal(report.ok, false); assert.equal(report.deferredVerification, true); assert.equal(report.reason, 'NONCE_MISMATCH'); assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'new-exe-v0.2.0'); assert.equal(fs.readFileSync(path.join(installDir, 'resources', 'index.html'), 'utf8'), 'new'); const audit = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); const events = audit.map((line) => JSON.parse(line).event); assert.ok(events.includes('upgrade.health-pending')); assert.ok(!events.includes('rollback.done')); const pending = audit.map((line) => JSON.parse(line)).find((e) => e.event === 'upgrade.health-pending'); assert.equal(pending.result, 'PENDING'); assert.equal(pending.payload.reason, 'NONCE_MISMATCH'); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } }); test('applyUpdate health 403 without nonce still rolls back (round-46)', async () => { const { root, installDir, backupDir, packageDir, apsHome } = fixture(); const { server, base } = await startHealthServer403(); try { const { manifestPath, packagePath } = makeUpgrade({ packageDir }); const updater = new Updater({ apsHome, logger: { info() {}, error() {}, warn() {} } }); const install = async () => { fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'new-exe-v0.2.0'); fs.writeFileSync(path.join(installDir, 'resources', 'index.html'), 'new'); }; await assert.rejects( updater.applyUpdate({ manifestPath, packagePath, installDir, backupDir, currentVersion: '0.1.0', healthBase: base, install, }), (err) => err.code === 'HEALTH_HTTP_STATUS', ); assert.equal(fs.readFileSync(path.join(installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0'); assert.equal(fs.readFileSync(path.join(installDir, 'resources', 'index.html'), 'utf8'), 'old'); const audit = fs.readFileSync(path.join(apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8').trim().split('\n'); const events = audit.map((line) => JSON.parse(line).event); assert.ok(events.includes('rollback.done')); } finally { server.close(); fs.rmSync(root, { recursive: true, force: true }); } });