feat: gate node replacement loading on server feature flag (#8750)

## Summary

Gates the node replacement store's `load()` call behind the
`node_replacements` server feature flag, so the frontend only calls
`/api/node_replacements` when the backend advertises support.

## Changes

- Added `NODE_REPLACEMENTS = 'node_replacements'` to `ServerFeatureFlag`
enum
- Added `nodeReplacementsEnabled` getter to `useFeatureFlags()`
- Added `api.serverSupportsFeature('node_replacements')` guard in
`useNodeReplacementStore.load()`

## Context

Without this guard, the frontend would attempt to fetch node
replacements from backends that don't support the endpoint, causing 404
errors.

Companion backend PR: https://github.com/Comfy-Org/ComfyUI/pull/12362

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8750-feat-gate-node-replacement-loading-on-server-feature-flag-3026d73d365081ec9246d77ad88f5bdc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2026-02-17 11:39:07 -08:00
committed by GitHub
parent 821c1e74ff
commit e83e396c09
3 changed files with 35 additions and 2 deletions

View File

@@ -20,7 +20,8 @@ export enum ServerFeatureFlag {
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled', ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled',
LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled', LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled',
TEAM_WORKSPACES_ENABLED = 'team_workspaces_enabled', TEAM_WORKSPACES_ENABLED = 'team_workspaces_enabled',
USER_SECRETS_ENABLED = 'user_secrets_enabled' USER_SECRETS_ENABLED = 'user_secrets_enabled',
NODE_REPLACEMENTS = 'node_replacements'
} }
/** /**
@@ -96,6 +97,9 @@ export function useFeatureFlags() {
remoteConfig.value.user_secrets_enabled ?? remoteConfig.value.user_secrets_enabled ??
api.getServerFeature(ServerFeatureFlag.USER_SECRETS_ENABLED, false) api.getServerFeature(ServerFeatureFlag.USER_SECRETS_ENABLED, false)
) )
},
get nodeReplacementsEnabled() {
return api.getServerFeature(ServerFeatureFlag.NODE_REPLACEMENTS, false)
} }
}) })

View File

@@ -15,6 +15,18 @@ vi.mock('./nodeReplacementService', () => ({
fetchNodeReplacements: vi.fn() fetchNodeReplacements: vi.fn()
})) }))
const mockNodeReplacementsEnabled = vi.hoisted(() => ({ value: true }))
vi.mock('@/composables/useFeatureFlags', () => ({
useFeatureFlags: vi.fn(() => ({
flags: {
get nodeReplacementsEnabled() {
return mockNodeReplacementsEnabled.value
}
}
}))
}))
function mockSettingStore(enabled: boolean) { function mockSettingStore(enabled: boolean) {
vi.mocked(useSettingStore, { partial: true }).mockReturnValue({ vi.mocked(useSettingStore, { partial: true }).mockReturnValue({
get: vi.fn().mockImplementation((key: string) => { get: vi.fn().mockImplementation((key: string) => {
@@ -27,9 +39,10 @@ function mockSettingStore(enabled: boolean) {
}) })
} }
function createStore(enabled = true) { function createStore(enabled = true, featureEnabled = true) {
setActivePinia(createPinia()) setActivePinia(createPinia())
mockSettingStore(enabled) mockSettingStore(enabled)
mockNodeReplacementsEnabled.value = featureEnabled
return useNodeReplacementStore() return useNodeReplacementStore()
} }
@@ -38,6 +51,7 @@ describe('useNodeReplacementStore', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockNodeReplacementsEnabled.value = true
store = createStore(true) store = createStore(true)
}) })
@@ -257,5 +271,15 @@ describe('useNodeReplacementStore', () => {
expect(fetchNodeReplacements).not.toHaveBeenCalled() expect(fetchNodeReplacements).not.toHaveBeenCalled()
expect(store.isLoaded).toBe(false) expect(store.isLoaded).toBe(false)
}) })
it('should not call API when server feature flag is disabled', async () => {
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
store = createStore(true, false)
await store.load()
expect(fetchNodeReplacements).not.toHaveBeenCalled()
expect(store.isLoaded).toBe(false)
})
}) })
}) })

View File

@@ -3,6 +3,7 @@ import type { NodeReplacement, NodeReplacementResponse } from './types'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { fetchNodeReplacements } from './nodeReplacementService' import { fetchNodeReplacements } from './nodeReplacementService'
@@ -14,8 +15,12 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
settingStore.get('Comfy.NodeReplacement.Enabled') settingStore.get('Comfy.NodeReplacement.Enabled')
) )
const { flags } = useFeatureFlags()
async function load() { async function load() {
if (!isEnabled.value || isLoaded.value) return if (!isEnabled.value || isLoaded.value) return
if (!flags.nodeReplacementsEnabled) return
try { try {
replacements.value = await fetchNodeReplacements() replacements.value = await fetchNodeReplacements()
isLoaded.value = true isLoaded.value = true