buster/packages/ai/tests/tools/integration/create-dashboards-file-tool...

679 lines
20 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import {
dashboardFiles,
db,
eq,
inArray,
metricFiles,
metricFilesToDashboardFiles,
} from '@buster/database';
import type { RuntimeContext } from '@mastra/core/runtime-context';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { createDashboards } from '../../../src/tools/visualization-tools/create-dashboards-file-tool';
import { validateArrayAccess } from '../../../src/utils/validation-helpers';
// Type for runtime context
type MockRuntimeContext = {
get: (key: string) => string | undefined;
};
describe('Create Dashboards File Tool Integration Tests', () => {
let mockRuntimeContext: MockRuntimeContext;
let testDataSourceId: string;
let testUserId: string;
let testOrgId: string;
let createdMetricIds: string[] = [];
let createdDashboardIds: string[] = [];
beforeEach(() => {
// Use real test environment IDs
testDataSourceId = 'cc3ef3bc-44ec-4a43-8dc4-681cae5c996a';
testUserId = '1fe85021-e799-471b-8837-953e9ae06e4c';
testOrgId = 'bf58d19a-8bb9-4f1d-a257-2d2105e7f1ce';
mockRuntimeContext = {
get: (key: string) => {
const values: Record<string, string> = {
userId: testUserId,
organizationId: testOrgId,
};
return values[key];
},
};
// Reset created IDs arrays
createdMetricIds = [];
createdDashboardIds = [];
});
afterEach(async () => {
// Clean up created dashboards and metrics
try {
if (createdDashboardIds.length > 0) {
await db
.delete(dashboardFiles)
.where(inArray(dashboardFiles.id, createdDashboardIds))
.execute();
}
if (createdMetricIds.length > 0) {
await db.delete(metricFiles).where(inArray(metricFiles.id, createdMetricIds)).execute();
}
} catch (error) {
// Ignore cleanup errors
}
});
// Helper function to create test metrics for dashboard testing
async function createTestMetrics(count = 1): Promise<string[]> {
const metricIds: string[] = [];
for (let i = 1; i <= count; i++) {
const metricId = randomUUID();
const metricYml = {
name: `Test Metric ${i}`,
description: `A test metric ${i} for dashboard testing`,
timeFrame: 'Last 30 days',
sql: `SELECT COUNT(*) as count_${i} FROM test_table_${i}`,
chartConfig: {
selectedChartType: 'table',
columnLabelFormats: {
[`count_${i}`]: {
columnType: 'number',
style: 'number',
numberSeparatorStyle: ',',
replaceMissingDataWith: 0,
},
},
},
};
await db
.insert(metricFiles)
.values({
id: metricId,
name: `Test Metric ${i}`,
fileName: `test-metric-${i}`,
content: metricYml,
verification: 'notRequested',
organizationId: testOrgId,
createdBy: testUserId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
versionHistory: {
versions: [
{
versionNumber: 1,
content: metricYml,
createdAt: new Date().toISOString(),
},
],
},
dataSourceId: testDataSourceId,
})
.execute();
metricIds.push(metricId);
createdMetricIds.push(metricId);
}
return metricIds;
}
test('should have correct tool configuration', () => {
expect(createDashboards.id).toBe('create-dashboards-file');
expect(createDashboards.description).toContain('Creates dashboard configuration files');
expect(createDashboards.inputSchema).toBeDefined();
expect(createDashboards.outputSchema).toBeDefined();
expect(createDashboards.execute).toBeDefined();
});
test('should validate tool input schema', () => {
const validInput = {
files: [
{
name: 'Test Dashboard',
yml_content: `
name: Sales Dashboard
description: A comprehensive view of sales metrics
rows:
- id: 1
items:
- id: f47ac10b-58cc-4372-a567-0e02b2c3d479
columnSizes:
- 12
`,
},
],
};
const result = createDashboards.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate tool output schema', () => {
const validOutput = {
message: 'Successfully created 1 dashboard file.',
duration: 1000,
files: [
{
id: randomUUID(),
name: 'Test Dashboard',
file_type: 'dashboard',
yml_content: 'name: Test Dashboard',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
version_number: 1,
},
],
failed_files: [],
};
const result = createDashboards.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('should handle runtime context requirements', async () => {
const contextWithoutUserId = {
get: (key: string) => {
if (key === 'userId') return undefined;
return 'test-value';
},
};
const validYaml = `
name: Test Dashboard
description: Test dashboard
rows:
- id: 1
items:
- id: f47ac10b-58cc-4372-a567-0e02b2c3d479
columnSizes:
- 12
`;
const input = {
files: [{ name: 'Test Dashboard', yml_content: validYaml }],
runtimeContext: contextWithoutUserId,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: contextWithoutUserId as unknown as RuntimeContext,
});
expect(result.message).toBe('Unable to verify your identity. Please log in again.');
expect(result.files).toHaveLength(0);
expect(result.failed_files).toHaveLength(0);
});
test('should reject dashboard with invalid YAML in integration context', async () => {
const invalidYaml = `
name: Invalid Dashboard
description: Invalid dashboard
# Missing rows
`;
const input = {
files: [{ name: 'Invalid Dashboard', yml_content: invalidYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(0);
expect(result.failed_files).toHaveLength(1);
const failedFile = validateArrayAccess(result.failed_files, 0, 'failed_files access');
expect(failedFile.name).toBe('Invalid Dashboard');
expect(failedFile.error).toContain('dashboard configuration format is incorrect');
});
test('should reject dashboard with invalid column sizes', async () => {
const invalidColumnSizesYaml = `
name: Invalid Column Dashboard
description: Dashboard with invalid column sizes
rows:
- id: 1
items:
- id: f47ac10b-58cc-4372-a567-0e02b2c3d479
columnSizes:
- 10
`;
const input = {
files: [{ name: 'Invalid Column Dashboard', yml_content: invalidColumnSizesYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(0);
expect(result.failed_files).toHaveLength(1);
const failedFile = validateArrayAccess(result.failed_files, 0, 'failed_files access');
expect(failedFile.error).toContain('dashboard configuration format is incorrect');
});
test('should reject dashboard with non-existent metric IDs', async () => {
const nonExistentMetricYaml = `
name: Non-existent Metric Dashboard
description: Dashboard referencing non-existent metrics
rows:
- id: 1
items:
- id: 00000000-0000-0000-0000-000000000000
columnSizes:
- 12
`;
const input = {
files: [{ name: 'Non-existent Metric Dashboard', yml_content: nonExistentMetricYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(0);
expect(result.failed_files).toHaveLength(1);
const failedFile = validateArrayAccess(result.failed_files, 0, 'failed_files access');
expect(failedFile.error).toContain('metrics referenced in the dashboard do not exist');
});
test('should successfully create dashboard with valid metrics', async () => {
// Create test metrics first
const metricIds = await createTestMetrics(2);
const validDashboardYaml = `
name: Valid Dashboard
description: A valid dashboard with existing metrics
rows:
- id: 1
items:
- id: ${metricIds[0]}
- id: ${metricIds[1]}
columnSizes:
- 6
- 6
`;
const input = {
files: [{ name: 'Valid Dashboard', yml_content: validDashboardYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(1);
expect(result.failed_files).toHaveLength(0);
const createdFile = validateArrayAccess(result.files, 0, 'files access');
expect(createdFile.name).toBe('Valid Dashboard');
expect(createdFile.file_type).toBe('dashboard');
expect(createdFile.version_number).toBe(1);
expect(result.message).toBe('Successfully created 1 dashboard files.');
// Track created dashboard for cleanup
createdDashboardIds.push(createdFile.id);
// Verify metric-dashboard associations were created
const associations = await db
.select()
.from(metricFilesToDashboardFiles)
.where(eq(metricFilesToDashboardFiles.dashboardFileId, createdFile.id))
.execute();
expect(associations).toHaveLength(2);
expect(associations.map((a) => a.metricFileId).sort()).toEqual(metricIds.sort());
});
test('should handle mixed success and failure scenarios', async () => {
// Create test metrics
const metricIds = await createTestMetrics(1);
const validDashboardYaml = `
name: Valid Dashboard
description: This should succeed
rows:
- id: 1
items:
- id: ${metricIds[0]}
columnSizes:
- 12
`;
const invalidDashboardYaml = `
name: Invalid Dashboard
description: This should fail
rows:
- id: 1
items:
- id: 00000000-0000-0000-0000-000000000000
columnSizes:
- 12
`;
const input = {
files: [
{ name: 'Valid Dashboard', yml_content: validDashboardYaml },
{ name: 'Invalid Dashboard', yml_content: invalidDashboardYaml },
],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(1);
expect(result.failed_files).toHaveLength(1);
// The success should be the valid dashboard
const createdFile = validateArrayAccess(result.files, 0, 'files access');
expect(createdFile.name).toBe('Valid Dashboard');
// The failure should be due to invalid metric reference
const failure = result.failed_files.find((f) => f.name === 'Invalid Dashboard');
expect(failure?.error).toContain('metrics referenced in the dashboard do not exist');
// Track created dashboard for cleanup
createdDashboardIds.push(createdFile.id);
});
test('should properly format response timing', async () => {
// Create test metric
const metricIds = await createTestMetrics(1);
const validYaml = `
name: Timing Test Dashboard
description: Test response timing
rows:
- id: 1
items:
- id: ${metricIds[0]}
columnSizes:
- 12
`;
const input = {
files: [{ name: 'Timing Test Dashboard', yml_content: validYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.duration).toBeGreaterThan(0);
expect(typeof result.duration).toBe('number');
expect(result.duration).toBeLessThan(10000); // Should complete within 10 seconds
// Track created dashboard for cleanup
if (result.files.length > 0) {
const createdFile = validateArrayAccess(result.files, 0, 'files access');
createdDashboardIds.push(createdFile.id);
}
});
test('should handle bulk dashboard operations correctly', async () => {
// Create test metrics
const metricIds = await createTestMetrics(4);
const createDashboardYaml = (index: number) => `
name: Bulk Dashboard ${index}
description: Dashboard ${index} for bulk testing
rows:
- id: 1
items:
- id: ${metricIds[index - 1]}
columnSizes:
- 12
`;
const files = Array.from({ length: 3 }, (_, i) => ({
name: `Bulk Dashboard ${i + 1}`,
yml_content: createDashboardYaml(i + 1),
}));
const input = {
files,
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(3);
expect(result.failed_files).toHaveLength(0);
expect(result.message).toBe('Successfully created 3 dashboard files.');
// Verify all files have proper structure
for (const file of result.files) {
expect(file.file_type).toBe('dashboard');
expect(file.version_number).toBe(1);
expect(file.name).toContain('Bulk Dashboard');
// Note: yml_content is not returned in the output schema
}
// Track created dashboards for cleanup
createdDashboardIds.push(...result.files.map((f) => f.id));
});
test('should handle complex dashboard with multiple rows', async () => {
// Create test metrics
const metricIds = await createTestMetrics(4);
const complexDashboardYaml = `
name: Complex Dashboard
description: Dashboard with multiple rows and different layouts
rows:
- id: 1
items:
- id: ${metricIds[0]}
columnSizes:
- 12
- id: 2
items:
- id: ${metricIds[1]}
- id: ${metricIds[2]}
columnSizes:
- 6
- 6
- id: 3
items:
- id: ${metricIds[3]}
columnSizes:
- 12
`;
const input = {
files: [{ name: 'Complex Dashboard', yml_content: complexDashboardYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(1);
expect(result.failed_files).toHaveLength(0);
const createdFile = validateArrayAccess(result.files, 0, 'files access');
expect(createdFile.name).toBe('Complex Dashboard');
// Track created dashboard for cleanup
createdDashboardIds.push(createdFile.id);
// Verify all metric-dashboard associations were created
const associations = await db
.select()
.from(metricFilesToDashboardFiles)
.where(eq(metricFilesToDashboardFiles.dashboardFileId, createdFile.id))
.execute();
expect(associations).toHaveLength(4);
expect(associations.map((a) => a.metricFileId).sort()).toEqual(metricIds.sort());
});
test('should handle dashboard with maximum items per row', async () => {
// Create test metrics
const metricIds = await createTestMetrics(4);
const maxItemsDashboardYaml = `
name: Max Items Dashboard
description: Dashboard with maximum 4 items per row
rows:
- id: 1
items:
- id: ${metricIds[0]}
- id: ${metricIds[1]}
- id: ${metricIds[2]}
- id: ${metricIds[3]}
columnSizes:
- 3
- 3
- 3
- 3
`;
const input = {
files: [{ name: 'Max Items Dashboard', yml_content: maxItemsDashboardYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(1);
expect(result.failed_files).toHaveLength(0);
const createdFile = validateArrayAccess(result.files, 0, 'files access');
expect(createdFile.name).toBe('Max Items Dashboard');
// Track created dashboard for cleanup
createdDashboardIds.push(createdFile.id);
// Verify dashboard content
const dashboardFile = await db
.select()
.from(dashboardFiles)
.where(eq(dashboardFiles.id, createdFile.id))
.execute();
expect(dashboardFile).toHaveLength(1);
const dashboard = validateArrayAccess(dashboardFile, 0, 'dashboardFile access');
// Define a minimal type for dashboard content
interface DashboardContent {
rows: Array<{
items: unknown[];
columnSizes: number[];
}>;
}
const content = dashboard.content as DashboardContent;
const firstRow = validateArrayAccess(content.rows, 0, 'rows access');
expect(firstRow.items).toHaveLength(4);
expect(firstRow.columnSizes).toEqual([3, 3, 3, 3]);
});
test('should generate appropriate success and error messages', async () => {
// Test success message
const metricIds = await createTestMetrics(1);
const validYaml = `
name: Success Message Test
description: Test success message generation
rows:
- id: 1
items:
- id: ${metricIds[0]}
columnSizes:
- 12
`;
const successInput = {
files: [{ name: 'Success Message Test', yml_content: validYaml }],
runtimeContext: mockRuntimeContext,
};
const successResult = await createDashboards.execute({
context: successInput,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(successResult.message).toBe('Successfully created 1 dashboard files.');
// Track created dashboard for cleanup
if (successResult.files.length > 0) {
const createdFile = validateArrayAccess(successResult.files, 0, 'files access');
createdDashboardIds.push(createdFile.id);
}
// Test failure message
const invalidYaml = 'invalid yaml structure for dashboard';
const failureInput = {
files: [{ name: 'Failure Test', yml_content: invalidYaml }],
runtimeContext: mockRuntimeContext,
};
const failureResult = await createDashboards.execute({
context: failureInput,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(failureResult.message).toContain("Failed to create 'Failure Test'");
});
test('should validate dashboard with different column size combinations', async () => {
// Create test metrics
const metricIds = await createTestMetrics(3);
const validCombinationsYaml = `
name: Column Combinations Dashboard
description: Dashboard testing different valid column combinations
rows:
- id: 1
items:
- id: ${metricIds[0]}
- id: ${metricIds[1]}
columnSizes:
- 4
- 8
- id: 2
items:
- id: ${metricIds[2]}
columnSizes:
- 12
`;
const input = {
files: [{ name: 'Column Combinations Dashboard', yml_content: validCombinationsYaml }],
runtimeContext: mockRuntimeContext,
};
const result = await createDashboards.execute({
context: input,
runtimeContext: mockRuntimeContext as unknown as RuntimeContext,
});
expect(result.files).toHaveLength(1);
expect(result.failed_files).toHaveLength(0);
const createdFile = validateArrayAccess(result.files, 0, 'files access');
expect(createdFile.name).toBe('Column Combinations Dashboard');
// Track created dashboard for cleanup
createdDashboardIds.push(createdFile.id);
});
});