n8n/packages/cli/test/integration/execution.service.integration.test.ts

1147 lines
34 KiB
TypeScript

import {
createTeamProject,
createWorkflow,
linkUserToProject,
testDb,
} from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import type { ExecutionSummaries, User } from '@n8n/db';
import { ExecutionMetadataRepository, ExecutionRepository, WorkflowRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { ExecutionService } from '@/executions/execution.service';
import { annotateExecution, createAnnotationTags, createExecution } from './shared/db/executions';
import { createMember, createOwner } from './shared/db/users';
describe('ExecutionService', () => {
let executionService: ExecutionService;
let executionRepository: ExecutionRepository;
let member: User;
let owner: User;
const globalConfig = Container.get(GlobalConfig);
beforeAll(async () => {
await testDb.init();
executionRepository = Container.get(ExecutionRepository);
executionService = new ExecutionService(
globalConfig,
mock(),
mock(),
mock(),
mock(),
executionRepository,
mock(),
mock(),
Container.get(WorkflowRepository),
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
);
owner = await createOwner();
member = await createMember();
});
beforeEach(() => {
globalConfig.executions.concurrency.productionLimit = -1;
globalConfig.executions.mode = 'regular';
});
afterEach(async () => {
await testDb.truncate(['ExecutionEntity']);
});
afterAll(async () => {
await testDb.terminate();
});
describe('findRangeWithCount', () => {
test('should return execution summaries', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
const summaryShape = {
id: expect.any(String),
workflowId: expect.any(String),
mode: expect.any(String),
retryOf: null,
status: expect.any(String),
createdAt: expect.any(String),
startedAt: expect.any(String),
stoppedAt: expect.any(String),
waitTill: null,
retrySuccessId: null,
workflowName: expect.any(String),
annotation: {
tags: expect.arrayContaining([]),
vote: null,
},
};
expect(output.count).toBe(2);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([summaryShape, summaryShape]);
});
test('should limit executions', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 2 },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(3);
expect(output.estimated).toBe(false);
expect(output.results).toHaveLength(2);
});
test('should retrieve executions before `lastId`, excluding it', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const [firstId, secondId] = await executionRepository.getAllIds();
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20, lastId: secondId },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(4);
expect(output.estimated).toBe(false);
expect(output.results).toEqual(
expect.arrayContaining([expect.objectContaining({ id: firstId })]),
);
});
test('should retrieve executions after `firstId`, excluding it', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const [firstId, secondId, thirdId, fourthId] = await executionRepository.getAllIds();
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20, firstId },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(4);
expect(output.estimated).toBe(false);
expect(output.results).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: fourthId }),
expect.objectContaining({ id: thirdId }),
expect.objectContaining({ id: secondId }),
]),
);
});
test('should filter executions by `status`', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'waiting' }, workflow),
createExecution({ status: 'waiting' }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(2);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
expect.objectContaining({ status: 'success' }),
expect.objectContaining({ status: 'success' }),
]);
});
test('should filter executions by `workflowId`', async () => {
const firstWorkflow = await createWorkflow({}, owner);
const secondWorkflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, firstWorkflow),
createExecution({ status: 'success' }, secondWorkflow),
createExecution({ status: 'success' }, secondWorkflow),
createExecution({ status: 'success' }, secondWorkflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
workflowId: firstWorkflow.id,
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual(
expect.arrayContaining([expect.objectContaining({ workflowId: firstWorkflow.id })]),
);
});
test('should filter executions by `startedBefore`', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ startedAt: new Date('2020-06-01') }, workflow),
createExecution({ startedAt: new Date('2020-12-31') }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
startedBefore: '2020-07-01',
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
expect.objectContaining({ startedAt: '2020-06-01T00:00:00.000Z' }),
]);
});
test('should filter executions by `startedAfter`', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ startedAt: new Date('2020-06-01') }, workflow),
createExecution({ startedAt: new Date('2020-12-31') }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
startedAfter: '2020-07-01',
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
expect.objectContaining({ startedAt: '2020-12-31T00:00:00.000Z' }),
]);
});
test('should filter executions by `metadata` with an exact match by default', async () => {
const workflow = await createWorkflow({}, owner);
const key = 'myKey';
const value = 'myValue';
await Promise.all([
createExecution({ status: 'success', metadata: [{ key, value }] }, workflow),
createExecution({ status: 'error', metadata: [{ key, value: `${value}2` }] }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
metadata: [{ key, value, exactMatch: true }],
};
const output = await executionService.findRangeWithCount(query);
expect(output).toEqual({
count: 1,
estimated: false,
results: [expect.objectContaining({ status: 'success' })],
});
});
test('should filter executions by `metadata` with a partial match', async () => {
const workflow = await createWorkflow({}, owner);
const key = 'myKey';
await Promise.all([
createExecution({ status: 'success', metadata: [{ key, value: 'myValue' }] }, workflow),
createExecution({ status: 'error', metadata: [{ key, value: 'var' }] }, workflow),
createExecution({ status: 'success', metadata: [{ key, value: 'evaluation' }] }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
metadata: [{ key, value: 'val', exactMatch: false }],
};
const output = await executionService.findRangeWithCount(query);
expect(output).toEqual({
count: 2,
estimated: false,
results: [
expect.objectContaining({ status: 'success' }),
expect.objectContaining({ status: 'success' }),
],
});
});
test('should filter executions by `projectId`', async () => {
const firstProject = await createTeamProject();
const secondProject = await createTeamProject();
const firstWorkflow = await createWorkflow(undefined, firstProject);
const secondWorkflow = await createWorkflow(undefined, secondProject);
await createExecution({ status: 'success' }, firstWorkflow);
await createExecution({ status: 'success' }, firstWorkflow);
await createExecution({ status: 'success' }, secondWorkflow); // to filter out
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
projectId: firstProject.id,
};
const output = await executionService.findRangeWithCount(query);
expect(output).toEqual({
count: 2,
estimated: false,
results: expect.arrayContaining([
expect.objectContaining({ workflowId: firstWorkflow.id }),
expect.objectContaining({ workflowId: firstWorkflow.id }),
// execution for workflow in second project was filtered out
]),
});
});
test('should filter executions by `projectId` and expected `status`', async () => {
const firstProject = await createTeamProject();
const secondProject = await createTeamProject();
const firstWorkflow = await createWorkflow(undefined, firstProject);
const secondWorkflow = await createWorkflow(undefined, secondProject);
await createExecution({ status: 'success' }, firstWorkflow);
await createExecution({ status: 'error' }, firstWorkflow);
await createExecution({ status: 'success' }, secondWorkflow);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
projectId: firstProject.id,
status: ['error'],
};
const output = await executionService.findRangeWithCount(query);
expect(output).toEqual({
count: 1,
estimated: false,
results: expect.arrayContaining([
expect.objectContaining({ workflowId: firstWorkflow.id, status: 'error' }),
]),
});
});
test.each([
{
name: 'waitTill',
filter: { waitTill: true },
matchingParams: { waitTill: new Date() },
nonMatchingParams: { waitTill: undefined },
},
{
name: 'metadata',
filter: { metadata: [{ key: 'testKey', value: 'testValue' }] },
matchingParams: { metadata: [{ key: 'testKey', value: 'testValue' }] },
nonMatchingParams: { metadata: [{ key: 'otherKey', value: 'otherValue' }] },
},
{
name: 'startedAfter',
filter: { startedAfter: '2023-01-01' },
matchingParams: { startedAt: new Date('2023-06-01') },
nonMatchingParams: { startedAt: new Date('2022-01-01') },
},
{
name: 'startedBefore',
filter: { startedBefore: '2023-12-31' },
matchingParams: { startedAt: new Date('2023-06-01') },
nonMatchingParams: { startedAt: new Date('2024-01-01') },
},
])(
'should filter executions by `projectId` and expected `$name`',
async ({ filter, matchingParams, nonMatchingParams }) => {
const firstProject = await createTeamProject();
const secondProject = await createTeamProject();
const firstWorkflow = await createWorkflow(undefined, firstProject);
const secondWorkflow = await createWorkflow(undefined, secondProject);
await Promise.all([
createExecution(matchingParams, firstWorkflow),
createExecution(nonMatchingParams, secondWorkflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
projectId: firstProject.id,
...filter,
};
const output = await executionService.findRangeWithCount(query);
expect(output).toEqual({
count: 1,
estimated: false,
results: expect.arrayContaining([
expect.objectContaining({ workflowId: firstWorkflow.id }),
]),
});
},
);
test('should exclude executions by inaccessible `workflowId`', async () => {
const accessibleWorkflow = await createWorkflow({}, member);
const inaccessibleWorkflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, accessibleWorkflow),
createExecution({ status: 'success' }, inaccessibleWorkflow),
createExecution({ status: 'success' }, inaccessibleWorkflow),
createExecution({ status: 'success' }, inaccessibleWorkflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
workflowId: inaccessibleWorkflow.id,
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner'],
projectRoles: ['project:personalOwner'],
},
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(0);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([]);
});
test('should support advanced filters', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([createExecution({}, workflow), createExecution({}, workflow)]);
const [firstId, secondId] = await executionRepository.getAllIds();
const executionMetadataRepository = Container.get(ExecutionMetadataRepository);
await executionMetadataRepository.save({
key: 'key1',
value: 'value1',
execution: { id: firstId },
});
await executionMetadataRepository.save({
key: 'key2',
value: 'value2',
execution: { id: secondId },
});
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
metadata: [{ key: 'key1', value: 'value1' }],
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([expect.objectContaining({ id: firstId })]);
});
});
describe('findRangeWithCount — subquery approach', () => {
test('should scope results to user accessible workflows', async () => {
const workflow1 = await createWorkflow({}, member);
const workflow2 = await createWorkflow({}, member);
const inaccessibleWorkflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, workflow1),
createExecution({ status: 'success' }, workflow1),
createExecution({ status: 'error' }, workflow2),
createExecution({ status: 'success' }, inaccessibleWorkflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner'],
projectRoles: ['project:personalOwner'],
},
};
const result = await executionService.findRangeWithCount(query);
// member owns workflow1 and workflow2 → sees 3 executions, not the inaccessible one
expect(result.count).toBe(3);
const workflowIds = result.results.map((r) => r.workflowId);
expect(workflowIds).toContain(workflow1.id);
expect(workflowIds).toContain(workflow2.id);
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
});
test('should filter by status correctly', async () => {
const workflow = await createWorkflow({}, member);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'error' }, workflow),
]);
const arrayQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
status: ['success'],
user: owner,
};
const subqueryQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
status: ['success'],
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner'],
projectRoles: ['project:personalOwner'],
},
};
const [arrayResult, subqueryResult] = await Promise.all([
executionService.findRangeWithCount(arrayQuery),
executionService.findRangeWithCount(subqueryQuery),
]);
expect(arrayResult.count).toBe(2);
expect(subqueryResult.count).toBe(2);
expect(subqueryResult.results.map((r) => r.id)).toEqual(arrayResult.results.map((r) => r.id));
});
test('should filter by workflowId correctly', async () => {
const workflow1 = await createWorkflow({}, member);
const workflow2 = await createWorkflow({}, member);
await Promise.all([
createExecution({ status: 'success' }, workflow1),
createExecution({ status: 'success' }, workflow2),
createExecution({ status: 'success' }, workflow2),
]);
const arrayQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
workflowId: workflow1.id,
user: owner,
};
const subqueryQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
workflowId: workflow1.id,
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner'],
projectRoles: ['project:personalOwner'],
},
};
const [arrayResult, subqueryResult] = await Promise.all([
executionService.findRangeWithCount(arrayQuery),
executionService.findRangeWithCount(subqueryQuery),
]);
expect(arrayResult.count).toBe(1);
expect(subqueryResult.count).toBe(1);
expect(subqueryResult.results[0].workflowId).toBe(workflow1.id);
});
test('should work with team project', async () => {
const teamProject = await createTeamProject();
const personalWorkflow = await createWorkflow({}, owner);
const teamWorkflow = await createWorkflow({}, teamProject);
await Promise.all([
createExecution({ status: 'success' }, personalWorkflow),
createExecution({ status: 'success' }, teamWorkflow),
]);
const arrayQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const arrayResult = await executionService.findRangeWithCount(arrayQuery);
expect(arrayResult.count).toBe(2);
});
test('should work with sharing-enabled roles (team project admin)', async () => {
// Simulates the isSharingEnabled() === true path in the controller
const teamProject = await createTeamProject(undefined, member);
const personalWorkflow = await createWorkflow({}, member);
const teamWorkflow = await createWorkflow({}, teamProject);
const inaccessibleWorkflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, personalWorkflow),
createExecution({ status: 'success' }, teamWorkflow),
createExecution({ status: 'error' }, teamWorkflow),
createExecution({ status: 'success' }, inaccessibleWorkflow),
]);
// Sharing-enabled roles: member can see workflows they own OR are admin/editor of
const sharingEnabledQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner', 'workflow:editor'],
projectRoles: ['project:personalOwner', 'project:admin', 'project:editor'],
},
};
const result = await executionService.findRangeWithCount(sharingEnabledQuery);
// member owns personalWorkflow and is admin of teamProject → sees 3 executions
expect(result.count).toBe(3);
const workflowIds = result.results.map((r) => r.workflowId);
expect(workflowIds).toContain(personalWorkflow.id);
expect(workflowIds).toContain(teamWorkflow.id);
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
});
test('should work with sharing-enabled roles (team project editor)', async () => {
// member is linked as project:editor to a team project they didn't create
const teamProject = await createTeamProject();
await linkUserToProject(member, teamProject, 'project:editor');
const teamWorkflow = await createWorkflow({}, teamProject);
const personalWorkflow = await createWorkflow({}, member);
const inaccessibleWorkflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'success' }, teamWorkflow),
createExecution({ status: 'success' }, personalWorkflow),
createExecution({ status: 'success' }, inaccessibleWorkflow),
]);
const sharingEnabledQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner', 'workflow:editor'],
projectRoles: ['project:personalOwner', 'project:admin', 'project:editor'],
},
};
const result = await executionService.findRangeWithCount(sharingEnabledQuery);
// member owns personalWorkflow and is editor in teamProject → sees 2 executions
expect(result.count).toBe(2);
const workflowIds = result.results.map((r) => r.workflowId);
expect(workflowIds).toContain(teamWorkflow.id);
expect(workflowIds).toContain(personalWorkflow.id);
expect(workflowIds).not.toContain(inaccessibleWorkflow.id);
});
});
describe('findLatestCurrentAndCompleted — subquery approach', () => {
test('should return same results as array approach', async () => {
const workflow = await createWorkflow({}, member);
await Promise.all([
createExecution({ status: 'running', stoppedAt: undefined }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'error' }, workflow),
]);
const arrayQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const subqueryQuery: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: member,
sharingOptions: {
workflowRoles: ['workflow:owner'],
projectRoles: ['project:personalOwner'],
},
};
const [arrayResult, subqueryResult] = await Promise.all([
executionService.findLatestCurrentAndCompleted(arrayQuery),
executionService.findLatestCurrentAndCompleted(subqueryQuery),
]);
expect(arrayResult.count).toBe(subqueryResult.count);
expect(arrayResult.results).toHaveLength(subqueryResult.results.length);
const arrayIds = arrayResult.results.map((r) => r.id).sort();
const subqueryIds = subqueryResult.results.map((r) => r.id).sort();
expect(subqueryIds).toEqual(arrayIds);
});
});
describe('getConcurrentExecutionsCount', () => {
test('should return concurrentExecutionsCount when concurrency is enabled', async () => {
globalConfig.executions.concurrency.productionLimit = 4;
const workflow = await createWorkflow({}, owner);
const concurrentExecutionsData = await Promise.all([
createExecution({ status: 'running', mode: 'webhook' }, workflow),
createExecution({ status: 'running', mode: 'trigger' }, workflow),
]);
await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'crashed' }, workflow),
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'running', mode: 'manual' }, workflow),
]);
const output = await executionService.getConcurrentExecutionsCount();
expect(output).toEqual(concurrentExecutionsData.length);
});
test('should set concurrentExecutionsCount to -1 when concurrency is disabled', async () => {
globalConfig.executions.concurrency.productionLimit = -1;
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'running', mode: 'webhook' }, workflow),
createExecution({ status: 'running', mode: 'trigger' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'crashed' }, workflow),
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'running', mode: 'manual' }, workflow),
]);
const output = await executionService.getConcurrentExecutionsCount();
expect(output).toEqual(-1);
});
test('should set concurrentExecutionsCount to -1 in queue mode', async () => {
globalConfig.executions.mode = 'queue';
globalConfig.executions.concurrency.productionLimit = 4;
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'running', mode: 'webhook' }, workflow),
createExecution({ status: 'running', mode: 'trigger' }, workflow),
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'crashed' }, workflow),
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'running', mode: 'manual' }, workflow),
]);
const output = await executionService.getConcurrentExecutionsCount();
expect(output).toEqual(-1);
});
});
describe('findLatestCurrentAndCompleted', () => {
test('should return latest current and completed executions', async () => {
const workflow = await createWorkflow({}, owner);
const totalCompleted = 21;
await Promise.all([
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'running' }, workflow),
...new Array(totalCompleted)
.fill(null)
.map(async () => await createExecution({ status: 'success' }, workflow)),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const output = await executionService.findLatestCurrentAndCompleted(query);
expect(output.results).toHaveLength(23); // 3 current + 20 completed (excludes 21st)
expect(output.count).toBe(totalCompleted); // 21 finished, excludes current
expect(output.estimated).toBe(false);
});
test('should handle zero current executions', async () => {
const workflow = await createWorkflow({}, owner);
const totalFinished = 5;
await Promise.all(
new Array(totalFinished)
.fill(null)
.map(async () => await createExecution({ status: 'success' }, workflow)),
);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const output = await executionService.findLatestCurrentAndCompleted(query);
expect(output.results).toHaveLength(totalFinished); // 5 finished
expect(output.count).toBe(totalFinished); // 5 finished, excludes active
expect(output.estimated).toBe(false);
});
test('should handle zero completed executions', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'running' }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const output = await executionService.findLatestCurrentAndCompleted(query);
expect(output.results).toHaveLength(3); // 3 finished
expect(output.count).toBe(0); // 0 finished, excludes active
expect(output.estimated).toBe(false);
});
test('should handle zero executions', async () => {
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 20 },
user: owner,
};
const output = await executionService.findLatestCurrentAndCompleted(query);
expect(output.results).toHaveLength(0);
expect(output.count).toBe(0);
expect(output.estimated).toBe(false);
});
test('should prioritize `running` over `new` executions', async () => {
const workflow = await createWorkflow({}, owner);
await Promise.all([
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'running' }, workflow),
createExecution({ status: 'new' }, workflow),
createExecution({ status: 'new' }, workflow),
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
range: { limit: 2 },
user: owner,
};
const { results } = await executionService.findLatestCurrentAndCompleted(query);
expect(results).toHaveLength(2);
expect(results[0].status).toBe('running');
expect(results[1].status).toBe('running');
});
});
describe('annotation', () => {
const summaryShape = {
id: expect.any(String),
workflowId: expect.any(String),
mode: expect.any(String),
retryOf: null,
status: expect.any(String),
createdAt: expect.any(String),
startedAt: expect.any(String),
stoppedAt: expect.any(String),
waitTill: null,
retrySuccessId: null,
workflowName: expect.any(String),
};
afterEach(async () => {
await testDb.truncate(['AnnotationTagEntity', 'ExecutionAnnotation']);
});
test('should add and retrieve annotation', async () => {
const workflow = await createWorkflow({}, owner);
const execution1 = await createExecution({ status: 'success' }, workflow);
const execution2 = await createExecution({ status: 'success' }, workflow);
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
await annotateExecution(
execution1.id,
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
[workflow.id],
);
await annotateExecution(execution2.id, { vote: 'down', tags: [annotationTags[2].id] }, [
workflow.id,
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(2);
expect(output.estimated).toBe(false);
expect(output.results).toEqual(
expect.arrayContaining([
{
...summaryShape,
annotation: {
tags: [expect.objectContaining({ name: 'tag3' })],
vote: 'down',
},
},
{
...summaryShape,
annotation: {
tags: expect.arrayContaining([
expect.objectContaining({ name: 'tag1' }),
expect.objectContaining({ name: 'tag2' }),
]),
vote: 'up',
},
},
]),
);
});
test('should update annotation', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecution({ status: 'success' }, workflow);
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
await annotateExecution(execution.id, { vote: 'up', tags: [annotationTags[0].id] }, [
workflow.id,
]);
await annotateExecution(execution.id, { vote: 'down', tags: [annotationTags[1].id] }, [
workflow.id,
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
{
...summaryShape,
annotation: {
tags: [expect.objectContaining({ name: 'tag2' })],
vote: 'down',
},
},
]);
});
test('should filter by annotation tags', async () => {
const workflow = await createWorkflow({}, owner);
const executions = await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
await annotateExecution(
executions[0].id,
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
[workflow.id],
);
await annotateExecution(executions[1].id, { vote: 'down', tags: [annotationTags[2].id] }, [
workflow.id,
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
annotationTags: [annotationTags[0].id],
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
{
...summaryShape,
annotation: {
tags: expect.arrayContaining([
expect.objectContaining({ name: 'tag1' }),
expect.objectContaining({ name: 'tag2' }),
]),
vote: 'up',
},
},
]);
});
test('should filter by annotation vote', async () => {
const workflow = await createWorkflow({}, owner);
const executions = await Promise.all([
createExecution({ status: 'success' }, workflow),
createExecution({ status: 'success' }, workflow),
]);
const annotationTags = await createAnnotationTags(['tag1', 'tag2', 'tag3']);
await annotateExecution(
executions[0].id,
{ vote: 'up', tags: [annotationTags[0].id, annotationTags[1].id] },
[workflow.id],
);
await annotateExecution(executions[1].id, { vote: 'down', tags: [annotationTags[2].id] }, [
workflow.id,
]);
const query: ExecutionSummaries.RangeQuery = {
kind: 'range',
status: ['success'],
range: { limit: 20 },
user: owner,
vote: 'up',
};
const output = await executionService.findRangeWithCount(query);
expect(output.count).toBe(1);
expect(output.estimated).toBe(false);
expect(output.results).toEqual([
{
...summaryShape,
annotation: {
tags: expect.arrayContaining([
expect.objectContaining({ name: 'tag1' }),
expect.objectContaining({ name: 'tag2' }),
]),
vote: 'up',
},
},
]);
});
});
});