mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): repair Windows updater and installer flow (#801)
Prevent stale same-version update metadata from surfacing another install prompt, avoid showing a fake desktop version fallback, and configure the Windows NSIS installer to expose install directory selection. Fixes #801 Tested: bun run verify Confidence: high Scope-risk: moderate
This commit is contained in:
parent
be4984009c
commit
978dfb2efb
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@ -398,8 +398,10 @@ jobs:
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
artifacts/release-assets/**/*.dmg
|
||||
artifacts/release-assets/**/*.zip
|
||||
artifacts/release-assets/**/*.exe
|
||||
artifacts/release-assets/**/*.AppImage
|
||||
artifacts/release-assets/**/*.deb
|
||||
artifacts/release-assets/**/*.blockmap
|
||||
artifacts/update-metadata-standard/*.yml
|
||||
desktop/scripts/install-macos-unsigned.sh
|
||||
|
||||
@ -112,6 +112,12 @@ describe('formatToolUse', () => {
|
||||
expect(result).toContain('🔧 Bash')
|
||||
expect(result).toContain('npm test')
|
||||
})
|
||||
|
||||
it('summarizes file tools with a shortened path', () => {
|
||||
const result = formatToolUse('Read', { file_path: '/Users/test/project/src/index.ts' })
|
||||
|
||||
expect(result).toBe('🔧 Read …/project/src/index.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPermissionRequest', () => {
|
||||
|
||||
@ -88,6 +88,25 @@ describe('SessionStore', () => {
|
||||
expect(store2.get('anything')).toBeNull()
|
||||
})
|
||||
|
||||
it('uses CLAUDE_CONFIG_DIR for the default session store path', () => {
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
try {
|
||||
const defaultStore = new SessionStore()
|
||||
defaultStore.set('chat-default', 'uuid-default', '/default-project')
|
||||
|
||||
const savedPath = path.join(tmpDir, 'adapter-sessions.json')
|
||||
expect(fs.existsSync(savedPath)).toBe(true)
|
||||
expect(JSON.parse(fs.readFileSync(savedPath, 'utf8'))['chat-default'].sessionId).toBe('uuid-default')
|
||||
} finally {
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('lists all entries', () => {
|
||||
store.set('chat-1', 'uuid-1', '/a')
|
||||
store.set('chat-2', 'uuid-2', '/b')
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
"target": "nsis",
|
||||
"icon": "src-tauri/icons/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
|
||||
@ -1387,6 +1387,41 @@ describe('Settings > About tab', () => {
|
||||
expect(screen.getByText('Added markdown support')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show a fake fallback app version when desktop version IPC fails', async () => {
|
||||
window.desktopHost = {
|
||||
...browserHost,
|
||||
kind: 'electron',
|
||||
isDesktop: true,
|
||||
capabilities: {
|
||||
...browserHost.capabilities,
|
||||
updates: true,
|
||||
},
|
||||
app: {
|
||||
getVersion: vi.fn().mockRejectedValue(new Error('version IPC failed')),
|
||||
},
|
||||
}
|
||||
useUpdateStore.setState({
|
||||
status: 'up-to-date',
|
||||
availableVersion: null,
|
||||
releaseNotes: null,
|
||||
progressPercent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
error: null,
|
||||
checkedAt: Date.now(),
|
||||
shouldPrompt: false,
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
checkForUpdates: vi.fn().mockResolvedValue(null),
|
||||
installUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
dismissPrompt: vi.fn(),
|
||||
})
|
||||
|
||||
render(<Settings />)
|
||||
|
||||
expect(await screen.findByText('Unknown')).toBeInTheDocument()
|
||||
expect(screen.queryByText('0.1.0')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows downloaded bytes instead of a fake zero percent when total size is unknown', async () => {
|
||||
useUpdateStore.setState({
|
||||
status: 'downloading',
|
||||
|
||||
@ -3652,7 +3652,7 @@ function AboutSettings() {
|
||||
if (!cancelled) setVersion(value)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setVersion('0.1.0')
|
||||
if (!cancelled) setVersion('')
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
||||
@ -74,6 +74,43 @@ describe('updateStore', () => {
|
||||
expect(useUpdateStore.getState().shouldPrompt).toBe(true)
|
||||
})
|
||||
|
||||
it('treats same-version update metadata as already up to date', async () => {
|
||||
const download = vi.fn().mockResolvedValue(undefined)
|
||||
const close = vi.fn().mockResolvedValue(undefined)
|
||||
window.desktopHost = {
|
||||
...browserHost,
|
||||
kind: 'electron',
|
||||
isDesktop: true,
|
||||
capabilities: {
|
||||
...browserHost.capabilities,
|
||||
updates: true,
|
||||
},
|
||||
app: {
|
||||
getVersion: vi.fn().mockResolvedValue('0.4.1'),
|
||||
},
|
||||
updates: {
|
||||
...browserHost.updates,
|
||||
check: vi.fn().mockResolvedValue({
|
||||
version: '0.4.1',
|
||||
body: 'Already installed',
|
||||
download,
|
||||
close,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
vi.resetModules()
|
||||
const { useUpdateStore } = await import('./updateStore')
|
||||
|
||||
await expect(useUpdateStore.getState().checkForUpdates()).resolves.toBeNull()
|
||||
|
||||
expect(download).not.toHaveBeenCalled()
|
||||
expect(close).toHaveBeenCalledTimes(1)
|
||||
expect(useUpdateStore.getState().status).toBe('up-to-date')
|
||||
expect(useUpdateStore.getState().availableVersion).toBeNull()
|
||||
expect(useUpdateStore.getState().shouldPrompt).toBe(false)
|
||||
})
|
||||
|
||||
it('checks, installs, and relaunches through an injected desktop host', async () => {
|
||||
Reflect.deleteProperty(window, '__TAURI_INTERNALS__')
|
||||
const download = vi.fn(async (onEvent?: (event: unknown) => void) => {
|
||||
|
||||
@ -117,6 +117,45 @@ function getErrorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function parseAppVersion(version: string | null | undefined) {
|
||||
const match = version?.trim().replace(/^v/i, '').match(/^(\d+)\.(\d+)\.(\d+)/)
|
||||
if (!match) return null
|
||||
return match.slice(1).map(Number) as [number, number, number]
|
||||
}
|
||||
|
||||
function compareAppVersions(left: string | null | undefined, right: string | null | undefined) {
|
||||
const leftParts = parseAppVersion(left)
|
||||
const rightParts = parseAppVersion(right)
|
||||
if (!leftParts || !rightParts) return null
|
||||
|
||||
for (let index = 0; index < leftParts.length; index += 1) {
|
||||
const delta = leftParts[index]! - rightParts[index]!
|
||||
if (delta !== 0) return delta
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function isUpdateNewerThanCurrent(updateVersion: string, currentVersion: string | null) {
|
||||
const comparison = compareAppVersions(updateVersion, currentVersion)
|
||||
return comparison === null || comparison > 0
|
||||
}
|
||||
|
||||
async function getCurrentAppVersion(host: DesktopHost) {
|
||||
try {
|
||||
return await host.app.getVersion()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function closeIgnoredUpdate(update: DesktopUpdate) {
|
||||
try {
|
||||
await update.close()
|
||||
} catch {
|
||||
// Best effort: a stale same-version update should not keep the prompt alive.
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
status: 'idle',
|
||||
availableVersion: null,
|
||||
@ -156,6 +195,28 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
try {
|
||||
const updateProxyKey = getUpdateProxyKey()
|
||||
const update = await host.updates.check(getUpdateCheckOptions())
|
||||
|
||||
if (update && !isUpdateNewerThanCurrent(update.version, await getCurrentAppVersion(host))) {
|
||||
await closeIgnoredUpdate(update)
|
||||
await setPendingUpdate(null, null)
|
||||
|
||||
const checkedAt = Date.now()
|
||||
writeDismissedUpdateVersion(null)
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'up-to-date',
|
||||
availableVersion: null,
|
||||
releaseNotes: null,
|
||||
progressPercent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
checkedAt,
|
||||
error: null,
|
||||
shouldPrompt: false,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
|
||||
await setPendingUpdate(update, updateProxyKey)
|
||||
|
||||
const checkedAt = Date.now()
|
||||
|
||||
@ -191,8 +191,11 @@ describe('release desktop workflow', () => {
|
||||
expect(publishJob).toContain('Validate standard update metadata set')
|
||||
expect(publishJob).toContain('softprops/action-gh-release@v2')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.dmg')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.zip')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.exe')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.AppImage')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.deb')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.blockmap')
|
||||
expect(publishJob).toContain('artifacts/update-metadata-standard/*.yml')
|
||||
expect(publishJob).toContain('desktop/scripts/install-macos-unsigned.sh')
|
||||
expect(publishJob).toContain('fail_on_unmatched_files: true')
|
||||
@ -316,4 +319,18 @@ describe('release desktop workflow', () => {
|
||||
expect(desktopPackage.build.win?.publish).toBeUndefined()
|
||||
expect(desktopPackage.build.linux?.publish).toBeUndefined()
|
||||
})
|
||||
|
||||
test('Windows NSIS installer lets users choose the install directory', () => {
|
||||
const desktopPackage = JSON.parse(readFileSync('desktop/package.json', 'utf8')) as {
|
||||
build: {
|
||||
nsis?: {
|
||||
oneClick?: boolean
|
||||
allowToChangeInstallationDirectory?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(desktopPackage.build.nsis?.oneClick).toBe(false)
|
||||
expect(desktopPackage.build.nsis?.allowToChangeInstallationDirectory).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user