Files
n8n-enterprise-unlocked/packages/cli/src/modules/insights/__tests__/insights.service.test.ts

857 lines
25 KiB
TypeScript

import type { InsightsDateRange } from '@n8n/api-types';
import type { LicenseState } from '@n8n/backend-common';
import type { Project } from '@n8n/db';
import type { WorkflowEntity } from '@n8n/db';
import type { IWorkflowDb } from '@n8n/db';
import type { WorkflowExecuteAfterContext } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { DateTime } from 'luxon';
import type { IRun } from 'n8n-workflow';
import { mockLogger } from '@test/mocking';
import { createTeamProject } from '@test-integration/db/projects';
import { createWorkflow } from '@test-integration/db/workflows';
import * as testDb from '@test-integration/test-db';
import {
createCompactedInsightsEvent,
createMetadata,
createRawInsightsEvents,
} from '../database/entities/__tests__/db-utils';
import type { InsightsRaw } from '../database/entities/insights-raw';
import type { InsightsByPeriodRepository } from '../database/repositories/insights-by-period.repository';
import { InsightsCollectionService } from '../insights-collection.service';
import { InsightsCompactionService } from '../insights-compaction.service';
import type { InsightsPruningService } from '../insights-pruning.service';
import { InsightsConfig } from '../insights.config';
import { InsightsService } from '../insights.service';
// Initialize DB once for all tests
beforeAll(async () => {
await testDb.init();
});
beforeEach(async () => {
await testDb.truncate([
'InsightsRaw',
'InsightsByPeriod',
'InsightsMetadata',
'WorkflowEntity',
'Project',
]);
});
// Terminate DB once after all tests complete
afterAll(async () => {
await testDb.terminate();
});
describe('getInsightsSummary', () => {
let insightsService: InsightsService;
beforeAll(async () => {
insightsService = Container.get(InsightsService);
});
let project: Project;
let workflow: IWorkflowDb & WorkflowEntity;
beforeEach(async () => {
project = await createTeamProject();
workflow = await createWorkflow({}, project);
});
test('compacted data are summarized correctly', async () => {
// ARRANGE
// last 6 days
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ day: 2 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'failure',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
// last 12 days
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'runtime_ms',
value: 123,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
//Outside range should not be taken into account
await createCompactedInsightsEvent(workflow, {
type: 'runtime_ms',
value: 123,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 13 }),
});
// ACT
const summary = await insightsService.getInsightsSummary({ periodLengthInDays: 6 });
// ASSERT
expect(summary).toEqual({
averageRunTime: { deviation: -123, unit: 'millisecond', value: 0 },
failed: { deviation: 1, unit: 'count', value: 1 },
failureRate: { deviation: 0.333, unit: 'ratio', value: 0.333 },
timeSaved: { deviation: 0, unit: 'minute', value: 0 },
total: { deviation: 2, unit: 'count', value: 3 },
});
});
test('no data for previous period should return null deviation', async () => {
// ARRANGE
// last 7 days
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
// ACT
const summary = await insightsService.getInsightsSummary({ periodLengthInDays: 7 });
// ASSERT
expect(Object.values(summary).map((v) => v.deviation)).toEqual([null, null, null, null, null]);
});
test('mixed period data are summarized correctly', async () => {
// ARRANGE
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'hour',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ day: 1 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'failure',
value: 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 2,
periodUnit: 'hour',
periodStart: DateTime.utc().minus({ day: 10 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 3,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ day: 11 }),
});
// ACT
const summary = await insightsService.getInsightsSummary({ periodLengthInDays: 7 });
// ASSERT
expect(summary).toEqual({
averageRunTime: { deviation: 0, unit: 'millisecond', value: 0 },
failed: { deviation: 2, unit: 'count', value: 2 },
failureRate: { deviation: 0.5, unit: 'ratio', value: 0.5 },
timeSaved: { deviation: 0, unit: 'minute', value: 0 },
total: { deviation: -1, unit: 'count', value: 4 },
});
});
});
describe('getInsightsByWorkflow', () => {
let insightsService: InsightsService;
beforeAll(async () => {
insightsService = Container.get(InsightsService);
});
let project: Project;
let workflow1: IWorkflowDb & WorkflowEntity;
let workflow2: IWorkflowDb & WorkflowEntity;
let workflow3: IWorkflowDb & WorkflowEntity;
beforeEach(async () => {
project = await createTeamProject();
workflow1 = await createWorkflow({}, project);
workflow2 = await createWorkflow({}, project);
workflow3 = await createWorkflow({}, project);
});
test('compacted data are are grouped by workflow correctly', async () => {
// ARRANGE
for (const workflow of [workflow1, workflow2]) {
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: workflow === workflow1 ? 1 : 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ day: 2 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'failure',
value: 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
// last 14 days
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'runtime_ms',
value: 123,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
// Barely in range insight (should be included)
// 1 hour before 14 days ago
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'hour',
periodStart: DateTime.utc().minus({ days: 13, hours: 23 }),
});
// Out of date range insight (should not be included)
// 14 days ago
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 14 }),
});
}
// ACT
const byWorkflow = await insightsService.getInsightsByWorkflow({
maxAgeInDays: 14,
});
// ASSERT
expect(byWorkflow.count).toEqual(2);
expect(byWorkflow.data).toHaveLength(2);
// expect first workflow to be workflow 2, because it has a bigger total (default sorting)
expect(byWorkflow.data[0]).toMatchObject({
workflowId: workflow2.id,
workflowName: workflow2.name,
projectId: project.id,
projectName: project.name,
total: 7,
failed: 2,
runTime: 123,
succeeded: 5,
timeSaved: 0,
});
expect(byWorkflow.data[0].failureRate).toBeCloseTo(2 / 7);
expect(byWorkflow.data[0].averageRunTime).toBeCloseTo(123 / 7);
expect(byWorkflow.data[1]).toMatchObject({
workflowId: workflow1.id,
workflowName: workflow1.name,
projectId: project.id,
projectName: project.name,
total: 6,
failed: 2,
runTime: 123,
succeeded: 4,
timeSaved: 0,
});
expect(byWorkflow.data[1].failureRate).toBeCloseTo(2 / 6);
expect(byWorkflow.data[1].averageRunTime).toBeCloseTo(123 / 6);
});
test('compacted data are grouped by workflow correctly with sorting', async () => {
// ARRANGE
for (const workflow of [workflow1, workflow2]) {
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: workflow === workflow1 ? 1 : 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'failure',
value: 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'runtime_ms',
value: workflow === workflow1 ? 2 : 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
}
// ACT
const byWorkflow = await insightsService.getInsightsByWorkflow({
maxAgeInDays: 14,
sortBy: 'runTime:desc',
});
// ASSERT
expect(byWorkflow.count).toEqual(2);
expect(byWorkflow.data).toHaveLength(2);
expect(byWorkflow.data[0].workflowId).toEqual(workflow1.id);
});
test('compacted data are grouped by workflow correctly with pagination', async () => {
// ARRANGE
for (const workflow of [workflow1, workflow2, workflow3]) {
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: workflow === workflow1 ? 1 : workflow === workflow2 ? 2 : 3,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
}
// ACT
const byWorkflow = await insightsService.getInsightsByWorkflow({
maxAgeInDays: 14,
sortBy: 'succeeded:desc',
skip: 1,
take: 1,
});
// ASSERT
expect(byWorkflow.count).toEqual(3);
expect(byWorkflow.data).toHaveLength(1);
expect(byWorkflow.data[0].workflowId).toEqual(workflow2.id);
});
test('compacted data are grouped by workflow correctly even with 0 data (check division by 0)', async () => {
// ACT
const byWorkflow = await insightsService.getInsightsByWorkflow({
maxAgeInDays: 14,
});
// ASSERT
expect(byWorkflow.count).toEqual(0);
expect(byWorkflow.data).toHaveLength(0);
});
});
describe('getInsightsByTime', () => {
let insightsService: InsightsService;
beforeAll(async () => {
insightsService = Container.get(InsightsService);
});
let project: Project;
let workflow1: IWorkflowDb & WorkflowEntity;
let workflow2: IWorkflowDb & WorkflowEntity;
beforeEach(async () => {
project = await createTeamProject();
workflow1 = await createWorkflow({}, project);
workflow2 = await createWorkflow({}, project);
});
test('returns empty array when no insights exist', async () => {
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
expect(byTime).toEqual([]);
});
test('returns empty array when no insights in the time range exists', async () => {
await createCompactedInsightsEvent(workflow1, {
type: 'success',
value: 2,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 30 }),
});
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
expect(byTime).toEqual([]);
});
test('compacted data are are grouped by time correctly', async () => {
// ARRANGE
for (const workflow of [workflow1, workflow2]) {
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: workflow === workflow1 ? 1 : 2,
periodUnit: 'day',
periodStart: DateTime.utc(),
});
// Check that hourly data is grouped together with the previous daily data
await createCompactedInsightsEvent(workflow, {
type: 'failure',
value: 2,
periodUnit: 'hour',
periodStart: DateTime.utc(),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ day: 2 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
await createCompactedInsightsEvent(workflow, {
type: 'runtime_ms',
value: workflow === workflow1 ? 10 : 20,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 10 }),
});
// Barely in range insight (should be included)
// 1 hour before 14 days ago
await createCompactedInsightsEvent(workflow, {
type: workflow === workflow1 ? 'success' : 'failure',
value: 1,
periodUnit: 'hour',
periodStart: DateTime.utc().minus({ days: 13, hours: 23 }),
});
// Out of date range insight (should not be included)
// 14 days ago
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'day',
periodStart: DateTime.utc().minus({ days: 14 }),
});
}
// ACT
const byTime = await insightsService.getInsightsByTime({ maxAgeInDays: 14, periodUnit: 'day' });
// ASSERT
expect(byTime).toHaveLength(4);
// expect date to be sorted by oldest first
expect(byTime[0].date).toEqual(DateTime.utc().minus({ days: 14 }).startOf('day').toISO());
expect(byTime[1].date).toEqual(DateTime.utc().minus({ days: 10 }).startOf('day').toISO());
expect(byTime[2].date).toEqual(DateTime.utc().minus({ days: 2 }).startOf('day').toISO());
expect(byTime[3].date).toEqual(DateTime.utc().startOf('day').toISO());
expect(byTime[0].values).toEqual({
total: 2,
succeeded: 1,
failed: 1,
failureRate: 0.5,
averageRunTime: 0,
timeSaved: 0,
});
expect(byTime[1].values).toEqual({
total: 2,
succeeded: 2,
failed: 0,
failureRate: 0,
averageRunTime: 15,
timeSaved: 0,
});
expect(byTime[2].values).toEqual({
total: 2,
succeeded: 2,
failed: 0,
failureRate: 0,
averageRunTime: 0,
timeSaved: 0,
});
expect(byTime[3].values).toEqual({
total: 7,
succeeded: 3,
failed: 4,
failureRate: 4 / 7,
averageRunTime: 0,
timeSaved: 0,
});
});
});
describe('getAvailableDateRanges', () => {
let insightsService: InsightsService;
let licenseMock: jest.Mocked<LicenseState>;
beforeAll(() => {
licenseMock = mock<LicenseState>();
insightsService = new InsightsService(
mock<InsightsByPeriodRepository>(),
mock<InsightsCompactionService>(),
mock<InsightsCollectionService>(),
mock<InsightsPruningService>(),
licenseMock,
mockLogger(),
);
});
test('returns correct ranges when hourly data is enabled and max history is unlimited', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
const result = insightsService.getAvailableDateRanges();
expect(result).toEqual([
{ key: 'day', licensed: true, granularity: 'hour' },
{ key: 'week', licensed: true, granularity: 'day' },
{ key: '2weeks', licensed: true, granularity: 'day' },
{ key: 'month', licensed: true, granularity: 'day' },
{ key: 'quarter', licensed: true, granularity: 'week' },
{ key: '6months', licensed: true, granularity: 'week' },
{ key: 'year', licensed: true, granularity: 'week' },
]);
});
test('returns correct ranges when hourly data is enabled and max history is 365 days', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
const result = insightsService.getAvailableDateRanges();
expect(result).toEqual([
{ key: 'day', licensed: true, granularity: 'hour' },
{ key: 'week', licensed: true, granularity: 'day' },
{ key: '2weeks', licensed: true, granularity: 'day' },
{ key: 'month', licensed: true, granularity: 'day' },
{ key: 'quarter', licensed: true, granularity: 'week' },
{ key: '6months', licensed: true, granularity: 'week' },
{ key: 'year', licensed: true, granularity: 'week' },
]);
});
test('returns correct ranges when hourly data is disabled and max history is 30 days', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
const result = insightsService.getAvailableDateRanges();
expect(result).toEqual([
{ key: 'day', licensed: false, granularity: 'hour' },
{ key: 'week', licensed: true, granularity: 'day' },
{ key: '2weeks', licensed: true, granularity: 'day' },
{ key: 'month', licensed: true, granularity: 'day' },
{ key: 'quarter', licensed: false, granularity: 'week' },
{ key: '6months', licensed: false, granularity: 'week' },
{ key: 'year', licensed: false, granularity: 'week' },
]);
});
test('returns correct ranges when max history is less than 7 days', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(5);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
const result = insightsService.getAvailableDateRanges();
expect(result).toEqual([
{ key: 'day', licensed: false, granularity: 'hour' },
{ key: 'week', licensed: false, granularity: 'day' },
{ key: '2weeks', licensed: false, granularity: 'day' },
{ key: 'month', licensed: false, granularity: 'day' },
{ key: 'quarter', licensed: false, granularity: 'week' },
{ key: '6months', licensed: false, granularity: 'week' },
{ key: 'year', licensed: false, granularity: 'week' },
]);
});
test('returns correct ranges when max history is 90 days and hourly data is enabled', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
const result = insightsService.getAvailableDateRanges();
expect(result).toEqual([
{ key: 'day', licensed: true, granularity: 'hour' },
{ key: 'week', licensed: true, granularity: 'day' },
{ key: '2weeks', licensed: true, granularity: 'day' },
{ key: 'month', licensed: true, granularity: 'day' },
{ key: 'quarter', licensed: true, granularity: 'week' },
{ key: '6months', licensed: false, granularity: 'week' },
{ key: 'year', licensed: false, granularity: 'week' },
]);
});
});
describe('getMaxAgeInDaysAndGranularity', () => {
let insightsService: InsightsService;
let licenseMock: jest.Mocked<LicenseState>;
beforeAll(() => {
licenseMock = mock<LicenseState>();
insightsService = new InsightsService(
mock<InsightsByPeriodRepository>(),
mock<InsightsCompactionService>(),
mock<InsightsCollectionService>(),
mock<InsightsPruningService>(),
licenseMock,
mockLogger(),
);
});
test('returns correct maxAgeInDays and granularity for a valid licensed date range', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
const result = insightsService.getMaxAgeInDaysAndGranularity('month');
expect(result).toEqual({
key: 'month',
licensed: true,
granularity: 'day',
maxAgeInDays: 30,
});
});
test('throws an error if the date range is not available', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
expect(() => {
insightsService.getMaxAgeInDaysAndGranularity('invalidKey' as InsightsDateRange['key']);
}).toThrowError('The selected date range is not available');
});
test('throws an error if the date range is not licensed', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
expect(() => {
insightsService.getMaxAgeInDaysAndGranularity('year');
}).toThrowError('The selected date range exceeds the maximum history allowed by your license.');
});
test('returns correct maxAgeInDays and granularity for a valid date range with hourly data disabled', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
const result = insightsService.getMaxAgeInDaysAndGranularity('quarter');
expect(result).toEqual({
key: 'quarter',
licensed: true,
granularity: 'week',
maxAgeInDays: 90,
});
});
test('returns correct maxAgeInDays and granularity for a valid date range with unlimited history', () => {
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
const result = insightsService.getMaxAgeInDaysAndGranularity('day');
expect(result).toEqual({
key: 'day',
licensed: true,
granularity: 'hour',
maxAgeInDays: 1,
});
});
});
describe('shutdown', () => {
let insightsService: InsightsService;
const mockCollectionService = mock<InsightsCollectionService>({
shutdown: jest.fn().mockResolvedValue(undefined),
stopFlushingTimer: jest.fn(),
});
const mockCompactionService = mock<InsightsCompactionService>({
stopCompactionTimer: jest.fn(),
});
const mockPruningService = mock<InsightsPruningService>({
stopPruningTimer: jest.fn(),
});
beforeAll(() => {
insightsService = new InsightsService(
mock<InsightsByPeriodRepository>(),
mockCompactionService,
mockCollectionService,
mockPruningService,
mock<LicenseState>(),
mockLogger(),
);
});
test('shutdown stops timers and shuts down services', async () => {
// ACT
await insightsService.shutdown();
// ASSERT
expect(mockCollectionService.shutdown).toHaveBeenCalled();
expect(mockCompactionService.stopCompactionTimer).toHaveBeenCalled();
expect(mockPruningService.stopPruningTimer).toHaveBeenCalled();
});
});
describe('timers', () => {
let insightsService: InsightsService;
const mockCollectionService = mock<InsightsCollectionService>({
startFlushingTimer: jest.fn(),
stopFlushingTimer: jest.fn(),
});
const mockCompactionService = mock<InsightsCompactionService>({
startCompactionTimer: jest.fn(),
stopCompactionTimer: jest.fn(),
});
const mockPruningService = mock<InsightsPruningService>({
startPruningTimer: jest.fn(),
stopPruningTimer: jest.fn(),
isPruningEnabled: false,
});
const mockedLogger = mockLogger();
const mockedConfig = mock<InsightsConfig>({
maxAgeDays: -1,
});
beforeAll(() => {
insightsService = new InsightsService(
mock<InsightsByPeriodRepository>(),
mockCompactionService,
mockCollectionService,
mockPruningService,
mock<LicenseState>(),
mockedLogger,
);
});
test('startTimers starts timers except pruning', () => {
// ACT
insightsService.startTimers();
// ASSERT
expect(mockCompactionService.startCompactionTimer).toHaveBeenCalled();
expect(mockCollectionService.startFlushingTimer).toHaveBeenCalled();
expect(mockPruningService.startPruningTimer).not.toHaveBeenCalled();
});
test('startTimers starts pruning timer', () => {
// ARRANGE
mockedConfig.maxAgeDays = 30;
Object.defineProperty(mockPruningService, 'isPruningEnabled', { value: true });
// ACT
insightsService.startTimers();
// ASSERT
expect(mockPruningService.startPruningTimer).toHaveBeenCalled();
});
test('stopTimers stops timers', () => {
// ACT
insightsService.stopTimers();
// ASSERT
expect(mockCompactionService.stopCompactionTimer).toHaveBeenCalled();
expect(mockCollectionService.stopFlushingTimer).toHaveBeenCalled();
expect(mockPruningService.stopPruningTimer).toHaveBeenCalled();
});
});
describe('legacy sqlite (without pooling) handles concurrent insights db process without throwing', () => {
let initialFlushBatchSize: number;
let insightsConfig: InsightsConfig;
beforeAll(() => {
insightsConfig = Container.get(InsightsConfig);
initialFlushBatchSize = insightsConfig.flushBatchSize;
insightsConfig.flushBatchSize = 50;
});
afterAll(() => {
insightsConfig.flushBatchSize = initialFlushBatchSize;
});
test('should handle concurrent flush and compaction without error', async () => {
const insightsCollectionService = Container.get(InsightsCollectionService);
const insightsCompactionService = Container.get(InsightsCompactionService);
const project = await createTeamProject();
const workflow = await createWorkflow({}, project);
await createMetadata(workflow);
const ctx = mock<WorkflowExecuteAfterContext>({ workflow });
const startedAt = DateTime.utc();
const stoppedAt = startedAt.plus({ seconds: 5 });
ctx.runData = mock<IRun>({
mode: 'webhook',
status: 'success',
startedAt: startedAt.toJSDate(),
stoppedAt: stoppedAt.toJSDate(),
});
// Create test data
const rawInsights = [];
for (let i = 0; i < 100; i++) {
rawInsights.push({
type: 'success' as InsightsRaw['type'],
value: 1,
periodUnit: 'hour',
periodStart: DateTime.now().minus({ day: 91, hour: i + 1 }),
});
}
// Create raw insights events to be compacted
await createRawInsightsEvents(workflow, rawInsights);
//
for (let i = 0; i < 100; i++) {
await createCompactedInsightsEvent(workflow, {
type: 'success',
value: 1,
periodUnit: 'hour',
periodStart: DateTime.now().minus({ day: 91, hour: i + 1 }),
});
}
for (let i = 0; i < 100; i++) {
await insightsCollectionService.handleWorkflowExecuteAfter(ctx);
}
// ACT
const promises = [
insightsCollectionService.flushEvents(),
insightsCollectionService.flushEvents(),
insightsCompactionService.compactRawToHour(),
insightsCompactionService.compactHourToDay(),
];
await expect(Promise.all(promises)).resolves.toBeDefined();
});
});