fix: feature flags and manager state handling (#5317) - merge conflict resolve (#5410)

This commit is contained in:
Jin Yi
2025-09-07 12:24:51 +09:00
committed by GitHub
parent 2bb158c51c
commit 4f01333e74
23 changed files with 743 additions and 554 deletions

View File

@@ -15,10 +15,10 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from './composables/useConflictDetection'
import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()

View File

@@ -105,7 +105,7 @@ const showContactSupport = async () => {
onMounted(async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await systemStatsStore.refetchSystemStats()
}
try {

View File

@@ -54,19 +54,12 @@
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useDialogService } from '@/services/dialogService'
import { useManagerState } from '@/composables/useManagerState'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useToastStore } from '@/stores/toastStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
@@ -81,6 +74,7 @@ const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
const managerState = useManagerState()
// Check if any of the missing packs are currently being installed
const isInstalling = computed(() => {
@@ -111,47 +105,21 @@ const uniqueNodes = computed(() => {
})
})
const managerStateStore = useManagerStateStore()
// Show manager buttons unless manager is disabled
const showManagerButtons = computed(() => {
return managerStateStore.managerUIState !== ManagerUIState.DISABLED
return managerState.shouldShowManagerButtons.value
})
// Only show Install All button for NEW_UI (new manager with v4 support)
const showInstallAllButton = computed(() => {
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
return managerState.shouldShowInstallButton.value
})
const openManager = async () => {
const state = managerStateStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
useDialogService().showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
} catch {
// If legacy command doesn't exist, show toast
const { t } = useI18n()
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
break
case ManagerUIState.NEW_UI:
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
break
}
await managerState.openManager({
initialTab: ManagerTab.Missing,
showToastOnLegacyError: true
})
}
</script>

View File

@@ -42,9 +42,8 @@
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import Message from 'primevue/message'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
@@ -60,20 +59,11 @@ const hasMissingCoreNodes = computed(() => {
return Object.keys(props.missingCoreNodes).length > 0
})
const currentComfyUIVersion = ref<string | null>(null)
whenever(
hasMissingCoreNodes,
async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
}
currentComfyUIVersion.value =
systemStatsStore.systemStats?.system?.comfyui_version ?? null
},
{
immediate: true
}
)
// Use computed for reactive version tracking
const currentComfyUIVersion = computed<string | null>(() => {
if (!hasMissingCoreNodes.value) return null
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
})
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {

View File

@@ -34,7 +34,6 @@
<script setup lang="ts">
import Divider from 'primevue/divider'
import Tag from 'primevue/tag'
import { onMounted } from 'vue'
import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
@@ -44,10 +43,4 @@ import PanelTemplate from './PanelTemplate.vue'
const systemStatsStore = useSystemStatsStore()
const aboutPanelStore = useAboutPanelStore()
onMounted(async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
}
})
</script>

View File

@@ -142,11 +142,12 @@ import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useDialogService } from '@/services/dialogService'
import { useManagerState } from '@/composables/useManagerState'
import { type ReleaseNote } from '@/services/releaseService'
import { useCommandStore } from '@/stores/commandStore'
import { useReleaseStore } from '@/stores/releaseStore'
import { useSettingStore } from '@/stores/settingStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { formatVersionAnchor } from '@/utils/formatUtil'
@@ -191,7 +192,6 @@ const { t, locale } = useI18n()
const releaseStore = useReleaseStore()
const commandStore = useCommandStore()
const settingStore = useSettingStore()
const dialogService = useDialogService()
// Emits
const emit = defineEmits<{
@@ -313,8 +313,11 @@ const menuItems = computed<MenuItem[]>(() => {
icon: PuzzleIcon,
label: t('helpCenter.managerExtension'),
showRedDot: shouldShowManagerRedDot.value,
action: () => {
dialogService.showManagerDialog()
action: async () => {
await useManagerState().openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
})
emit('close')
}
},

View File

@@ -106,16 +106,13 @@ import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { useDialogService } from '@/services/dialogService'
import { useManagerState } from '@/composables/useManagerState'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
@@ -127,6 +124,8 @@ const dialogStore = useDialogStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const managerState = useManagerState()
const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(null)
@@ -159,29 +158,11 @@ const showSettings = (defaultPanel?: string) => {
})
}
const managerStateStore = useManagerStateStore()
const showManageExtensions = async () => {
const state = managerStateStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
showSettings('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
} catch {
// If legacy command doesn't exist, fall back to extensions panel
showSettings('extension')
}
break
case ManagerUIState.NEW_UI:
useDialogService().showManagerDialog()
break
}
await managerState.openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
})
}
const extraMenuItems = computed<MenuItem[]>(() => [

View File

@@ -61,7 +61,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
const nodeDef = nodeDefStore.nodeDefsByName[nodeName]
if (nodeDef?.nodeSource.type === 'core') {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await systemStatsStore.refetchSystemStats()
}
return {
id: CORE_NODES_PACK_NAME,

View File

@@ -1,3 +1,4 @@
import { until } from '@vueuse/core'
import { uniqBy } from 'es-toolkit/compat'
import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue'
@@ -78,9 +79,8 @@ export function useConflictDetection() {
try {
// Get system stats from store (primary source of system information)
const systemStatsStore = useSystemStatsStore()
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
}
// Wait for systemStats to be initialized if not already
await until(systemStatsStore.isInitialized)
// Fetch version information from backend (with error resilience)
const [frontendVersion] = await Promise.allSettled([
@@ -127,7 +127,7 @@ export function useConflictDetection() {
}
systemEnvironment.value = environment
console.log(
console.debug(
'[ConflictDetection] System environment detection completed:',
environment
)
@@ -427,7 +427,7 @@ export function useConflictDetection() {
Object.entries(bulkResult).forEach(([packageId, failInfo]) => {
if (failInfo !== null) {
importFailures[packageId] = failInfo
console.log(
console.debug(
`[ConflictDetection] Import failure found for ${packageId}:`,
failInfo
)
@@ -500,7 +500,7 @@ export function useConflictDetection() {
*/
async function performConflictDetection(): Promise<ConflictDetectionResponse> {
if (isDetecting.value) {
console.log('[ConflictDetection] Already detecting, skipping')
console.debug('[ConflictDetection] Already detecting, skipping')
return {
success: false,
error_message: 'Already detecting conflicts',
@@ -556,7 +556,10 @@ export function useConflictDetection() {
detectionSummary.value = summary
lastDetectionTime.value = new Date().toISOString()
console.log('[ConflictDetection] Conflict detection completed:', summary)
console.debug(
'[ConflictDetection] Conflict detection completed:',
summary
)
// Store conflict results for later UI display
// Dialog will be shown based on specific events, not on app mount
@@ -568,7 +571,7 @@ export function useConflictDetection() {
// Merge conflicts for packages with the same name
const mergedConflicts = mergeConflictsByPackageName(conflictedResults)
console.log(
console.debug(
'[ConflictDetection] Conflicts detected (stored for UI):',
mergedConflicts
)
@@ -632,11 +635,22 @@ export function useConflictDetection() {
/**
* Error-resilient initialization (called on app mount).
* Async function that doesn't block UI setup.
* Ensures proper order: installed -> system_stats -> versions bulk -> import_fail_info_bulk
* Ensures proper order: system_stats -> manager state -> installed -> versions bulk -> import_fail_info_bulk
*/
async function initializeConflictDetection(): Promise<void> {
try {
// Simply perform conflict detection
// Check if manager is new Manager before proceeding
const { useManagerState } = await import('@/composables/useManagerState')
const managerState = useManagerState()
if (!managerState.isNewManagerUI.value) {
console.debug(
'[ConflictDetection] Manager is not new Manager, skipping conflict detection'
)
return
}
// Manager is new Manager, perform conflict detection
// The useInstalledPacks will handle fetching installed list if needed
await performConflictDetection()
} catch (error) {
@@ -671,13 +685,13 @@ export function useConflictDetection() {
* Check if conflicts should trigger modal display after "What's New" dismissal
*/
async function shouldShowConflictModalAfterUpdate(): Promise<boolean> {
console.log(
console.debug(
'[ConflictDetection] Checking if conflict modal should show after update...'
)
// Ensure conflict detection has run
if (detectionResults.value.length === 0) {
console.log(
console.debug(
'[ConflictDetection] No detection results, running conflict detection...'
)
await performConflictDetection()
@@ -689,7 +703,7 @@ export function useConflictDetection() {
const hasActualConflicts = hasConflicts.value
const canShowModal = acknowledgment.shouldShowConflictModal.value
console.log('[ConflictDetection] Modal check:', {
console.debug('[ConflictDetection] Modal check:', {
hasConflicts: hasActualConflicts,
canShowModal: canShowModal,
conflictedPackagesCount: conflictedPackages.value.length

View File

@@ -1,5 +1,6 @@
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
@@ -20,15 +21,10 @@ import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -720,34 +716,9 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Custom Nodes Manager',
versionAdded: '1.12.10',
function: async () => {
const managerState = useManagerStateStore().managerUIState
switch (managerState) {
case ManagerUIState.DISABLED:
dialogService.showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute(
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
)
} catch (error) {
console.error('error', error)
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
dialogService.showManagerDialog()
}
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog()
break
}
await useManagerState().openManager({
showToastOnLegacyError: true
})
}
},
{
@@ -755,33 +726,25 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-sync',
label: 'Check for Custom Node Updates',
versionAdded: '1.17.0',
function: () => {
const managerStore = useManagerStateStore()
const state = managerStore.managerUIState
function: async () => {
const managerState = useManagerState()
const state = managerState.managerUIState.value
switch (state) {
case ManagerUIState.DISABLED:
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.notAvailable'),
life: 3000
})
break
case ManagerUIState.LEGACY_UI:
useCommandStore()
.execute('Comfy.Manager.Menu.ToggleVisibility')
.catch(() => {
// If legacy command doesn't exist, fall back to extensions panel
dialogService.showSettingsDialog('extension')
})
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog()
break
// For DISABLED state, show error toast instead of opening settings
if (state === ManagerUIState.DISABLED) {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.notAvailable'),
life: 3000
})
return
}
await managerState.openManager({
initialTab: ManagerTab.UpdateAvailable,
showToastOnLegacyError: false
})
}
},
{
@@ -790,32 +753,10 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Install Missing Custom Nodes',
versionAdded: '1.17.0',
function: async () => {
const managerStore = useManagerStateStore()
const state = managerStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
// When manager is disabled, open the extensions panel in settings
dialogService.showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute(
'Comfy.Manager.Menu.ToggleVisibility'
)
} catch {
// If legacy command doesn't exist, fall back to extensions panel
dialogService.showSettingsDialog('extension')
}
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog({
initialTab: ManagerTab.Missing
})
break
}
await useManagerState().openManager({
initialTab: ManagerTab.Missing,
showToastOnLegacyError: false
})
}
},
{
@@ -921,8 +862,11 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.OpenManagerDialog',
icon: 'mdi mdi-puzzle-outline',
label: 'Manager',
function: () => {
dialogService.showManagerDialog()
function: async () => {
await useManagerState().openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
})
}
},
{
@@ -987,18 +931,11 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Custom Nodes (Legacy)',
versionAdded: '1.16.4',
function: async () => {
try {
await useCommandStore().execute(
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
)
} catch (error) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
await useManagerState().openManager({
legacyCommand: 'Comfy.Manager.CustomNodesManager.ToggleVisibility',
showToastOnLegacyError: true,
isLegacyOnly: true
})
}
},
{
@@ -1007,16 +944,10 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Manager Menu (Legacy)',
versionAdded: '1.16.4',
function: async () => {
try {
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
} catch (error) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
await useManagerState().openManager({
showToastOnLegacyError: true,
isLegacyOnly: true
})
}
},
{

View File

@@ -0,0 +1,203 @@
import { storeToRefs } from 'pinia'
import { computed, readonly } from 'vue'
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { useToastStore } from '@/stores/toastStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
export enum ManagerUIState {
DISABLED = 'disabled',
LEGACY_UI = 'legacy',
NEW_UI = 'new'
}
export function useManagerState() {
const systemStatsStore = useSystemStatsStore()
const { systemStats, isInitialized: systemInitialized } =
storeToRefs(systemStatsStore)
/**
* The current manager UI state.
* Computed once and cached until dependencies change (which they don't during runtime).
* This follows Vue's conventions and provides better performance through caching.
*/
const managerUIState = readonly(
computed((): ManagerUIState => {
// Wait for systemStats to be initialized
if (!systemInitialized.value) {
// Default to DISABLED while loading
return ManagerUIState.DISABLED
}
// Get current values
const clientSupportsV4 =
api.getClientFeatureFlags().supports_manager_v4_ui ?? false
const serverSupportsV4 = api.getServerFeature(
'extension.manager.supports_v4'
)
// Check command line args first (highest priority)
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
return ManagerUIState.DISABLED
}
if (
systemStats.value?.system?.argv?.includes('--enable-manager-legacy-ui')
) {
return ManagerUIState.LEGACY_UI
}
// Both client and server support v4 = NEW_UI
if (clientSupportsV4 && serverSupportsV4 === true) {
return ManagerUIState.NEW_UI
}
// Server supports v4 but client doesn't = LEGACY_UI
if (serverSupportsV4 === true && !clientSupportsV4) {
return ManagerUIState.LEGACY_UI
}
// Server explicitly doesn't support v4 = LEGACY_UI
if (serverSupportsV4 === false) {
return ManagerUIState.LEGACY_UI
}
// If server feature flags haven't loaded yet, default to NEW_UI
// This is a temporary state - feature flags are exchanged immediately on WebSocket connection
// NEW_UI is the safest default since v2 API is the current standard
// If the server doesn't support v2, API calls will fail with 404 and be handled gracefully
if (serverSupportsV4 === undefined) {
return ManagerUIState.NEW_UI
}
// Should never reach here, but if we do, disable manager
return ManagerUIState.DISABLED
})
)
/**
* Check if manager is enabled (not DISABLED)
*/
const isManagerEnabled = readonly(
computed((): boolean => {
return managerUIState.value !== ManagerUIState.DISABLED
})
)
/**
* Check if manager UI is in NEW_UI mode
*/
const isNewManagerUI = readonly(
computed((): boolean => {
return managerUIState.value === ManagerUIState.NEW_UI
})
)
/**
* Check if manager UI is in LEGACY_UI mode
*/
const isLegacyManagerUI = readonly(
computed((): boolean => {
return managerUIState.value === ManagerUIState.LEGACY_UI
})
)
/**
* Check if install button should be shown (only in NEW_UI mode)
*/
const shouldShowInstallButton = readonly(
computed((): boolean => {
return isNewManagerUI.value
})
)
/**
* Check if manager buttons should be shown (when manager is not disabled)
*/
const shouldShowManagerButtons = readonly(
computed((): boolean => {
return isManagerEnabled.value
})
)
/**
* Opens the manager UI based on current state
* Centralizes the logic for opening manager across the app
* @param options - Optional configuration for opening the manager
* @param options.initialTab - Initial tab to show (for NEW_UI mode)
* @param options.legacyCommand - Legacy command to execute (for LEGACY_UI mode)
* @param options.showToastOnLegacyError - Whether to show toast on legacy command failure
* @param options.isLegacyOnly - If true, shows error in NEW_UI mode instead of opening manager
*/
const openManager = async (options?: {
initialTab?: ManagerTab
legacyCommand?: string
showToastOnLegacyError?: boolean
isLegacyOnly?: boolean
}): Promise<void> => {
const state = managerUIState.value
const dialogService = useDialogService()
const commandStore = useCommandStore()
switch (state) {
case ManagerUIState.DISABLED:
dialogService.showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI: {
const command =
options?.legacyCommand || 'Comfy.Manager.Menu.ToggleVisibility'
try {
await commandStore.execute(command)
} catch {
// If legacy command doesn't exist
if (options?.showToastOnLegacyError !== false) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
// Fallback to extensions panel if not showing toast
if (options?.showToastOnLegacyError === false) {
dialogService.showSettingsDialog('extension')
}
}
break
}
case ManagerUIState.NEW_UI:
if (options?.isLegacyOnly) {
// Legacy command is not available in NEW_UI mode
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
dialogService.showManagerDialog({ initialTab: ManagerTab.All })
} else {
dialogService.showManagerDialog(
options?.initialTab ? { initialTab: options.initialTab } : undefined
)
}
break
}
}
return {
managerUIState,
isManagerEnabled,
isNewManagerUI,
isLegacyManagerUI,
shouldShowInstallButton,
shouldShowManagerButtons,
openManager
}
}

View File

@@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosResponse } from 'axios'
import { v4 as uuidv4 } from 'uuid'
import { ref } from 'vue'
import { useManagerState } from '@/composables/useManagerState'
import { api } from '@/scripts/api'
import { components } from '@/types/generatedManagerTypes'
import { isAbortError } from '@/utils/typeGuardUtil'
@@ -44,11 +45,18 @@ const managerApiClient = axios.create({
/**
* Service for interacting with the ComfyUI Manager API
* Provides methods for managing packs, ComfyUI-Manager queue operations, and system functions
* Note: This service should only be used when Manager state is NEW_UI
*/
export const useComfyManagerService = () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
// Check if manager service should be available
const isManagerServiceAvailable = () => {
const managerState = useManagerState()
return managerState.isNewManagerUI.value
}
const handleRequestError = (
err: unknown,
context: string,
@@ -87,6 +95,12 @@ export const useComfyManagerService = () => {
): Promise<T | null> => {
const { errorContext, routeSpecificErrors, isQueueOperation } = options
// Block service calls if not in NEW_UI state
if (!isManagerServiceAvailable()) {
error.value = 'Manager service is not available in current mode'
return null
}
isLoading.value = true
error.value = null
@@ -151,6 +165,10 @@ export const useComfyManagerService = () => {
) => {
const errorContext = 'Fetching bulk import failure information'
if (!params.cnr_ids?.length && !params.urls?.length) {
return {}
}
return executeRequest<components['schemas']['ImportFailInfoBulkResponse']>(
() =>
managerApiClient.post(ManagerRoute.IMPORT_FAIL_INFO_BULK, params, {

View File

@@ -1,76 +0,0 @@
import { defineStore } from 'pinia'
import { computed, readonly } from 'vue'
import { api } from '@/scripts/api'
import { useExtensionStore } from '@/stores/extensionStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
export enum ManagerUIState {
DISABLED = 'disabled',
LEGACY_UI = 'legacy',
NEW_UI = 'new'
}
export const useManagerStateStore = defineStore('managerState', () => {
const systemStatsStore = useSystemStatsStore()
const extensionStore = useExtensionStore()
// Reactive computed manager state that updates when dependencies change
const managerUIState = computed(() => {
const systemStats = systemStatsStore.systemStats
const clientSupportsV4 =
api.getClientFeatureFlags().supports_manager_v4_ui ?? false
const hasLegacyManager = extensionStore.extensions.some(
(ext) => ext.name === 'Comfy.CustomNodesManager'
)
const serverSupportsV4 = api.getServerFeature(
'extension.manager.supports_v4'
)
console.log('[Manager State Debug]', {
systemStats: systemStats?.system?.argv,
clientSupportsV4,
serverSupportsV4,
hasLegacyManager,
extensions: extensionStore.extensions.map((e) => e.name)
})
// Check command line args first
if (systemStats?.system?.argv?.includes('--disable-manager')) {
return ManagerUIState.DISABLED // comfyui_manager package not installed
}
if (systemStats?.system?.argv?.includes('--enable-manager-legacy-ui')) {
return ManagerUIState.LEGACY_UI // forced legacy
}
// Both client and server support v4 = NEW_UI
if (clientSupportsV4 && serverSupportsV4 === true) {
return ManagerUIState.NEW_UI
}
// Server supports v4 but client doesn't = LEGACY_UI
if (serverSupportsV4 === true) {
return ManagerUIState.LEGACY_UI
}
// No server v4 support but legacy manager extension exists = LEGACY_UI
if (hasLegacyManager) {
return ManagerUIState.LEGACY_UI
}
// If server feature flags haven't loaded yet, return DISABLED for now
// This will update reactively once feature flags load
if (serverSupportsV4 === undefined) {
return ManagerUIState.DISABLED
}
// No manager at all = DISABLED
return ManagerUIState.DISABLED
})
return {
managerUIState: readonly(managerUIState)
}
})

View File

@@ -1,3 +1,4 @@
import { until } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
@@ -240,7 +241,7 @@ export const useReleaseStore = defineStore('release', () => {
try {
// Ensure system stats are loaded
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await until(systemStatsStore.isInitialized)
}
const fetchedReleases = await releaseService.getReleases({

View File

@@ -1,32 +1,34 @@
import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { isElectron } from '@/utils/envUtil'
export const useSystemStatsStore = defineStore('systemStats', () => {
const systemStats = ref<SystemStats | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
async function fetchSystemStats() {
isLoading.value = true
error.value = null
const fetchSystemStatsData = async () => {
try {
systemStats.value = await api.getSystemStats()
return await api.getSystemStats()
} catch (err) {
error.value =
err instanceof Error
? err.message
: 'An error occurred while fetching system stats'
console.error('Error fetching system stats:', err)
} finally {
isLoading.value = false
throw err
}
}
const {
state: systemStats,
isLoading,
error,
isReady: isInitialized,
execute: refetchSystemStats
} = useAsyncState<SystemStats | null>(
fetchSystemStatsData,
null, // initial value
{
immediate: true
}
)
function getFormFactor(): string {
if (!systemStats.value?.system?.os) {
return 'other'
@@ -62,7 +64,8 @@ export const useSystemStatsStore = defineStore('systemStats', () => {
systemStats,
isLoading,
error,
fetchSystemStats,
isInitialized,
refetchSystemStats,
getFormFactor
}
})

View File

@@ -1,4 +1,4 @@
import { useStorage } from '@vueuse/core'
import { until, useStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import * as semver from 'semver'
import { computed } from 'vue'
@@ -103,7 +103,7 @@ export const useVersionCompatibilityStore = defineStore(
async function checkVersionCompatibility() {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await until(systemStatsStore.isInitialized)
}
}

View File

@@ -33,14 +33,14 @@ const createMockNode = (type: string, version?: string): LGraphNode =>
describe('MissingCoreNodesMessage', () => {
const mockSystemStatsStore = {
systemStats: null as { system?: { comfyui_version?: string } } | null,
fetchSystemStats: vi.fn()
refetchSystemStats: vi.fn()
}
beforeEach(() => {
vi.clearAllMocks()
// Reset the mock store state
mockSystemStatsStore.systemStats = null
mockSystemStatsStore.fetchSystemStats = vi.fn()
mockSystemStatsStore.refetchSystemStats = vi.fn()
// @ts-expect-error - Mocking the return value of useSystemStatsStore for testing.
// The actual store has more properties, but we only need these for our tests.
useSystemStatsStore.mockReturnValue(mockSystemStatsStore)
@@ -86,15 +86,11 @@ describe('MissingCoreNodesMessage', () => {
expect(wrapper.findComponent(Message).exists()).toBe(true)
})
it('fetches and displays current ComfyUI version', async () => {
// Start with no systemStats to trigger fetch
mockSystemStatsStore.fetchSystemStats.mockImplementation(() => {
// Simulate the fetch setting the systemStats
mockSystemStatsStore.systemStats = {
system: { comfyui_version: '1.0.0' }
}
return Promise.resolve()
})
it('displays current ComfyUI version when available', async () => {
// Set systemStats directly (store auto-fetches with useAsyncState)
mockSystemStatsStore.systemStats = {
system: { comfyui_version: '1.0.0' }
}
const missingCoreNodes = {
'1.2.0': [createMockNode('TestNode', '1.2.0')]
@@ -102,20 +98,18 @@ describe('MissingCoreNodesMessage', () => {
const wrapper = mountComponent({ missingCoreNodes })
// Wait for all async operations
await nextTick()
await new Promise((resolve) => setTimeout(resolve, 0))
// Wait for component to render
await nextTick()
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
// No need to check if fetchSystemStats was called since useAsyncState auto-fetches
expect(wrapper.text()).toContain(
'Some nodes require a newer version of ComfyUI (current: 1.0.0)'
)
})
it('displays generic message when version is unavailable', async () => {
// Mock fetchSystemStats to resolve without setting systemStats
mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined)
// No systemStats set - version unavailable
mockSystemStatsStore.systemStats = null
const missingCoreNodes = {
'1.2.0': [createMockNode('TestNode', '1.2.0')]

View File

@@ -96,7 +96,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
}
const mockSystemStatsStore = {
fetchSystemStats: vi.fn(),
refetchSystemStats: vi.fn(),
systemStats: {
system: {
comfyui_version: '0.3.41',
@@ -133,7 +133,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
} as any
// Reset mock functions
mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined)
mockSystemStatsStore.refetchSystemStats.mockResolvedValue(undefined)
mockComfyManagerService.listInstalledPacks.mockReset()
mockComfyManagerService.getImportFailInfo.mockReset()
mockRegistryService.getPackByVersion.mockReset()
@@ -185,7 +185,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
it('should return fallback environment information when systemStatsStore fails', async () => {
// Mock systemStatsStore failure
mockSystemStatsStore.fetchSystemStats.mockRejectedValue(
mockSystemStatsStore.refetchSystemStats.mockRejectedValue(
new Error('Store failure')
)
mockSystemStatsStore.systemStats = null
@@ -754,7 +754,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
describe('error resilience with Registry Store', () => {
it('should continue execution even when system environment detection fails', async () => {
// Mock system stats store failure
mockSystemStatsStore.fetchSystemStats.mockRejectedValue(
mockSystemStatsStore.refetchSystemStats.mockRejectedValue(
new Error('Store error')
)
mockSystemStatsStore.systemStats = null
@@ -851,7 +851,7 @@ describe.skip('useConflictDetection with Registry Store', () => {
it('should handle complete system failure gracefully', async () => {
// Mock all stores/services failing
mockSystemStatsStore.fetchSystemStats.mockRejectedValue(
mockSystemStatsStore.refetchSystemStats.mockRejectedValue(
new Error('Critical error')
)
mockSystemStatsStore.systemStats = null

View File

@@ -0,0 +1,320 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
import { api } from '@/scripts/api'
import { useExtensionStore } from '@/stores/extensionStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
// Mock dependencies
vi.mock('@/scripts/api', () => ({
api: {
getClientFeatureFlags: vi.fn(),
getServerFeature: vi.fn()
}
}))
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: vi.fn(() => ({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
}))
}))
vi.mock('@/stores/extensionStore', () => ({
useExtensionStore: vi.fn()
}))
vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: vi.fn()
}))
vi.mock('@/services/dialogService', () => ({
useDialogService: vi.fn(() => ({
showManagerPopup: vi.fn(),
showLegacyManagerPopup: vi.fn(),
showSettingsDialog: vi.fn()
}))
}))
vi.mock('@/stores/commandStore', () => ({
useCommandStore: vi.fn(() => ({
execute: vi.fn()
}))
}))
vi.mock('@/stores/toastStore', () => ({
useToastStore: vi.fn(() => ({
add: vi.fn()
}))
}))
describe('useManagerState', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('managerUIState property', () => {
it('should return DISABLED state when --disable-manager is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
}),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED)
})
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
}),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI)
})
it('should return NEW_UI state when client and server both support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
})
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: false
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI)
})
it('should return LEGACY_UI state when legacy manager extension exists', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: [{ name: 'Comfy.CustomNodesManager' }]
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI)
})
it('should return NEW_UI state when server feature flags are undefined', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(api.getServerFeature).mockReturnValue(undefined)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: undefined },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
})
it('should return LEGACY_UI state when server does not support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(api.getServerFeature).mockReturnValue(false)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.LEGACY_UI)
})
it('should handle null systemStats gracefully', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref(null),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
})
})
describe('helper properties', () => {
it('isManagerEnabled should return true when state is not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.isManagerEnabled.value).toBe(true)
})
it('isManagerEnabled should return false when state is DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
}),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.isManagerEnabled.value).toBe(false)
})
it('isNewManagerUI should return true when state is NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.isNewManagerUI.value).toBe(true)
})
it('isLegacyManagerUI should return true when state is LEGACY_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
}),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.isLegacyManagerUI.value).toBe(true)
})
it('shouldShowInstallButton should return true only for NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.shouldShowInstallButton.value).toBe(true)
})
it('shouldShowManagerButtons should return true when not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const managerState = useManagerState()
expect(managerState.shouldShowManagerButtons.value).toBe(true)
})
})
})

View File

@@ -9,6 +9,10 @@ vi.mock('@/utils/envUtil')
vi.mock('@/services/releaseService')
vi.mock('@/stores/settingStore')
vi.mock('@/stores/systemStatsStore')
vi.mock('@vueuse/core', () => ({
until: vi.fn(() => Promise.resolve()),
useStorage: vi.fn(() => ({ value: {} }))
}))
describe('useReleaseStore', () => {
let store: ReturnType<typeof useReleaseStore>
@@ -49,7 +53,8 @@ describe('useReleaseStore', () => {
comfyui_version: '1.0.0'
}
},
fetchSystemStats: vi.fn(),
isInitialized: true,
refetchSystemStats: vi.fn(),
getFormFactor: vi.fn(() => 'git-windows')
}
@@ -334,12 +339,15 @@ describe('useReleaseStore', () => {
})
it('should fetch system stats if not available', async () => {
const { until } = await import('@vueuse/core')
mockSystemStatsStore.systemStats = null
mockSystemStatsStore.isInitialized = false
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.initialize()
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
expect(until).toHaveBeenCalled()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
it('should not set loading state when notifications disabled', async () => {
@@ -401,12 +409,14 @@ describe('useReleaseStore', () => {
})
it('should proceed with fetchReleases when system stats are not available', async () => {
const { until } = await import('@vueuse/core')
mockSystemStatsStore.systemStats = null
mockSystemStatsStore.isInitialized = false
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.fetchReleases()
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
expect(until).toHaveBeenCalled()
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
})
@@ -530,7 +540,7 @@ describe('useReleaseStore', () => {
await store.initialize()
// Should not fetch system stats when notifications disabled
expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled()
expect(mockSystemStatsStore.refetchSystemStats).not.toHaveBeenCalled()
})
it('should handle concurrent fetchReleases calls', async () => {

View File

@@ -21,18 +21,25 @@ describe('useSystemStatsStore', () => {
let store: ReturnType<typeof useSystemStatsStore>
beforeEach(() => {
// Mock API to prevent automatic fetch on store creation
vi.mocked(api.getSystemStats).mockResolvedValue(null as any)
setActivePinia(createPinia())
store = useSystemStatsStore()
vi.clearAllMocks()
})
it('should initialize with null systemStats', () => {
expect(store.systemStats).toBeNull()
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
it('should initialize and start fetching immediately', async () => {
// useAsyncState with immediate: true starts loading right away
// In test environment, the mock resolves immediately so loading might be false already
expect(store.systemStats).toBeNull() // Initial value is null
expect(store.error).toBeUndefined()
// Wait for initial fetch to complete
await new Promise((resolve) => setTimeout(resolve, 0))
expect(store.isInitialized).toBe(true) // Should be initialized after fetch
})
describe('fetchSystemStats', () => {
describe('refetchSystemStats', () => {
it('should fetch system stats successfully', async () => {
const mockStats = {
system: {
@@ -51,11 +58,12 @@ describe('useSystemStatsStore', () => {
vi.mocked(api.getSystemStats).mockResolvedValue(mockStats)
await store.fetchSystemStats()
await store.refetchSystemStats()
expect(store.systemStats).toEqual(mockStats)
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
expect(store.error).toBeUndefined() // useAsyncState uses undefined for no error
expect(store.isInitialized).toBe(true)
expect(api.getSystemStats).toHaveBeenCalled()
})
@@ -63,19 +71,19 @@ describe('useSystemStatsStore', () => {
const error = new Error('API Error')
vi.mocked(api.getSystemStats).mockRejectedValue(error)
await store.fetchSystemStats()
await store.refetchSystemStats()
expect(store.systemStats).toBeNull()
expect(store.systemStats).toBeNull() // Initial value stays null on error
expect(store.isLoading).toBe(false)
expect(store.error).toBe('API Error')
expect(store.error).toEqual(error) // useAsyncState stores the actual error object
})
it('should handle non-Error objects', async () => {
vi.mocked(api.getSystemStats).mockRejectedValue('String error')
await store.fetchSystemStats()
await store.refetchSystemStats()
expect(store.error).toBe('An error occurred while fetching system stats')
expect(store.error).toBe('String error') // useAsyncState stores the actual error
})
it('should set loading state correctly', async () => {
@@ -85,7 +93,7 @@ describe('useSystemStatsStore', () => {
})
vi.mocked(api.getSystemStats).mockReturnValue(promise)
const fetchPromise = store.fetchSystemStats()
const fetchPromise = store.refetchSystemStats()
expect(store.isLoading).toBe(true)
resolvePromise({})
@@ -112,11 +120,12 @@ describe('useSystemStatsStore', () => {
vi.mocked(api.getSystemStats).mockResolvedValue(updatedStats)
await store.fetchSystemStats()
await store.refetchSystemStats()
expect(store.systemStats).toEqual(updatedStats)
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
expect(store.error).toBeUndefined()
expect(store.isInitialized).toBe(true)
expect(api.getSystemStats).toHaveBeenCalled()
})
})

View File

@@ -13,10 +13,11 @@ vi.mock('@/config', () => ({
vi.mock('@/stores/systemStatsStore')
// Mock useStorage from VueUse
// Mock useStorage and until from VueUse
const mockDismissalStorage = ref({} as Record<string, number>)
vi.mock('@vueuse/core', () => ({
useStorage: vi.fn(() => mockDismissalStorage)
useStorage: vi.fn(() => mockDismissalStorage),
until: vi.fn(() => Promise.resolve())
}))
describe('useVersionCompatibilityStore', () => {
@@ -31,7 +32,8 @@ describe('useVersionCompatibilityStore', () => {
mockSystemStatsStore = {
systemStats: null,
fetchSystemStats: vi.fn()
isInitialized: false,
refetchSystemStats: vi.fn()
}
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
@@ -51,6 +53,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -68,6 +71,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.23.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -83,6 +87,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.24.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -98,6 +103,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: ''
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -113,6 +119,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: 'not-a-version' // invalid semver format
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -129,6 +136,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.23.0' // Required is 1.23.0, frontend 1.24.0 meets this
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -148,6 +156,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -167,6 +176,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -180,6 +190,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.24.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -195,6 +206,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -212,6 +224,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.24.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
@@ -230,6 +243,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.checkVersionCompatibility()
store.dismissWarning()
@@ -252,6 +266,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.initialize()
@@ -270,6 +285,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.25.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.initialize()
@@ -289,6 +305,7 @@ describe('useVersionCompatibilityStore', () => {
required_frontend_version: '1.26.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.initialize()
@@ -298,24 +315,28 @@ describe('useVersionCompatibilityStore', () => {
describe('initialization', () => {
it('should fetch system stats if not available', async () => {
const { until } = await import('@vueuse/core')
mockSystemStatsStore.systemStats = null
mockSystemStatsStore.isInitialized = false
await store.initialize()
expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled()
expect(until).toHaveBeenCalled()
})
it('should not fetch system stats if already available', async () => {
const { until } = await import('@vueuse/core')
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.24.0',
required_frontend_version: '1.24.0'
}
}
mockSystemStatsStore.isInitialized = true
await store.initialize()
expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled()
expect(until).not.toHaveBeenCalled()
})
})
})

View File

@@ -1,194 +0,0 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { api } from '@/scripts/api'
import { useExtensionStore } from '@/stores/extensionStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
// Mock dependencies
vi.mock('@/scripts/api', () => ({
api: {
getClientFeatureFlags: vi.fn(),
getServerFeature: vi.fn()
}
}))
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: vi.fn(() => ({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
}))
}))
vi.mock('@/stores/extensionStore', () => ({
useExtensionStore: vi.fn()
}))
vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: vi.fn()
}))
describe('useManagerStateStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
describe('managerUIState computed', () => {
it('should return DISABLED state when --disable-manager is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: {
system: { argv: ['python', 'main.py', '--disable-manager'] }
}
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
})
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: {
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
}
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
})
it('should return NEW_UI state when client and server both support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: { system: { argv: ['python', 'main.py'] } }
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.NEW_UI)
})
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: { system: { argv: ['python', 'main.py'] } }
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: false
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
})
it('should return LEGACY_UI state when legacy manager extension exists', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: { system: { argv: ['python', 'main.py'] } }
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: [{ name: 'Comfy.CustomNodesManager' }]
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
})
it('should return DISABLED state when feature flags are undefined', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: { system: { argv: ['python', 'main.py'] } }
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(api.getServerFeature).mockReturnValue(undefined)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: undefined },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
})
it('should return DISABLED state when no manager is available', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: { system: { argv: ['python', 'main.py'] } }
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
vi.mocked(api.getServerFeature).mockReturnValue(false)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: false },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
})
it('should handle null systemStats gracefully', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: null
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
supports_manager_v4_ui: true
})
vi.mocked(api.getServerFeature).mockReturnValue(true)
vi.mocked(useFeatureFlags).mockReturnValue({
flags: { supportsManagerV4: true },
featureFlag: vi.fn()
} as any)
vi.mocked(useExtensionStore).mockReturnValue({
extensions: []
} as any)
const store = useManagerStateStore()
expect(store.managerUIState).toBe(ManagerUIState.NEW_UI)
})
})
})