const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const { createUpgradeNonceCoordinator, normalizeUpgradeError, UPGRADE_NONCE_EVENT_VERIFIED, UPGRADE_NONCE_EVENT_FAILED, } = require('../upgrade-nonce.cjs'); /** 固定的时钟:startedAt=999000ms,当前=1000000ms(间隔 1s,不触发默认 10min 过期)。 */ const T0 = 999_000; const T1 = 1_000_000; function makePending(overrides = {}) { return { id: 'upgrade-1', currentVersion: '0.1.0', targetVersion: '0.2.0', expectedVersion: '0.2.0', healthBase: 'http://127.0.0.1:8000', audit: { auditApiUrl: 'http://127.0.0.1:8000', auditToken: 'audit-token', sidecarNonce: 'old-nonce' }, startedAt: new Date(T0).toISOString(), ...overrides, }; } function makeCoordinator(overrides = {}) { return createUpgradeNonceCoordinator({ verifyHealth: async ({ origin, nonce, expectedVersion }) => ({ ok: true, version: '0.2.0', nonce, expectedVersion, }), now: () => T1, ...overrides, }); } test('restarted 事件触发重验证:用新 nonce 探测(版本比对),成功标记完成并清空', async () => { const seen = []; const completed = []; const coordinator = makeCoordinator({ verifyHealth: async ({ origin, nonce, expectedVersion }) => { seen.push({ origin, nonce, expectedVersion }); return { ok: true, version: '0.2.0', nonce }; }, onComplete: async ({ pending, health, nonce }) => { completed.push({ id: pending.id, health, nonce }); }, }); const pending = coordinator.setPending(makePending()); assert.equal(coordinator.getPending().status, 'installing'); await coordinator.markInstalled({ id: pending.id, report: { ok: true, targetVersion: '0.2.0', nonce: 'old-nonce' } }); assert.equal(coordinator.getPending().status, 'verifying'); const result = await coordinator.onSidecarRestarted({ origin: 'http://127.0.0.1:9000', nonce: 'new-nonce-xyz', }); assert.equal(result.handled, true); assert.equal(result.ok, true); assert.equal(result.nonce, 'new-nonce-xyz'); // 重验证必须使用新 nonce + 目标版本比对 assert.equal(seen.length, 1); assert.deepEqual(seen[0], { origin: 'http://127.0.0.1:9000', nonce: 'new-nonce-xyz', expectedVersion: '0.2.0', }); assert.equal(completed.length, 1); assert.equal(completed[0].nonce, 'new-nonce-xyz'); assert.equal(coordinator.getPending(), null); // 已清空 const last = coordinator.getLastResult(); assert.equal(last.status, 'verified'); assert.equal(last.nonce, 'new-nonce-xyz'); assert.equal(last.verifiedVia, 'sidecar-restart'); }); test('版本不匹配失败留痕:onFailure 收到错误、pending 标记 failed 并清空', async () => { const failures = []; const coordinator = makeCoordinator({ verifyHealth: async () => { const error = new Error('健康检查版本不匹配: 期望=0.2.0 实际=0.1.0'); error.code = 'HEALTH_VERSION_MISMATCH'; error.details = { expectedVersion: '0.2.0', actual: '0.1.0' }; throw error; }, onFailure: async ({ pending, error }) => { failures.push({ id: pending.id, error }); }, }); const pending = coordinator.setPending(makePending()); await coordinator.markInstalled({ id: pending.id, report: { ok: true, nonce: 'old-nonce' } }); const result = await coordinator.onSidecarRestarted({ origin: 'http://127.0.0.1:9000', nonce: 'new-nonce' }); assert.equal(result.handled, true); assert.equal(result.ok, false); assert.equal(result.error.code, 'HEALTH_VERSION_MISMATCH'); assert.equal(result.error.details.actual, '0.1.0'); assert.equal(failures.length, 1); assert.equal(failures[0].error.code, 'HEALTH_VERSION_MISMATCH'); assert.equal(coordinator.getPending(), null); const last = coordinator.getLastResult(); assert.equal(last.status, 'failed'); assert.equal(last.error.code, 'HEALTH_VERSION_MISMATCH'); assert.equal(last.nonce, 'new-nonce'); }); test('无 pending 时 restarted 事件 no-op(不调用 verifyHealth)', async () => { let verifyCalls = 0; const coordinator = makeCoordinator({ verifyHealth: async () => { verifyCalls += 1; return { ok: true }; }, }); const result = await coordinator.onSidecarRestarted({ origin: 'http://127.0.0.1:9000', nonce: 'new-nonce' }); assert.deepEqual(result, { handled: false, reason: 'no-pending' }); assert.equal(verifyCalls, 0); assert.equal(coordinator.getPending(), null); assert.equal(coordinator.getLastResult(), null); }); test('安装期间 restarted 事件被延迟记录,markInstalled 后立即用新 nonce 补做重验证', async () => { const seen = []; const coordinator = makeCoordinator({ verifyHealth: async ({ nonce }) => { seen.push(nonce); return { ok: true, version: '0.2.0' }; }, }); const pending = coordinator.setPending(makePending()); // applyUpdate 尚未返回(installing)时 sidecar 已重启 const deferred = await coordinator.onSidecarRestarted({ origin: 'http://127.0.0.1:9000', nonce: 'new-nonce' }); assert.equal(deferred.reason, 'installing-deferred'); assert.equal(seen.length, 0); // applyUpdate 返回后补做最终校验 await coordinator.markInstalled({ id: pending.id, report: { ok: true, nonce: 'old-nonce' } }); assert.equal(seen.length, 1); assert.equal(seen[0], 'new-nonce'); assert.equal(coordinator.getPending(), null); assert.equal(coordinator.getLastResult().status, 'verified'); }); test('已终结(failed)的 pending 不再重复验证', async () => { let verifyCalls = 0; const coordinator = makeCoordinator({ verifyHealth: async () => { verifyCalls += 1; return { ok: true }; }, }); const pending = coordinator.setPending(makePending()); coordinator.markFailed({ id: pending.id, error: new Error('install failed') }); const result = await coordinator.onSidecarRestarted({ origin: 'http://x', nonce: 'n' }); assert.equal(result.reason, 'already-settled'); assert.equal(result.status, 'failed'); assert.equal(verifyCalls, 0); }); test('过期 pending(超过 maxPendingMs)不触发重验证并清理', async () => { let verifyCalls = 0; const coordinator = createUpgradeNonceCoordinator({ verifyHealth: async () => { verifyCalls += 1; return { ok: true }; }, maxPendingMs: 60_000, now: () => 2_000_000, // startedAt=999000 → 间隔约 1001s > 60s }); const pending = coordinator.setPending(makePending()); await coordinator.markInstalled({ id: pending.id, report: { ok: true } }); const result = await coordinator.onSidecarRestarted({ origin: 'http://x', nonce: 'n' }); assert.equal(result.reason, 'stale'); assert.equal(verifyCalls, 0); assert.equal(coordinator.getPending(), null); assert.equal(coordinator.getLastResult().status, 'stale'); }); test('setPending:进行中 pending 拒绝新登记;已终结可替换', async () => { const coordinator = makeCoordinator(); const first = coordinator.setPending(makePending({ id: 'u1' })); assert.equal(first.id, 'u1'); assert.equal(coordinator.setPending(makePending({ id: 'u2' })), null); // 进行中拒绝 coordinator.markFailed({ id: 'u1', error: new Error('x') }); const second = coordinator.setPending(makePending({ id: 'u3' })); assert.equal(second.id, 'u3'); // 已终结可替换 assert.equal(coordinator.getPending().status, 'installing'); }); test('deferFinalVerify=false(无 sidecar/dev 环境):applyUpdate 后直接完成并清空', async () => { let verifyCalls = 0; const coordinator = makeCoordinator({ verifyHealth: async () => { verifyCalls += 1; return { ok: true }; }, }); const pending = coordinator.setPending(makePending()); await coordinator.markInstalled({ id: pending.id, report: { ok: true, targetVersion: '0.2.0', health: { ok: true, version: '0.2.0' } }, deferFinalVerify: false, }); assert.equal(verifyCalls, 0); assert.equal(coordinator.getPending(), null); const last = coordinator.getLastResult(); assert.equal(last.status, 'verified'); assert.equal(last.verifiedVia, 'preliminary'); }); test('onComplete 抛错不阻断状态机(升级仍标记完成)', async () => { const coordinator = makeCoordinator({ onComplete: async () => { throw new Error('audit upload failed'); }, logger: { warn() {}, error() {}, info() {} }, }); const pending = coordinator.setPending(makePending()); await coordinator.markInstalled({ id: pending.id, report: { ok: true } }); const result = await coordinator.onSidecarRestarted({ origin: 'http://x', nonce: 'n' }); assert.equal(result.handled, true); assert.equal(result.ok, true); assert.equal(coordinator.getPending(), null); assert.equal(coordinator.getLastResult().status, 'verified'); }); test('expectedVersion 未提供时不自动回填 targetVersion(退化为 200 检查,避免旧 main 进程误报)', async () => { const seen = []; const coordinator = makeCoordinator({ verifyHealth: async (args) => { seen.push(args); return { ok: true }; }, }); const pending = coordinator.setPending(makePending({ expectedVersion: null, targetVersion: '0.2.0' })); await coordinator.markInstalled({ id: pending.id, report: { ok: true, targetVersion: '0.2.0' } }); await coordinator.onSidecarRestarted({ origin: 'http://127.0.0.1:9000', nonce: 'new-nonce' }); assert.equal(seen.length, 1); assert.equal(seen[0].expectedVersion, null); assert.equal(seen[0].nonce, 'new-nonce'); assert.equal(coordinator.getLastResult().status, 'verified'); }); test('normalizeUpgradeError 归一化 UpgradeError / 普通错误 / 原始值', () => { const upErr = new Error('boom'); upErr.code = 'HEALTH_TIMEOUT'; upErr.details = { a: 1 }; assert.deepEqual(normalizeUpgradeError(upErr), { code: 'HEALTH_TIMEOUT', message: 'boom', details: { a: 1 } }); assert.deepEqual(normalizeUpgradeError(new Error('plain')), { code: 'UNKNOWN', message: 'plain', details: null }); assert.deepEqual(normalizeUpgradeError('raw'), { code: 'UNKNOWN', message: 'raw', details: null }); assert.deepEqual(normalizeUpgradeError(null), { code: 'UNKNOWN', message: 'null', details: null }); }); test('审计事件名常量与 round-45 契约一致(可被 main 侧复用)', () => { assert.equal(UPGRADE_NONCE_EVENT_VERIFIED, 'upgrade.health-verified'); assert.equal(UPGRADE_NONCE_EVENT_FAILED, 'upgrade.health-verify-failed'); }); test('createUpgradeNonceCoordinator 缺 verifyHealth 时抛 TypeError', () => { assert.throws(() => createUpgradeNonceCoordinator({}), TypeError); assert.throws(() => createUpgradeNonceCoordinator(), TypeError); }); test('setPending 持久化:新协调器同 statePath 恢复 pending 为 verifying(round-48)', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-upgrade-nonce-')); const statePath = path.join(root, 'upgrade-state.json'); try { const first = makeCoordinator({ statePath }); first.setPending(makePending()); assert.ok(fs.existsSync(statePath), 'setPending 应写 upgrade-state.json'); const second = makeCoordinator({ statePath }); const restored = second.getPending(); assert.ok(restored, '新协调器应恢复 pending'); assert.equal(restored.id, 'upgrade-1'); assert.equal(restored.status, 'verifying'); assert.equal(restored.currentVersion, '0.1.0'); // 恢复后的 pending 可被 restarted 事件续接(用新 nonce 补验成功) const result = await second.onSidecarRestarted({ origin: 'http://127.0.0.1:8000', nonce: 'new-nonce' }); assert.equal(result.handled, true); assert.equal(result.ok, true); assert.equal(second.getPending(), null); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('恢复时过期 pending 被丢弃(round-48)', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-upgrade-nonce-')); const statePath = path.join(root, 'upgrade-state.json'); try { const first = makeCoordinator({ statePath, maxPendingMs: 10_000 }); first.setPending(makePending()); // startedAt=T0(999s),now=T1(1000s) assert.ok(fs.existsSync(statePath)); // 新协调器 maxPendingMs=100(< 1s 间隔)→ 恢复时判定过期丢弃 const second = makeCoordinator({ statePath, maxPendingMs: 100 }); assert.equal(second.getPending(), null); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('settle 后持久化状态清空:新协调器无 pending(round-48)', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-upgrade-nonce-')); const statePath = path.join(root, 'upgrade-state.json'); try { const first = makeCoordinator({ statePath }); first.setPending(makePending()); await first.markInstalled({ report: { ok: true, targetVersion: '0.2.0' }, deferFinalVerify: false }); assert.equal(first.getPending(), null); const saved = JSON.parse(fs.readFileSync(statePath, 'utf8')); assert.equal(saved.pending, null); const second = makeCoordinator({ statePath }); assert.equal(second.getPending(), null); } finally { fs.rmSync(root, { recursive: true, force: true }); } });