feat(desktop): manual-download update check and OSS release manifest

Adds an unsigned, manual-download update flow. The main process fetches the OSS
updates/releases.json manifest (DSH_UPDATE_URL overrides), compares the latest
version, and on startup and hourly prompts to open the per-platform installer
URL; a preload bridge exposes the same check to the SPA's About "check for
updates" button, which renders the version, notes, and a download link.
scripts/generate-release-json.mjs builds the manifest from the packaged
.dmg/.exe. Windows nsis packaging is configured.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-14 20:45:21 +08:00
parent e18cfbfb93
commit e87a3084ea
9 changed files with 290 additions and 16 deletions
@@ -72,14 +72,31 @@ describe('AboutSection', () => {
expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
})
it('checks for updates and reports the latest version', async () => {
it('reports up-to-date on a web surface without the desktop bridge', async () => {
render(<AboutSection {...aboutProps} />)
fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
expect(screen.getByText(en['about.checking'])).toBeTruthy()
await waitFor(() => {
expect(screen.getByRole('status').textContent).toContain(en['about.upToDate'])
})
})
it('shows a download link when the bridge reports a newer version', async () => {
const checkUpdate = vi.fn(async () => ({
status: 'update-available' as const, current: '0.1.0-rc.5', latest: '0.1.0-rc.6',
notes: 'New release', url: 'https://example.com/app.dmg',
}))
;(window as { dshApp?: unknown }).dshApp = { checkUpdate }
try {
render(<AboutSection {...aboutProps} />)
fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
const status = await screen.findByRole('status')
expect(status.textContent).toContain(en['about.updateAvailable'])
expect(status.textContent).toContain('0.1.0-rc.6')
expect(screen.getByRole('link', { name: en['about.download'] }).getAttribute('href')).toBe('https://example.com/app.dmg')
} finally {
delete (window as { dshApp?: unknown }).dshApp
}
})
})
describe('SettingsDocumentAction', () => {