Compare commits

...
Author SHA1 Message Date
sonarly-bot 9be0a279c6 fix(workflow): handle IS_NOT_NULL for date step filters
https://sonarly.com/issue/41881?type=bug

A workflow if/else step crashed because a DATE/DATE_TIME filter used operand

Fix: Implemented the root-cause fix in the workflow date filter evaluator by handling `ViewFilterOperand.IS_NOT_NULL` in `evaluateDateFilter()` with the same non-empty semantics as `IS_NOT_EMPTY` for date values. This prevents runtime throws (`Operand IS_NOT_NULL not supported for date filter`) when legacy/stored workflow filters use that operand.

Also added a regression test in the existing filter evaluator spec to validate DATE behavior for `IS_NOT_NULL`:
- non-null date value returns `true`
- null value returns `false`

Authored by Sonarly by autonomous analysis (run 47785).
2026-06-01 10:29:21 +00:00
4 changed files with 95 additions and 0 deletions
@@ -1042,6 +1042,27 @@ describe('evaluateFilterConditions', () => {
true,
);
});
it('should handle date IsNotNull operand', () => {
const notNullFilter = createFilter(
ViewFilterOperand.IS_NOT_NULL,
now,
null,
'DATE',
);
const nullFilter = createFilter(
ViewFilterOperand.IS_NOT_NULL,
null,
null,
'DATE',
);
expect(evaluateFilterConditions({ filters: [notNullFilter] })).toBe(
true,
);
expect(evaluateFilterConditions({ filters: [nullFilter] })).toBe(false);
});
});
describe('currency operands', () => {
@@ -248,6 +248,7 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean {
case ViewFilterOperand.IS_EMPTY:
return !isDefined(filter.leftOperand) || filter.leftOperand === '';
case ViewFilterOperand.IS_NOT_NULL:
case ViewFilterOperand.IS_NOT_EMPTY:
return isDefined(filter.leftOperand) && filter.leftOperand !== '';
@@ -13,6 +13,10 @@ import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-res
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/should-execute-step.util';
import { shouldFailSafely } from 'src/modules/workflow/workflow-executor/utils/should-fail-safely.util';
@@ -705,6 +709,70 @@ describe('WorkflowExecutorWorkspaceService', () => {
});
});
describe('executeStep', () => {
const mockWorkflowRunId = 'workflow-run-id';
const mockWorkspaceId = 'workspace-id';
it('should capture system errors with workflow context', async () => {
const systemError = new Error('system failure');
(workflowActionFactory.get as jest.Mock).mockReturnValue({
execute: jest.fn().mockRejectedValue(systemError),
});
await service['executeStep']({
step: {
id: 'step-1',
type: WorkflowActionType.IF_ELSE,
settings: {},
nextStepIds: [],
} as unknown as WorkflowAction,
steps: [],
stepInfos: {},
workflowRunId: mockWorkflowRunId,
workspaceId: mockWorkspaceId,
});
expect(mockExceptionHandlerService.captureExceptions).toHaveBeenCalledWith(
[systemError],
{
workspace: { id: mockWorkspaceId },
additionalData: {
workflowRunId: mockWorkflowRunId,
stepId: 'step-1',
stepType: WorkflowActionType.IF_ELSE,
},
},
);
});
it('should not capture user errors', async () => {
const userError = new WorkflowStepExecutorException(
'Invalid input',
WorkflowStepExecutorExceptionCode.INVALID_STEP_INPUT,
);
(workflowActionFactory.get as jest.Mock).mockReturnValue({
execute: jest.fn().mockRejectedValue(userError),
});
await service['executeStep']({
step: {
id: 'step-1',
type: WorkflowActionType.IF_ELSE,
settings: {},
nextStepIds: [],
} as unknown as WorkflowAction,
steps: [],
stepInfos: {},
workflowRunId: mockWorkflowRunId,
workspaceId: mockWorkspaceId,
});
expect(mockExceptionHandlerService.captureExceptions).not.toHaveBeenCalled();
});
});
describe('sendWorkflowNodeRunEvent', () => {
it('should emit a billing event', async () => {
await service['sendWorkflowNodeRunEvent']('workspace-id', 'workflow-id');
@@ -517,6 +517,11 @@ export class WorkflowExecutorWorkspaceService {
if (!isUserError) {
this.exceptionHandlerService.captureExceptions([error], {
workspace: { id: workspaceId },
additionalData: {
workflowRunId,
stepId,
stepType: step.type,
},
});
await this.metricsService.incrementCounterForEvent({