fix(stage-tamagotchi): updater regressions, downgrade warnings, and cross-platform cache cleanup
- Fixed architecture-sensitive updater tests (26/26 passing) - Added downgrade-warning UI via semver comparison - Restored Windows custom log path and implemented cross-platform cache cleanup - Made updater diagnostics always-on (no gating)
This commit is contained in:
@@ -79,6 +79,8 @@ vi.mock('~build/git', () => ({
|
||||
}))
|
||||
|
||||
describe('setupAutoUpdater', () => {
|
||||
const expectedChannelByArch = process.arch === 'arm64' ? 'latest-arm64' : 'latest-x64'
|
||||
|
||||
const laneReleaseTagMap = {
|
||||
stable: 'v0.9.9',
|
||||
beta: 'v0.9.10-beta.3',
|
||||
@@ -142,7 +144,7 @@ describe('setupAutoUpdater', () => {
|
||||
provider: 'generic',
|
||||
url: 'https://github.com/moeru-ai/airi/releases/download/v0.9.0-beta.6',
|
||||
})
|
||||
expect(updaterState.instance.channel).toBe('latest-arm64')
|
||||
expect(updaterState.instance.channel).toBe(expectedChannelByArch)
|
||||
})
|
||||
|
||||
it('ignores UPDATE_SERVER_URL in non-dev runtime', async () => {
|
||||
@@ -255,9 +257,9 @@ describe('setupAutoUpdater', () => {
|
||||
expect(service.state.diagnostics).toEqual(expect.objectContaining({
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
channel: 'latest-arm64',
|
||||
channel: expectedChannelByArch,
|
||||
executablePath: expect.any(String),
|
||||
logFilePath: '/tmp/airi/logs',
|
||||
logFilePath: expect.stringMatching(/stage-tamagotchi-updater[\\/]updater-log\.txt$/),
|
||||
isOverrideActive: false,
|
||||
}))
|
||||
expect(service.state.diagnostics).not.toHaveProperty('updaterCacheDir')
|
||||
|
||||
@@ -3,8 +3,11 @@ import type { AutoUpdaterState } from '@proj-airi/electron-eventa/electron-updat
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
|
||||
import { join } from 'node:path'
|
||||
|
||||
import electronUpdater from 'electron-updater'
|
||||
import semver from 'semver'
|
||||
|
||||
@@ -34,6 +37,32 @@ const GITHUB_RELEASES_ATOM_URL = 'https://github.com/moeru-ai/airi/releases.atom
|
||||
const GITHUB_RELEASE_DOWNLOAD_BASE_URL = 'https://github.com/moeru-ai/airi/releases/download'
|
||||
const UPDATE_CHANNEL_ENV_KEY = 'AIRI_UPDATE_CHANNEL'
|
||||
|
||||
function getCacheRoot() {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return process.env.LOCALAPPDATA || join(process.env.USERPROFILE || '', 'AppData', 'Local')
|
||||
case 'darwin':
|
||||
return join(process.env.HOME || '', 'Library', 'Caches')
|
||||
default:
|
||||
return process.env.XDG_CACHE_HOME || join(process.env.HOME || '', '.cache')
|
||||
}
|
||||
}
|
||||
|
||||
const UPDATER_DEBUG_CACHE_DIR = join(getCacheRoot(), 'stage-tamagotchi-updater')
|
||||
const UPDATER_LOG_FILE = join(UPDATER_DEBUG_CACHE_DIR, 'updater-log.txt')
|
||||
const OFFICIAL_UPDATER_CACHE_DIR = join(getCacheRoot(), 'ai.moeru.airi-updater')
|
||||
|
||||
async function logToFile(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string) {
|
||||
await fs.promises.mkdir(UPDATER_DEBUG_CACHE_DIR, { recursive: true }).catch(() => {})
|
||||
await fs.promises.appendFile(UPDATER_LOG_FILE, `${new Date().toISOString()} [${level}] ${message}\n`).catch(() => {})
|
||||
}
|
||||
|
||||
async function cleanupStaleUpdateFiles() {
|
||||
// Remove the updater cache root after the app relaunches so stale installer files do not linger.
|
||||
await fs.promises.rm(OFFICIAL_UPDATER_CACHE_DIR, { recursive: true, force: true }).catch(() => {})
|
||||
await logToFile('INFO', `Updater cache cleanup attempted: ${OFFICIAL_UPDATER_CACHE_DIR}`)
|
||||
}
|
||||
|
||||
export type UpdateLane = 'stable' | 'alpha' | 'beta' | 'nightly' | 'canary'
|
||||
interface GitHubReleaseRecord {
|
||||
tag_name?: string
|
||||
@@ -208,14 +237,27 @@ export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater
|
||||
|
||||
autoUpdater.allowPrerelease = isPrereleaseBuild
|
||||
autoUpdater.autoDownload = false
|
||||
void cleanupStaleUpdateFiles()
|
||||
if (activeFeedUrlOverride)
|
||||
autoUpdater.channel = releaseChannelName
|
||||
autoUpdater.forceDevUpdateConfig = !!feedUrlOverride && !app.isPackaged
|
||||
autoUpdater.logger = {
|
||||
info: (message: string) => log.log(message),
|
||||
warn: (message: string) => log.warn(message),
|
||||
error: (message: string) => log.error(message),
|
||||
debug: (message: string) => log.debug(message),
|
||||
info: (message: string) => {
|
||||
log.log(message)
|
||||
void logToFile('INFO', message)
|
||||
},
|
||||
warn: (message: string) => {
|
||||
log.warn(message)
|
||||
void logToFile('WARN', message)
|
||||
},
|
||||
error: (message: string) => {
|
||||
log.error(message)
|
||||
void logToFile('ERROR', message)
|
||||
},
|
||||
debug: (message: string) => {
|
||||
log.debug(message)
|
||||
void logToFile('DEBUG', message)
|
||||
},
|
||||
}
|
||||
|
||||
if (activeFeedUrlOverride)
|
||||
@@ -227,7 +269,7 @@ export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
channel: autoUpdater.channel || releaseChannelName,
|
||||
logFilePath: app.getPath('logs'),
|
||||
logFilePath: UPDATER_LOG_FILE,
|
||||
executablePath: process.execPath,
|
||||
isOverrideActive: !!activeFeedUrlOverride,
|
||||
...(activeFeedUrlOverride ? { feedUrl: activeFeedUrlOverride } : {}),
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { BugReportDialogSubmitPayload } from '@proj-airi/stage-ui/component
|
||||
|
||||
import type { ElectronUpdaterChannel } from '../../shared/eventa'
|
||||
|
||||
import semver from 'semver'
|
||||
|
||||
import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { AboutContent, BugReportDialog, createBugReportPageContext, MarkdownRenderer } from '@proj-airi/stage-ui/components'
|
||||
import { useBreakpoints } from '@proj-airi/stage-ui/composables'
|
||||
@@ -88,6 +90,22 @@ const restartButtonLabel = computed(() => {
|
||||
: t('tamagotchi.stage.about.update.actions.restart-install')
|
||||
})
|
||||
|
||||
function normalizeSemver(version: string | undefined) {
|
||||
if (!version)
|
||||
return undefined
|
||||
|
||||
return semver.valid(version) ?? semver.valid(version.startsWith('v') ? version.slice(1) : version)
|
||||
}
|
||||
|
||||
const isDowngradeUpdate = computed(() => {
|
||||
const currentVersion = normalizeSemver(buildInfo.value.version)
|
||||
const targetVersion = normalizeSemver(updateState.value.info?.version)
|
||||
if (!currentVersion || !targetVersion)
|
||||
return false
|
||||
|
||||
return semver.lt(targetVersion, currentVersion)
|
||||
})
|
||||
|
||||
const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences)
|
||||
const setUpdaterPreferences = useElectronEventaInvoke(electronSetUpdaterPreferences)
|
||||
|
||||
@@ -244,6 +262,12 @@ onMounted(() => {
|
||||
<div :class="['i-solar:arrow-right-line-duotone text-lg text-neutral-400']" />
|
||||
<span :class="['font-mono text-pink-500 dark:text-pink-400 font-bold']">v{{ updateState.info?.version }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isDowngradeUpdate"
|
||||
:class="['text-sm rounded-xl border border-amber-500/30 bg-amber-500/10 p-3 text-amber-700 dark:text-amber-200']"
|
||||
>
|
||||
Selected channel offers an older build than your current version. Installing this update will downgrade AIRI.
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -272,6 +296,12 @@ onMounted(() => {
|
||||
<div :class="['text-sm text-emerald-600 dark:text-emerald-400']">
|
||||
{{ downloadedStatusText }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isDowngradeUpdate"
|
||||
:class="['text-sm rounded-xl border border-amber-500/30 bg-amber-500/10 p-3 text-amber-700 dark:text-amber-200']"
|
||||
>
|
||||
Downgrade package downloaded from selected channel. Restart will install an older version.
|
||||
</div>
|
||||
<div>
|
||||
<DoubleCheckButton
|
||||
variant="primary"
|
||||
|
||||
Reference in New Issue
Block a user