Skip to content

Commit d2de2fc

Browse files
fix: normalize essentials_category to canonical form for case-insensitive backend matching
Backend sends title-cased categories (e.g. 'Image Generation') while frontend uses lowercase (e.g. 'image generation'). Add canonical lookup map to resolve backend values to the exact form in ESSENTIALS_CATEGORIES, preserving special cases like '3D'. Addresses review feedback from benceruleanlu. Amp-Thread-ID: https://ampcode.com/threads/T-019c93b8-dae2-77ba-92c5-c7a71a424a3f
1 parent 560b0ef commit d2de2fc

5 files changed

Lines changed: 113 additions & 9 deletions

File tree

src/constants/essentialsNodes.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
22

33
import {
44
ESSENTIALS_CATEGORIES,
5+
ESSENTIALS_CATEGORY_CANONICAL,
56
ESSENTIALS_CATEGORY_MAP,
67
ESSENTIALS_NODES,
78
TOOLKIT_BLUEPRINT_MODULES,
@@ -50,4 +51,12 @@ describe('essentialsNodes', () => {
5051
it('TOOLKIT_BLUEPRINT_MODULES contains comfy_essentials', () => {
5152
expect(TOOLKIT_BLUEPRINT_MODULES.has('comfy_essentials')).toBe(true)
5253
})
54+
55+
it('ESSENTIALS_CATEGORY_CANONICAL maps every category case-insensitively', () => {
56+
for (const category of ESSENTIALS_CATEGORIES) {
57+
expect(ESSENTIALS_CATEGORY_CANONICAL.get(category.toLowerCase())).toBe(
58+
category
59+
)
60+
}
61+
})
5362
})

src/constants/essentialsNodes.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,22 @@ export const ESSENTIALS_NODES: Record<EssentialsCategory, readonly string[]> = {
7878
* Flat map: node name → category (derived from ESSENTIALS_NODES).
7979
* Used as mock/fallback when backend doesn't provide essentials_category.
8080
*/
81-
export const ESSENTIALS_CATEGORY_MAP: Record<string, string> =
81+
export const ESSENTIALS_CATEGORY_MAP: Record<string, EssentialsCategory> =
8282
Object.fromEntries(
8383
Object.entries(ESSENTIALS_NODES).flatMap(([category, nodes]) =>
8484
nodes.map((node) => [node, category])
8585
)
86-
)
86+
) as Record<string, EssentialsCategory>
87+
88+
/**
89+
* Case-insensitive lookup: lowercase category → canonical category.
90+
* Used to normalize backend categories (which may be title-cased) to the
91+
* canonical form used in ESSENTIALS_CATEGORIES.
92+
*/
93+
export const ESSENTIALS_CATEGORY_CANONICAL: ReadonlyMap<
94+
string,
95+
EssentialsCategory
96+
> = new Map(ESSENTIALS_CATEGORIES.map((c) => [c.toLowerCase(), c]))
8797

8898
/**
8999
* "Novel" toolkit nodes for telemetry — basics excluded.

src/stores/subgraphStore.test.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,35 @@ describe('useSubgraphStore', () => {
516516
})
517517

518518
describe('essentials_category passthrough', () => {
519+
it('should prefer GlobalSubgraphData essentials_category over definition fallback', async () => {
520+
const graphWithEssentials = {
521+
...mockGraph,
522+
definitions: {
523+
subgraphs: [
524+
{
525+
...mockGraph.definitions?.subgraphs?.[0],
526+
essentials_category: 'Image Tools'
527+
}
528+
]
529+
}
530+
}
531+
await mockFetch(
532+
{},
533+
{
534+
bp_precedence: {
535+
name: 'Precedence Blueprint',
536+
info: { node_pack: 'test_pack' },
537+
data: JSON.stringify(graphWithEssentials),
538+
essentials_category: 'Video Generation'
539+
}
540+
}
541+
)
542+
const nodeDef = useNodeDefStore().nodeDefs.find(
543+
(d) => d.name === 'SubgraphBlueprint.bp_precedence'
544+
)
545+
expect(nodeDef?.essentials_category).toBe('video generation')
546+
})
547+
519548
it('should pass essentials_category from GlobalSubgraphData to node def', async () => {
520549
await mockFetch(
521550
{},
@@ -532,7 +561,7 @@ describe('useSubgraphStore', () => {
532561
(d) => d.name === 'SubgraphBlueprint.bp_essentials'
533562
)
534563
expect(nodeDef).toBeDefined()
535-
expect(nodeDef?.essentials_category).toBe('Image Generation')
564+
expect(nodeDef?.essentials_category).toBe('image generation')
536565
})
537566

538567
it('should extract essentials_category from subgraph definition as fallback', async () => {
@@ -561,7 +590,26 @@ describe('useSubgraphStore', () => {
561590
(d) => d.name === 'SubgraphBlueprint.bp_fallback'
562591
)
563592
expect(nodeDef).toBeDefined()
564-
expect(nodeDef?.essentials_category).toBe('Image Tools')
593+
expect(nodeDef?.essentials_category).toBe('image tools')
594+
})
595+
596+
it('should normalize title-cased essentials_category to canonical form', async () => {
597+
await mockFetch(
598+
{},
599+
{
600+
bp_3d: {
601+
name: 'Test 3D Blueprint',
602+
info: { node_pack: 'test_pack', category: 'Test Category' },
603+
data: JSON.stringify(mockGraph),
604+
essentials_category: '3d'
605+
}
606+
}
607+
)
608+
const nodeDef = useNodeDefStore().nodeDefs.find(
609+
(d) => d.name === 'SubgraphBlueprint.bp_3d'
610+
)
611+
expect(nodeDef).toBeDefined()
612+
expect(nodeDef?.essentials_category).toBe('3D')
565613
})
566614
})
567615

src/types/nodeSource.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
3+
import {
4+
NodeSourceType,
5+
getEssentialsCategory,
6+
getNodeSource
7+
} from '@/types/nodeSource'
48

59
describe('getNodeSource', () => {
610
it('should return UNKNOWN_NODE_SOURCE when python_module is undefined', () => {
@@ -105,6 +109,33 @@ describe('getNodeSource', () => {
105109
const result = getNodeSource('nodes.some_module', undefined, 'LoadImage')
106110
expect(result.type).toBe(NodeSourceType.Essentials)
107111
})
112+
113+
it('should normalize title-cased backend categories to lowercase', () => {
114+
const result = getNodeSource(
115+
'nodes.some_module',
116+
'Image Generation',
117+
'SomeNode'
118+
)
119+
expect(result.type).toBe(NodeSourceType.Essentials)
120+
})
121+
})
122+
123+
describe('getEssentialsCategory', () => {
124+
it('should normalize title-cased essentials_category to canonical form', () => {
125+
expect(getEssentialsCategory('SomeNode', 'Image Generation')).toBe(
126+
'image generation'
127+
)
128+
expect(getEssentialsCategory('SomeNode', '3d')).toBe('3D')
129+
expect(getEssentialsCategory('SomeNode', '3D')).toBe('3D')
130+
})
131+
132+
it('should fall back to ESSENTIALS_CATEGORY_MAP when no category provided', () => {
133+
expect(getEssentialsCategory('LoadImage')).toBe('basics')
134+
})
135+
136+
it('should return undefined for unknown node without category', () => {
137+
expect(getEssentialsCategory('UnknownNode')).toBeUndefined()
138+
})
108139
})
109140

110141
describe('blueprint nodes', () => {

src/types/nodeSource.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { ESSENTIALS_CATEGORY_MAP } from '@/constants/essentialsNodes'
1+
import {
2+
ESSENTIALS_CATEGORY_CANONICAL,
3+
ESSENTIALS_CATEGORY_MAP
4+
} from '@/constants/essentialsNodes'
25

36
export enum NodeSourceType {
47
Core = 'core',
@@ -35,9 +38,12 @@ export function getEssentialsCategory(
3538
name?: string,
3639
essentials_category?: string
3740
): string | undefined {
38-
return (
39-
essentials_category ?? (name ? ESSENTIALS_CATEGORY_MAP[name] : undefined)
40-
)
41+
const normalizedCategory = essentials_category?.trim().toLowerCase()
42+
const canonical = normalizedCategory
43+
? (ESSENTIALS_CATEGORY_CANONICAL.get(normalizedCategory) ??
44+
normalizedCategory)
45+
: undefined
46+
return canonical ?? (name ? ESSENTIALS_CATEGORY_MAP[name] : undefined)
4147
}
4248

4349
export const getNodeSource = (

0 commit comments

Comments
 (0)