const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const Database = require('better-sqlite3-multiple-ciphers'); const { executeUpdate } = require('../lib/updateWorker'); for (const mode of ['success', 'build-failure', 'restart-failure', 'health-failure', 'rollback-failure']) { test(`web update deployment: ${mode}`, async t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'happy-update-')); let live; t.after(() => { if (live?.open) live.close(); fs.rmSync(root, { recursive: true, force: true }); }); const data = path.join(root, 'backend/data'); const dir = path.join(data, 'system-update'); const jobDir = path.join(dir, 'test-job'); const stage = path.join(jobDir, 'source'); const modules = path.join(root, 'backend/node_modules'); const dist = path.join(root, 'frontend/dist'); for (const folder of [jobDir, modules, dist]) fs.mkdirSync(folder, { recursive: true }); fs.writeFileSync(path.join(modules, 'marker'), 'old modules'); fs.writeFileSync(path.join(dist, 'index.html'), 'old page'); fs.writeFileSync(path.join(root, 'server-code'), 'old'); fs.writeFileSync(path.join(dir, 'status.json'), JSON.stringify({ state: 'queued' })); fs.writeFileSync(path.join(dir, 'lock'), 'locked'); live = new Database(path.join(data, 'trade.db')); live.pragma('journal_mode = WAL'); live.exec('CREATE TABLE assets (value TEXT); INSERT INTO assets VALUES (\'real business data\')'); const commands = []; const hooks = { run: async (command, args, cwd) => { commands.push([command, ...args]); if (command === 'git') { if (args[0] === 'rev-parse') return args[1] === 'HEAD' ? 'old-commit' : 'new-commit'; if (args[0] === 'status') return ''; if (args[0] === 'worktree' && args[1] === 'add') { for (const folder of ['backend/node_modules', 'frontend/dist']) fs.mkdirSync(path.join(stage, folder), { recursive: true }); fs.writeFileSync(path.join(stage, 'package.json'), JSON.stringify({ version: '2.0.0' })); fs.writeFileSync(path.join(stage, 'backend/node_modules/marker'), 'new modules'); fs.writeFileSync(path.join(stage, 'frontend/dist/index.html'), 'new page'); } if (args[0] === 'reset') fs.writeFileSync(path.join(root, 'server-code'), args[2] === 'old-commit' ? 'old' : 'new'); if (args[0] === 'worktree' && args[1] === 'remove') fs.rmSync(stage, { recursive: true, force: true }); } if (command === 'npm' && args[0] === 'run' && mode === 'build-failure') throw new Error('build failed'); if (command === 'systemctl' && args[0] === 'restart' && ['restart-failure', 'rollback-failure'].includes(mode)) throw new Error('restart failed'); if (command === 'systemctl' && args[0] === 'start' && mode === 'rollback-failure') throw new Error('rollback failed'); return ''; }, backup: destination => live.backup(destination), healthy: async expected => { if (expected && mode === 'health-failure') throw new Error('health check failed'); } }; await executeUpdate({ root, data, dir, jobDir, service: 'erp', repo: 'owner/repo', tag: 'v2.0.0', version: '2.0.0', node: process.execPath, port: '4300' }, hooks); const status = JSON.parse(fs.readFileSync(path.join(dir, 'status.json'), 'utf8')); const success = mode === 'success'; assert.equal(status.state, success ? 'completed' : 'failed'); assert.equal(fs.readFileSync(path.join(modules, 'marker'), 'utf8'), success ? 'new modules' : 'old modules'); assert.equal(fs.readFileSync(path.join(dist, 'index.html'), 'utf8'), success ? 'new page' : 'old page'); assert.equal(fs.readFileSync(path.join(root, 'server-code'), 'utf8'), success ? 'new' : 'old'); assert.equal(fs.existsSync(path.join(dir, 'lock')), false); assert.equal(live.prepare('SELECT value FROM assets').get().value, 'real business data'); if (mode === 'build-failure') { assert.ok(!commands.some(args => args[0] === 'systemctl' || args[1] === 'reset')); } else { const backup = new Database(path.join(jobDir, 'trade-before-update.db'), { readonly: true }); assert.equal(backup.prepare('SELECT value FROM assets').get().value, 'real business data'); backup.close(); } if (['restart-failure', 'health-failure'].includes(mode)) assert.match(status.message, /已恢复上一版/); if (mode === 'rollback-failure') assert.match(status.message, /自动恢复失败/); }); }