mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
fix(sharing): match regular load policy on shared workflow validation
Switch to raw workflow_json fallback when Zod validation fails, matching the policy at scripts/app.ts:1191-1198. Share service no longer validates directly — loadGraphData applies the same fallback under Comfy.Validation.Workflows, removing the cross-path policy inconsistency. Drops the tolerantArray combinator and per-field linearData.inputs relaxation added earlier in this PR. Zod schemas stay strict as the canonical spec; tolerance lives at the consumer boundary. Fixes the reported share=21e32125c692 load failure and any future extra.* shape drift, not just linearData.inputs.
This commit is contained in:
@@ -15,14 +15,6 @@ vi.mock('@/scripts/app', () => ({
|
||||
const mockGetShareableAssets = vi.fn()
|
||||
const mockFetchApi = vi.fn()
|
||||
|
||||
vi.mock(
|
||||
'@/platform/workflow/validation/schemas/workflowSchema',
|
||||
async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
validateComfyWorkflow: vi.fn(async (json: unknown) => json)
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getShareableAssets: (...args: unknown[]) => mockGetShareableAssets(...args),
|
||||
@@ -369,6 +361,36 @@ describe(useWorkflowShareService, () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('returns raw workflow_json when it does not match ComfyWorkflowJSON schema', async () => {
|
||||
const rawWorkflowJson = {
|
||||
extra: {
|
||||
linearData: {
|
||||
inputs: [
|
||||
[1, 'prompt'],
|
||||
[2, 'seed', 'invalid-third-element']
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
}
|
||||
}
|
||||
mockFetchApi.mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
share_id: 'share-raw',
|
||||
workflow_id: 'wf-raw',
|
||||
name: 'Raw',
|
||||
listed: false,
|
||||
publish_time: null,
|
||||
workflow_json: rawWorkflowJson,
|
||||
assets: []
|
||||
})
|
||||
)
|
||||
|
||||
const service = useWorkflowShareService()
|
||||
const shared = await service.getSharedWorkflow('share-raw')
|
||||
|
||||
expect(shared.workflowJson).toEqual(rawWorkflowJson)
|
||||
})
|
||||
|
||||
it('treats malformed publish-status payload as unpublished', async () => {
|
||||
mockFetchApi.mockResolvedValue(mockJsonResponse({ is_published: true }))
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
} from '@/platform/workflow/sharing/types/shareTypes'
|
||||
import type { ThumbnailType } from '@/platform/workflow/sharing/types/comfyHubTypes'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { AssetInfo } from '@/schemas/apiSchema'
|
||||
import {
|
||||
zHubWorkflowPrefillResponse,
|
||||
@@ -246,12 +245,6 @@ export function useWorkflowShareService() {
|
||||
throw new Error('Failed to load shared workflow: invalid response')
|
||||
}
|
||||
|
||||
const validated = await validateComfyWorkflow(workflow.workflowJson)
|
||||
if (!validated) {
|
||||
throw new Error('Failed to load shared workflow: invalid workflow data')
|
||||
}
|
||||
workflow.workflowJson = validated
|
||||
|
||||
return workflow
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from 'fs'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { defaultGraph } from '@/scripts/defaultGraph'
|
||||
@@ -111,56 +111,13 @@ describe('parseComfyWorkflow', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('drops entries that do not match the strict union, keeps valid ones', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
it('rejects invalid config shape', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: {
|
||||
inputs: [
|
||||
[1, 'prompt'],
|
||||
[2, 'seed', 'invalid-third-element'],
|
||||
[3, 'cfg', { height: 100 }],
|
||||
[4, 'steps', 240],
|
||||
[5, 'sampler', { height: 80 }, 'future-field'],
|
||||
[6]
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
linearData: { inputs: [[1, 'prompt', 'invalid']], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.extra!.linearData!.inputs).toEqual([
|
||||
[1, 'prompt'],
|
||||
[3, 'cfg', { height: 100 }]
|
||||
])
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('extra.linearData.inputs')
|
||||
)
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('loads the workflow even when every linearData.inputs entry is invalid', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1], 'garbage', null], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.extra!.linearData!.inputs).toEqual([])
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('does not warn when every entry is valid', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1, 'prompt']], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
warn.mockRestore()
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -282,44 +282,6 @@ const zConfig = z
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const zLinearInputConfig = z
|
||||
.object({ height: z.number().optional() })
|
||||
.passthrough()
|
||||
|
||||
/** Canonical `linearData.inputs` entry shape. Strict — no tolerance here. */
|
||||
const zLinearInput = z.union([
|
||||
z.tuple([zNodeId, z.string(), zLinearInputConfig]),
|
||||
z.tuple([zNodeId, z.string()])
|
||||
])
|
||||
|
||||
/**
|
||||
* Array combinator that drops entries failing the item schema instead of
|
||||
* rejecting the whole array. Item schema stays strict (spec); tolerance
|
||||
* lives here at the container so the same policy applies uniformly to any
|
||||
* `extra.*` array field without per-field consumer logic.
|
||||
*
|
||||
* Reserved for non-essential metadata in `extra.*`. Execution-critical
|
||||
* arrays (`nodes`, `links`, `widget_values`) must use `z.array` directly so
|
||||
* malformed entries fail loudly.
|
||||
*/
|
||||
function tolerantArray<T extends z.ZodTypeAny>(itemSchema: T, label: string) {
|
||||
return z.array(z.unknown()).transform((items): z.infer<T>[] => {
|
||||
const valid: z.infer<T>[] = []
|
||||
let dropped = 0
|
||||
for (const item of items) {
|
||||
const result = itemSchema.safeParse(item)
|
||||
if (result.success) valid.push(result.data)
|
||||
else dropped++
|
||||
}
|
||||
if (dropped > 0) {
|
||||
console.warn(
|
||||
`[workflowSchema] dropped ${dropped} invalid entr${dropped === 1 ? 'y' : 'ies'} in ${label}`
|
||||
)
|
||||
}
|
||||
return valid
|
||||
})
|
||||
}
|
||||
|
||||
const zExtra = z
|
||||
.object({
|
||||
ds: zDS.optional(),
|
||||
@@ -332,10 +294,18 @@ const zExtra = z
|
||||
linearMode: z.boolean().optional(),
|
||||
linearData: z
|
||||
.object({
|
||||
inputs: tolerantArray(
|
||||
zLinearInput,
|
||||
'extra.linearData.inputs'
|
||||
).optional(),
|
||||
inputs: z
|
||||
.array(
|
||||
z.union([
|
||||
z.tuple([
|
||||
zNodeId,
|
||||
z.string(),
|
||||
z.object({ height: z.number().optional() }).passthrough()
|
||||
]),
|
||||
z.tuple([zNodeId, z.string()])
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
outputs: z.array(zNodeId).optional()
|
||||
})
|
||||
.optional()
|
||||
|
||||
Reference in New Issue
Block a user