Compare commits

...
Author SHA1 Message Date
sonarly-bot ceb07b709b fix(workflow): handle IS_NOT_NULL in filter evaluator
https://sonarly.com/issue/40432?type=bug

A workflow run crashed in the filter action because the runtime evaluator throws on operand

Fix: I fixed the root cause by adding support for `ViewFilterOperand.IS_NOT_NULL` in the default filter evaluator:

- Updated `evaluateDefaultFilter` so `IS_NOT_NULL` falls through to the same behavior as `IS_NOT_EMPTY` (`isNotEmptyTextOrArray(leftValue)`).
- This prevents runtime throws like `Operand IS_NOT_NULL not supported for string filter type` when default/string-like filters resolve through the fallback evaluator.
- Added a regression test in the filter evaluator spec for unknown/default filter type:
  - non-empty left operand + `IS_NOT_NULL` => `true`
  - null/empty left operand + `IS_NOT_NULL` => `false`

This keeps backward compatibility with operand conversion (`isNotNull` -> `IS_NOT_NULL`) and matches existing operand semantics used across other filter evaluators.

Authored by Sonarly by autonomous analysis (run 46094).
2026-05-25 03:38:47 +00:00
4 changed files with 66 additions and 4 deletions
@@ -47,10 +47,21 @@ export class FilterWorkflowAction implements WorkflowAction {
leftOperand: resolveInput(filter.stepOutputKey, context),
}));
const matchesFilter = evaluateFilterConditions({
filterGroups: stepFilterGroups,
filters: resolvedFilters,
});
let matchesFilter: boolean;
try {
matchesFilter = evaluateFilterConditions({
filterGroups: stepFilterGroups,
filters: resolvedFilters,
});
} catch (error) {
throw new WorkflowStepExecutorException(
error instanceof Error
? error.message
: 'Filter action contains invalid filter configuration',
WorkflowStepExecutorExceptionCode.INVALID_STEP_INPUT,
);
}
return {
result: {
@@ -1392,6 +1392,24 @@ describe('evaluateFilterConditions', () => {
expect(evaluateFilterConditions({ filters: [filter5] })).toBe(false);
});
it('should handle IsNotNull operand with unknown type', () => {
const filter1 = createFilter(
ViewFilterOperand.IS_NOT_NULL,
'not empty',
'',
'unknown',
);
const filter2 = createFilter(
ViewFilterOperand.IS_NOT_NULL,
null,
'',
'unknown',
);
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
});
it('should handle GreaterThanOrEqual operand with unknown type', () => {
const filter1 = createFilter(
ViewFilterOperand.GREATER_THAN_OR_EQUAL,
@@ -424,6 +424,7 @@ function evaluateDefaultFilter(filter: ResolvedFilter): boolean {
return leftValue != rightValue;
case ViewFilterOperand.IS_EMPTY:
return !isNotEmptyTextOrArray(leftValue);
case ViewFilterOperand.IS_NOT_NULL:
case ViewFilterOperand.IS_NOT_EMPTY:
return isNotEmptyTextOrArray(leftValue);
case ViewFilterOperand.CONTAINS:
@@ -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';
@@ -314,6 +318,34 @@ describe('WorkflowExecutorWorkspaceService', () => {
workflowRunId: mockWorkflowRunId,
workspaceId: 'workspace-id',
});
expect(mockExceptionHandlerService.captureExceptions).toHaveBeenCalledWith(
[expect.any(Error)],
{
workspace: { id: mockWorkspaceId },
},
);
expect(mockMetricsService.incrementCounterForEvent).toHaveBeenCalledTimes(
1,
);
});
it('should not capture user-facing step input errors', async () => {
mockWorkflowExecutor.execute.mockRejectedValueOnce(
new WorkflowStepExecutorException(
'Invalid step input',
WorkflowStepExecutorExceptionCode.INVALID_STEP_INPUT,
),
);
await service.executeFromSteps({
workflowRunId: mockWorkflowRunId,
stepIds: ['step-1'],
workspaceId: mockWorkspaceId,
});
expect(mockExceptionHandlerService.captureExceptions).not.toHaveBeenCalled();
expect(mockMetricsService.incrementCounterForEvent).not.toHaveBeenCalled();
});
it('should handle pending events', async () => {