Skip to content
99 changes: 97 additions & 2 deletions src/stores/modelToNodeStore.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Open an issue to parameterize this test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create GitHub issue for parameterizing tests

gh issue create \
  --title "Parameterize repetitive test cases in modelToNodeStore.test.ts" \
  --body "## Context
Identified during review of PR `#8468`: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468
Comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468#discussion_r2776248903

## Description
Several test cases in \`src/stores/modelToNodeStore.test.ts\` follow similar patterns and could be parameterized using Vitest's \`it.each()\` or similar approaches to reduce code duplication and improve maintainability.

## Test Cases to Parameterize

The following tests in the \`getNodeProvider\` describe block have similar structures:

1. **\"should return provider for new extension model types\"** - tests multiple model types (sam2, sams, ipadapter, depthanything) with assertions on nodeDef name and key
2. **\"should use hierarchical fallback for ultralytics subcategories\"** - tests ultralytics/bbox and ultralytics/segm
3. **\"should return provider for FlashVSR nodes with empty key (auto-load)\"** - tests FlashVSR and FlashVSR-v1.1
4. **\"should return provider for segformer models\"** - tests segformer_b2_clothes and segformer_b3_fashion

## Suggested Approach

Use Vitest's \`it.each()\` to create a test table with columns for:
- Model type
- Expected node definition name
- Expected key value

Example pattern:
\`\`\`typescript
it.each([
  ['sam2', 'DownloadAndLoadSAM2Model', 'model'],
  ['sams', 'SAMLoader', 'model_name'],
  ['ipadapter', 'IPAdapterModelLoader', 'ipadapter_file'],
  // ... more test cases
])('should return provider for %s model type', (modelType, expectedNodeName, expectedKey) => {
  const modelToNodeStore = useModelToNodeStore()
  modelToNodeStore.registerDefaults()
  
  const provider = modelToNodeStore.getNodeProvider(modelType)
  expect(provider?.nodeDef?.name).toBe(expectedNodeName)
  expect(provider?.key).toBe(expectedKey)
})
\`\`\`

## Benefits
- Reduces code duplication
- Makes it easier to add new test cases
- Improves test maintainability
- Clearer test structure

---
Requested by: `@DrJKL`" \
  --assignee DrJKL

Length of output: 57


@DrJKL, I've opened issue #8703 to track the test parameterization work: #8703

The issue outlines using Vitest's it.each() to consolidate the repetitive test cases for model type providers, which should reduce code duplication and make it easier to add new test cases in the future.


🧬 Code Graph Analysis Results

[src/stores/modelToNodeStore.ts:22-245]

export const useModelToNodeStore = defineStore('modelToNode', () => {
const modelToNodeMap = ref<Record<string, ModelNodeProvider[]>>({})
const nodeDefStore = useNodeDefStore()
const haveDefaultsLoaded = ref(false)

/** Internal computed for reactive caching of registered node types */
const registeredNodeTypes = computed<Record<string, string>>(() => {
return Object.fromEntries(
Object.values(modelToNodeMap.value)
.flat()
.filter((provider) => !!provider.nodeDef)
.map((provider) => [provider.nodeDef.name, provider.key])
)
})

/** Internal computed for efficient reverse lookup: nodeType -> category */
const nodeTypeToCategory = computed(() => {
const lookup: Record<string, string> = {}
for (const [category, providers] of Object.entries(modelToNodeMap.value)) {
for (const provider of providers) {
// Extension nodes may not be installed
if (!provider.nodeDef) continue
// Only store the first category for each node type (matches current assetService behavior)
if (!lookup[provider.nodeDef.name]) {
lookup[provider.nodeDef.name] = category
}
}
}
return lookup
})

/** Get set of all registered node types for efficient lookup */
function getRegisteredNodeTypes(): Record<string, string> {
registerDefaults()
return registeredNodeTypes.value
}

/**

  • Get the category for a given node type.
  • Performs efficient O(1) lookup using cached reverse map.
  • @param nodeType The node type name to find the category for
  • @returns The category name, or undefined if not found
    */
    function getCategoryForNodeType(nodeType: string): string | undefined {
    registerDefaults()
// Handle invalid input gracefully
if (!nodeType || typeof nodeType !== 'string') {
  return undefined
}

return nodeTypeToCategory.value[nodeType]

}

/**

  • Find providers for modelType with hierarchical fallback.
  • Tries exact match first, then falls back to top-level segment (e.g., "parent/child" → "parent").
  • Note: Only falls back one level; "a/b/c" tries "a/b/c" then "a", not "a/b".
    */
    function findProvidersWithFallback(
    modelType: string
    ): ModelNodeProvider[] | undefined {
    if (!modelType || typeof modelType !== 'string') {
    return undefined
    }
const exactMatch = modelToNodeMap.value[modelType]
if (exactMatch && exactMatch.length > 0) return exactMatch

const topLevel = modelType.split('/')[0]
if (topLevel === modelType) return undefined

const fallback = modelToNodeMap.value[topLevel]

if (fallback && fallback.length > 0) return fallback

return undefined

}

/**

  • Get the node provider for the given model type name.
  • Supports hierarchical lookups: if "parent/child" has no match, falls back to "parent".
  • @param modelType The name of the model type to get the node provider for.
  • @returns The node provider for the given model type name.
    */
    function getNodeProvider(modelType: string): ModelNodeProvider | undefined {
    registerDefaults()
    return findProvidersWithFallback(modelType)?.[0]
    }

/**

  • Get the list of all valid node providers for the given model type name.
  • Supports hierarchical lookups: if "parent/child" has no match, falls back to "parent".
  • @param modelType The name of the model type to get the node providers for.
  • @returns The list of all valid node providers for the given model type name.
    /
    function getAllNodeProviders(modelType: string): ModelNodeProvider[] {
    registerDefaults()
    return findProvidersWithFallback(modelType) ?? []
    }
    /
    *
  • Register a node provider for the given model type name.
  • @param modelType The name of the model type to register the node provider for.
  • @param nodeProvider The node provider to register.
    /
    function registerNodeProvider(
    modelType: string,
    nodeProvider: ModelNodeProvider
    ) {
    registerDefaults()
    if (!nodeProvider.nodeDef) return
    if (!modelToNodeMap.value[modelType]) {
    modelToNodeMap.value[modelType] = []
    }
    modelToNodeMap.value[modelType].push(nodeProvider)
    }
    /
    *
  • Register a node provider for the given simple names.
  • @param modelType The name of the model type to register the node provider for.
  • @param nodeClass The node class name to register.
  • @param key The key to use for the node input.
    */
    function quickRegister(modelType: string, nodeClass: string, key: string) {
    registerNodeProvider(
    modelType,
    new ModelNodeProvider(nodeDefStore.nodeDefsByName[nodeClass], key)
    )
    }

function registerDefaults() {
if (haveDefaultsLoaded.value) {
return
}
if (Object.keys(nodeDefStore.nodeDefsByName).length === 0) {
return
}
haveDefaultsLoaded.value = true

quickRegister('checkpoints', 'CheckpointLoaderSimple', 'ckpt_name')
quickRegister('checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name')
quickRegister('loras', 'LoraLoader', 'lora_name')
quickRegister('loras', 'LoraLoaderModelOnly', 'lora_name')
quickRegister('vae', 'VAELoader', 'vae_name')
quickRegister('controlnet', 'ControlNetLoader', 'control_net_name')
quickRegister('diffusion_models', 'UNETLoader', 'unet_name')
quickRegister('upscale_models', 'UpscaleModelLoader', 'model_name')
quickRegister('style_models', 'StyleModelLoader', 'style_model_name')
quickRegister('gligen', 'GLIGENLoader', 'gligen_name')
quickRegister('clip_vision', 'CLIPVisionLoader', 'clip_name')
quickRegister('text_encoders', 'CLIPLoader', 'clip_name')
quickRegister('audio_encoders', 'AudioEncoderLoader', 'audio_encoder_name')
quickRegister('model_patches', 'ModelPatchLoader', 'name')
quickRegister(
  'animatediff_models',
  'ADE_LoadAnimateDiffModel',
  'model_name'
)
quickRegister(
  'animatediff_motion_lora',
  'ADE_AnimateDiffLoRALoader',
  'name'
)

// Chatterbox TTS nodes: empty key means the node auto-loads models without
// a widget selector (createModelNodeFromAsset skips widget assignment)
quickRegister('chatterbox/chatterbox', 'FL_ChatterboxTTS', '')
quickRegister('chatterbox/chatterbox_turbo', 'FL_ChatterboxTurboTTS', '')
quickRegister(
  'chatterbox/chatterbox_multilingual',
  'FL_ChatterboxMultilingualTTS',
  ''
)
quickRegister('chatterbox/chatterbox_vc', 'FL_ChatterboxVC', '')

// Latent upscale models (ComfyUI core - nodes_hunyuan.py)
quickRegister(
  'latent_upscale_models',
  'LatentUpscaleModelLoader',
  'model_name'
)

// SAM/SAM2 segmentation models (comfyui-segment-anything-2, comfyui-impact-pack)
quickRegister('sam2', 'DownloadAndLoadSAM2Model', 'model')
quickRegister('sams', 'SAMLoader', 'model_name')

// Ultralytics detection models (comfyui-impact-subpack)
// Note: ultralytics/bbox and ultralytics/segm fall back to this via hierarchical lookup
quickRegister('ultralytics', 'UltralyticsDetectorProvider', 'model_name')

// DepthAnything models (comfyui-depthanythingv2)
quickRegister(
  'depthanything',
  'DownloadAndLoadDepthAnythingV2Model',
  'model'
)

// IP-Adapter models (comfyui_ipadapter_plus)
quickRegister('ipadapter', 'IPAdapterModelLoader', 'ipadapter_file')

// Segformer clothing/fashion segmentation models (comfyui_layerstyle)
quickRegister('segformer_b2_clothes', 'LS_LoadSegformerModel', 'model_name')
quickRegister('segformer_b3_clothes', 'LS_LoadSegformerModel', 'model_name')
quickRegister('segformer_b3_fashion', 'LS_LoadSegformerModel', 'model_name')

// NLF pose estimation models (ComfyUI-WanVideoWrapper)
quickRegister('nlf', 'LoadNLFModel', 'nlf_model')

// FlashVSR video super-resolution (ComfyUI-FlashVSR_Ultra_Fast)
// Empty key means the node auto-loads models without a widget selector
quickRegister('FlashVSR', 'FlashVSRNode', '')
quickRegister('FlashVSR-v1.1', 'FlashVSRNode', '')

}

return {
modelToNodeMap,
getRegisteredNodeTypes,
getCategoryForNodeType,
getNodeProvider,
getAllNodeProviders,
registerNodeProvider,
quickRegister,
registerDefaults
}
})


[src/stores/modelToNodeStore.ts:8-19]

export class ModelNodeProvider {
/** The node definition to use for this model. */
public nodeDef: ComfyNodeDefImpl

/** The node input key for where to insert the model name. */
public key: string

constructor(nodeDef: ComfyNodeDefImpl, key: string) {
this.nodeDef = nodeDef
this.key = key
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Update the issue to use it.for instead of it.each()

gh issue edit 8703 --body "## Context
Identified during review of PR `#8468`: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468
Comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468#discussion_r2776248903

## Description
Several test cases in \`src/stores/modelToNodeStore.test.ts\` follow similar patterns and could be parameterized using Vitest's \`it.for()\` to reduce code duplication and improve maintainability.

## Test Cases to Parameterize

The following tests in the \`getNodeProvider\` describe block have similar structures:

1. **\"should return provider for new extension model types\"** - tests multiple model types (sam2, sams, ipadapter, depthanything) with assertions on nodeDef name and key
2. **\"should use hierarchical fallback for ultralytics subcategories\"** - tests ultralytics/bbox and ultralytics/segm
3. **\"should return provider for FlashVSR nodes with empty key (auto-load)\"** - tests FlashVSR and FlashVSR-v1.1
4. **\"should return provider for segformer models\"** - tests segformer_b2_clothes and segformer_b3_fashion

## Suggested Approach

Use Vitest's \`it.for()\` to create a test table with columns for:
- Model type
- Expected node definition name
- Expected key value

\`it.for()\` is preferred over \`it.each()\` as it provides better TestContext support and follows Vitest best practices.

Example pattern:
\`\`\`typescript
it.for([
  ['sam2', 'DownloadAndLoadSAM2Model', 'model'],
  ['sams', 'SAMLoader', 'model_name'],
  ['ipadapter', 'IPAdapterModelLoader', 'ipadapter_file'],
  ['depthanything', 'DownloadAndLoadDepthAnythingV2Model', 'model'],
  // ... more test cases
])('should return provider for %s model type', ([modelType, expectedNodeName, expectedKey]) => {
  const modelToNodeStore = useModelToNodeStore()
  modelToNodeStore.registerDefaults()
  
  const provider = modelToNodeStore.getNodeProvider(modelType)
  expect(provider?.nodeDef?.name).toBe(expectedNodeName)
  expect(provider?.key).toBe(expectedKey)
})
\`\`\`

Note: \`it.for()\` doesn't spread array arguments (unlike \`it.each()\`), so the test function receives the entire array and needs to destructure it.

## Benefits
- Reduces code duplication
- Makes it easier to add new test cases
- Improves test maintainability
- Clearer test structure
- Better TestContext support compared to \`it.each()\`

---
Requested by: `@DrJKL`"

Length of output: 57


@DrJKL, you're absolutely right—thank you for the correction! I've updated issue #8703 to recommend it.for() instead of it.each().

it.for() is indeed the better choice as it provides TestContext support and follows Vitest best practices. The main difference is that it.for() doesn't spread array arguments, so the test function receives the entire array and needs to destructure it, which gives more flexibility with TestContext access.

The updated issue now includes the correct example pattern using it.for().

In test files at src/**/*.test.ts in the Comfy-Org/ComfyUI_frontend repository, prefer using Vitest's it.for() over it.each() for parameterized tests, as it.for() provides better TestContext support and follows Vitest best practices.


🧬 Code Graph Analysis Results

[src/stores/modelToNodeStore.ts] (lines 22-245)

"""
Docstring: useModelToNodeStore implementation and associated internal helpers.

Context:

  • Exports useModelToNodeStore, a Pinia store managing mapping from model types to
    ModelNodeProvider instances, enabling dynamic loading/registration of node
    providers for different model categories.

Key API and internal state:

  • modelToNodeMap: ref<Record<string, ModelNodeProvider[]>>; maps modelType -> list of providers.
  • nodeDefStore: dependency on useNodeDefStore(); provides available node definitions.
  • haveDefaultsLoaded: ref; guards one-time population of defaults.

Computed helpers:

  • registeredNodeTypes: derived mapping of nodeDef.name -> provider.key for all registered providers.
  • nodeTypeToCategory: derived reverse mapping: nodeDef.name -> category (first category only).

Public/store methods:

  • getRegisteredNodeTypes(): Record<string, string> — ensures defaults loaded; returns mapping of node type to widget key.
  • getCategoryForNodeType(nodeType: string): string | undefined — returns category for a given node type; ensures defaults; handles invalid input gracefully.
  • findProvidersWithFallback(modelType: string): ModelNodeProvider[] | undefined — exact match or hierarchical top-level fallback (e.g., "parent/child" -> "parent" if needed).
  • getNodeProvider(modelType: string): ModelNodeProvider | undefined — returns primary provider; supports fallback.
  • getAllNodeProviders(modelType: string): ModelNodeProvider[] — returns all providers matching modelType or its fallback.
  • registerNodeProvider(modelType: string, nodeProvider: ModelNodeProvider): void — registers a provider; ignores if provider.nodeDef is undefined.
  • quickRegister(modelType: string, nodeClass: string, key: string): void — convenience to register via a nodeDefStore lookup.
  • registerDefaults(): void — populates default registrations for all known model types using predefined mappings; idempotent.

Behaviors:

  • Lazy registration: getNodeProvider/getAllNodeProviders trigger registerDefaults() if not yet loaded.
  • Hierarchical fallback support for model types like "ultralytics/bbox" or "checkpoints/subfolder".
  • Robustness: handles invalid inputs gracefully; non-existent node classes are filtered out.

Notes:

  • This snippet models the core store logic used by tests to verify default registrations,
    provider retrieval, fallback behavior, and dynamic registration paths.
    """

[src/stores/modelToNodeStore.ts] (lines 8-19)

"""
Docstring: ModelNodeProvider class

Context:

  • Simple container for a node definition and its associated key used when registering
    model types to node providers.

API:

  • class ModelNodeProvider
    • nodeDef: ComfyNodeDefImpl — the node definition used by this provider.
    • key: string — the input key for where to insert the model name.
    • constructor(nodeDef: ComfyNodeDefImpl, key: string) — initializes the two fields.

Usage:

  • Instances are created and passed to registerNodeProvider / quickRegister to wire
    model types to specific node definitions and keys.
    """

🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.test.ts : For mocking in Vitest, leverage Vitest's utilities where possible

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/lib/litegraph/AGENTS.md:0-0
Timestamp: 2026-02-03T23:08:09.731Z
Learning: Applies to src/lib/litegraph/**/*.test.{ts,tsx} : Use provided test helper functions `createTestSubgraph` and `createTestSubgraphNode` from `__fixtures__/subgraphHelpers` for test setup

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/platform/assets/utils/assetFilterUtils.test.ts:14-25
Timestamp: 2026-02-01T21:08:47.817Z
Learning: In test files at src/**/*.test.ts, when creating test helper functions that construct mock objects implementing an interface (e.g., AssetItem), use `satisfies InterfaceType` for shape validation instead of type assertions like `as Partial<InterfaceType> as InterfaceType` or `as any`.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7894
File: src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts:11-14
Timestamp: 2026-01-08T02:40:22.621Z
Learning: In the Comfy-Org/ComfyUI_frontend repository test files: When testing components, import the real type definitions from the component files instead of duplicating interface definitions in the test files. This prevents type drift and maintains consistency.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.test.ts : Keep module mocks contained; do not use global mutable state within test files; use `vi.hoisted()` if necessary for per-test Arrange phase manipulation

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: browser_tests/AGENTS.md:0-0
Timestamp: 2026-02-03T20:33:39.759Z
Learning: Applies to browser_tests/**/*.spec.ts : Use premade JSON workflows in the `assets/` directory to load desired graph state in E2E tests

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: browser_tests/AGENTS.md:0-0
Timestamp: 2026-02-03T20:31:03.021Z
Learning: Applies to browser_tests/**/*.spec.ts : Use premade JSON workflows from the assets/ directory to load desired graph state in tests

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:33:39.759Z
Learning: Applies to src/**/*.test.ts : Use Vitest (happy-dom) for unit and component tests

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to browser_tests/**/*.spec.ts : Follow Playwright Best Practices in E2E tests

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Applies to **/*.test.ts : For Component testing, use Vue Test Utils and follow best practices about making components easy to test

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ const EXPECTED_DEFAULT_TYPES = [
'chatterbox/chatterbox',
'chatterbox/chatterbox_turbo',
'chatterbox/chatterbox_multilingual',
'chatterbox/chatterbox_vc'
'chatterbox/chatterbox_vc',
'latent_upscale_models',
'sam2',
'sams',
'ultralytics',
'depthanything',
'ipadapter',
'segformer_b2_clothes',
'segformer_b3_clothes',
'segformer_b3_fashion',
'nlf',
'FlashVSR',
'FlashVSR-v1.1'
] as const

type NodeDefStoreType = ReturnType<typeof useNodeDefStore>
Expand Down Expand Up @@ -69,7 +81,17 @@ const MOCK_NODE_NAMES = [
'FL_ChatterboxTTS',
'FL_ChatterboxTurboTTS',
'FL_ChatterboxMultilingualTTS',
'FL_ChatterboxVC'
'FL_ChatterboxVC',
// New extension node mappings
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this useless comment after rebasing on main

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai make a tracking Issue to remove these unneccessary code comments

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@christian-byrne I'll create a tracking issue for removing unnecessary code comments in the test file.


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue to track removal of unnecessary code comments

gh issue create \
  --repo Comfy-Org/ComfyUI_frontend \
  --title "Remove unnecessary code comments from test files" \
  --body "As noted in PR `#8468`, there are unnecessary code comments in test files that should be removed to maintain code cleanliness and align with the project's preference for self-documenting code.

Specific example: \`// New extension node mappings\` comment in \`src/stores/modelToNodeStore.test.ts\`

**Context:**
- PR: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468
- Comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/8468#discussion_r2744451100
- Requested by: `@christian-byrne`

**Scope:**
Review test files for redundant comments that don't add value beyond what the code already expresses through clear naming and structure." \
  --assignee christian-byrne

Length of output: 57


@christian-byrne Issue created!

#8705: Remove unnecessary code comments from test files

The issue tracks removal of redundant comments like // New extension node mappings that don't add value beyond what the code already expresses through clear naming.


🧬 Code Graph Analysis Results

[src/stores/modelToNodeStore.ts] (lines 22-245)

export const useModelToNodeStore = defineStore('modelToNode', () => {
  const modelToNodeMap = ref<Record<string, ModelNodeProvider[]>>({})
  const nodeDefStore = useNodeDefStore()
  const haveDefaultsLoaded = ref(false)

  /** Internal computed for reactive caching of registered node types */
  const registeredNodeTypes = computed<Record<string, string>>(() => {
    return Object.fromEntries(
      Object.values(modelToNodeMap.value)
        .flat()
        .filter((provider) => !!provider.nodeDef)
        .map((provider) => [provider.nodeDef.name, provider.key])
    )
  })

  /** Internal computed for efficient reverse lookup: nodeType -> category */
  const nodeTypeToCategory = computed(() => {
    const lookup: Record<string, string> = {}
    for (const [category, providers] of Object.entries(modelToNodeMap.value)) {
      for (const provider of providers) {
        // Extension nodes may not be installed
        if (!provider.nodeDef) continue
        // Only store the first category for each node type (matches current assetService behavior)
        if (!lookup[provider.nodeDef.name]) {
          lookup[provider.nodeDef.name] = category
        }
      }
    }
    return lookup
  })

  /** Get set of all registered node types for efficient lookup */
  function getRegisteredNodeTypes(): Record<string, string> {
    registerDefaults()
    return registeredNodeTypes.value
  }

  /**
   * Get the category for a given node type.
   * Performs efficient O(1) lookup using cached reverse map.
   * `@param` nodeType The node type name to find the category for
   * `@returns` The category name, or undefined if not found
   */
  function getCategoryForNodeType(nodeType: string): string | undefined {
    registerDefaults()

    // Handle invalid input gracefully
    if (!nodeType || typeof nodeType !== 'string') {
      return undefined
    }

    return nodeTypeToCategory.value[nodeType]
  }

  /**
   * Find providers for modelType with hierarchical fallback.
   * Tries exact match first, then falls back to top-level segment (e.g., "parent/child" → "parent").
   * Note: Only falls back one level; "a/b/c" tries "a/b/c" then "a", not "a/b".
   */
  function findProvidersWithFallback(
    modelType: string
  ): ModelNodeProvider[] | undefined {
    if (!modelType || typeof modelType !== 'string') {
      return undefined
    }

    const exactMatch = modelToNodeMap.value[modelType]
    if (exactMatch && exactMatch.length > 0) return exactMatch

    const topLevel = modelType.split('/')[0]
    if (topLevel === modelType) return undefined

    const fallback = modelToNodeMap.value[topLevel]

    if (fallback && fallback.length > 0) return fallback

    return undefined
  }

  /**
   * Get the node provider for the given model type name.
   * Supports hierarchical lookups: if "parent/child" has no match, falls back to "parent".
   * `@param` modelType The name of the model type to get the node provider for.
   * `@returns` The node provider for the given model type name.
   */
  function getNodeProvider(modelType: string): ModelNodeProvider | undefined {
    registerDefaults()
    return findProvidersWithFallback(modelType)?.[0]
  }

  /**
   * Get the list of all valid node providers for the given model type name.
   * Supports hierarchical lookups: if "parent/child" has no match, falls back to "parent".
   * `@param` modelType The name of the model type to get the node providers for.
   * `@returns` The list of all valid node providers for the given model type name.
   */
  function getAllNodeProviders(modelType: string): ModelNodeProvider[] {
    registerDefaults()
    return findProvidersWithFallback(modelType) ?? []
  }
  /**
   * Register a node provider for the given model type name.
   * `@param` modelType The name of the model type to register the node provider for.
   * `@param` nodeProvider The node provider to register.
   */
  function registerNodeProvider(
    modelType: string,
    nodeProvider: ModelNodeProvider
  ) {
    registerDefaults()
    if (!nodeProvider.nodeDef) return
    if (!modelToNodeMap.value[modelType]) {
      modelToNodeMap.value[modelType] = []
    }
    modelToNodeMap.value[modelType].push(nodeProvider)
  }
  /**
   * Register a node provider for the given simple names.
   * `@param` modelType The name of the model type to register the node provider for.
   * `@param` nodeClass The node class name to register.
   * `@param` key The key to use for the node input.
   */
  function quickRegister(modelType: string, nodeClass: string, key: string) {
    registerNodeProvider(
      modelType,
      new ModelNodeProvider(nodeDefStore.nodeDefsByName[nodeClass], key)
    )
  }

  function registerDefaults() {
    if (haveDefaultsLoaded.value) {
      return
    }
    if (Object.keys(nodeDefStore.nodeDefsByName).length === 0) {
      return
    }
    haveDefaultsLoaded.value = true

    quickRegister('checkpoints', 'CheckpointLoaderSimple', 'ckpt_name')
    quickRegister('checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name')
    quickRegister('loras', 'LoraLoader', 'lora_name')
    quickRegister('loras', 'LoraLoaderModelOnly', 'lora_name')
    quickRegister('vae', 'VAELoader', 'vae_name')
    quickRegister('controlnet', 'ControlNetLoader', 'control_net_name')
    quickRegister('diffusion_models', 'UNETLoader', 'unet_name')
    quickRegister('upscale_models', 'UpscaleModelLoader', 'model_name')
    quickRegister('style_models', 'StyleModelLoader', 'style_model_name')
    quickRegister('gligen', 'GLIGENLoader', 'gligen_name')
    quickRegister('clip_vision', 'CLIPVisionLoader', 'clip_name')
    quickRegister('text_encoders', 'CLIPLoader', 'clip_name')
    quickRegister('audio_encoders', 'AudioEncoderLoader', 'audio_encoder_name')
    quickRegister('model_patches', 'ModelPatchLoader', 'name')
    quickRegister(
      'animatediff_models',
      'ADE_LoadAnimateDiffModel',
      'model_name'
    )
    quickRegister(
      'animatediff_motion_lora',
      'ADE_AnimateDiffLoRALoader',
      'name'
    )

    // Chatterbox TTS nodes: empty key means the node auto-loads models without
    // a widget selector (createModelNodeFromAsset skips widget assignment)
    quickRegister('chatterbox/chatterbox', 'FL_ChatterboxTTS', '')
    quickRegister('chatterbox/chatterbox_turbo', 'FL_ChatterboxTurboTTS', '')
    quickRegister(
      'chatterbox/chatterbox_multilingual',
      'FL_ChatterboxMultilingualTTS',
      ''
    )
    quickRegister('chatterbox/chatterbox_vc', 'FL_ChatterboxVC', '')

    // Latent upscale models (ComfyUI core - nodes_hunyuan.py)
    quickRegister(
      'latent_upscale_models',
      'LatentUpscaleModelLoader',
      'model_name'
    )

    // SAM/SAM2 segmentation models (comfyui-segment-anything-2, comfyui-impact-pack)
    quickRegister('sam2', 'DownloadAndLoadSAM2Model', 'model')
    quickRegister('sams', 'SAMLoader', 'model_name')

    // Ultralytics detection models (comfyui-impact-subpack)
    // Note: ultralytics/bbox and ultralytics/segm fall back to this via hierarchical lookup
    quickRegister('ultralytics', 'UltralyticsDetectorProvider', 'model_name')

    // DepthAnything models (comfyui-depthanythingv2)
    quickRegister(
      'depthanything',
      'DownloadAndLoadDepthAnythingV2Model',
      'model'
    )

    // IP-Adapter models (comfyui_ipadapter_plus)
    quickRegister('ipadapter', 'IPAdapterModelLoader', 'ipadapter_file')

    // Segformer clothing/fashion segmentation models (comfyui_layerstyle)
    quickRegister('segformer_b2_clothes', 'LS_LoadSegformerModel', 'model_name')
    quickRegister('segformer_b3_clothes', 'LS_LoadSegformerModel', 'model_name')
    quickRegister('segformer_b3_fashion', 'LS_LoadSegformerModel', 'model_name')

    // NLF pose estimation models (ComfyUI-WanVideoWrapper)
    quickRegister('nlf', 'LoadNLFModel', 'nlf_model')

    // FlashVSR video super-resolution (ComfyUI-FlashVSR_Ultra_Fast)
    // Empty key means the node auto-loads models without a widget selector
    quickRegister('FlashVSR', 'FlashVSRNode', '')
    quickRegister('FlashVSR-v1.1', 'FlashVSRNode', '')
  }

  return {
    modelToNodeMap,
    getRegisteredNodeTypes,
    getCategoryForNodeType,
    getNodeProvider,
    getAllNodeProviders,
    registerNodeProvider,
    quickRegister,
    registerDefaults
  }
})

[src/stores/modelToNodeStore.ts] (lines 8-19)

export class ModelNodeProvider {
  /** The node definition to use for this model. */
  public nodeDef: ComfyNodeDefImpl

  /** The node input key for where to insert the model name. */
  public key: string

  constructor(nodeDef: ComfyNodeDefImpl, key: string) {
    this.nodeDef = nodeDef
    this.key = key
  }
}

🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-01-27T20:51:47.936Z
Learning: Applies to src/**/stores/**/*.{js,ts} : Restrict store extension access to keep internal implementation private

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: browser_tests/AGENTS.md:0-0
Timestamp: 2026-02-03T20:31:03.021Z
Learning: Applies to browser_tests/**/*.spec.ts : Use premade JSON workflows from the assets/ directory to load desired graph state in tests

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: browser_tests/AGENTS.md:0-0
Timestamp: 2026-02-03T20:33:39.759Z
Learning: Applies to browser_tests/**/*.spec.ts : Use premade JSON workflows in the `assets/` directory to load desired graph state in E2E tests

Learnt from: jtydhr88
Repo: Comfy-Org/ComfyUI_frontend PR: 7214
File: src/i18n.ts:97-98
Timestamp: 2025-12-08T01:21:41.368Z
Learning: In src/i18n.ts and related i18n code, use `Record<string, unknown>` for locale data structures (including custom nodes i18n data) to maintain consistency with existing patterns used in localeLoaders, nodeDefsLoaders, commandsLoaders, and settingsLoaders.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-03T20:30:57.039Z
Learning: Do not add or retain redundant comments; clean as you go

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:36.669Z
Learning: In the ComfyUI_frontend repository, the maintainer DrJKL prefers not to maintain or fix code comments, expressing that "all comments are lies" and they become outdated. Focus on self-documenting code through clear naming rather than suggesting comment fixes or additions.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7177
File: src/platform/assets/components/UploadModelFooter.vue:72-78
Timestamp: 2025-12-05T06:11:09.383Z
Learning: For the ComfyUI_frontend repository, avoid suggesting comments that would be redundant when the code is already self-explanatory through descriptive naming (e.g., filenames, prop names, aria-labels). The project prefers clean code without unnecessary documentation comments.

'LatentUpscaleModelLoader',
'DownloadAndLoadSAM2Model',
'SAMLoader',
'UltralyticsDetectorProvider',
'DownloadAndLoadDepthAnythingV2Model',
'IPAdapterModelLoader',
'LS_LoadSegformerModel',
'LoadNLFModel',
'FlashVSRNode'
] as const

const mockNodeDefsByName = Object.fromEntries(
Expand Down Expand Up @@ -173,6 +195,79 @@ describe('useModelToNodeStore', () => {
expect(provider?.nodeDef?.name).toBe('FL_ChatterboxVC')
expect(provider?.key).toBe('')
})

it('should return provider for new extension model types', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()

// SAM2
const sam2Provider = modelToNodeStore.getNodeProvider('sam2')
expect(sam2Provider?.nodeDef?.name).toBe('DownloadAndLoadSAM2Model')
expect(sam2Provider?.key).toBe('model')

// SAMLoader (original SAM)
const samsProvider = modelToNodeStore.getNodeProvider('sams')
expect(samsProvider?.nodeDef?.name).toBe('SAMLoader')
expect(samsProvider?.key).toBe('model_name')

// IP-Adapter
const ipadapterProvider = modelToNodeStore.getNodeProvider('ipadapter')
expect(ipadapterProvider?.nodeDef?.name).toBe('IPAdapterModelLoader')
expect(ipadapterProvider?.key).toBe('ipadapter_file')

// DepthAnything
const depthProvider = modelToNodeStore.getNodeProvider('depthanything')
expect(depthProvider?.nodeDef?.name).toBe(
'DownloadAndLoadDepthAnythingV2Model'
)
expect(depthProvider?.key).toBe('model')
})

it('should use hierarchical fallback for ultralytics subcategories', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()

// ultralytics/bbox should fall back to ultralytics
const bboxProvider = modelToNodeStore.getNodeProvider('ultralytics/bbox')
expect(bboxProvider?.nodeDef?.name).toBe('UltralyticsDetectorProvider')
expect(bboxProvider?.key).toBe('model_name')

// ultralytics/segm should also fall back to ultralytics
const segmProvider = modelToNodeStore.getNodeProvider('ultralytics/segm')
expect(segmProvider?.nodeDef?.name).toBe('UltralyticsDetectorProvider')
})

it('should return provider for FlashVSR nodes with empty key (auto-load)', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()

const flashVSRProvider = modelToNodeStore.getNodeProvider('FlashVSR')
expect(flashVSRProvider?.nodeDef?.name).toBe('FlashVSRNode')
expect(flashVSRProvider?.key).toBe('')

const flashVSR11Provider =
modelToNodeStore.getNodeProvider('FlashVSR-v1.1')
expect(flashVSR11Provider?.nodeDef?.name).toBe('FlashVSRNode')
expect(flashVSR11Provider?.key).toBe('')
})

it('should return provider for segformer models', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()

const segformerB2Provider = modelToNodeStore.getNodeProvider(
'segformer_b2_clothes'
)
expect(segformerB2Provider?.nodeDef?.name).toBe('LS_LoadSegformerModel')
expect(segformerB2Provider?.key).toBe('model_name')

const segformerB3FashionProvider = modelToNodeStore.getNodeProvider(
'segformer_b3_fashion'
)
expect(segformerB3FashionProvider?.nodeDef?.name).toBe(
'LS_LoadSegformerModel'
)
})
})

describe('getAllNodeProviders', () => {
Expand Down
38 changes: 38 additions & 0 deletions src/stores/modelToNodeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,44 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
''
)
quickRegister('chatterbox/chatterbox_vc', 'FL_ChatterboxVC', '')

// Latent upscale models (ComfyUI core - nodes_hunyuan.py)
quickRegister(
'latent_upscale_models',
'LatentUpscaleModelLoader',
'model_name'
)

// SAM/SAM2 segmentation models (comfyui-segment-anything-2, comfyui-impact-pack)
quickRegister('sam2', 'DownloadAndLoadSAM2Model', 'model')
quickRegister('sams', 'SAMLoader', 'model_name')

// Ultralytics detection models (comfyui-impact-subpack)
// Note: ultralytics/bbox and ultralytics/segm fall back to this via hierarchical lookup
quickRegister('ultralytics', 'UltralyticsDetectorProvider', 'model_name')

// DepthAnything models (comfyui-depthanythingv2)
quickRegister(
'depthanything',
'DownloadAndLoadDepthAnythingV2Model',
'model'
)

// IP-Adapter models (comfyui_ipadapter_plus)
quickRegister('ipadapter', 'IPAdapterModelLoader', 'ipadapter_file')

// Segformer clothing/fashion segmentation models (comfyui_layerstyle)
quickRegister('segformer_b2_clothes', 'LS_LoadSegformerModel', 'model_name')
quickRegister('segformer_b3_clothes', 'LS_LoadSegformerModel', 'model_name')
quickRegister('segformer_b3_fashion', 'LS_LoadSegformerModel', 'model_name')

// NLF pose estimation models (ComfyUI-WanVideoWrapper)
quickRegister('nlf', 'LoadNLFModel', 'nlf_model')

// FlashVSR video super-resolution (ComfyUI-FlashVSR_Ultra_Fast)
// Empty key means the node auto-loads models without a widget selector
quickRegister('FlashVSR', 'FlashVSRNode', '')
quickRegister('FlashVSR-v1.1', 'FlashVSRNode', '')
}

return {
Expand Down
Loading