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:
dante01yoon
2026-05-14 11:19:35 +09:00
parent db969dd50e
commit 9dd5bbb025
4 changed files with 46 additions and 104 deletions

View File

@@ -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 }))

View File

@@ -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
}

View File

@@ -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()
})
})

View File

@@ -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()