test(onboarding): drop vendored template fixtures from resolver unit test

The 5 committed template JSONs (~340KB) were hand-vendored copies of upstream
Comfy-Org/workflow_templates consumed only by roleResolver.test.ts. They drift
silently from what the backend serves and bloat the repo, while production
fetches templates live.

Delete the JSONs, their fs-based loader, and the linguist-generated entries.
Prune roleResolver.test.ts to its fixture-free coverage: the synthetic-graph
logic and security tests (prompt selection, subgraph traversal, prototype-
pollution guards, malformed-link resilience, registry sink fallback) stay as
fast offline unit tests; the three cases that read a fixture are rebuilt on
synthetic graphs. The curated-template pins move to an e2e that verifies the
resolver against the live templates (next commit).
This commit is contained in:
Maanil Verma
2026-07-17 17:22:03 +05:30
parent 4e7f6a6a6b
commit 3772623297
8 changed files with 92 additions and 12891 deletions

3
.gitattributes vendored
View File

@@ -6,6 +6,3 @@ packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true
packages/ingest-types/src/types.gen.ts linguist-generated=true
packages/ingest-types/src/zod.gen.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
# Vendored upstream workflow templates used as resolver test fixtures
src/renderer/extensions/firstRunTour/__fixtures__/templates/*.json linguist-generated=true

View File

@@ -1,27 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import {
zComfyWorkflow,
zComfyWorkflow1
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type { CuratedTemplateId } from '../roleResolver'
const TEMPLATES_DIR = path.join(import.meta.dirname, 'templates')
/** Loads a curated template's real workflow, validated so a bad fixture fails loudly. */
export function loadTemplateWorkflow(id: CuratedTemplateId): ComfyWorkflowJSON {
const file = path.join(TEMPLATES_DIR, `${id}.json`)
const data: unknown = JSON.parse(fs.readFileSync(file, 'utf-8'))
const schema =
(data as { version?: number }).version === 1
? zComfyWorkflow1
: zComfyWorkflow
const result = schema.safeParse(data)
if (!result.success) {
throw new Error(`Invalid template fixture "${id}": ${result.error.message}`)
}
return result.data
}

View File

@@ -3,133 +3,13 @@ import { describe, expect, it } from 'vitest'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { toNodeId } from '@/types/nodeId'
import { loadTemplateWorkflow } from './__fixtures__/loadTemplateWorkflow'
import {
fromWorkflowJson,
resolveRoles,
templateOverrides
} from './roleResolver'
import type { CuratedTemplateId } from './roleResolver'
interface CuratedExpectation {
sourceId: number | null
promptInnerId: number
promptWidget: string
engineId: number
sinkId: number
mediaKind: 'image' | 'video'
}
const CURATED: Record<CuratedTemplateId, CuratedExpectation> = {
image_krea2_turbo_t2i: {
sourceId: null,
promptInnerId: 19,
promptWidget: 'value',
engineId: 3,
sinkId: 29,
mediaKind: 'image'
},
image_z_image_turbo: {
sourceId: null,
promptInnerId: 27,
promptWidget: 'text',
engineId: 3,
sinkId: 9,
mediaKind: 'image'
},
video_ltx2_3_i2v: {
sourceId: 269,
promptInnerId: 319,
promptWidget: 'value',
engineId: 283,
sinkId: 75,
mediaKind: 'video'
},
video_wan2_2_14B_i2v: {
sourceId: 97,
promptInnerId: 93,
promptWidget: 'text',
engineId: 86,
sinkId: 108,
mediaKind: 'video'
},
flux_kontext_dev_basic: {
sourceId: 190,
promptInnerId: 6,
promptWidget: 'text',
engineId: 31,
sinkId: 136,
mediaKind: 'image'
}
}
const curatedIds = Object.keys(CURATED) as CuratedTemplateId[]
describe('resolveRoles — curated templates, heuristics only (no override)', () => {
it.for(curatedIds)('resolves %s from its real JSON', (id) => {
const expected = CURATED[id]
const roles = resolveRoles(loadTemplateWorkflow(id))
if (expected.sourceId === null) {
expect(roles.source).toBeNull()
} else {
expect(roles.source?.nodeId).toBe(toNodeId(expected.sourceId))
}
expect(roles.prompt?.innerNodeId).toBe(toNodeId(expected.promptInnerId))
expect(roles.prompt?.widgetName).toBe(expected.promptWidget)
expect(roles.engine?.nodeId).toBe(toNodeId(expected.engineId))
expect(roles.sink?.nodeId).toBe(toNodeId(expected.sinkId))
expect(roles.mediaKind).toBe(expected.mediaKind)
})
it('rejects the internal-fed decoy CLIPTextEncode (krea 6, ltx 303)', () => {
expect(
resolveRoles(loadTemplateWorkflow('image_krea2_turbo_t2i')).prompt
?.innerNodeId
).not.toBe(toNodeId(6))
expect(
resolveRoles(loadTemplateWorkflow('video_ltx2_3_i2v')).prompt?.innerNodeId
).not.toBe(toNodeId(303))
})
it('picks the boundary-fed user prompt over a sibling system prompt (krea)', () => {
const roles = resolveRoles(loadTemplateWorkflow('image_krea2_turbo_t2i'))
expect(roles.prompt?.innerNodeId).toBe(toNodeId(19))
})
it('picks the positive CLIPTextEncode over the negative one (wan)', () => {
const roles = resolveRoles(loadTemplateWorkflow('video_wan2_2_14B_i2v'))
expect(roles.prompt?.innerNodeId).toBe(toNodeId(93))
})
it('picks one source when several LoadImage exist (flux)', () => {
const roles = resolveRoles(loadTemplateWorkflow('flux_kontext_dev_basic'))
expect(roles.source?.nodeId).toBe(toNodeId(190))
})
it('exposes the subgraph node id and prompt port for fallback', () => {
const zImage = resolveRoles(loadTemplateWorkflow('image_z_image_turbo'))
expect(zImage.prompt?.subgraphNodeId).toBe(toNodeId(57))
expect(zImage.prompt?.portFallback).toBe('text')
const ltx = resolveRoles(loadTemplateWorkflow('video_ltx2_3_i2v'))
expect(ltx.prompt?.portFallback).toBe('value')
})
})
describe('templateOverrides pin the heuristic result', () => {
it('matches the heuristic result for every curated id', () => {
for (const id of curatedIds) {
const roles = resolveRoles(loadTemplateWorkflow(id))
const pin = templateOverrides[id]
expect(pin.promptNodeId).toBe(roles.prompt?.innerNodeId)
expect(pin.engineNodeId).toBe(roles.engine?.nodeId)
expect(pin.sinkNodeId).toBe(roles.sink?.nodeId)
expect(pin.mediaKind).toBe(roles.mediaKind)
if (roles.source) expect(pin.sourceNodeId).toBe(roles.source.nodeId)
}
})
describe('templateOverrides applied to the graph', () => {
it('override sink, engine, and media win over the heuristics on the graph', () => {
const pin = templateOverrides.image_z_image_turbo
const roles = resolveRoles(
@@ -162,22 +42,53 @@ describe('templateOverrides pin the heuristic result', () => {
})
it('spotlights the subgraph host, not the pinned inner node, for a nested prompt', () => {
const subgraphId = '33333333-3333-3333-3333-333333333333'
const pin = templateOverrides.image_z_image_turbo
const roles = resolveRoles(
loadTemplateWorkflow('flux_kontext_dev_basic'),
'flux_kontext_dev_basic'
{
...workflow([
node(50, subgraphId, { inputs: [] }),
node(9, 'SaveImage', {
inputs: [{ name: 'images', type: 'IMAGE', link: 8 }]
})
]),
definitions: {
subgraphs: [
{
id: subgraphId,
inputs: [],
nodes: [cte(Number(pin.promptNodeId), 4, 'a red fox')],
links: []
}
]
}
} as unknown as ComfyWorkflowJSON,
'image_z_image_turbo'
)
expect(roles.prompt?.innerNodeId).toBe(toNodeId(6))
expect(roles.prompt?.subgraphNodeId).toBe(toNodeId(192))
expect(roles.prompt?.innerNodeId).toBe(pin.promptNodeId)
expect(roles.prompt?.subgraphNodeId).toBe(toNodeId(50))
})
it('ignores an unknown template id and falls back to heuristics', () => {
const withUnknown = resolveRoles(
loadTemplateWorkflow('image_z_image_turbo'),
'not_a_real_template'
const graph = workflow(
[
cte(6, 4, 'a red fox'),
node(3, 'KSampler', {
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 4 }]
}),
node(9, 'SaveImage', {
inputs: [{ name: 'images', type: 'IMAGE', link: 8 }]
})
],
[
[4, 6, 0, 3, 1, 'CONDITIONING'],
[8, 3, 0, 9, 0, 'IMAGE']
]
)
expect(resolveRoles(graph, 'not_a_real_template')).toEqual(
resolveRoles(graph)
)
const heuristic = resolveRoles(loadTemplateWorkflow('image_z_image_turbo'))
expect(withUnknown).toEqual(heuristic)
})
})
@@ -370,16 +281,46 @@ describe('fromWorkflowJson normalizes the serialized shape', () => {
})
it('reads object links and the boundary origin inside subgraphs', () => {
const graph = fromWorkflowJson(loadTemplateWorkflow('image_z_image_turbo'))
const subgraph = graph.subgraphs[0]
const clip = subgraph.nodes.find((n) => n.id === toNodeId(27))
const subgraphId = '44444444-4444-4444-4444-444444444444'
const graph = fromWorkflowJson({
...workflow([node(50, subgraphId, { inputs: [] })]),
definitions: {
subgraphs: [
{
id: subgraphId,
inputs: [{ name: 'text', type: 'STRING' }],
nodes: [
node(27, 'CLIPTextEncode', {
inputs: [{ name: 'text', type: 'STRING', link: 7 }]
})
],
links: [
{
id: 7,
origin_id: -10,
origin_slot: 0,
target_id: 27,
target_slot: 0
}
]
}
]
}
} as unknown as ComfyWorkflowJSON)
const clip = graph.subgraphs[0].nodes.find((n) => n.id === toNodeId(27))
const textInput = clip?.inputs.find((i) => i.name === 'text')
expect(textInput?.origin).toEqual({ kind: 'boundary', slot: 0 })
})
it('flags the hosting subgraph node', () => {
const graph = fromWorkflowJson(loadTemplateWorkflow('image_z_image_turbo'))
const subgraphId = '44444444-4444-4444-4444-444444444444'
const graph = fromWorkflowJson({
...workflow([node(50, subgraphId, { inputs: [] })]),
definitions: {
subgraphs: [{ id: subgraphId, inputs: [], nodes: [], links: [] }]
}
} as unknown as ComfyWorkflowJSON)
const host = graph.nodes.find((n) => n.subgraphId !== null)
expect(host?.subgraphId).toBe(graph.subgraphs[0].id)
@@ -489,11 +430,24 @@ describe('resolveRoles — hostile node types do not match prototype members', (
})
it('ignores a template id that names a prototype member', () => {
const heuristic = resolveRoles(loadTemplateWorkflow('image_z_image_turbo'))
const graph = workflow(
[
cte(6, 4, 'a red fox'),
node(3, 'KSampler', {
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 4 }]
}),
node(9, 'SaveImage', {
inputs: [{ name: 'images', type: 'IMAGE', link: 8 }]
})
],
[
[4, 6, 0, 3, 1, 'CONDITIONING'],
[8, 3, 0, 9, 0, 'IMAGE']
]
)
const heuristic = resolveRoles(graph)
for (const id of ['constructor', 'toString', 'hasOwnProperty']) {
expect(
resolveRoles(loadTemplateWorkflow('image_z_image_turbo'), id)
).toEqual(heuristic)
expect(resolveRoles(graph, id)).toEqual(heuristic)
}
})
})