980 lines
30 KiB
TypeScript
980 lines
30 KiB
TypeScript
/**
|
|
* Full Round-Trip Performance Profiling Suite
|
|
*
|
|
* Measures complete flow: Frontend → gRPC → Backend → Database → Response
|
|
*
|
|
* Covers:
|
|
* - Meeting CRUD operations (create, read, update, delete)
|
|
* - Recording flow simulation
|
|
* - Diarization pipeline
|
|
* - Transcription segments
|
|
* - Summarization generation
|
|
* - Entity extraction
|
|
* - Annotation CRUD
|
|
* - Task management
|
|
* - Analytics aggregation
|
|
* - Export operations
|
|
*/
|
|
|
|
import { expect, test } from '@playwright/test';
|
|
import { callAPI, navigateTo, TEST_DATA, waitForAPI, waitForLoadingComplete } from './fixtures';
|
|
|
|
const shouldRun = process.env.NOTEFLOW_E2E === '1';
|
|
|
|
// Performance thresholds for full round-trip (milliseconds)
|
|
const RT_THRESHOLDS = {
|
|
// Fast operations (simple DB lookup)
|
|
SIMPLE_READ: 500,
|
|
SIMPLE_WRITE: 1000,
|
|
|
|
// Medium operations (joins, aggregations)
|
|
MEDIUM_READ: 2000,
|
|
MEDIUM_WRITE: 3000,
|
|
|
|
// Heavy operations (AI processing, large data)
|
|
HEAVY_PROCESSING: 30000,
|
|
AI_OPERATION: 60000,
|
|
|
|
// Full page with multiple API calls
|
|
FULL_PAGE_LOAD: 5000,
|
|
};
|
|
|
|
interface RoundTripMetric {
|
|
operation: string;
|
|
flow: string;
|
|
duration: number;
|
|
threshold: number;
|
|
passed: boolean;
|
|
dataSize?: number;
|
|
details?: string;
|
|
}
|
|
|
|
const allMetrics: RoundTripMetric[] = [];
|
|
|
|
function recordRoundTrip(
|
|
operation: string,
|
|
flow: string,
|
|
duration: number,
|
|
threshold: number,
|
|
dataSize?: number,
|
|
details?: string
|
|
) {
|
|
const passed = duration <= threshold;
|
|
allMetrics.push({ operation, flow, duration, threshold, passed, dataSize, details });
|
|
|
|
const sizeInfo = dataSize ? ` [${dataSize} items]` : '';
|
|
const status = passed ? '✓' : '✗';
|
|
console.log(
|
|
`[RT] ${status} ${operation} (${flow}): ${duration.toFixed(0)}ms${sizeInfo} (threshold: ${threshold}ms)`
|
|
);
|
|
return passed;
|
|
}
|
|
|
|
async function measureRoundTrip<T>(fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
|
|
const start = performance.now();
|
|
const result = await fn();
|
|
const duration = performance.now() - start;
|
|
return { result, duration };
|
|
}
|
|
|
|
// =============================================================================
|
|
// 1. MEETING CRUD - Full Round Trip
|
|
// =============================================================================
|
|
|
|
test.describe('Meeting CRUD Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
let testMeetingId: string | null = null;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Cleanup test meeting if created
|
|
if (testMeetingId) {
|
|
try {
|
|
await callAPI(page, 'deleteMeeting', { meeting_id: testMeetingId });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
testMeetingId = null;
|
|
}
|
|
});
|
|
|
|
test('CREATE meeting - full round trip', async ({ page }) => {
|
|
const testTitle = `Perf Test Meeting ${Date.now()}`;
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ id: string; title: string }>(page, 'createMeeting', {
|
|
title: testTitle,
|
|
metadata: { test: 'performance' },
|
|
});
|
|
});
|
|
|
|
testMeetingId = result.id;
|
|
const passed = recordRoundTrip('CREATE Meeting', 'FE→gRPC→DB→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
expect(result.title).toBe(testTitle);
|
|
expect(passed).toBe(true);
|
|
});
|
|
|
|
test('READ meeting list - full round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ meetings: unknown[]; total_count: number }>(page, 'listMeetings', {
|
|
limit: 50,
|
|
offset: 0,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Meeting List',
|
|
'FE→gRPC→DB(query+count)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.meetings.length
|
|
);
|
|
});
|
|
|
|
test('READ meeting list with filters - full round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ meetings: unknown[] }>(page, 'listMeetings', {
|
|
limit: 50,
|
|
states: ['completed'],
|
|
sort_order: 'newest',
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Meeting List (filtered)',
|
|
'FE→gRPC→DB(filtered query)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.meetings.length
|
|
);
|
|
});
|
|
|
|
test('READ single meeting with segments - full round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
|
|
if (meetings.meetings.length === 0) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const meetingId = meetings.meetings[0].id;
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ segments?: unknown[] }>(page, 'getMeeting', {
|
|
meeting_id: meetingId,
|
|
include_segments: true,
|
|
include_summary: true,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Meeting Detail',
|
|
'FE→gRPC→DB(meeting+segments+summary)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.segments?.length || 0
|
|
);
|
|
});
|
|
|
|
test('DELETE meeting - full round trip', async ({ page }) => {
|
|
// Create a meeting to delete
|
|
const createResult = await callAPI<{ id: string }>(page, 'createMeeting', {
|
|
title: `Delete Test ${Date.now()}`,
|
|
});
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'deleteMeeting', { meeting_id: createResult.id });
|
|
});
|
|
|
|
recordRoundTrip('DELETE Meeting', 'FE→gRPC→DB(cascade delete)→Response', duration, RT_THRESHOLDS.MEDIUM_WRITE);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 2. SPEAKER/DIARIZATION ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Diarization Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('READ speaker stats - aggregation round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ speakers: unknown[] }>(page, 'listSpeakerStats', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Speaker Stats',
|
|
'FE→gRPC→DB(aggregate speakers)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.speakers.length
|
|
);
|
|
});
|
|
|
|
test('READ speaker stats with project filter - filtered aggregation', async ({ page }) => {
|
|
const projects = await callAPI<{ projects: { id: string }[] }>(page, 'listProjects', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
|
|
if (projects.projects.length === 0) {
|
|
test.skip(true, 'No projects available');
|
|
return;
|
|
}
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ speakers: unknown[] }>(page, 'listSpeakerStats', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
project_id: projects.projects[0].id,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Speaker Stats (project filtered)',
|
|
'FE→gRPC→DB(filtered aggregate)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.speakers.length
|
|
);
|
|
});
|
|
|
|
test('GET diarization job status - status check round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
|
|
if (meetings.meetings.length === 0) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'getDiarizationJobStatus', {
|
|
meeting_id: meetings.meetings[0].id,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip('READ Diarization Status', 'FE→gRPC→DB(job lookup)→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
|
|
});
|
|
|
|
test('RENAME speaker - update round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
|
|
if (meetings.meetings.length === 0) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const meeting = await callAPI<{ segments?: { speaker_id: string }[] }>(page, 'getMeeting', {
|
|
meeting_id: meetings.meetings[0].id,
|
|
include_segments: true,
|
|
});
|
|
|
|
const speakerId = meeting.segments?.[0]?.speaker_id;
|
|
if (!speakerId) {
|
|
test.skip(true, 'No speakers in meeting');
|
|
return;
|
|
}
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'renameSpeaker', {
|
|
meeting_id: meetings.meetings[0].id,
|
|
speaker_id: speakerId,
|
|
new_name: `Speaker ${Date.now()}`,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip('UPDATE Speaker Name', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 3. ANALYTICS AGGREGATION ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Analytics Aggregation Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('GET analytics overview - complex aggregation', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ daily?: unknown[]; total_meetings: number }>(page, 'getAnalyticsOverview', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Analytics Overview',
|
|
'FE→gRPC→DB(multi-table aggregate)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.daily?.length || 0,
|
|
`${result.total_meetings} total meetings`
|
|
);
|
|
});
|
|
|
|
test('GET entity analytics - NER aggregation', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ total_entities: number; total_mentions: number }>(page, 'getEntityAnalytics', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Entity Analytics',
|
|
'FE→gRPC→DB(entity aggregate)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.total_entities,
|
|
`${result.total_mentions} mentions`
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 4. TASK MANAGEMENT ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Task Management Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('READ task list - join query round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ tasks: unknown[]; total_count: number }>(page, 'listTasks', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
limit: 50,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Task List',
|
|
'FE→gRPC→DB(tasks+meetings join)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.tasks.length
|
|
);
|
|
});
|
|
|
|
test('READ task list with status filter - filtered join', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ tasks: unknown[] }>(page, 'listTasks', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
statuses: ['open'],
|
|
limit: 50,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Task List (filtered)',
|
|
'FE→gRPC→DB(filtered join)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.tasks.length
|
|
);
|
|
});
|
|
|
|
test('UPDATE task status - status update round trip', async ({ page }) => {
|
|
const tasks = await callAPI<{ tasks: { task: { id: string; status: string } }[] }>(page, 'listTasks', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
limit: 1,
|
|
});
|
|
|
|
if (tasks.tasks.length === 0) {
|
|
test.skip(true, 'No tasks available');
|
|
return;
|
|
}
|
|
|
|
const task = tasks.tasks[0].task;
|
|
const newStatus = task.status === 'open' ? 'in_progress' : 'open';
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'updateTask', {
|
|
task_id: task.id,
|
|
status: newStatus,
|
|
});
|
|
});
|
|
|
|
// Revert status
|
|
await callAPI(page, 'updateTask', { task_id: task.id, status: task.status });
|
|
|
|
recordRoundTrip('UPDATE Task Status', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 5. ANNOTATION CRUD ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Annotation CRUD Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
let testAnnotationId: string | null = null;
|
|
let testMeetingId: string | null = null;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
|
|
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
|
|
if (meetings.meetings.length > 0) {
|
|
testMeetingId = meetings.meetings[0].id;
|
|
}
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
if (testAnnotationId && testMeetingId) {
|
|
try {
|
|
await callAPI(page, 'deleteAnnotation', { annotation_id: testAnnotationId });
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
testAnnotationId = null;
|
|
}
|
|
});
|
|
|
|
test('CREATE annotation - insert round trip', async ({ page }) => {
|
|
if (!testMeetingId) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ id: string }>(page, 'addAnnotation', {
|
|
meeting_id: testMeetingId,
|
|
annotation_type: 'note',
|
|
text: `Performance test annotation ${Date.now()}`,
|
|
start_time: 0,
|
|
end_time: 1000,
|
|
});
|
|
});
|
|
|
|
testAnnotationId = result.id;
|
|
recordRoundTrip('CREATE Annotation', 'FE→gRPC→DB(insert)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
});
|
|
|
|
test('READ annotations - list query round trip', async ({ page }) => {
|
|
if (!testMeetingId) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ annotations: unknown[] }>(page, 'listAnnotations', {
|
|
meeting_id: testMeetingId,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Annotations',
|
|
'FE→gRPC→DB(query)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.SIMPLE_READ,
|
|
result.annotations.length
|
|
);
|
|
});
|
|
|
|
test('UPDATE annotation - update round trip', async ({ page }) => {
|
|
if (!testMeetingId) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
// Create an annotation to update
|
|
const created = await callAPI<{ id: string }>(page, 'addAnnotation', {
|
|
meeting_id: testMeetingId,
|
|
annotation_type: 'note',
|
|
text: 'Original text',
|
|
start_time: 0,
|
|
end_time: 1000,
|
|
});
|
|
testAnnotationId = created.id;
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'updateAnnotation', {
|
|
annotation_id: testAnnotationId,
|
|
text: 'Updated text',
|
|
});
|
|
});
|
|
|
|
recordRoundTrip('UPDATE Annotation', 'FE→gRPC→DB(update)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
});
|
|
|
|
test('DELETE annotation - delete round trip', async ({ page }) => {
|
|
if (!testMeetingId) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
// Create an annotation to delete
|
|
const created = await callAPI<{ id: string }>(page, 'addAnnotation', {
|
|
meeting_id: testMeetingId,
|
|
annotation_type: 'note',
|
|
text: 'To be deleted',
|
|
start_time: 0,
|
|
end_time: 1000,
|
|
});
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'deleteAnnotation', { annotation_id: created.id });
|
|
});
|
|
|
|
testAnnotationId = null; // Already deleted
|
|
recordRoundTrip('DELETE Annotation', 'FE→gRPC→DB(delete)→Response', duration, RT_THRESHOLDS.SIMPLE_WRITE);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 6. PROJECT OPERATIONS ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Project Operations Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('READ project list - list query round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ projects: unknown[] }>(page, 'listProjects', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Project List',
|
|
'FE→gRPC→DB(query)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.SIMPLE_READ,
|
|
result.projects.length
|
|
);
|
|
});
|
|
|
|
test('READ meetings by project - filtered query round trip', async ({ page }) => {
|
|
const projects = await callAPI<{ projects: { id: string }[] }>(page, 'listProjects', {
|
|
workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID,
|
|
});
|
|
|
|
if (projects.projects.length === 0) {
|
|
test.skip(true, 'No projects available');
|
|
return;
|
|
}
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ meetings: unknown[] }>(page, 'listMeetings', {
|
|
project_id: projects.projects[0].id,
|
|
limit: 50,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Meetings by Project',
|
|
'FE→gRPC→DB(filtered query)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ,
|
|
result.meetings.length
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 7. WEBHOOKS ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Webhooks Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('READ webhook list - list query round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ webhooks: unknown[] }>(page, 'listWebhooks', {
|
|
include_test: false,
|
|
});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Webhook List',
|
|
'FE→gRPC→DB(query)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.SIMPLE_READ,
|
|
result.webhooks.length
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 8. SETTINGS/PREFERENCES ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Settings Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('READ preferences - config lookup round trip', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'getPreferences', {});
|
|
});
|
|
|
|
recordRoundTrip('READ Preferences', 'FE→gRPC→Config/DB→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
|
|
});
|
|
|
|
test('READ server info - system status round trip', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'getServerInfo', {});
|
|
});
|
|
|
|
recordRoundTrip('READ Server Info', 'FE→gRPC→System→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
|
|
});
|
|
|
|
test('READ cloud consent status - auth/config round trip', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'getCloudConsentStatus', {});
|
|
});
|
|
|
|
recordRoundTrip('READ Cloud Consent', 'FE→gRPC→Config→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
|
|
});
|
|
|
|
test('READ audio devices - system enumeration round trip', async ({ page }) => {
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
return callAPI<{ input: unknown[]; output: unknown[] }>(page, 'listAudioDevices', {});
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'READ Audio Devices',
|
|
'FE→Rust/Tauri→System→Response',
|
|
duration,
|
|
RT_THRESHOLDS.SIMPLE_READ,
|
|
(result.input?.length || 0) + (result.output?.length || 0)
|
|
);
|
|
});
|
|
|
|
test('READ trigger status - config lookup round trip', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return callAPI(page, 'getTriggerStatus', {});
|
|
});
|
|
|
|
recordRoundTrip('READ Trigger Status', 'FE→Rust/Tauri→Config→Response', duration, RT_THRESHOLDS.SIMPLE_READ);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 9. AI ASSISTANT ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('AI Assistant Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test('ASK assistant - AI inference round trip', async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
|
|
const meetings = await callAPI<{ meetings: { id: string }[] }>(page, 'listMeetings', { limit: 1 });
|
|
if (meetings.meetings.length === 0) {
|
|
test.skip(true, 'No meetings available');
|
|
return;
|
|
}
|
|
|
|
const { result, duration } = await measureRoundTrip(async () => {
|
|
try {
|
|
return await callAPI<{ answer: string }>(page, 'askAssistant', {
|
|
question: 'What is the main topic?',
|
|
meeting_id: meetings.meetings[0].id,
|
|
});
|
|
} catch (e) {
|
|
return { answer: '', error: String(e) };
|
|
}
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'AI Assistant Query',
|
|
'FE→gRPC→LLM(inference)→DB(context)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.AI_OPERATION,
|
|
undefined,
|
|
`Answer length: ${result.answer?.length || 0}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 10. POST-PROCESSING PIPELINE ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Post-Processing Pipeline Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
test('TRIGGER diarization refinement - async job round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(page, 'listMeetings', {
|
|
limit: 10,
|
|
states: ['completed'],
|
|
});
|
|
|
|
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
|
|
if (!completedMeeting) {
|
|
test.skip(true, 'No completed meetings');
|
|
return;
|
|
}
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
try {
|
|
return await callAPI(page, 'refineSpeakers', {
|
|
meeting_id: completedMeeting.id,
|
|
});
|
|
} catch {
|
|
return { skipped: true };
|
|
}
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'TRIGGER Diarization',
|
|
'FE→gRPC→JobQueue→Audio Processing→DB→Response',
|
|
duration,
|
|
RT_THRESHOLDS.HEAVY_PROCESSING
|
|
);
|
|
});
|
|
|
|
test('TRIGGER summary generation - AI generation round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(page, 'listMeetings', {
|
|
limit: 10,
|
|
states: ['completed'],
|
|
});
|
|
|
|
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
|
|
if (!completedMeeting) {
|
|
test.skip(true, 'No completed meetings');
|
|
return;
|
|
}
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
try {
|
|
return await callAPI(page, 'generateSummary', {
|
|
meeting_id: completedMeeting.id,
|
|
});
|
|
} catch {
|
|
return { skipped: true };
|
|
}
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'TRIGGER Summary Generation',
|
|
'FE→gRPC→LLM(inference)→DB(store)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.AI_OPERATION
|
|
);
|
|
});
|
|
|
|
test('TRIGGER entity extraction - NER round trip', async ({ page }) => {
|
|
const meetings = await callAPI<{ meetings: { id: string; state: string }[] }>(page, 'listMeetings', {
|
|
limit: 10,
|
|
states: ['completed'],
|
|
});
|
|
|
|
const completedMeeting = meetings.meetings.find((m) => m.state === 'completed');
|
|
if (!completedMeeting) {
|
|
test.skip(true, 'No completed meetings');
|
|
return;
|
|
}
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
try {
|
|
return await callAPI(page, 'extractEntities', {
|
|
meeting_id: completedMeeting.id,
|
|
});
|
|
} catch {
|
|
return { skipped: true };
|
|
}
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'TRIGGER Entity Extraction',
|
|
'FE→gRPC→NER Model→DB(store)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.HEAVY_PROCESSING
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 11. CONCURRENT OPERATIONS
|
|
// =============================================================================
|
|
|
|
test.describe('Concurrent Operations Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test('CONCURRENT reads - parallel query round trip', async ({ page }) => {
|
|
await navigateTo(page, '/');
|
|
await waitForAPI(page);
|
|
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
return Promise.all([
|
|
callAPI(page, 'listMeetings', { limit: 20 }),
|
|
callAPI(page, 'listSpeakerStats', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
|
|
callAPI(page, 'listTasks', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID, limit: 20 }),
|
|
callAPI(page, 'getAnalyticsOverview', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
|
|
callAPI(page, 'listProjects', { workspace_id: TEST_DATA.DEFAULT_WORKSPACE_ID }),
|
|
]);
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'CONCURRENT 5 Reads',
|
|
'FE→gRPC(parallel)→DB(5 queries)→Response',
|
|
duration,
|
|
RT_THRESHOLDS.MEDIUM_READ * 1.5, // Allow 50% overhead for parallelism
|
|
5
|
|
);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// 12. FULL PAGE LOAD ROUND TRIP
|
|
// =============================================================================
|
|
|
|
test.describe('Full Page Load Round Trip', () => {
|
|
test.skip(!shouldRun, 'Set NOTEFLOW_E2E=1 to enable');
|
|
|
|
test('HOME page - full load with API calls', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
await page.goto('/');
|
|
await waitForLoadingComplete(page);
|
|
await waitForAPI(page);
|
|
});
|
|
|
|
recordRoundTrip('FULL PAGE: Home', 'Navigate→Render→API calls→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
|
|
});
|
|
|
|
test('MEETINGS page - full load with list query', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
await page.goto('/meetings');
|
|
await waitForLoadingComplete(page);
|
|
});
|
|
|
|
recordRoundTrip('FULL PAGE: Meetings', 'Navigate→Render→listMeetings→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
|
|
});
|
|
|
|
test('ANALYTICS page - full load with aggregations', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
await page.goto('/analytics');
|
|
await waitForLoadingComplete(page);
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'FULL PAGE: Analytics',
|
|
'Navigate→Render→Aggregations→Charts→Complete',
|
|
duration,
|
|
RT_THRESHOLDS.FULL_PAGE_LOAD
|
|
);
|
|
});
|
|
|
|
test('PEOPLE page - full load with speaker stats', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
await page.goto('/people');
|
|
await waitForLoadingComplete(page);
|
|
});
|
|
|
|
recordRoundTrip(
|
|
'FULL PAGE: People',
|
|
'Navigate→Render→listSpeakerStats→Complete',
|
|
duration,
|
|
RT_THRESHOLDS.FULL_PAGE_LOAD
|
|
);
|
|
});
|
|
|
|
test('TASKS page - full load with task list', async ({ page }) => {
|
|
const { duration } = await measureRoundTrip(async () => {
|
|
await page.goto('/tasks');
|
|
await waitForLoadingComplete(page);
|
|
});
|
|
|
|
recordRoundTrip('FULL PAGE: Tasks', 'Navigate→Render→listTasks→Complete', duration, RT_THRESHOLDS.FULL_PAGE_LOAD);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// FINAL SUMMARY REPORT
|
|
// =============================================================================
|
|
|
|
test.afterAll(() => {
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
console.log('FULL ROUND-TRIP PERFORMANCE PROFILING REPORT');
|
|
console.log('='.repeat(80));
|
|
|
|
const passed = allMetrics.filter((m) => m.passed);
|
|
const failed = allMetrics.filter((m) => !m.passed);
|
|
|
|
console.log(`\nTotal Operations Profiled: ${allMetrics.length}`);
|
|
console.log(`Passed: ${passed.length} (${((passed.length / allMetrics.length) * 100).toFixed(1)}%)`);
|
|
console.log(`Failed: ${failed.length} (${((failed.length / allMetrics.length) * 100).toFixed(1)}%)`);
|
|
|
|
// Group by flow type
|
|
const byFlow = new Map<string, RoundTripMetric[]>();
|
|
for (const m of allMetrics) {
|
|
const key = m.flow.split('→')[0];
|
|
if (!byFlow.has(key)) {
|
|
byFlow.set(key, []);
|
|
}
|
|
byFlow.get(key)?.push(m);
|
|
}
|
|
|
|
console.log('\n--- BY OPERATION TYPE ---');
|
|
for (const [flow, metrics] of byFlow) {
|
|
const avgDuration = metrics.reduce((sum, m) => sum + m.duration, 0) / metrics.length;
|
|
const passRate = (metrics.filter((m) => m.passed).length / metrics.length) * 100;
|
|
console.log(`${flow}: avg ${avgDuration.toFixed(0)}ms, ${passRate.toFixed(0)}% pass rate`);
|
|
}
|
|
|
|
if (failed.length > 0) {
|
|
console.log('\n--- FAILED OPERATIONS (VULNERABILITIES) ---');
|
|
for (const m of failed) {
|
|
console.log(
|
|
` ✗ ${m.operation}: ${m.duration.toFixed(0)}ms (threshold: ${m.threshold}ms) - ${m.flow}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Find slowest operations
|
|
const sorted = [...allMetrics].sort((a, b) => b.duration - a.duration);
|
|
console.log('\n--- TOP 10 SLOWEST OPERATIONS ---');
|
|
for (const m of sorted.slice(0, 10)) {
|
|
const pct = ((m.duration / m.threshold) * 100).toFixed(0);
|
|
console.log(` ${m.operation}: ${m.duration.toFixed(0)}ms (${pct}% of threshold)`);
|
|
}
|
|
|
|
console.log('\n--- ALL METRICS ---');
|
|
for (const m of allMetrics) {
|
|
const status = m.passed ? '✓' : '✗';
|
|
const sizeInfo = m.dataSize !== undefined ? ` [${m.dataSize}]` : '';
|
|
console.log(` ${status} ${m.operation}: ${m.duration.toFixed(0)}ms${sizeInfo}`);
|
|
}
|
|
|
|
console.log(`\n${'='.repeat(80)}`);
|
|
});
|