buster/packages/ai/tests/tools/unit/modify-metrics-file-tool.te...

607 lines
20 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import * as yaml from 'yaml';
import { z } from 'zod';
import { validateArrayAccess } from '../../../src/utils/validation-helpers';
// Import the schemas we want to test (extracted from the tool file)
const columnLabelFormatSchema = z.object({
columnType: z.enum(['number', 'string', 'date']),
style: z.enum(['currency', 'percent', 'number', 'date', 'string']),
multiplier: z.number().optional(),
displayName: z.string().optional(),
numberSeparatorStyle: z.string().nullable().optional(),
minimumFractionDigits: z.number().optional(),
maximumFractionDigits: z.number().optional(),
prefix: z.string().optional(),
suffix: z.string().optional(),
replaceMissingDataWith: z.number().nullable().optional(),
compactNumbers: z.boolean().optional(),
currency: z.string().optional(),
dateFormat: z.string().optional(),
useRelativeTime: z.boolean().optional(),
isUtc: z.boolean().optional(),
convertNumberTo: z.enum(['day_of_week', 'month_of_year', 'quarter']).optional(),
});
const baseChartConfigSchema = z.object({
selectedChartType: z.enum(['bar', 'line', 'scatter', 'pie', 'combo', 'metric', 'table']),
columnLabelFormats: z.record(columnLabelFormatSchema),
columnSettings: z.record(z.any()).optional(),
colors: z.array(z.string()).optional(),
showLegend: z.boolean().optional(),
gridLines: z.boolean().optional(),
showLegendHeadline: z.union([z.boolean(), z.string()]).optional(),
goalLines: z.array(z.any()).optional(),
trendlines: z.array(z.any()).optional(),
disableTooltip: z.boolean().optional(),
});
const tableChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('table'),
tableColumnOrder: z.array(z.string()).optional(),
});
const metricChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('metric'),
metricColumnId: z.string(),
});
const barChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('bar'),
});
const lineChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('line'),
});
const scatterChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('scatter'),
});
const pieChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('pie'),
});
const comboChartConfigSchema = baseChartConfigSchema.extend({
selectedChartType: z.literal('combo'),
});
const chartConfigSchema = z.discriminatedUnion('selectedChartType', [
tableChartConfigSchema,
metricChartConfigSchema,
barChartConfigSchema,
lineChartConfigSchema,
scatterChartConfigSchema,
pieChartConfigSchema,
comboChartConfigSchema,
]);
const metricYmlSchema = z.object({
name: z.string().min(1),
description: z.string().min(1),
timeFrame: z.string().min(1),
sql: z.string().min(1),
chartConfig: chartConfigSchema,
});
// Test the core validation functions
function validateSqlBasic(sqlQuery: string): { success: boolean; error?: string } {
if (!sqlQuery.trim()) {
return { success: false, error: 'SQL query cannot be empty' };
}
if (!sqlQuery.toLowerCase().includes('select')) {
return { success: false, error: 'SQL query must contain SELECT statement' };
}
if (!sqlQuery.toLowerCase().includes('from')) {
return { success: false, error: 'SQL query must contain FROM clause' };
}
return { success: true };
}
function parseAndValidateYaml(ymlContent: string): {
success: boolean;
error?: string;
data?: z.infer<typeof metricYmlSchema>;
} {
try {
const parsedYml = yaml.parse(ymlContent);
const validationResult = metricYmlSchema.safeParse(parsedYml);
if (!validationResult.success) {
return {
success: false,
error: `Invalid YAML structure: ${validationResult.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
};
}
return { success: true, data: validationResult.data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'YAML parsing failed',
};
}
}
describe('Modify Metrics File Tool Unit Tests', () => {
describe('YAML Schema Validation for Updates', () => {
test('should validate correct updated table chart YAML', () => {
const updatedTableYaml = `
name: Updated Sales Summary
description: Updated summary of sales data
timeFrame: Last 60 days
sql: |
SELECT
product_name,
order_date,
SUM(amount) as total_amount,
COUNT(*) as order_count
FROM sales
WHERE order_date >= CURRENT_DATE - INTERVAL '60 days'
GROUP BY product_name, order_date
ORDER BY order_date DESC
chartConfig:
selectedChartType: table
tableColumnOrder:
- product_name
- order_date
- total_amount
- order_count
columnLabelFormats:
product_name:
columnType: string
style: string
numberSeparatorStyle: null
replaceMissingDataWith: null
order_date:
columnType: date
style: date
dateFormat: "MMM D, YYYY"
numberSeparatorStyle: null
replaceMissingDataWith: null
total_amount:
columnType: number
style: currency
currency: "USD"
numberSeparatorStyle: ","
replaceMissingDataWith: 0
order_count:
columnType: number
style: number
numberSeparatorStyle: ","
replaceMissingDataWith: 0
`;
const result = parseAndValidateYaml(updatedTableYaml);
expect(result.success).toBe(true);
if (result.success && result.data) {
expect(result.data.name).toBe('Updated Sales Summary');
expect(result.data.timeFrame).toBe('Last 60 days');
expect(result.data.chartConfig.selectedChartType).toBe('table');
}
});
test('should validate correct updated metric chart YAML', () => {
const updatedMetricYaml = `
name: Updated Total Revenue
description: Updated total revenue amount
timeFrame: Year to Date
sql: |
SELECT SUM(amount) as total_revenue
FROM sales
WHERE EXTRACT(YEAR FROM order_date) = EXTRACT(YEAR FROM CURRENT_DATE)
chartConfig:
selectedChartType: metric
metricColumnId: total_revenue
columnLabelFormats:
total_revenue:
columnType: number
style: currency
currency: "USD"
numberSeparatorStyle: ","
replaceMissingDataWith: 0
compactNumbers: true
`;
const result = parseAndValidateYaml(updatedMetricYaml);
expect(result.success).toBe(true);
if (result.success && result.data) {
expect(result.data.name).toBe('Updated Total Revenue');
expect(result.data.timeFrame).toBe('Year to Date');
expect(result.data.chartConfig.selectedChartType).toBe('metric');
if (result.data.chartConfig.selectedChartType === 'metric') {
expect(result.data.chartConfig.metricColumnId).toBe('total_revenue');
}
}
});
test('should reject updated YAML with missing required fields', () => {
const incompleteYaml = `
name: Incomplete Update
description: Missing timeFrame and sql
chartConfig:
selectedChartType: table
columnLabelFormats:
test:
columnType: string
style: string
numberSeparatorStyle: null
replaceMissingDataWith: null
`;
const result = parseAndValidateYaml(incompleteYaml);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid YAML structure');
});
test('should reject updated YAML with invalid chart type', () => {
const invalidChartTypeYaml = `
name: Invalid Chart Update
description: Testing invalid chart type
timeFrame: Today
sql: SELECT * FROM test
chartConfig:
selectedChartType: invalid_type
columnLabelFormats:
test:
columnType: string
style: string
numberSeparatorStyle: null
replaceMissingDataWith: null
`;
const result = parseAndValidateYaml(invalidChartTypeYaml);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid YAML structure');
});
test('should reject updated metric chart without required metricColumnId', () => {
const invalidMetricUpdateYaml = `
name: Invalid Metric Update
description: Missing metricColumnId
timeFrame: Today
sql: SELECT * FROM test
chartConfig:
selectedChartType: metric
# Missing metricColumnId
columnLabelFormats:
test:
columnType: number
style: number
numberSeparatorStyle: null
replaceMissingDataWith: null
`;
const result = parseAndValidateYaml(invalidMetricUpdateYaml);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid YAML structure');
});
test('should validate complex chart configuration updates', () => {
const complexUpdateYaml = `
name: Complex Chart Update
description: Testing complex chart configuration
timeFrame: Last Quarter
sql: |
SELECT
DATE_TRUNC('month', order_date) as month,
product_category,
SUM(amount) as revenue,
COUNT(*) as orders
FROM sales
WHERE order_date >= DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '3 months'
GROUP BY DATE_TRUNC('month', order_date), product_category
ORDER BY month, product_category
chartConfig:
selectedChartType: bar
showLegend: true
gridLines: true
colors: ["#FF5733", "#33FF57", "#3357FF"]
columnLabelFormats:
month:
columnType: date
style: date
dateFormat: "MMM YYYY"
numberSeparatorStyle: null
replaceMissingDataWith: null
product_category:
columnType: string
style: string
numberSeparatorStyle: null
replaceMissingDataWith: null
revenue:
columnType: number
style: currency
currency: "USD"
numberSeparatorStyle: ","
replaceMissingDataWith: 0
compactNumbers: true
orders:
columnType: number
style: number
numberSeparatorStyle: ","
replaceMissingDataWith: 0
`;
const result = parseAndValidateYaml(complexUpdateYaml);
expect(result.success).toBe(true);
if (result.success && result.data) {
expect(result.data.chartConfig.showLegend).toBe(true);
expect(result.data.chartConfig.colors).toEqual(['#FF5733', '#33FF57', '#3357FF']);
}
});
});
describe('SQL Validation for Updates', () => {
test('should accept valid updated SELECT SQL', () => {
const updatedSql =
"SELECT id, name, amount, created_at FROM updated_sales WHERE created_at > NOW() - INTERVAL '7 days'";
const result = validateSqlBasic(updatedSql);
expect(result.success).toBe(true);
});
test('should reject empty SQL in updates', () => {
const result = validateSqlBasic('');
expect(result.success).toBe(false);
expect(result.error).toBe('SQL query cannot be empty');
});
test('should reject SQL without SELECT in updates', () => {
const noSelectSql = 'UPDATE test SET value = 1 WHERE id = 1';
const result = validateSqlBasic(noSelectSql);
expect(result.success).toBe(false);
expect(result.error).toBe('SQL query must contain SELECT statement');
});
test('should reject SQL without FROM in updates', () => {
const noFromSql = 'SELECT NOW()';
const result = validateSqlBasic(noFromSql);
expect(result.success).toBe(false);
expect(result.error).toBe('SQL query must contain FROM clause');
});
test('should handle complex updated SQL queries', () => {
const complexUpdatedSql = `
SELECT
c.customer_name,
p.product_name,
COUNT(o.id) as order_count,
SUM(o.total_amount) as total_spent,
AVG(o.total_amount) as avg_order_value
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products p ON o.product_id = p.id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)
AND o.status = 'completed'
GROUP BY c.customer_name, p.product_name
HAVING order_count >= 2
ORDER BY total_spent DESC
LIMIT 50
`;
const result = validateSqlBasic(complexUpdatedSql);
expect(result.success).toBe(true);
});
});
describe('Input Schema Validation for Updates', () => {
test('should validate correct update input format', () => {
const validUpdateInput = {
files: [
{
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
yml_content:
'name: Updated Metric\ndescription: Updated description\ntimeFrame: Today\nsql: SELECT * FROM updated_test\nchartConfig:\n selectedChartType: table\n columnLabelFormats:\n test:\n columnType: string\n style: string\n numberSeparatorStyle: null\n replaceMissingDataWith: null',
},
],
};
// Basic validation that files array exists and has proper structure
expect(validUpdateInput.files).toHaveLength(1);
const firstFile = validateArrayAccess(validUpdateInput.files, 0, 'files');
expect(firstFile?.id).toBe('f47ac10b-58cc-4372-a567-0e02b2c3d479');
expect(typeof firstFile?.yml_content).toBe('string');
});
test('should reject update input without file ID', () => {
const invalidInput = {
files: [
{
// Missing id
yml_content: 'name: Test',
},
],
};
// This would fail our ID validation requirement
expect(invalidInput.files?.[0]).not.toHaveProperty('id');
});
test('should reject update input with invalid UUID', () => {
const invalidUuidInput = {
files: [
{
id: 'not-a-valid-uuid',
yml_content: 'name: Test',
},
],
};
// This would fail our UUID validation
expect(invalidUuidInput.files[0].id).toBe('not-a-valid-uuid');
// In real validation, this would be rejected as not a valid UUID
});
test('should validate bulk update input', () => {
const bulkUpdateInput = {
files: [
{
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
yml_content:
'name: First Updated Metric\ndescription: First update\ntimeFrame: Today\nsql: SELECT * FROM test1\nchartConfig:\n selectedChartType: table\n columnLabelFormats:\n test:\n columnType: string\n style: string\n numberSeparatorStyle: null\n replaceMissingDataWith: null',
},
{
id: 'a47ac10b-58cc-4372-a567-0e02b2c3d480',
yml_content:
'name: Second Updated Metric\ndescription: Second update\ntimeFrame: Last 7 days\nsql: SELECT * FROM test2\nchartConfig:\n selectedChartType: metric\n metricColumnId: test\n columnLabelFormats:\n test:\n columnType: number\n style: number\n numberSeparatorStyle: null\n replaceMissingDataWith: null',
},
],
};
expect(bulkUpdateInput.files).toHaveLength(2);
expect(bulkUpdateInput.files.every((f) => f.id && f.yml_content)).toBe(true);
});
});
describe('Column Label Format Validation for Updates', () => {
test('should validate updated number column format', () => {
const updatedNumberFormat = {
columnType: 'number' as const,
style: 'percent' as const,
numberSeparatorStyle: ',',
replaceMissingDataWith: 0,
minimumFractionDigits: 2,
maximumFractionDigits: 4,
};
const result = columnLabelFormatSchema.safeParse(updatedNumberFormat);
expect(result.success).toBe(true);
});
test('should validate updated date column format', () => {
const updatedDateFormat = {
columnType: 'date' as const,
style: 'date' as const,
dateFormat: 'YYYY-MM-DD',
useRelativeTime: true,
isUtc: false,
numberSeparatorStyle: null,
replaceMissingDataWith: null,
};
const result = columnLabelFormatSchema.safeParse(updatedDateFormat);
expect(result.success).toBe(true);
});
test('should validate updated string column format with display name', () => {
const updatedStringFormat = {
columnType: 'string' as const,
style: 'string' as const,
displayName: 'Updated Column Name',
prefix: 'Updated: ',
suffix: ' (modified)',
numberSeparatorStyle: null,
replaceMissingDataWith: null,
};
const result = columnLabelFormatSchema.safeParse(updatedStringFormat);
expect(result.success).toBe(true);
});
});
describe('Error Message Generation for Updates', () => {
test('should generate appropriate error message for invalid YAML in updates', () => {
const invalidUpdateYaml = 'invalid: yaml: [structure for update';
const result = parseAndValidateYaml(invalidUpdateYaml);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(typeof result.error).toBe('string');
});
test('should generate appropriate error message for SQL validation in updates', () => {
const invalidSql = 'DELETE FROM test WHERE id = 1';
const result = validateSqlBasic(invalidSql);
expect(result.success).toBe(false);
expect(result.error).toBe('SQL query must contain SELECT statement');
});
test('should handle nested validation errors in updates', () => {
const complexInvalidYaml = `
name: Test Update
description: Test
timeFrame: Today
sql: SELECT * FROM test
chartConfig:
selectedChartType: metric
# Missing required metricColumnId for metric type
columnLabelFormats:
test:
columnType: invalid_type # Invalid column type
style: string
numberSeparatorStyle: null
replaceMissingDataWith: null
`;
const result = parseAndValidateYaml(complexInvalidYaml);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid YAML structure');
});
});
describe('Version Management Concepts', () => {
test('should understand version increment logic', () => {
// Test the concept of version management
const currentVersion = 3;
const nextVersion = currentVersion + 1;
expect(nextVersion).toBe(4);
});
test('should handle version history concepts', () => {
// Mock version history structure
const mockVersionHistory = {
versions: [
{ versionNumber: 1, content: { name: 'Original' } },
{ versionNumber: 2, content: { name: 'First Update' } },
{ versionNumber: 3, content: { name: 'Second Update' } },
],
getLatestVersion: function () {
return this.versions[this.versions.length - 1];
},
};
const latestVersion = mockVersionHistory.getLatestVersion();
expect(latestVersion.versionNumber).toBe(3);
expect(latestVersion.content.name).toBe('Second Update');
});
});
describe('Modification Result Tracking', () => {
test('should track successful modifications', () => {
const successResult = {
file_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
file_name: 'Updated Test Metric',
success: true,
modification_type: 'content',
timestamp: new Date().toISOString(),
duration: 150,
};
expect(successResult.success).toBe(true);
expect(successResult.modification_type).toBe('content');
expect(typeof successResult.timestamp).toBe('string');
});
test('should track failed modifications', () => {
const failureResult = {
file_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
file_name: 'Failed Update Metric',
success: false,
error: 'Invalid YAML structure',
modification_type: 'validation',
timestamp: new Date().toISOString(),
duration: 75,
};
expect(failureResult.success).toBe(false);
expect(failureResult.error).toBe('Invalid YAML structure');
expect(failureResult.modification_type).toBe('validation');
});
});
});