import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' import { createServer } from 'node:net' import { spawn } from 'node:child_process' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { _electron as electron, expect } from '@playwright/test' async function freePort() { const server = createServer() await new Promise(done => server.listen(0, '127.0.0.1', done)) const port = server.address().port await new Promise(done => server.close(done)) return port } test('the real desktop workspace downloads in place and waits for an explicit restart', { timeout: 60000 }, async () => { const root = resolve(import.meta.dirname, '..'), home = await mkdtemp(join(tmpdir(), 'yaoyao-inline-update-')) const port = await freePort(), upstream = await freePort(), origin = `http://127.0.0.1:${port}` await mkdir(join(home, 'server')) const server = spawn(process.execPath, ['--import', 'tsx', 'tests/fixtures/workspace-server.ts'], { cwd: root, env: { ...process.env, WORKSPACE_FIXTURE_HOME: join(home, 'server'), WORKSPACE_FIXTURE_PORT: String(port), WORKSPACE_FIXTURE_UPSTREAM_PORT: String(upstream) }, stdio: 'ignore', }) const desktopHome = join(home, 'desktop'); await mkdir(desktopHome) await writeFile(join(desktopHome, 'desktop-preferences.json'), JSON.stringify({ startupChoice: 'remote', remoteServer: origin })) let app try { await expect.poll(async () => { try { return (await fetch(origin + '/healthz')).status } catch { return 0 } }, { timeout: 30000 }).toBe(200) app = await electron.launch({ args: [root], cwd: root, env: { ...process.env, HERMES_YAOYAO_DESKTOP_CLIENT_ONLY: '1', HERMES_YAOYAO_DESKTOP_TEST_HOME: desktopHome, HERMES_YAOYAO_DESKTOP_TEST_AUTO_UPDATE: '1' } }) const page = await app.firstWindow() await app.evaluate(({ app }, root) => { const { createRequire } = process.getBuiltinModule('node:module') const { autoUpdater } = createRequire(root + '/package.json')(root + '/.desktop-build-client/electron-updater.cjs') globalThis.inlineFixture = { installs: 0 } autoUpdater.checkForUpdates = async () => ({ isUpdateAvailable: true, updateInfo: { version: '99.0.0' } }) autoUpdater.downloadUpdate = () => new Promise(resolve => { inlineFixture.ready = () => resolve([]) inlineFixture.stage = () => autoUpdater.emit('update-downloaded') autoUpdater.emit('download-progress', { transferred: 37, total: 100 }) }) autoUpdater.quitAndInstall = () => { inlineFixture.installs++ } }, root) await expect.poll(() => page.evaluate(() => window.yaoyaoDesktop.status().then(state => state.phase))).toBe('ready') await page.reload() await page.locator('#username').fill('fixture') await page.locator('#password').fill('fixture-pass') await page.locator('#submit').click() await page.waitForURL(origin + '/**') await page.goto(origin + '/conversations') await expect(page.locator('.desktop-sidebar .sidebar-account-switcher__main')).toBeVisible() await page.evaluate(() => window.yaoyaoDesktop.updateAction('check')) const footer = page.locator('.desktop-sidebar .desktop-update-entry') await expect(footer.getByRole('button', { name: '可更新', exact: true })).toBeVisible() await expect(page.locator('#desktop-update-entry')).toHaveCount(0) assert.equal(app.windows().length, 1) const output = join(root, 'test-results/desktop/inline-workspace'); await mkdir(output, { recursive: true }) await page.screenshot({ path: join(output, 'available.png') }) await footer.getByRole('button', { name: '可更新', exact: true }).click() await expect(footer.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '37') await page.locator('.desktop-sidebar .sidebar-account-switcher__main').click() const menu = page.locator('.workspace-settings-menu .desktop-update-entry') await expect(menu).toContainText('正在下载 · 37%') await page.screenshot({ path: join(output, 'downloading-menu.png') }) await page.keyboard.press('Escape') await expect(menu).toHaveCount(0) await app.evaluate(() => inlineFixture.stage()) await expect(footer).toContainText('正在准备…') await page.reload() await expect(footer).toContainText('正在准备…') await expect(footer.getByRole('button')).toBeDisabled() await app.evaluate(() => inlineFixture.ready()) await expect(footer.getByRole('button', { name: '重新启动', exact: true })).toBeEnabled() assert.equal(await app.evaluate(() => inlineFixture.installs), 0) assert.equal(app.windows().length, 1) await page.screenshot({ path: join(output, 'ready.png') }) await page.evaluate(() => document.documentElement.classList.add('dark')) await page.screenshot({ path: join(output, 'ready-dark.png') }) await footer.getByRole('button', { name: '重新启动', exact: true }).click() await expect.poll(() => app.evaluate(() => inlineFixture.installs)).toBe(1) await expect(footer.getByRole('button', { name: '正在重启…', exact: true })).toBeDisabled() assert.equal((await fetch(origin + '/healthz')).status, 200) } finally { await app?.close().catch(() => {}) server.kill('SIGTERM') await new Promise(done => { if (server.exitCode !== null) return done() const timer = setTimeout(() => { server.kill('SIGKILL'); done() }, 3000) server.once('exit', () => { clearTimeout(timer); done() }) }) await rm(home, { recursive: true, force: true }) } })