From 7e044fc6026d0b5cabb1c7e14946299922c2edc0 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 14 Apr 2026 11:29:21 +0800 Subject: [PATCH] sanitize assigner collaboration payloads Collaboration restores some empty assigner variable selectors as null, which later reaches the assigner panel and crashes variable-selector reads that expect arrays. Normalize assigner operation items when converting versioned payloads and when applying operation list updates so variable-mode selectors always stay arrays. Add targeted tests for both the helper path and useConfig exposure. --- .../__tests__/use-config.helpers.spec.ts | 16 +++++++ .../assigner/__tests__/use-config.spec.tsx | 20 +++++++++ .../nodes/assigner/use-config.helpers.ts | 3 +- .../workflow/nodes/assigner/utils.ts | 43 ++++++++++++++++--- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/web/app/components/workflow/nodes/assigner/__tests__/use-config.helpers.spec.ts b/web/app/components/workflow/nodes/assigner/__tests__/use-config.helpers.spec.ts index 427fcd32c8..1a2beeff5b 100644 --- a/web/app/components/workflow/nodes/assigner/__tests__/use-config.helpers.spec.ts +++ b/web/app/components/workflow/nodes/assigner/__tests__/use-config.helpers.spec.ts @@ -65,4 +65,20 @@ describe('assigner use-config helpers', () => { expect(updateOperationItems(legacyInputs, nextItems).items).toEqual(nextItems) expect(legacyInputs.items).toHaveLength(1) }) + + it('sanitizes variable-selector items restored from collaboration payloads', () => { + const dirtyItems = [{ + variable_selector: null as unknown as AssignerNodeType['items'][number]['variable_selector'], + input_type: AssignerNodeInputType.variable, + operation: WriteMode.overwrite, + value: null, + }] + + expect(updateOperationItems(createInputs('2'), dirtyItems).items).toEqual([{ + variable_selector: [], + input_type: AssignerNodeInputType.variable, + operation: WriteMode.overwrite, + value: [], + }]) + }) }) diff --git a/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx index 60bd1dd0f7..b24473bbd9 100644 --- a/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx @@ -95,4 +95,24 @@ describe('useConfig', () => { expect(result.current.filterToAssignedVar({ type: VarType.string } as never, VarType.arrayString, WriteMode.append)).toBe(true) expect(result.current.filterToAssignedVar({ type: VarType.number } as never, VarType.arrayString, WriteMode.append)).toBe(false) }) + + it('should normalize collaboration-restored null selectors before exposing inputs', () => { + const dirtyPayload = createPayload({ + version: '2', + items: [createOperation({ + variable_selector: null as unknown as AssignerNodeOperation['variable_selector'], + input_type: AssignerNodeInputType.variable, + value: null, + })], + }) + + const { result } = renderHook(() => useConfig('assigner-node', dirtyPayload)) + + expect(result.current.inputs.items).toEqual([expect.objectContaining({ + variable_selector: [], + input_type: AssignerNodeInputType.variable, + operation: WriteMode.overwrite, + value: [], + })]) + }) }) diff --git a/web/app/components/workflow/nodes/assigner/use-config.helpers.ts b/web/app/components/workflow/nodes/assigner/use-config.helpers.ts index 1bd3ac6d35..ef8ad5520c 100644 --- a/web/app/components/workflow/nodes/assigner/use-config.helpers.ts +++ b/web/app/components/workflow/nodes/assigner/use-config.helpers.ts @@ -3,6 +3,7 @@ import type { AssignerNodeOperation, AssignerNodeType } from './types' import { produce } from 'immer' import { VarType } from '../../types' import { WriteMode } from './types' +import { normalizeOperationItems } from './utils' export const filterVarByType = (varType: VarType) => { return (variable: Var) => { @@ -86,5 +87,5 @@ export const updateOperationItems = ( inputs: AssignerNodeType, items: AssignerNodeOperation[], ) => produce(inputs, (draft) => { - draft.items = [...items] + draft.items = normalizeOperationItems(items) }) diff --git a/web/app/components/workflow/nodes/assigner/utils.ts b/web/app/components/workflow/nodes/assigner/utils.ts index 0702a15409..f891504258 100644 --- a/web/app/components/workflow/nodes/assigner/utils.ts +++ b/web/app/components/workflow/nodes/assigner/utils.ts @@ -1,4 +1,4 @@ -import type { AssignerNodeType } from './types' +import type { AssignerNodeOperation, AssignerNodeType } from './types' import type { I18nKeysByPrefix } from '@/types/i18n' import { AssignerNodeInputType, WriteMode } from './types' @@ -62,18 +62,49 @@ const convertOldWriteMode = (oldMode: string): WriteMode => { } } +const normalizeVariableSelector = (value: unknown) => { + return Array.isArray(value) ? value : [] +} + +export const normalizeOperationItems = (items: unknown): AssignerNodeOperation[] => { + if (!Array.isArray(items)) + return [] + + return items.map((item) => { + const operationItem = (item || {}) as Partial + const inputType = operationItem.input_type === AssignerNodeInputType.constant + ? AssignerNodeInputType.constant + : AssignerNodeInputType.variable + + return { + variable_selector: normalizeVariableSelector(operationItem.variable_selector), + input_type: inputType, + operation: Object.values(WriteMode).includes(operationItem.operation as WriteMode) + ? operationItem.operation as WriteMode + : WriteMode.overwrite, + value: inputType === AssignerNodeInputType.variable + ? normalizeVariableSelector(operationItem.value) + : operationItem.value, + } + }) +} + export const convertV1ToV2 = (payload: any): AssignerNodeType => { - if (payload.version === '2' && payload.items) - return payload as AssignerNodeType + if (payload.version === '2' && payload.items) { + return { + ...payload, + items: normalizeOperationItems(payload.items), + } as AssignerNodeType + } return { + ...payload, version: '2', - items: [{ + items: normalizeOperationItems([{ variable_selector: payload.assigned_variable_selector || [], input_type: AssignerNodeInputType.variable, operation: convertOldWriteMode(payload.write_mode), value: payload.input_variable_selector || [], - }], - ...payload, + }]), } }