Compare commits

...

3 Commits

Author SHA1 Message Date
Jedrzej Kosinski
57c74f46c9 fix: gate version-update notifications on setting, default off for local installs
Amp-Thread-ID: https://ampcode.com/threads/T-019f0347-dc00-723a-a4d2-558539329035
Co-authored-by: Amp <amp@ampcode.com>
2026-06-26 15:13:00 -07:00
Jedrzej Kosinski
df7eaab505 fix: wait for server feature flags before fetching releases
Gates release fetching in releaseStore.initialize() on the server feature flags arriving (non-empty), with a 3s timeout fallback, so a host that set show_version_updates=false isn't briefly treated as enabled on first load before negotiation completes. Also replaces the change-detector setting test with behavioral coverage.

Amp-Thread-ID: https://ampcode.com/threads/T-019f0347-dc00-723a-a4d2-558539329035
Co-authored-by: Amp <amp@ampcode.com>
2026-06-26 03:23:09 -07:00
Jedrzej Kosinski
9198f13636 feat: drive ShowVersionUpdates default from server feature flag
The Comfy.Notification.ShowVersionUpdates default now reads the show_version_updates server feature flag (defaulting to true), letting launchers like ComfyUI Desktop change whether new-release notifications show by default. The user setting still overrides it.

Amp-Thread-ID: https://ampcode.com/threads/T-019f0347-dc00-723a-a4d2-558539329035
Co-authored-by: Amp <amp@ampcode.com>
2026-06-26 02:55:33 -07:00
5 changed files with 127 additions and 107 deletions

View File

@@ -30,7 +30,8 @@ export enum ServerFeatureFlag {
COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled',
SHOW_SIGNIN_BUTTON = 'show_signin_button',
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth',
SIGNUP_TURNSTILE = 'signup_turnstile'
SIGNUP_TURNSTILE = 'signup_turnstile',
SHOW_VERSION_UPDATES = 'show_version_updates'
}
/**

View File

@@ -0,0 +1,55 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { CORE_SETTINGS } from './coreSettings'
const mockDist = vi.hoisted(() => ({ isCloud: false }))
vi.mock('@/platform/distribution/types', () => ({
isDesktop: false,
isNightly: false,
get isCloud() {
return mockDist.isCloud
}
}))
function getDefault(id: string) {
const setting = CORE_SETTINGS.find((s) => s.id === id)
if (!setting) throw new Error(`Setting ${id} not found`)
const { defaultValue } = setting
return typeof defaultValue === 'function' ? defaultValue() : defaultValue
}
describe('Comfy.Notification.ShowVersionUpdates default', () => {
beforeEach(() => {
mockDist.isCloud = false
})
afterEach(() => {
vi.restoreAllMocks()
})
it('resolves to the show_version_updates server flag value', () => {
vi.spyOn(api, 'getServerFeature').mockReturnValue(true)
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(true)
vi.spyOn(api, 'getServerFeature').mockReturnValue(false)
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(false)
})
it('falls back to off on local installs when the server flag is unset', () => {
vi.spyOn(api, 'getServerFeature').mockImplementation(
(_name, fallback) => fallback
)
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(false)
})
it('falls back to on for Cloud when the server flag is unset', () => {
mockDist.isCloud = true
vi.spyOn(api, 'getServerFeature').mockImplementation(
(_name, fallback) => fallback
)
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(true)
})
})

View File

@@ -6,6 +6,8 @@ import {
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import { ServerFeatureFlag } from '@/composables/useFeatureFlags'
import { api } from '@/scripts/api'
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
import type { Keybinding } from '@/platform/keybindings/types'
import { NodeBadgeMode } from '@/types/nodeSource'
@@ -484,7 +486,8 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Show version updates',
tooltip: 'Show updates for new models, and major new features.',
type: 'boolean',
defaultValue: true
defaultValue: () =>
api.getServerFeature(ServerFeatureFlag.SHOW_VERSION_UPDATES, isCloud)
},
{
id: 'Comfy.ConfirmClear',

View File

@@ -18,13 +18,13 @@ vi.mock('semver', () => ({
valid: vi.fn(() => '1.0.0')
}))
const mockData = vi.hoisted(() => ({ isDesktop: true }))
const mockData = vi.hoisted(() => ({ isCloud: false }))
vi.mock('@/platform/distribution/types', () => ({
get isDesktop() {
return mockData.isDesktop
},
isCloud: false
isDesktop: false,
get isCloud() {
return mockData.isCloud
}
}))
vi.mock('@/platform/updates/common/releaseService', () => {
@@ -40,6 +40,14 @@ vi.mock('@/platform/updates/common/releaseService', () => {
}
})
vi.mock('@/scripts/api', () => ({
api: {
// Non-empty => feature flags already received, so fetchReleases' gate
// resolves immediately instead of waiting on the websocket.
serverFeatureFlags: ref<Record<string, unknown>>({ ready: true })
}
}))
vi.mock('@/platform/settings/settingStore', () => {
const get = vi.fn((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
@@ -95,7 +103,12 @@ vi.mock('@/stores/systemStatsStore', () => {
}
})
vi.mock('@vueuse/core', () => ({
until: vi.fn(() => Promise.resolve()),
// until() is awaited directly in some call sites and chained as
// until(...).toBe(...) in others; support both shapes.
until: vi.fn(() => {
const resolved = Promise.resolve()
return Object.assign(resolved, { toBe: vi.fn(() => Promise.resolve()) })
}),
useStorage: vi.fn(() => ({ value: {} })),
createSharedComposable: vi.fn((fn) => fn)
}))
@@ -115,6 +128,7 @@ describe('useReleaseStore', () => {
vi.resetAllMocks()
mockSystemStatsState.reset()
mockData.isCloud = false
})
describe('initial state', () => {
@@ -442,11 +456,11 @@ describe('useReleaseStore', () => {
vi.mocked(releaseService.getReleases).mockReturnValue(promise)
const initPromise = store.initialize()
const fetchPromise = store.fetchReleases()
expect(store.isLoading).toBe(true)
resolvePromise!([mockRelease])
await initPromise
await fetchPromise
expect(store.isLoading).toBe(false)
})
@@ -681,102 +695,40 @@ describe('useReleaseStore', () => {
})
})
describe('isDesktop environment checks', () => {
describe('when running on desktop', () => {
beforeEach(() => {
mockData.isDesktop = true
})
it('should show toast when conditions are met', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
vi.mocked(compare).mockReturnValue(1)
expect(store.shouldShowToast).toBe(true)
})
it('should show red dot when new version available', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
vi.mocked(compare).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(true)
})
it('should show popup for latest version', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
const systemStatsStore = useSystemStatsStore()
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(true)
describe('cloud distribution', () => {
beforeEach(() => {
mockData.isCloud = true
const settingStore = useSettingStore()
vi.mocked(settingStore.get).mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null
})
})
describe('when NOT running on desktop (web)', () => {
beforeEach(() => {
mockData.isDesktop = false
})
it('suppresses the toast even when a new version is available', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
vi.mocked(compare).mockReturnValue(1)
it('should NOT show toast even when all other conditions are met', () => {
const store = useReleaseStore()
vi.mocked(compare).mockReturnValue(1)
expect(store.shouldShowToast).toBe(false)
})
// Set up all conditions that would normally show toast
store.releases = [mockRelease]
it('suppresses the red dot even when a new version is available', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
vi.mocked(compare).mockReturnValue(1)
expect(store.shouldShowToast).toBe(false)
})
expect(store.shouldShowRedDot).toBe(false)
})
it('should NOT show red dot even when new version available', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
vi.mocked(compare).mockReturnValue(1)
it("still shows the what's-new popup for the latest version", () => {
const store = useReleaseStore()
store.releases = [mockRelease]
const systemStatsStore = useSystemStatsStore()
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
expect(store.shouldShowRedDot).toBe(false)
})
it('should NOT show toast regardless of attention level', () => {
const store = useReleaseStore()
vi.mocked(compare).mockReturnValue(1)
// Test with high attention releases
const highRelease = {
...mockRelease,
id: 2,
attention: 'high' as const
}
const mediumRelease = {
...mockRelease,
id: 3,
attention: 'medium' as const
}
store.releases = [highRelease, mediumRelease]
expect(store.shouldShowToast).toBe(false)
})
it('should NOT show red dot even with high attention release', () => {
const store = useReleaseStore()
vi.mocked(compare).mockReturnValue(1)
store.releases = [{ ...mockRelease, attention: 'high' as const }]
expect(store.shouldShowRedDot).toBe(false)
})
it('should NOT show popup even for latest version', () => {
const store = useReleaseStore()
store.releases = [mockRelease]
const systemStatsStore = useSystemStatsStore()
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(false)
})
expect(store.shouldShowPopup).toBe(true)
})
})
})

View File

@@ -3,8 +3,9 @@ import { defineStore } from 'pinia'
import { compare, valid } from 'semver'
import { computed, ref } from 'vue'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { stringToLocale } from '@/utils/formatUtil'
@@ -93,8 +94,8 @@ export const useReleaseStore = defineStore('release', () => {
// Show toast if needed
const shouldShowToast = computed(() => {
// Only show on desktop version
if (!isDesktop || isCloud) {
// Cloud has its own update surface; the toast is for local installs.
if (isCloud) {
return false
}
@@ -125,8 +126,8 @@ export const useReleaseStore = defineStore('release', () => {
// Show red-dot indicator
const shouldShowRedDot = computed(() => {
// Only show on desktop version
if (!isDesktop || isCloud) {
// Cloud has its own update surface; the red dot is for local installs.
if (isCloud) {
return false
}
@@ -171,10 +172,8 @@ export const useReleaseStore = defineStore('release', () => {
})
const shouldShowPopup = computed(() => {
if (!isDesktop && !isCloud) {
return false
}
// Gated purely by the setting: off by default on local installs,
// on by default on Cloud (seeded by the server feature flag fallback).
if (!showVersionUpdates.value) {
return false
}
@@ -294,6 +293,16 @@ export const useReleaseStore = defineStore('release', () => {
// Initialize store
async function initialize(): Promise<void> {
// showVersionUpdates' default is seeded by the show_version_updates server
// feature flag, which arrives over the websocket shortly after connect. Wait
// for the flags (non-empty once received) so a host that disabled updates
// isn't briefly treated as enabled on first load; fall back after a timeout
// so a missing/late flag message can't block fetching forever.
if (!isCloud) {
await until(
() => Object.keys(api.serverFeatureFlags.value).length > 0
).toBe(true, { timeout: 3000 })
}
await fetchReleases()
}