service-sync.test.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { mkdtemp, readFile, writeFile, rm, cp, realpath } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { dirname, join, resolve } from 'node:path'
  6. import { createServer } from 'node:net'
  7. import { spawn } from 'node:child_process'
  8. import { createHash } from 'node:crypto'
  9. import { _electron as electron } from '@playwright/test'
  10. import { LaunchAgentService, localRequest, synchronizeDesktop } from '../bin/lib/service-update.mjs'
  11. import { sealRuntime, verifyRuntimePackage } from '../bin/lib/runtime-release.mjs'
  12. const root = resolve(import.meta.dirname, '..')
  13. const payloadRoot = process.env.DESKTOP_TEST_EXECUTABLE
  14. ? resolve(dirname(process.env.DESKTOP_TEST_EXECUTABLE), '../Resources/runtime/web-service')
  15. : join(root, '.desktop-build', 'web-service')
  16. async function freePort() {
  17. const server = createServer(); await new Promise(done => server.listen(0, '127.0.0.1', done))
  18. const port = server.address().port; await new Promise(done => server.close(done)); return port
  19. }
  20. async function until(read, accept, timeout = 60000) {
  21. const deadline = Date.now() + timeout
  22. while (Date.now() < deadline) {
  23. try { const value = await read(); if (accept(value)) return value } catch {}
  24. await new Promise(done => setTimeout(done, 200))
  25. }
  26. throw new Error('等待同步服务状态超时')
  27. }
  28. test('App first launch installs an independent Web; Web remains up after quit and a newer Web is retained', { timeout: 150000 }, async () => {
  29. const home = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-sync-native-')))
  30. const port = await freePort(), upstreamPort = await freePort()
  31. const releaseRoot = join(home, 'updates', 'fixture-releases')
  32. const label = `cn.samien.yaoyao.sync-test.${createHash('sha256').update(home).digest('hex').slice(0, 16)}`
  33. const driver = new LaunchAgentService({ home, port, releaseRoot, label, plistPath: join(home, 'updates', 'fixture-service.plist') })
  34. const upstream = spawn(process.execPath, [join(root, 'tests/fixtures/fake-hermes.mjs')], {
  35. cwd: root, env: { ...process.env, FAKE_HERMES_PORT: String(upstreamPort), FAKE_HERMES_LOCAL_AUTH: '1' }, stdio: 'ignore',
  36. })
  37. let app
  38. const launch = () => electron.launch({
  39. ...(process.env.DESKTOP_TEST_EXECUTABLE ? { executablePath: process.env.DESKTOP_TEST_EXECUTABLE } : {}),
  40. args: process.env.DESKTOP_TEST_EXECUTABLE ? [] : [root], cwd: root,
  41. env: { ...process.env, HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_PORT: String(port),
  42. HERMES_YAOYAO_DESKTOP_TEST_SYNC: '1', HERMES_YAOYAO_UPSTREAM: `http://127.0.0.1:${upstreamPort}` }, timeout: 30000,
  43. })
  44. try {
  45. await until(() => localRequest(`http://127.0.0.1:${upstreamPort}/api/status`), value => value.status === 200)
  46. app = await launch()
  47. const page = await app.firstWindow()
  48. await page.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
  49. const record = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
  50. assert.equal(record.desktopOwned, false)
  51. const identity = await localRequest(`http://127.0.0.1:${port}/desktop/service`, record.token)
  52. const original = verifyRuntimePackage(payloadRoot)
  53. assert.equal(identity.body.build.artifactDigest, original.artifactDigest)
  54. assert.equal(identity.body.quiesced, false)
  55. const support = await page.evaluate(async () => {
  56. const bootstrap = await (await fetch('/api/app/bootstrap')).json()
  57. const setup = await fetch('/api/app/setup', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': bootstrap.csrfToken },
  58. body: JSON.stringify({ username: 'sync-fixture', password: 'sync-fixture-password' }) })
  59. if (!setup.ok) throw new Error('验收账号创建失败')
  60. return (await fetch('/api/app/system/update/status')).json()
  61. })
  62. assert.equal(support.installationMode, 'release'); assert.equal(support.supported, true)
  63. await app.close(); app = undefined
  64. assert.equal((await localRequest(`http://127.0.0.1:${port}/healthz`)).body.ok, true)
  65. assert.equal(driver.pid(), record.pid, 'quitting the App must leave the independent Web running')
  66. const saved = driver.snapshot().plist
  67. saved.EnvironmentVariables.YAOYAO_TEST_PRESERVE = 'keep-existing-settings'
  68. driver.writePlist(saved)
  69. // Install a later release through the same transaction engine used by Web's updater.
  70. const newer = join(home, 'updates', 'newer-package')
  71. await cp(payloadRoot, newer, { recursive: true })
  72. await writeFile(join(newer, 'release.json'), JSON.stringify({ schemaVersion: 1, releaseVersion: '0.3.33', webVersion: '0.3.33', gitTag: 'v0.3.33' }))
  73. const build = JSON.parse(await readFile(join(newer, 'build-info.json'), 'utf8'))
  74. await writeFile(join(newer, 'build-info.json'), JSON.stringify({ ...build, commit: 'c'.repeat(40), ancestors: [build.commit, ...build.ancestors], dirty: false }))
  75. sealRuntime(newer)
  76. await synchronizeDesktop({ home, runtimeRoot: newer, releaseRoot, driver })
  77. const updated = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
  78. assert.equal(updated.version, '0.3.33'); assert.notEqual(updated.pid, record.pid)
  79. assert.equal(driver.snapshot().plist.EnvironmentVariables.YAOYAO_TEST_PRESERVE, 'keep-existing-settings')
  80. app = await launch()
  81. const reopened = await app.firstWindow(); await reopened.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
  82. assert.equal(driver.pid(), updated.pid, 'older App must retain the newer Web without restarting it')
  83. const marker = JSON.parse(await readFile(join(home, 'updates', 'desktop-sync.json'), 'utf8'))
  84. assert.equal(marker.action, 'newer')
  85. assert.equal(marker.artifactDigest, original.artifactDigest)
  86. await app.close(); app = undefined
  87. assert.equal((await localRequest(`http://127.0.0.1:${port}/healthz`)).body.ok, true)
  88. await driver.stop()
  89. app = await launch()
  90. const restarted = await app.firstWindow(); await restarted.waitForURL(`http://127.0.0.1:${port}/**`, { timeout: 90000 })
  91. const resumed = JSON.parse(await readFile(join(home, 'service-instance.json'), 'utf8'))
  92. assert.equal(resumed.version, '0.3.33'); assert.equal(resumed.desktopOwned, false)
  93. assert.notEqual(resumed.pid, updated.pid)
  94. await app.close(); app = undefined
  95. } catch (error) {
  96. // Only this fixture's server log, which contains startup diagnostics.
  97. const log = await readFile(join(home, 'service.log'), 'utf8').catch(() => '')
  98. if (log) process.stderr.write(log.slice(-3000))
  99. throw error
  100. } finally {
  101. await app?.close().catch(() => {})
  102. await driver.stop()
  103. upstream.kill('SIGTERM')
  104. await rm(home, { recursive: true, force: true })
  105. }
  106. })