updater.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { lstatSync, mkdtempSync, rmSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it, vi } from 'vitest'
  5. import { launchAgentPlist } from '../bin/hermes-yaoyao.mjs'
  6. import {
  7. currentTarget,
  8. formatCommandFailure,
  9. restorePreviousService,
  10. serviceInstallInvocation,
  11. switchCurrent,
  12. validateManifest,
  13. } from '../bin/hermes-yaoyao-updater.mjs'
  14. const roots: string[] = []
  15. afterEach(() => {
  16. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  17. })
  18. describe('standalone updater primitives', () => {
  19. it('atomically switches the stable current symlink', () => {
  20. const root = mkdtempSync(join(tmpdir(), 'hermes-updater-'))
  21. roots.push(root)
  22. const first = join(root, 'releases', '0.2.0-a')
  23. const second = join(root, 'releases', '0.3.0-b')
  24. switchCurrent(root, first, 'first')
  25. expect(currentTarget(root)).toBe(first)
  26. switchCurrent(root, second, 'second')
  27. expect(currentTarget(root)).toBe(second)
  28. expect(lstatSync(join(root, 'current')).isSymbolicLink()).toBe(true)
  29. })
  30. it('rejects a mismatched release manifest before any service transition', () => {
  31. expect(() => validateManifest({
  32. schemaVersion: 1,
  33. releaseVersion: '0.3.0',
  34. webVersion: '0.3.1',
  35. pluginVersion: '1.8.0',
  36. gitTag: 'v0.3.0',
  37. })).toThrow('版本组合不一致')
  38. })
  39. it('keeps useful command output while redacting credentials', () => {
  40. const error = Object.assign(new Error('Command failed'), {
  41. status: 127,
  42. stdout: '\u001B[31m发布版本校验通过\u001B[0m',
  43. stderr: 'https://secret@example.test/release.git\nsh: vue-tsc: command not found',
  44. })
  45. const message = formatCommandFailure('npm', ['run', 'build'], error)
  46. expect(message).toContain('npm run build 失败,退出码 127')
  47. expect(message).toContain('发布版本校验通过')
  48. expect(message).toContain('https://***@example.test/release.git')
  49. expect(message).toContain('vue-tsc: command not found')
  50. expect(message).not.toContain('secret')
  51. expect(message).not.toContain('\u001B')
  52. })
  53. it('uses the new lifecycle code while starting a rolled-back release', () => {
  54. const releaseRoot = '/Users/test/.local/share/hermes-yaoyao'
  55. const oldRelease = join(releaseRoot, 'releases', '0.2.11-old')
  56. const newRelease = join(releaseRoot, 'releases', '0.2.12-new')
  57. const invocation = serviceInstallInvocation(
  58. oldRelease,
  59. { releaseRoot, source: 'https://example.test/hermes-yaoyao.git' },
  60. true,
  61. newRelease,
  62. )
  63. expect(invocation.cli).toBe(join(newRelease, 'bin', 'hermes-yaoyao.mjs'))
  64. expect(invocation.env.HERMES_YAOYAO_SERVICE_ROOT).toBe(join(releaseRoot, 'current'))
  65. expect(invocation.env.HERMES_YAOYAO_RELEASE_SOURCE).toBe('https://example.test/hermes-yaoyao.git')
  66. const sourceRollback = serviceInstallInvocation(
  67. oldRelease,
  68. { releaseRoot, source: 'https://example.test/hermes-yaoyao.git' },
  69. false,
  70. newRelease,
  71. )
  72. expect(sourceRollback.cli).toBe(join(newRelease, 'bin', 'hermes-yaoyao.mjs'))
  73. expect(sourceRollback.env.HERMES_YAOYAO_SERVICE_ROOT).toBe(oldRelease)
  74. const rollbackPlist = launchAgentPlist({ serviceRoot: sourceRollback.env.HERMES_YAOYAO_SERVICE_ROOT })
  75. expect(rollbackPlist).toContain(`<string>${join(oldRelease, 'dist-server', 'server', 'index.js')}</string>`)
  76. expect(rollbackPlist).toContain(`<key>WorkingDirectory</key><string>${oldRelease}</string>`)
  77. })
  78. it('routes release and source rollbacks through the new lifecycle root', () => {
  79. const plan = {
  80. releaseRoot: '/Users/test/.local/share/hermes-yaoyao',
  81. source: 'https://example.test/hermes-yaoyao.git',
  82. }
  83. const lifecycleRoot = join(plan.releaseRoot, 'releases', '0.2.12-new')
  84. const previousRelease = join(plan.releaseRoot, 'releases', '0.2.11-old')
  85. const previousSource = '/Users/test/git/hermes-yaoyao'
  86. const switchCurrentCommand = vi.fn()
  87. const removeCurrentCommand = vi.fn()
  88. const startServiceCommand = vi.fn()
  89. restorePreviousService({
  90. plan,
  91. previousCurrentTarget: previousRelease,
  92. previousServiceRoot: previousSource,
  93. lifecycleRoot,
  94. token: 'release-rollback',
  95. switchCurrentCommand,
  96. removeCurrentCommand,
  97. startServiceCommand,
  98. })
  99. expect(switchCurrentCommand).toHaveBeenCalledWith(plan.releaseRoot, previousRelease, 'release-rollback')
  100. expect(removeCurrentCommand).not.toHaveBeenCalled()
  101. expect(startServiceCommand).toHaveBeenCalledWith(
  102. join(plan.releaseRoot, 'current'), plan, true, lifecycleRoot,
  103. )
  104. switchCurrentCommand.mockClear()
  105. startServiceCommand.mockClear()
  106. restorePreviousService({
  107. plan,
  108. previousCurrentTarget: undefined,
  109. previousServiceRoot: previousSource,
  110. lifecycleRoot,
  111. token: 'source-rollback',
  112. switchCurrentCommand,
  113. removeCurrentCommand,
  114. startServiceCommand,
  115. })
  116. expect(switchCurrentCommand).not.toHaveBeenCalled()
  117. expect(removeCurrentCommand).toHaveBeenCalledWith(plan.releaseRoot)
  118. expect(startServiceCommand).toHaveBeenCalledWith(previousSource, plan, false, lifecycleRoot)
  119. })
  120. it('refuses rollback without a usable lifecycle root', () => {
  121. expect(() => restorePreviousService({
  122. plan: { releaseRoot: '/tmp/releases', source: 'https://example.test/repo.git' },
  123. previousCurrentTarget: '/tmp/releases/old',
  124. previousServiceRoot: '/tmp/source',
  125. lifecycleRoot: undefined,
  126. token: 'missing-lifecycle',
  127. })).toThrow('当前发布版本不可用')
  128. })
  129. })