dataMigration.test.ts 5.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. // @vitest-environment node
  2. import { afterEach, describe, expect, it } from 'vitest'
  3. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { DatabaseSync } from 'node:sqlite'
  7. import { resolveDataHome } from '../../bin/lib/data-home.mjs'
  8. import { migrateDataHome } from '../../bin/lib/data-migration.mjs'
  9. const roots: string[] = []
  10. function fixture() {
  11. const userHome = mkdtempSync(join(tmpdir(), 'yaoyao-migration-')); roots.push(userHome)
  12. const old = join(userHome, '.hermes-yaoyao'), home = join(userHome, '.yaoyao')
  13. mkdirSync(old, { mode: 0o700 })
  14. return { userHome, old, home }
  15. }
  16. afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
  17. describe('data directory migration', () => {
  18. it('normalizes the old default and preserves explicit custom homes', () => {
  19. const { userHome, old, home } = fixture()
  20. expect(resolveDataHome(undefined, { userHome })).toBe(home)
  21. expect(resolveDataHome(old, { userHome })).toBe(home)
  22. expect(resolveDataHome('/srv/custom', { userHome })).toBe('/srv/custom')
  23. })
  24. it('moves accounts, binary keys, WAL data, Runner state and files and rebases only internal paths', () => {
  25. const { userHome, old, home } = fixture(), key = Buffer.from([0, 23, 255, 8])
  26. writeFileSync(join(old, 'workspace-key.bin'), key)
  27. writeFileSync(join(old, 'users.json'), '{"id":"same-user"}')
  28. mkdirSync(join(old, 'runner-state', 'runner'), { recursive: true })
  29. writeFileSync(join(old, 'runner-state', 'runner', 'config.enc'), key)
  30. mkdirSync(join(old, 'uploads')); writeFileSync(join(old, 'uploads', 'example.txt'), 'file-content')
  31. mkdirSync(join(old, 'updates')); writeFileSync(join(old, 'updates', 'last-success.json'), JSON.stringify({snapshot: old + '/updates/backups/1', previous: {plist: {EnvironmentVariables: {HERMES_YAOYAO_HOME: old}}}}))
  32. const db = new DatabaseSync(join(old, 'uploads.sqlite3'))
  33. db.exec('PRAGMA journal_mode=WAL; CREATE TABLE uploads(id TEXT, path TEXT, message TEXT)')
  34. db.prepare('INSERT INTO uploads VALUES(?,?,?)').run('1', old + '/uploads/example.txt', old + '/leave-message-text')
  35. db.prepare('INSERT INTO uploads VALUES(?,?,?)').run('2', old + '-other/file', 'external')
  36. db.close()
  37. expect(migrateDataHome(home, { userHome })).toBe(true)
  38. expect(existsSync(old)).toBe(false)
  39. expect(readFileSync(join(home, 'workspace-key.bin'))).toEqual(key)
  40. expect(readFileSync(join(home, 'runner-state/runner/config.enc'))).toEqual(key)
  41. expect(readFileSync(join(home, 'users.json'), 'utf8')).toContain('same-user')
  42. const moved = new DatabaseSync(join(home, 'uploads.sqlite3'))
  43. expect(moved.prepare('SELECT * FROM uploads ORDER BY id').all()).toEqual([
  44. { id: '1', path: home + '/uploads/example.txt', message: old + '/leave-message-text' },
  45. { id: '2', path: old + '-other/file', message: 'external' },
  46. ])
  47. expect(moved.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' }); moved.close()
  48. expect(JSON.parse(readFileSync(join(home, 'updates/last-success.json'), 'utf8')).snapshot).toBe(home + '/updates/backups/1')
  49. expect(migrateDataHome(home, { userHome })).toBe(false)
  50. })
  51. it('refuses live service or Runner locks without moving either directory', () => {
  52. const { userHome, old, home } = fixture()
  53. mkdirSync(join(old, 'runner-state/r1'), { recursive: true })
  54. const db = new DatabaseSync(join(old, 'runner-state/r1/service-instance.sqlite3')); db.exec('BEGIN EXCLUSIVE')
  55. try { expect(() => migrateDataHome(home, { userHome })).toThrow('使用'); expect(existsSync(old)).toBe(true); expect(existsSync(home)).toBe(false) }
  56. finally { db.close() }
  57. })
  58. it('preserves both populated directories rather than overwriting a newer installation', () => {
  59. const { userHome, old, home } = fixture(); mkdirSync(home)
  60. writeFileSync(join(old, 'users.json'), 'old'); writeFileSync(join(home, 'users.json'), 'new')
  61. expect(() => migrateDataHome(home, { userHome })).toThrow('同时存在')
  62. expect(readFileSync(join(old, 'users.json'), 'utf8')).toBe('old'); expect(readFileSync(join(home, 'users.json'), 'utf8')).toBe('new')
  63. })
  64. it('defers old updater transactions and resumes a crash after the directory rename', () => {
  65. const { userHome, old, home } = fixture(); mkdirSync(join(old, 'updates'))
  66. writeFileSync(join(old, 'updates/active.lock'), JSON.stringify({ pid: process.pid }))
  67. expect(resolveDataHome(undefined, { userHome, preserveActiveUpdate: true })).toBe(old)
  68. expect(() => migrateDataHome(home, { userHome })).toThrow('事务')
  69. rmSync(join(old, 'updates/active.lock')); expect(migrateDataHome(home, { userHome })).toBe(true)
  70. const marker = join(home, '.data-home-migration.json'); writeFileSync(marker, JSON.stringify({ source: old, target: home, phase: 'moving' }))
  71. migrateDataHome(home, { userHome }); expect(JSON.parse(readFileSync(marker, 'utf8')).phase).toBe('complete')
  72. })
  73. })