first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
+311
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Integration test to compare workflow statistics with insights data.
|
||||
* This test verifies that both systems report consistent execution counts,
|
||||
* properly distinguishing between root executions and subworkflow executions.
|
||||
*
|
||||
* This test actually executes workflows (not just mocking) to ensure end-to-end correctness.
|
||||
* It configures the system for fast compaction and waits for automatic processing.
|
||||
*/
|
||||
|
||||
import { createTeamProject, createWorkflow, testDb, testModules } from '@n8n/backend-test-utils';
|
||||
import type { Project, WorkflowEntity } from '@n8n/db';
|
||||
import { ExecutionRepository, StatisticsNames, WorkflowStatisticsRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { readFileSync } from 'fs';
|
||||
import { InstanceSettings, UnrecognizedNodeTypeError } from 'n8n-core';
|
||||
import type { INodeType, INodeTypeData, NodeLoadingDetails } from 'n8n-workflow';
|
||||
import { createRunExecutionData } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
|
||||
import { InsightsByPeriodRepository } from '@/modules/insights/database/repositories/insights-by-period.repository';
|
||||
import { InsightsRawRepository } from '@/modules/insights/database/repositories/insights-raw.repository';
|
||||
import { InsightsCollectionService } from '@/modules/insights/insights-collection.service';
|
||||
import { InsightsCompactionService } from '@/modules/insights/insights-compaction.service';
|
||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
|
||||
import * as utils from '../shared/utils';
|
||||
import { createSimpleWorkflowFixture } from '../shared/workflow-fixtures';
|
||||
|
||||
// ============================================================
|
||||
// Helper to load nodes from dist folder
|
||||
// ============================================================
|
||||
|
||||
const BASE_DIR = path.resolve(__dirname, '../../../..');
|
||||
|
||||
function loadNodesFromDist(nodeNames: string[]): INodeTypeData {
|
||||
const nodeTypes: INodeTypeData = {};
|
||||
|
||||
const knownNodes = JSON.parse(
|
||||
readFileSync(path.join(BASE_DIR, 'nodes-base/dist/known/nodes.json'), 'utf-8'),
|
||||
) as Record<string, NodeLoadingDetails>;
|
||||
|
||||
for (const nodeName of nodeNames) {
|
||||
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
|
||||
if (!loadInfo) {
|
||||
throw new UnrecognizedNodeTypeError('n8n-nodes-base', nodeName);
|
||||
}
|
||||
// Load from dist .js files (sourcePath already includes 'dist/')
|
||||
const nodeDistPath = path.join(BASE_DIR, 'nodes-base', loadInfo.sourcePath);
|
||||
const node = new (require(nodeDistPath)[loadInfo.className])() as INodeType;
|
||||
nodeTypes[nodeName] = {
|
||||
sourcePath: '',
|
||||
type: node,
|
||||
};
|
||||
}
|
||||
|
||||
return nodeTypes;
|
||||
}
|
||||
|
||||
describe('Insights vs Workflow Statistics Integration', () => {
|
||||
beforeAll(async () => {
|
||||
// Configure insights for fast flushing and compaction BEFORE loading modules
|
||||
process.env.N8N_INSIGHTS_FLUSH_BATCH_SIZE = '10'; // Flush after 10 events
|
||||
process.env.N8N_INSIGHTS_FLUSH_INTERVAL_SECONDS = '1'; // Flush every 1 second
|
||||
process.env.N8N_INSIGHTS_COMPACTION_INTERVAL_MINUTES = '0.05'; // Compact every ~3 seconds
|
||||
process.env.N8N_INSIGHTS_COMPACTION_BATCH_SIZE = '100'; // Process up to 100 items per batch
|
||||
|
||||
await testModules.loadModules(['insights']);
|
||||
await testDb.init();
|
||||
|
||||
// Load required node types from dist folder
|
||||
const nodeTypes = loadNodesFromDist([
|
||||
'n8n-nodes-base.manualTrigger',
|
||||
'n8n-nodes-base.executeWorkflow',
|
||||
'n8n-nodes-base.executeWorkflowTrigger',
|
||||
]);
|
||||
|
||||
await utils.initNodeTypes(nodeTypes);
|
||||
await utils.initBinaryDataService();
|
||||
|
||||
// Mark instance as leader to enable compaction
|
||||
Container.get(InstanceSettings).markAsLeader();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
'InsightsRaw',
|
||||
'InsightsByPeriod',
|
||||
'InsightsMetadata',
|
||||
'WorkflowEntity',
|
||||
'WorkflowStatistics',
|
||||
'ExecutionEntity',
|
||||
'Project',
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
let insightsCollectionService: InsightsCollectionService;
|
||||
let insightsCompactionService: InsightsCompactionService;
|
||||
let insightsByPeriodRepository: InsightsByPeriodRepository;
|
||||
let insightsRawRepository: InsightsRawRepository;
|
||||
let workflowStatisticsRepository: WorkflowStatisticsRepository;
|
||||
let workflowRunner: WorkflowRunner;
|
||||
let executionRepository: ExecutionRepository;
|
||||
|
||||
let project: Project;
|
||||
let workflow: WorkflowEntity;
|
||||
|
||||
beforeAll(() => {
|
||||
// CRITICAL: Ensure SKIP_STATISTICS_EVENTS is not set
|
||||
// The WorkflowStatisticsService checks this in its constructor
|
||||
delete process.env.SKIP_STATISTICS_EVENTS;
|
||||
|
||||
// IMPORTANT: Get WorkflowStatisticsService early to ensure its event listeners are set up
|
||||
// This must be done BEFORE any workflows are executed
|
||||
Container.get(WorkflowStatisticsService);
|
||||
|
||||
insightsCollectionService = Container.get(InsightsCollectionService);
|
||||
insightsCompactionService = Container.get(InsightsCompactionService);
|
||||
insightsByPeriodRepository = Container.get(InsightsByPeriodRepository);
|
||||
insightsRawRepository = Container.get(InsightsRawRepository);
|
||||
workflowStatisticsRepository = Container.get(WorkflowStatisticsRepository);
|
||||
workflowRunner = Container.get(WorkflowRunner);
|
||||
executionRepository = Container.get(ExecutionRepository);
|
||||
|
||||
// Initialize insights collection service (config already set via env vars)
|
||||
insightsCollectionService.init();
|
||||
|
||||
// Start automatic compaction timer
|
||||
insightsCompactionService.startCompactionTimer();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Stop compaction timer
|
||||
insightsCompactionService.stopCompactionTimer();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
project = await createTeamProject('Test Project');
|
||||
|
||||
// Create workflow 1 - standalone workflow with manual trigger
|
||||
workflow = await createWorkflow(
|
||||
{
|
||||
name: 'Workflow 1 - Standalone',
|
||||
...createSimpleWorkflowFixture(),
|
||||
settings: {
|
||||
timeSavedPerExecution: 5,
|
||||
},
|
||||
},
|
||||
project,
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to wait for an execution to complete by polling the database
|
||||
*/
|
||||
async function waitForExecution(executionId: string, timeout = 10000): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
const execution = await executionRepository.findOneBy({ id: executionId });
|
||||
if (execution?.finished) {
|
||||
// Log execution status for debugging
|
||||
if (execution.status !== 'success') {
|
||||
console.log(`Execution ${executionId} finished with status: ${execution.status}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
throw new Error(`Execution ${executionId} did not complete within ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for workflow statistics to be recorded
|
||||
*/
|
||||
async function waitForStatistics(
|
||||
workflowId: string,
|
||||
expectedCount: number,
|
||||
timeout = 10000,
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
const stats = await workflowStatisticsRepository.findOne({
|
||||
where: {
|
||||
workflowId,
|
||||
name: StatisticsNames.productionSuccess,
|
||||
},
|
||||
});
|
||||
if (stats && stats.count >= expectedCount) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
throw new Error(
|
||||
`Workflow statistics did not reach expected count ${expectedCount} within ${timeout}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for insights to be compacted (raw insights cleared and compacted data available)
|
||||
*/
|
||||
async function waitForCompaction(workflowId: string, timeout = 10000): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
// Check if raw insights have been compacted (should be low or zero)
|
||||
const rawInsights = await insightsRawRepository.find();
|
||||
// Check if compacted insights exist for this workflow
|
||||
const compactedInsights = await insightsByPeriodRepository.find({
|
||||
where: {
|
||||
metadata: { workflowId },
|
||||
},
|
||||
relations: ['metadata'],
|
||||
});
|
||||
|
||||
// Compaction is done if we have compacted data and few/no raw insights
|
||||
if (compactedInsights.length > 0 && rawInsights.length < 10) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Insights compaction did not complete within ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to execute a workflow in webhook/trigger mode
|
||||
*/
|
||||
async function executeWorkflow(
|
||||
workflow: WorkflowEntity,
|
||||
mode: 'webhook' | 'trigger' = 'webhook',
|
||||
): Promise<string> {
|
||||
const executionData = createRunExecutionData({});
|
||||
|
||||
const executionId = await workflowRunner.run(
|
||||
{
|
||||
workflowData: workflow,
|
||||
userId: project.id,
|
||||
executionMode: mode,
|
||||
executionData,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return executionId;
|
||||
}
|
||||
|
||||
test('should match execution counts between insights and workflow statistics for standalone workflow', async () => {
|
||||
// ============================================================
|
||||
// ACT: Execute workflow 1 (standalone) ten times in webhook mode
|
||||
// ============================================================
|
||||
const execution1Ids: string[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const executionId = await executeWorkflow(workflow, 'webhook');
|
||||
execution1Ids.push(executionId);
|
||||
}
|
||||
|
||||
// Wait for all executions to complete
|
||||
await Promise.all(execution1Ids.map(async (id) => await waitForExecution(id)));
|
||||
|
||||
// Wait for workflow statistics to be recorded
|
||||
await waitForStatistics(workflow.id, 10);
|
||||
|
||||
// Wait for automatic compaction to complete
|
||||
await waitForCompaction(workflow.id);
|
||||
|
||||
// ============================================================
|
||||
// ASSERT: Query workflow statistics
|
||||
// ============================================================
|
||||
const stats1 = await workflowStatisticsRepository.findOne({
|
||||
where: {
|
||||
workflowId: workflow.id,
|
||||
name: StatisticsNames.productionSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify workflow statistics counts
|
||||
expect(stats1).toBeDefined();
|
||||
expect(stats1?.count).toBe(10); // Total executions
|
||||
expect(stats1?.rootCount).toBe(10); // All are root executions
|
||||
|
||||
// ============================================================
|
||||
// ASSERT: Query insights data (compacted)
|
||||
// ============================================================
|
||||
const allInsights1 = await insightsByPeriodRepository.find({
|
||||
where: {
|
||||
metadata: { workflowId: workflow.id },
|
||||
},
|
||||
relations: ['metadata'],
|
||||
});
|
||||
|
||||
// Filter by type 'success'
|
||||
const insights1 = allInsights1.filter((insight) => insight.type === 'success');
|
||||
const insights1SuccessCount = insights1.reduce((sum, insight) => sum + insight.value, 0);
|
||||
|
||||
// Insights should match root execution counts
|
||||
expect(insights1SuccessCount).toBe(10);
|
||||
expect(insights1SuccessCount).toBe(stats1?.rootCount);
|
||||
|
||||
// Verify runtime metrics
|
||||
const insights1Runtime = allInsights1.filter((insight) => insight.type === 'runtime_ms');
|
||||
const totalRuntime1 = insights1Runtime.reduce((sum, insight) => sum + insight.value, 0);
|
||||
expect(totalRuntime1).toBeGreaterThan(0);
|
||||
|
||||
// Verify time saved metrics
|
||||
const insights1TimeSaved = allInsights1.filter((insight) => insight.type === 'time_saved_min');
|
||||
const totalTimeSaved1 = insights1TimeSaved.reduce((sum, insight) => sum + insight.value, 0);
|
||||
expect(totalTimeSaved1).toBe(10 * 5); // 10 executions * 5 minutes saved per execution
|
||||
}, 60000);
|
||||
});
|
||||
@@ -0,0 +1,328 @@
|
||||
import type { InsightsDateRange } from '@n8n/api-types';
|
||||
import { mockInstance, createWorkflow, createTeamProject, testDb } from '@n8n/backend-test-utils';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { createCompactedInsightsEvent } from '@/modules/insights/database/entities/__tests__/db-utils';
|
||||
|
||||
import { createUser } from '../shared/db/users';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import * as utils from '../shared/utils';
|
||||
import { GLOBAL_ADMIN_ROLE, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
|
||||
|
||||
mockInstance(Telemetry);
|
||||
|
||||
const agents: Record<string, SuperAgentTest> = {};
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['insights', 'license', 'auth'],
|
||||
enabledFeatures: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||
quotas: { 'quota:insights:maxHistoryDays': 365 },
|
||||
modules: ['insights'],
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
const admin = await createUser({ role: GLOBAL_ADMIN_ROLE });
|
||||
const member = await createUser({ role: GLOBAL_MEMBER_ROLE });
|
||||
agents.owner = testServer.authAgentFor(owner);
|
||||
agents.admin = testServer.authAgentFor(admin);
|
||||
agents.member = testServer.authAgentFor(member);
|
||||
});
|
||||
|
||||
describe('GET /insights routes work for owner and admins for server with dashboard license', () => {
|
||||
test.each(['owner', 'admin', 'member'])(
|
||||
'Call should work and return empty summary for user %s',
|
||||
async (agentName: string) => {
|
||||
const authAgent = agents[agentName];
|
||||
await authAgent.get('/insights/summary').expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent.get('/insights/by-time').expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent
|
||||
.get('/insights/by-time/time-saved')
|
||||
.expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent.get('/insights/by-workflow').expect(agentName.includes('member') ? 403 : 200);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('GET /insights routes return 403 for dashboard routes when summary license only', () => {
|
||||
beforeAll(() => {
|
||||
testServer.license.setDefaults({ features: ['feat:insights:viewSummary'] });
|
||||
});
|
||||
test.each(['owner', 'admin', 'member'])(
|
||||
'Call should work and return empty summary for user %s',
|
||||
async (agentName: string) => {
|
||||
const authAgent = agents[agentName];
|
||||
await authAgent.get('/insights/summary').expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent.get('/insights/by-time').expect(403);
|
||||
await authAgent
|
||||
.get('/insights/by-time/time-saved')
|
||||
.expect(agentName.includes('member') ? 403 : 200);
|
||||
await authAgent.get('/insights/by-workflow').expect(403);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('GET /insights routes return 403 if date range outside license limits', () => {
|
||||
beforeAll(() => {
|
||||
testServer.license.setDefaults({ quotas: { 'quota:insights:maxHistoryDays': 3 } });
|
||||
});
|
||||
|
||||
test('Call should throw forbidden for default week insights', async () => {
|
||||
const authAgent = agents.admin;
|
||||
await authAgent.get('/insights/summary').expect(403);
|
||||
await authAgent.get('/insights/by-time').expect(403);
|
||||
await authAgent.get('/insights/by-time/time-saved').expect(403);
|
||||
await authAgent.get('/insights/by-workflow').expect(403);
|
||||
});
|
||||
|
||||
test('Call should throw forbidden for daily data without viewHourlyData enabled', async () => {
|
||||
const authAgent = agents.admin;
|
||||
await authAgent.get('/insights/summary?dateRange=day').expect(403);
|
||||
await authAgent.get('/insights/by-time?dateRange=day').expect(403);
|
||||
await authAgent.get('/insights/by-time/time-saved?dateRange=day').expect(403);
|
||||
await authAgent.get('/insights/by-workflow?dateRange=day').expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /insights routes return 200 if date range inside license limits', () => {
|
||||
beforeAll(() => {
|
||||
testServer.license.setDefaults({
|
||||
features: [
|
||||
'feat:insights:viewSummary',
|
||||
'feat:insights:viewDashboard',
|
||||
'feat:insights:viewHourlyData',
|
||||
],
|
||||
quotas: { 'quota:insights:maxHistoryDays': 365 },
|
||||
});
|
||||
});
|
||||
|
||||
test.each<InsightsDateRange['key']>([
|
||||
'day',
|
||||
'week',
|
||||
'2weeks',
|
||||
'month',
|
||||
'quarter',
|
||||
'6months',
|
||||
'year',
|
||||
])('Call should work for date range %s', async (dateRange) => {
|
||||
const authAgent = agents.admin;
|
||||
await authAgent.get(`/insights/summary?dateRange=${dateRange}`).expect(200);
|
||||
await authAgent.get(`/insights/by-time?dateRange=${dateRange}`).expect(200);
|
||||
await authAgent.get(`/insights/by-time/time-saved?dateRange=${dateRange}`).expect(200);
|
||||
await authAgent.get(`/insights/by-workflow?dateRange=${dateRange}`).expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /insights/by-workflow', () => {
|
||||
beforeAll(() => {
|
||||
testServer.license.setDefaults({
|
||||
features: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
skip: '10',
|
||||
take: '20',
|
||||
sortBy: 'total:desc',
|
||||
},
|
||||
{
|
||||
skip: '1',
|
||||
take: '25',
|
||||
sortBy: 'workflowName:asc',
|
||||
},
|
||||
])('Call should work with valid query parameters: %s', async (queryParams) => {
|
||||
await agents.owner.get('/insights/by-workflow').query(queryParams).expect(200);
|
||||
});
|
||||
|
||||
test.each<{ skip: string; take?: string; sortBy?: string }>([
|
||||
{
|
||||
skip: 'not_a_number',
|
||||
take: '20',
|
||||
},
|
||||
{
|
||||
skip: '1',
|
||||
take: 'not_a_number',
|
||||
},
|
||||
])(
|
||||
'Call should return bad request with invalid pagination query parameters',
|
||||
async (queryParams) => {
|
||||
await agents.owner.get('/insights/by-workflow').query(queryParams).expect(400);
|
||||
},
|
||||
);
|
||||
|
||||
test('Call should return bad request with invalid sortby query parameters', async () => {
|
||||
await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({
|
||||
skip: '1',
|
||||
take: '20',
|
||||
sortBy: 'not_a_sortby',
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test.each([
|
||||
'total:asc',
|
||||
'total:desc',
|
||||
'succeeded:asc',
|
||||
'succeeded:desc',
|
||||
'failed:asc',
|
||||
'failed:desc',
|
||||
'failureRate:asc',
|
||||
'failureRate:desc',
|
||||
'timeSaved:asc',
|
||||
'timeSaved:desc',
|
||||
'runTime:asc',
|
||||
'runTime:desc',
|
||||
'averageRunTime:asc',
|
||||
'averageRunTime:desc',
|
||||
'workflowName:asc',
|
||||
'workflowName:desc',
|
||||
])('Call should return 200 with valid sortBy option: %s', async (sortBy) => {
|
||||
const response = await agents.owner.get('/insights/by-workflow').query({ sortBy }).expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('data');
|
||||
expect(response.body.data).toHaveProperty('count');
|
||||
expect(Array.isArray(response.body.data.data)).toBe(true);
|
||||
});
|
||||
|
||||
describe('sorting order verification', () => {
|
||||
afterEach(async () => {
|
||||
await testDb.truncate([
|
||||
'InsightsRaw',
|
||||
'InsightsByPeriod',
|
||||
'InsightsMetadata',
|
||||
'SharedWorkflow',
|
||||
'WorkflowEntity',
|
||||
'Project',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return workflows sorted by total:desc', async () => {
|
||||
const project = await createTeamProject('Test Project 1');
|
||||
const testData = [
|
||||
{ name: 'Workflow A', total: 10 },
|
||||
{ name: 'Workflow B', total: 5 },
|
||||
{ name: 'Workflow C', total: 15 },
|
||||
];
|
||||
|
||||
const workflows = await Promise.all(
|
||||
testData.map(async (data) => await createWorkflow({ name: data.name }, project)),
|
||||
);
|
||||
|
||||
const periodStart = DateTime.utc().startOf('day');
|
||||
|
||||
await Promise.all(
|
||||
workflows.map(
|
||||
async (workflow, index) =>
|
||||
await createCompactedInsightsEvent(workflow, {
|
||||
type: 'success',
|
||||
value: testData[index].total,
|
||||
periodUnit: 'day',
|
||||
periodStart,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const response = await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({ sortBy: 'total:desc' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.count).toBe(3);
|
||||
expect(response.body.data.data).toHaveLength(3);
|
||||
|
||||
// Verify descending order by total
|
||||
const expectedOrder = [15, 10, 5];
|
||||
response.body.data.data.forEach((item: any, index: number) => {
|
||||
expect(item.total).toBe(expectedOrder[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return workflows sorted by workflowName:asc', async () => {
|
||||
const project = await createTeamProject('Test Project A');
|
||||
const workflowNames = ['Zebra Workflow', 'Alpha Workflow', 'Beta Workflow'];
|
||||
|
||||
const workflows = await Promise.all(
|
||||
workflowNames.map(async (name) => await createWorkflow({ name }, project)),
|
||||
);
|
||||
|
||||
const periodStart = DateTime.utc().startOf('day');
|
||||
|
||||
await Promise.all(
|
||||
workflows.map(
|
||||
async (workflow) =>
|
||||
await createCompactedInsightsEvent(workflow, {
|
||||
type: 'success',
|
||||
value: 5,
|
||||
periodUnit: 'day',
|
||||
periodStart,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const response = await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({ sortBy: 'workflowName:asc' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.count).toBe(3);
|
||||
expect(response.body.data.data).toHaveLength(3);
|
||||
|
||||
// Verify ascending order by workflow name
|
||||
const expectedOrder = ['Alpha Workflow', 'Beta Workflow', 'Zebra Workflow'];
|
||||
response.body.data.data.forEach((item: any, index: number) => {
|
||||
expect(item.workflowName).toBe(expectedOrder[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return workflows sorted by failureRate:asc', async () => {
|
||||
const project = await createTeamProject('Another Test Project');
|
||||
const testData = [
|
||||
{ name: 'Low Failure', success: 9, failure: 1 }, // 10% failure rate
|
||||
{ name: 'High Failure', success: 2, failure: 8 }, // 80% failure rate
|
||||
{ name: 'Medium Failure', success: 5, failure: 5 }, // 50% failure rate
|
||||
];
|
||||
|
||||
const workflows = await Promise.all(
|
||||
testData.map(async (data) => await createWorkflow({ name: data.name }, project)),
|
||||
);
|
||||
|
||||
const periodStart = DateTime.utc().startOf('day');
|
||||
|
||||
// Create insights events for each workflow individually to avoid race conditions
|
||||
for (const [index, workflow] of workflows.entries()) {
|
||||
const data = testData[index];
|
||||
|
||||
await createCompactedInsightsEvent(workflow, {
|
||||
type: 'success',
|
||||
value: data.success,
|
||||
periodUnit: 'day',
|
||||
periodStart,
|
||||
});
|
||||
|
||||
await createCompactedInsightsEvent(workflow, {
|
||||
type: 'failure',
|
||||
value: data.failure,
|
||||
periodUnit: 'day',
|
||||
periodStart,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await agents.owner
|
||||
.get('/insights/by-workflow')
|
||||
.query({ sortBy: 'failureRate:asc' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.count).toBe(3);
|
||||
expect(response.body.data.data).toHaveLength(3);
|
||||
|
||||
// Verify ascending order by failure rate
|
||||
const expectedOrder = [0.1, 0.5, 0.8]; // 10%, 50%, 80%
|
||||
response.body.data.data.forEach((item: any, index: number) => {
|
||||
expect(item.failureRate).toBe(expectedOrder[index]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user