feat(desktop): dynamic About version, update manifest date, and a unified publish flow
The About section hardcoded its version; the main process now exposes get-app-info so it reads app.getVersion() dynamically, and the update result carries the manifest release date. The manifest generator orders versions numerically (0.10.0 outranks 0.9.0). A new stage-release script copies releases.json plus installers into the deepseek-harness-web site root's updates/, which deploy.py publishes to OSS alongside the download page, so the app and web share one update channel. The About section also gains links to the release site and official site. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+85
-13
@@ -122,28 +122,100 @@ The bundled harness is multi-GB uncompressed (≈2 GB), dominated by
|
||||
~1 GB. This is the inherent footprint of shipping the full harness runtime
|
||||
standalone, and is the accepted trade-off for a zero-external-dependency app.
|
||||
|
||||
## Updates (manual download)
|
||||
## Release process
|
||||
|
||||
The app is **not code-signed**, so updates are a manual-download flow rather
|
||||
than a silent swap. On startup (and hourly), and from the Settings → About
|
||||
"check for updates" button (through a preload bridge), the main process fetches
|
||||
the OSS manifest (`updates/releases.json`, `DSH_UPDATE_URL` overrides) and
|
||||
compares the latest version against `app.getVersion()`. When a newer release
|
||||
exists it prompts the user to open the per-platform installer URL.
|
||||
the manifest (`updates/releases.json`, `DSH_UPDATE_URL` overrides) and compares
|
||||
the latest version against `app.getVersion()`. When a newer release exists it
|
||||
prompts the user to open the per-platform installer URL. The web `download/`
|
||||
page and the desktop update check read the same manifest, so **one deploy
|
||||
publishes both the site and the update channel**.
|
||||
|
||||
To publish a release:
|
||||
### 1. Bump the app version
|
||||
|
||||
The release version lives in **`apps/desktop/package.json` → `version`**. It
|
||||
feeds three things: `app.getVersion()` (shown dynamically by the About section
|
||||
and compared against the manifest), electron-builder's installer filenames
|
||||
(`DeepSeek Harness-<version>-arm64.dmg`), and — derived from those filenames —
|
||||
the manifest's `latest`. So bump this one field to the new version:
|
||||
|
||||
```sh
|
||||
pnpm --filter @deepseek-ai/dsh-desktop run build:harness
|
||||
pnpm desktop:pack # produces .dmg (mac) / .nsis .exe (win)
|
||||
DSH_RELEASE_NOTES="…" pnpm --filter @deepseek-ai/dsh-desktop run generate-release-json
|
||||
# e.g. 0.1.0-rc.5 → 0.1.0-rc.6
|
||||
```
|
||||
|
||||
`scripts/generate-release-json.mjs` writes `dist/releases.json` from the built
|
||||
installers (`DSH_UPDATE_BASE` defaults to `https://deepseek.pinesound.cn/updates/`).
|
||||
Upload `releases.json` plus the `.dmg`/`.exe` to the OSS `updates/` folder, and
|
||||
the `deepseek-harness-web` `download/` page renders the same manifest. Building
|
||||
the Windows `.exe` on macOS needs wine (or build on a Windows host).
|
||||
The scheme is `0.1.0-rc.N`; `generate-release-json` orders versions
|
||||
numerically, so `rc.6` correctly outranks `rc.5` (and `0.10.0` outranks
|
||||
`0.9.0`). The root `package.json` version is the monorepo workspace version —
|
||||
bump it too if you keep them in sync, but only the desktop one is user-visible.
|
||||
|
||||
### 2. Build and package
|
||||
|
||||
```sh
|
||||
pnpm --filter @deepseek-ai/dsh-desktop run build:harness # assemble the self-contained runtime
|
||||
pnpm desktop:pack # electron-builder: .dmg (mac) / .nsis .exe (win)
|
||||
```
|
||||
|
||||
Artifacts land in `apps/desktop/dist/`. Each target OS/arch needs its own
|
||||
harness (`build/harness` bundles a platform Node + native addons), so regenerate
|
||||
it on the target platform before packing that platform. Building the Windows
|
||||
`.exe` on macOS needs wine (or build on a Windows host).
|
||||
|
||||
### 3. Generate the update manifest
|
||||
|
||||
```sh
|
||||
DSH_RELEASE_NOTES="…本次更新的说明…" pnpm --filter @deepseek-ai/dsh-desktop run generate-release-json
|
||||
```
|
||||
|
||||
`scripts/generate-release-json.mjs` scans `dist/` for installers and writes
|
||||
`dist/releases.json` (single-latest structure): `latest.version`, `latest.date`,
|
||||
`releaseNotes`, and per-platform URLs rooted at `DSH_UPDATE_BASE` (default
|
||||
`https://deepseek.pinesound.cn/updates/`).
|
||||
|
||||
### 4. Stage into the web site (the upload location)
|
||||
|
||||
```sh
|
||||
pnpm --filter @deepseek-ai/dsh-desktop run stage-release # --dry-run to preview
|
||||
```
|
||||
|
||||
`scripts/stage-release.mjs` copies `dist/releases.json` and the installers
|
||||
(`.dmg`/`.exe`) into **`deepseek-harness-web/updates/`** — the site root's
|
||||
`updates/` folder — and prunes stale installers left from older releases. This
|
||||
is the single upload location: everything the update check and download page
|
||||
need lives under `updates/`.
|
||||
|
||||
### 5. Deploy the site
|
||||
|
||||
```sh
|
||||
cd ../deepseek-harness-web && python3 deploy.py
|
||||
```
|
||||
|
||||
`deploy.py` uploads the whole `deepseek-harness-web/` repo root (including
|
||||
`download/` and `updates/`) to the OSS bucket root, served at
|
||||
`https://deepseek.pinesound.cn/`. Because the desktop default
|
||||
`DSH_UPDATE_URL` is `https://deepseek.pinesound.cn/updates/releases.json` and
|
||||
the download page fetches `/updates/releases.json` (same origin), no further
|
||||
configuration is needed. `deploy.py` wipes the bucket then uploads, so the repo
|
||||
root is the source of truth.
|
||||
|
||||
### 6. The web pages
|
||||
|
||||
The release is installable from the web through the pages under
|
||||
`deepseek-harness-web/`:
|
||||
|
||||
- **`download/index.html`** — the download page, **required** for a web
|
||||
release. Reads `updates/releases.json` and renders the latest version, date,
|
||||
release notes, and per-platform install buttons. Always include this in any
|
||||
release.
|
||||
- **`index.html`** — the site homepage (link to `download/`).
|
||||
- **`privacy/`, `data-processing/`** — supporting pages (already part of the
|
||||
site).
|
||||
|
||||
`deploy.py` serves every file under the repo root automatically (site root =
|
||||
bucket root), so a new page is published by just adding it at the root and
|
||||
re-deploying. If you add a page, link it from the homepage or the download page
|
||||
so it is discoverable.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"build": "tsc -b tsconfig.json",
|
||||
"build:harness": "node scripts/build-harness.mjs",
|
||||
"generate-release-json": "node scripts/generate-release-json.mjs",
|
||||
"stage-release": "node scripts/stage-release.mjs",
|
||||
"typecheck": "tsc -b tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"pack": "electron-builder"
|
||||
|
||||
@@ -26,6 +26,23 @@ function versionOf(filename) {
|
||||
return match === null ? undefined : match[1]
|
||||
}
|
||||
|
||||
/** Order two versions: numeric segments first, then the pre-release suffix. */
|
||||
function compareVersions(a, b) {
|
||||
const num = (v) => v.split(/[-+]/)[0].split('.').map((n) => Number(n))
|
||||
const pre = (v) => { const i = v.indexOf('-'); return i === -1 ? '' : v.slice(i + 1) }
|
||||
const an = num(a), bn = num(b)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (an[i] !== bn[i]) return an[i] - bn[i]
|
||||
}
|
||||
// Equal numeric core: a release with a pre-release suffix is OLDER than a
|
||||
// stable one (e.g. `1.0.0-rc.1` < `1.0.0`); two suffixes compare by string.
|
||||
const ap = pre(a), bp = pre(b)
|
||||
if (ap === bp) return 0
|
||||
if (ap === '') return 1
|
||||
if (bp === '') return -1
|
||||
return ap < bp ? -1 : 1
|
||||
}
|
||||
|
||||
const platforms = {}
|
||||
let latestVersion
|
||||
let latestDate = process.env.DSH_RELEASE_DATE ?? new Date().toISOString().slice(0, 10)
|
||||
@@ -38,7 +55,7 @@ for (const entry of readdirSync(dist)) {
|
||||
? entry.includes('arm64') ? 'mac-arm64' : 'mac-x64'
|
||||
: 'win-x64'
|
||||
platforms[key] = { url: `${base}${encodeURIComponent(entry)}` }
|
||||
if (latestVersion === undefined || version > latestVersion) {
|
||||
if (latestVersion === undefined || compareVersions(version, latestVersion) > 0) {
|
||||
latestVersion = version
|
||||
latestDate = process.env.DSH_RELEASE_DATE ?? new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Stage a built release into the `deepseek-harness-web` site so it can be
|
||||
* published in one deploy. Copies `apps/desktop/dist`'s `releases.json` and the
|
||||
* platform installers (.dmg/.exe) into `../deepseek-harness-web/updates/` — the
|
||||
* site root's `updates/` folder (deploy.py uploads the repo root to the bucket
|
||||
* root) that both the web download page and the desktop update check
|
||||
* (`DSH_UPDATE_URL`) read from.
|
||||
*
|
||||
* Run after `desktop:pack` + `generate-release-json`, then deploy the site:
|
||||
* cd ../deepseek-harness-web && python3 deploy.py
|
||||
*
|
||||
* Flags:
|
||||
* --dry-run print what would be copied without touching the filesystem.
|
||||
* --site <path> override the deepseek-harness-web repo path (default: sibling dir).
|
||||
*/
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const dist = join(root, 'apps/desktop/dist')
|
||||
const args = process.argv.slice(2)
|
||||
const dryRun = args.includes('--dry-run')
|
||||
const siteFlag = args.find((a) => a.startsWith('--site='))
|
||||
const siteRoot = siteFlag === undefined
|
||||
? resolve(root, '../deepseek-harness-web')
|
||||
: resolve(root, siteFlag.slice('--site='.length))
|
||||
|
||||
/** Names the site root's update channel lives at (`updates/`). */
|
||||
const updatesDir = join(siteRoot, 'updates')
|
||||
|
||||
/** Platform installer and manifest files staged to the site. */
|
||||
const artifactNames = () => {
|
||||
if (!existsSync(dist)) return []
|
||||
return readdirSync(dist).filter((f) => f === 'releases.json' || f.endsWith('.dmg') || f.endsWith('.exe'))
|
||||
}
|
||||
|
||||
const artifacts = artifactNames()
|
||||
if (artifacts.length === 0) {
|
||||
console.error(`stage-release: no releases.json or installers found in ${dist}`)
|
||||
console.error('Run `desktop:pack` and `generate-release-json` first.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`staging ${artifacts.length} artifact(s) from ${dist}`)
|
||||
console.log(` → ${updatesDir}${dryRun ? ' (dry run)' : ''}`)
|
||||
for (const name of artifacts) {
|
||||
console.log(` ${dryRun ? 'would copy' : 'copying'} ${name}`)
|
||||
if (dryRun) continue
|
||||
mkdirSync(updatesDir, { recursive: true })
|
||||
cpSync(join(dist, name), join(updatesDir, name))
|
||||
}
|
||||
|
||||
// A stale installer left from a previous release should not linger on the site.
|
||||
const stale = existsSync(updatesDir)
|
||||
? readdirSync(updatesDir).filter((name) => !artifacts.includes(name) && (name.endsWith('.dmg') || name.endsWith('.exe')))
|
||||
: []
|
||||
for (const name of stale) {
|
||||
console.log(` ${dryRun ? 'would remove stale' : 'removing stale'} ${name}`)
|
||||
if (!dryRun) rmSync(join(updatesDir, name), { force: true })
|
||||
}
|
||||
|
||||
if (dryRun) console.log('(dry run: no files were written)')
|
||||
console.log('done. Deploy with `cd ../deepseek-harness-web && python3 deploy.py`.')
|
||||
@@ -34,9 +34,12 @@ const UPDATE_MANIFEST_URL = process.env.DSH_UPDATE_URL ?? 'https://deepseek.pine
|
||||
/** The renderer's update-check IPC channel. */
|
||||
const UPDATE_CHANNEL = 'check-update'
|
||||
|
||||
/** The renderer's static build-identity IPC channel. */
|
||||
const APP_INFO_CHANNEL = 'get-app-info'
|
||||
|
||||
/** The release manifest published on OSS (`updates/releases.json`). */
|
||||
interface ReleaseManifest {
|
||||
readonly latest: { readonly version: string }
|
||||
readonly latest: { readonly version: string; readonly date?: string }
|
||||
readonly releaseNotes?: string
|
||||
readonly platforms: Partial<Record<'mac-arm64' | 'mac-x64' | 'win-x64', { readonly url: string }>>
|
||||
}
|
||||
@@ -180,6 +183,7 @@ async function checkForUpdate(): Promise<UpdateCheckResult> {
|
||||
status: 'update-available',
|
||||
current,
|
||||
latest,
|
||||
date: manifest.latest.date,
|
||||
notes: manifest.releaseNotes,
|
||||
url: platformDownloadUrl(manifest),
|
||||
}
|
||||
@@ -290,6 +294,13 @@ app.whenReady().then(() => {
|
||||
installAppMenu()
|
||||
// The SPA's About "check for updates" button asks the main process.
|
||||
ipcMain.handle(UPDATE_CHANNEL, () => checkForUpdate())
|
||||
// The SPA's About reads this build's version from the main process (no network).
|
||||
ipcMain.handle(APP_INFO_CHANNEL, () => ({
|
||||
version: app.getVersion(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
productName: PRODUCT_NAME,
|
||||
}))
|
||||
startSession()
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0 && session !== undefined) {
|
||||
|
||||
@@ -14,11 +14,27 @@ export interface UpdateCheckResult {
|
||||
readonly status: 'up-to-date' | 'update-available' | 'error'
|
||||
readonly current: string
|
||||
readonly latest?: string | undefined
|
||||
/** Release date of the newest release (YYYY-MM-DD), when the manifest supplies it. */
|
||||
readonly date?: string | undefined
|
||||
readonly notes?: string | undefined
|
||||
readonly url?: string | undefined
|
||||
}
|
||||
|
||||
/** Static identity of this build, read from the main process (no network). */
|
||||
export interface AppInfo {
|
||||
/** The packaged version (`app.getVersion()`). */
|
||||
readonly version: string
|
||||
/** Platform the app runs on (`process.platform`). */
|
||||
readonly platform: NodeJS.Platform
|
||||
/** CPU architecture (`process.arch`). */
|
||||
readonly arch: string
|
||||
/** User-facing product name. */
|
||||
readonly productName: string
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('dshApp', {
|
||||
/** Ask the main process for the latest release; resolves the check result. */
|
||||
checkUpdate: (): Promise<UpdateCheckResult> => ipcRenderer.invoke('check-update') as Promise<UpdateCheckResult>,
|
||||
/** Ask the main process for this build's static identity (no network). */
|
||||
getAppInfo: (): Promise<AppInfo> => ipcRenderer.invoke('get-app-info') as Promise<AppInfo>,
|
||||
})
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.check:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 0;
|
||||
opacity: 0.75;
|
||||
@@ -74,3 +69,36 @@
|
||||
.download {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* External-site links styled as the design-system outline capsule button. */
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 18px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.link:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-state-business-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
* on a plain-web surface without the bridge it reports up-to-date.
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import css from './AboutSection.module.css'
|
||||
|
||||
/** This build's version, mirrored from apps/desktop/package.json. */
|
||||
const APP_VERSION = '0.1.0-rc.5'
|
||||
/**
|
||||
* Plain-web fallback for the current version when the desktop bridge is absent:
|
||||
* there is no packaged identity to report, so the version row shows this rather
|
||||
* than a stale hardcoded value. On the desktop the real version comes from
|
||||
* `getAppInfo` (`app.getVersion()`).
|
||||
*/
|
||||
const FALLBACK_VERSION = '—'
|
||||
|
||||
/** The update-check bridge the desktop preload injects (absent on plain web). */
|
||||
interface DshAppBridge {
|
||||
@@ -19,16 +25,18 @@ interface DshAppBridge {
|
||||
readonly status: 'up-to-date' | 'update-available' | 'error'
|
||||
readonly current: string
|
||||
readonly latest?: string | undefined
|
||||
readonly date?: string | undefined
|
||||
readonly notes?: string | undefined
|
||||
readonly url?: string | undefined
|
||||
}>
|
||||
getAppInfo: () => Promise<{ readonly version: string }>
|
||||
}
|
||||
|
||||
type UpdateState =
|
||||
| { readonly status: 'idle' }
|
||||
| { readonly status: 'checking' }
|
||||
| { readonly status: 'up-to-date' }
|
||||
| { readonly status: 'available'; readonly latest: string; readonly notes?: string | undefined; readonly url?: string | undefined }
|
||||
| { readonly status: 'available'; readonly latest: string; readonly date?: string | undefined; readonly notes?: string | undefined; readonly url?: string | undefined }
|
||||
| { readonly status: 'error'; readonly detail?: string | undefined }
|
||||
|
||||
/** Full component props for the About section. */
|
||||
@@ -42,7 +50,15 @@ export type AboutSectionComponentProps =
|
||||
*/
|
||||
export function AboutSection({ t }: AboutSectionComponentProps) {
|
||||
const [state, setState] = useState<UpdateState>({ status: 'idle' })
|
||||
const [version, setVersion] = useState<string>(FALLBACK_VERSION)
|
||||
const checking = state.status === 'checking'
|
||||
// Read this build's real version from the desktop main process; plain web has
|
||||
// no packaged identity, so it keeps the fallback.
|
||||
useEffect(() => {
|
||||
const bridge = (window as { dshApp?: DshAppBridge }).dshApp
|
||||
if (bridge === undefined) return
|
||||
void bridge.getAppInfo().then((info) => { setVersion(info.version) }, () => {})
|
||||
}, [])
|
||||
const check = (): void => {
|
||||
if (checking) return
|
||||
const bridge = (window as { dshApp?: DshAppBridge }).dshApp
|
||||
@@ -55,7 +71,7 @@ export function AboutSection({ t }: AboutSectionComponentProps) {
|
||||
void bridge.checkUpdate().then(
|
||||
(result) => {
|
||||
if (result.status === 'update-available') {
|
||||
setState({ status: 'available', latest: result.latest ?? '', notes: result.notes, url: result.url })
|
||||
setState({ status: 'available', latest: result.latest ?? '', date: result.date, notes: result.notes, url: result.url })
|
||||
} else if (result.status === 'up-to-date') {
|
||||
setState({ status: 'up-to-date' })
|
||||
} else {
|
||||
@@ -76,24 +92,25 @@ export function AboutSection({ t }: AboutSectionComponentProps) {
|
||||
<p className={css.productName}>{t('about.product')}</p>
|
||||
<dl className={css.version}>
|
||||
<dt>{t('about.currentVersion')}</dt>
|
||||
<dd data-version>{APP_VERSION}</dd>
|
||||
<dd data-version>{version}</dd>
|
||||
</dl>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
className={css.check}
|
||||
variant="primary"
|
||||
disabled={checking}
|
||||
aria-busy={checking || undefined}
|
||||
onClick={check}
|
||||
>
|
||||
{checking ? t('about.checking') : t('about.checkUpdates')}
|
||||
</button>
|
||||
</Button>
|
||||
{state.status === 'up-to-date' && !checking
|
||||
? <p className={css.status} role="status">{t('about.upToDate')} · v{APP_VERSION}</p>
|
||||
? <p className={css.status} role="status">{t('about.upToDate')} · v{version}</p>
|
||||
: null}
|
||||
{state.status === 'available'
|
||||
? (
|
||||
<p className={css.status} role="status">
|
||||
{t('about.updateAvailable')} · v{state.latest}
|
||||
{state.date === undefined || state.date.length === 0 ? '' : ` (${state.date})`}
|
||||
{state.notes === undefined || state.notes.length === 0 ? '' : ` — ${state.notes}`}
|
||||
</p>
|
||||
)
|
||||
@@ -104,6 +121,10 @@ export function AboutSection({ t }: AboutSectionComponentProps) {
|
||||
{state.status === 'error'
|
||||
? <p className={css.status} role="status">{t('about.updateCheckFailed')}{state.detail === undefined || state.detail.length === 0 ? '' : `: ${state.detail}`}</p>
|
||||
: null}
|
||||
<div className={css.links}>
|
||||
<a className={css.link} href="https://deepseek.pinesound.cn/" target="_blank" rel="noreferrer">{t('about.releaseSite')}</a>
|
||||
<a className={css.link} href="https://www.deepseek.com/" target="_blank" rel="noreferrer">{t('about.officialSite')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,6 +20,8 @@ export const zh = {
|
||||
'about.updateAvailable': '发现新版本',
|
||||
'about.download': '去下载',
|
||||
'about.updateCheckFailed': '检查更新失败',
|
||||
'about.releaseSite': '发布站',
|
||||
'about.officialSite': '官网',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
/** The settings namespace key union. */
|
||||
@@ -45,4 +47,6 @@ export const en = {
|
||||
'about.updateAvailable': 'A new version is available',
|
||||
'about.download': 'Download',
|
||||
'about.updateCheckFailed': 'Could not check for updates',
|
||||
'about.releaseSite': 'Release site',
|
||||
'about.officialSite': 'Official site',
|
||||
} satisfies Record<SettingsKey, string>
|
||||
|
||||
@@ -63,15 +63,26 @@ describe('GeneralSection', () => {
|
||||
describe('AboutSection', () => {
|
||||
const aboutProps: AboutSectionComponentProps = { close: vi.fn(), t } as never
|
||||
|
||||
it('renders the company copy and the current version', () => {
|
||||
it('renders the company copy and a current-version row (fallback on plain web)', () => {
|
||||
render(<AboutSection {...aboutProps} />)
|
||||
expect(screen.getByText('PineSound')).toBeTruthy()
|
||||
expect(screen.getByText('AI audio creation platform')).toBeTruthy()
|
||||
expect(screen.getByText(en['about.currentVersion'])).toBeTruthy()
|
||||
expect(screen.getByText('0.1.0-rc.5')).toBeTruthy()
|
||||
// No desktop bridge on a plain web surface: the version row falls back.
|
||||
expect(screen.getByText('—')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the packaged version from the desktop bridge', async () => {
|
||||
;(window as { dshApp?: unknown }).dshApp = { getAppInfo: vi.fn(async () => ({ version: '0.1.0-rc.5' })) }
|
||||
try {
|
||||
render(<AboutSection {...aboutProps} />)
|
||||
await screen.findByText('0.1.0-rc.5')
|
||||
} finally {
|
||||
delete (window as { dshApp?: unknown }).dshApp
|
||||
}
|
||||
})
|
||||
|
||||
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'] }))
|
||||
@@ -83,20 +94,31 @@ describe('AboutSection', () => {
|
||||
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',
|
||||
date: '2026-08-01', notes: 'New release', url: 'https://example.com/app.dmg',
|
||||
}))
|
||||
;(window as { dshApp?: unknown }).dshApp = { checkUpdate }
|
||||
;(window as { dshApp?: unknown }).dshApp = {
|
||||
checkUpdate,
|
||||
getAppInfo: vi.fn(async () => ({ version: '0.1.0-rc.5' })),
|
||||
}
|
||||
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(status.textContent).toContain('2026-08-01')
|
||||
expect(status.textContent).toContain('New release')
|
||||
expect(screen.getByRole('link', { name: en['about.download'] }).getAttribute('href')).toBe('https://example.com/app.dmg')
|
||||
} finally {
|
||||
delete (window as { dshApp?: unknown }).dshApp
|
||||
}
|
||||
})
|
||||
|
||||
it('links to the release site and the official site', () => {
|
||||
render(<AboutSection {...aboutProps} />)
|
||||
expect(screen.getByRole('link', { name: en['about.releaseSite'] }).getAttribute('href')).toBe('https://deepseek.pinesound.cn/')
|
||||
expect(screen.getByRole('link', { name: en['about.officialSite'] }).getAttribute('href')).toBe('https://www.deepseek.com/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsDocumentAction', () => {
|
||||
|
||||
Reference in New Issue
Block a user