mirror of https://github.com/buster-so/buster.git
834 lines
27 KiB
TypeScript
834 lines
27 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import type {
|
|
ChatMessageReasoningMessage,
|
|
ChatMessageResponseMessage,
|
|
} from '../../../../../server/src/types/chat-types/chat-message.type';
|
|
import { hasFailureIndicators, hasFileFailureIndicators } from '../../../src/utils/database/types';
|
|
|
|
// Import the functions we want to test (we'll need to export them from analyst-step.ts)
|
|
// For now, I'll copy them here for testing
|
|
interface ExtractedFile {
|
|
id: string;
|
|
fileType: 'metric' | 'dashboard';
|
|
fileName: string;
|
|
status: 'completed' | 'failed' | 'loading';
|
|
ymlContent?: string;
|
|
}
|
|
|
|
function extractFilesFromReasoning(
|
|
reasoningHistory: ChatMessageReasoningMessage[]
|
|
): ExtractedFile[] {
|
|
const files: ExtractedFile[] = [];
|
|
|
|
for (const entry of reasoningHistory) {
|
|
// Multi-layer safety checks:
|
|
// 1. Must be a files entry with completed status
|
|
// 2. Must not have any failure indicators (additional safety net)
|
|
// 3. Individual files must have completed status
|
|
if (
|
|
entry.type === 'files' &&
|
|
entry.status === 'completed' &&
|
|
entry.files &&
|
|
!hasFailureIndicators(entry)
|
|
) {
|
|
for (const fileId of entry.file_ids || []) {
|
|
const file = entry.files[fileId];
|
|
|
|
// Enhanced file validation:
|
|
// - File must exist and have completed status
|
|
// - File must not have error indicators
|
|
// - File must have required properties (file_type, file_name)
|
|
if (
|
|
file &&
|
|
file.status === 'completed' &&
|
|
file.file_type &&
|
|
file.file_name &&
|
|
!hasFileFailureIndicators(file)
|
|
) {
|
|
files.push({
|
|
id: fileId,
|
|
fileType: file.file_type as 'metric' | 'dashboard',
|
|
fileName: file.file_name,
|
|
status: 'completed',
|
|
ymlContent: file.file?.text,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function selectFilesForResponse(files: ExtractedFile[]): ExtractedFile[] {
|
|
// Separate dashboards and metrics
|
|
const dashboards = files.filter((f) => f.fileType === 'dashboard');
|
|
const metrics = files.filter((f) => f.fileType === 'metric');
|
|
|
|
// Apply priority logic
|
|
if (dashboards.length > 0) {
|
|
return dashboards; // Return all dashboards
|
|
}
|
|
if (metrics.length > 0) {
|
|
return metrics; // Return all metrics
|
|
}
|
|
|
|
return []; // No files to return
|
|
}
|
|
|
|
function createFileResponseMessages(files: ExtractedFile[]): ChatMessageResponseMessage[] {
|
|
return files.map((file) => ({
|
|
id: crypto.randomUUID(),
|
|
type: 'file' as const,
|
|
file_type: file.fileType,
|
|
file_name: file.fileName,
|
|
version_number: 1,
|
|
filter_version_id: null,
|
|
metadata: [
|
|
{
|
|
status: 'completed' as const,
|
|
message: `${file.fileType === 'dashboard' ? 'Dashboard' : 'Metric'} created successfully`,
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
}));
|
|
}
|
|
|
|
describe('Analyst Step File Selection', () => {
|
|
describe('extractFilesFromReasoning', () => {
|
|
test('should extract completed metric files', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating 2 metrics',
|
|
status: 'completed',
|
|
file_ids: ['file-1', 'file-2'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'revenue_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: {
|
|
text: 'metric: revenue\ntype: sum',
|
|
},
|
|
},
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'user_count_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: {
|
|
text: 'metric: user_count\ntype: count',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
expect(extracted).toHaveLength(2);
|
|
expect(extracted[0]).toEqual({
|
|
id: 'file-1',
|
|
fileType: 'metric',
|
|
fileName: 'revenue_metric.yml',
|
|
status: 'completed',
|
|
ymlContent: 'metric: revenue\ntype: sum',
|
|
});
|
|
expect(extracted[1]).toEqual({
|
|
id: 'file-2',
|
|
fileType: 'metric',
|
|
fileName: 'user_count_metric.yml',
|
|
status: 'completed',
|
|
ymlContent: 'metric: user_count\ntype: count',
|
|
});
|
|
});
|
|
|
|
test('should extract completed dashboard files', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating 1 dashboard',
|
|
status: 'completed',
|
|
file_ids: ['dash-1'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'sales_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: {
|
|
text: 'dashboard: sales\nmetrics: [revenue, user_count]',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
expect(extracted).toHaveLength(1);
|
|
expect(extracted[0]).toEqual({
|
|
id: 'dash-1',
|
|
fileType: 'dashboard',
|
|
fileName: 'sales_dashboard.yml',
|
|
status: 'completed',
|
|
ymlContent: 'dashboard: sales\nmetrics: [revenue, user_count]',
|
|
});
|
|
});
|
|
|
|
test('should skip files with loading or failed status', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed', // Entry is completed but individual files may not be
|
|
file_ids: ['file-1', 'file-2', 'file-3'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'metric1.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'completed metric' },
|
|
},
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'metric2.yml',
|
|
version_number: 1,
|
|
status: 'loading',
|
|
file: { text: 'loading metric' },
|
|
},
|
|
'file-3': {
|
|
id: 'file-3',
|
|
file_type: 'metric',
|
|
file_name: 'metric3.yml',
|
|
version_number: 1,
|
|
status: 'failed',
|
|
file: { text: 'failed metric' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
expect(extracted).toHaveLength(1);
|
|
const firstExtracted = extracted[0];
|
|
expect(firstExtracted).toBeDefined();
|
|
expect(firstExtracted?.id).toBe('file-1');
|
|
expect(firstExtracted?.status).toBe('completed');
|
|
});
|
|
|
|
test('should skip reasoning entries with non-completed status', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'loading', // Still in progress
|
|
file_ids: ['file-1'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric content' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle mixed file types from multiple entries', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-1', 'metric-2'],
|
|
files: {
|
|
'metric-1': {
|
|
id: 'metric-1',
|
|
file_type: 'metric',
|
|
file_name: 'metric1.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 1' },
|
|
},
|
|
'metric-2': {
|
|
id: 'metric-2',
|
|
file_type: 'metric',
|
|
file_name: 'metric2.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 2' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'reason-2',
|
|
type: 'files',
|
|
title: 'Creating dashboard',
|
|
status: 'completed',
|
|
file_ids: ['dash-1'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard content' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
expect(extracted).toHaveLength(3);
|
|
expect(extracted.filter((f) => f.fileType === 'metric')).toHaveLength(2);
|
|
expect(extracted.filter((f) => f.fileType === 'dashboard')).toHaveLength(1);
|
|
});
|
|
|
|
test('should handle empty reasoning history', () => {
|
|
const extracted = extractFilesFromReasoning([]);
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
|
|
test('should reject files from entries with error indicators (failure detection)', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'entry-with-error',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed', // Status says completed
|
|
error: 'Validation warnings occurred', // But has error field - this should prevent file extraction
|
|
file_ids: ['file-1'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: test' },
|
|
},
|
|
},
|
|
} as any,
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should extract no files due to error indicator in entry
|
|
// With our new logic, entry-level errors should still prevent extraction
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
|
|
test('should reject individual files with error properties (enhanced validation)', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'entry-clean',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['file-1', 'file-2'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'good_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: good' },
|
|
},
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'problematic_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed', // Status completed
|
|
error: 'Schema validation warning', // But has error property
|
|
file: { text: 'metric: problematic' },
|
|
} as any,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the file without error properties
|
|
expect(extracted).toHaveLength(1);
|
|
const goodFile = extracted[0];
|
|
expect(goodFile).toBeDefined();
|
|
expect(goodFile?.id).toBe('file-1');
|
|
expect(goodFile?.fileName).toBe('good_metric.yml');
|
|
});
|
|
|
|
test('should skip non-file reasoning entries', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'text',
|
|
title: 'Thinking...',
|
|
status: 'completed',
|
|
message: 'Some thinking process',
|
|
},
|
|
{
|
|
id: 'reason-2',
|
|
type: 'pills',
|
|
title: 'Analysis',
|
|
status: 'completed',
|
|
pill_containers: [],
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('selectFilesForResponse', () => {
|
|
test('should return all dashboards when dashboards exist', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'dash-1', fileType: 'dashboard', fileName: 'dash1.yml', status: 'completed' },
|
|
{ id: 'dash-2', fileType: 'dashboard', fileName: 'dash2.yml', status: 'completed' },
|
|
{ id: 'metric-1', fileType: 'metric', fileName: 'metric1.yml', status: 'completed' },
|
|
{ id: 'metric-2', fileType: 'metric', fileName: 'metric2.yml', status: 'completed' },
|
|
];
|
|
|
|
const selected = selectFilesForResponse(files);
|
|
|
|
expect(selected).toHaveLength(2);
|
|
expect(selected.every((f) => f.fileType === 'dashboard')).toBe(true);
|
|
expect(selected.map((f) => f.id)).toEqual(['dash-1', 'dash-2']);
|
|
});
|
|
|
|
test('should return all metrics when only metrics exist', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'metric-1', fileType: 'metric', fileName: 'metric1.yml', status: 'completed' },
|
|
{ id: 'metric-2', fileType: 'metric', fileName: 'metric2.yml', status: 'completed' },
|
|
{ id: 'metric-3', fileType: 'metric', fileName: 'metric3.yml', status: 'completed' },
|
|
];
|
|
|
|
const selected = selectFilesForResponse(files);
|
|
|
|
expect(selected).toHaveLength(3);
|
|
expect(selected.every((f) => f.fileType === 'metric')).toBe(true);
|
|
expect(selected.map((f) => f.id)).toEqual(['metric-1', 'metric-2', 'metric-3']);
|
|
});
|
|
|
|
test('should return single metric when only one metric exists', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'metric-1', fileType: 'metric', fileName: 'metric1.yml', status: 'completed' },
|
|
];
|
|
|
|
const selected = selectFilesForResponse(files);
|
|
|
|
expect(selected).toHaveLength(1);
|
|
const firstSelected = selected[0];
|
|
expect(firstSelected).toBeDefined();
|
|
expect(firstSelected?.id).toBe('metric-1');
|
|
expect(firstSelected?.fileType).toBe('metric');
|
|
});
|
|
|
|
test('should return single dashboard when only one dashboard exists', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'dash-1', fileType: 'dashboard', fileName: 'dash1.yml', status: 'completed' },
|
|
];
|
|
|
|
const selected = selectFilesForResponse(files);
|
|
|
|
expect(selected).toHaveLength(1);
|
|
const firstSelected = selected[0];
|
|
expect(firstSelected).toBeDefined();
|
|
expect(firstSelected?.id).toBe('dash-1');
|
|
expect(firstSelected?.fileType).toBe('dashboard');
|
|
});
|
|
|
|
test('should return empty array when no files exist', () => {
|
|
const selected = selectFilesForResponse([]);
|
|
expect(selected).toHaveLength(0);
|
|
});
|
|
|
|
test('should prioritize single dashboard over multiple metrics', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'metric-1', fileType: 'metric', fileName: 'metric1.yml', status: 'completed' },
|
|
{ id: 'metric-2', fileType: 'metric', fileName: 'metric2.yml', status: 'completed' },
|
|
{ id: 'metric-3', fileType: 'metric', fileName: 'metric3.yml', status: 'completed' },
|
|
{ id: 'dash-1', fileType: 'dashboard', fileName: 'dash1.yml', status: 'completed' },
|
|
];
|
|
|
|
const selected = selectFilesForResponse(files);
|
|
|
|
expect(selected).toHaveLength(1);
|
|
const firstSelected = selected[0];
|
|
expect(firstSelected).toBeDefined();
|
|
expect(firstSelected?.id).toBe('dash-1');
|
|
expect(firstSelected?.fileType).toBe('dashboard');
|
|
});
|
|
});
|
|
|
|
describe('createFileResponseMessages', () => {
|
|
test('should create response messages for metric files', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'metric-1', fileType: 'metric', fileName: 'revenue.yml', status: 'completed' },
|
|
{ id: 'metric-2', fileType: 'metric', fileName: 'users.yml', status: 'completed' },
|
|
];
|
|
|
|
const messages = createFileResponseMessages(files);
|
|
|
|
expect(messages).toHaveLength(2);
|
|
|
|
const firstMessage = messages[0];
|
|
expect(firstMessage).toBeDefined();
|
|
expect(firstMessage).toMatchObject({
|
|
type: 'file',
|
|
file_type: 'metric',
|
|
file_name: 'revenue.yml',
|
|
version_number: 1,
|
|
filter_version_id: null,
|
|
});
|
|
if (firstMessage && firstMessage.type === 'file' && firstMessage.metadata) {
|
|
const firstMetadata = firstMessage.metadata[0];
|
|
expect(firstMetadata).toBeDefined();
|
|
expect(firstMetadata).toMatchObject({
|
|
status: 'completed',
|
|
message: 'Metric created successfully',
|
|
});
|
|
expect(firstMetadata?.timestamp).toBeTypeOf('number');
|
|
}
|
|
|
|
expect(messages[1]).toMatchObject({
|
|
type: 'file',
|
|
file_type: 'metric',
|
|
file_name: 'users.yml',
|
|
version_number: 1,
|
|
});
|
|
});
|
|
|
|
test('should create response messages for dashboard files', () => {
|
|
const files: ExtractedFile[] = [
|
|
{
|
|
id: 'dash-1',
|
|
fileType: 'dashboard',
|
|
fileName: 'sales_dashboard.yml',
|
|
status: 'completed',
|
|
},
|
|
];
|
|
|
|
const messages = createFileResponseMessages(files);
|
|
|
|
expect(messages).toHaveLength(1);
|
|
|
|
const firstMessage = messages[0];
|
|
expect(firstMessage).toBeDefined();
|
|
expect(firstMessage).toMatchObject({
|
|
type: 'file',
|
|
file_type: 'dashboard',
|
|
file_name: 'sales_dashboard.yml',
|
|
version_number: 1,
|
|
filter_version_id: null,
|
|
});
|
|
if (firstMessage && firstMessage.type === 'file' && firstMessage.metadata) {
|
|
const firstMetadata = firstMessage.metadata[0];
|
|
expect(firstMetadata).toBeDefined();
|
|
expect(firstMetadata).toMatchObject({
|
|
status: 'completed',
|
|
message: 'Dashboard created successfully',
|
|
});
|
|
}
|
|
});
|
|
|
|
test('should generate unique IDs for each message', () => {
|
|
const files: ExtractedFile[] = [
|
|
{ id: 'file-1', fileType: 'metric', fileName: 'metric1.yml', status: 'completed' },
|
|
{ id: 'file-2', fileType: 'metric', fileName: 'metric2.yml', status: 'completed' },
|
|
];
|
|
|
|
const messages = createFileResponseMessages(files);
|
|
|
|
const firstMessage = messages[0];
|
|
const secondMessage = messages[1];
|
|
expect(firstMessage).toBeDefined();
|
|
expect(secondMessage).toBeDefined();
|
|
expect(firstMessage?.id).toBeTypeOf('string');
|
|
expect(secondMessage?.id).toBeTypeOf('string');
|
|
expect(firstMessage?.id).not.toBe(secondMessage?.id);
|
|
});
|
|
|
|
test('should handle empty file array', () => {
|
|
const messages = createFileResponseMessages([]);
|
|
expect(messages).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Integration: Complete File Selection Flow', () => {
|
|
test('Scenario 1: Only dashboards created - return all dashboards', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating dashboards',
|
|
status: 'completed',
|
|
file_ids: ['dash-1', 'dash-2'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'sales_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard 1 content' },
|
|
},
|
|
'dash-2': {
|
|
id: 'dash-2',
|
|
file_type: 'dashboard',
|
|
file_name: 'marketing_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard 2 content' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(2);
|
|
expect(selected).toHaveLength(2);
|
|
expect(messages).toHaveLength(2);
|
|
expect(messages.every((m) => m.type === 'file' && m.file_type === 'dashboard')).toBe(true);
|
|
});
|
|
|
|
test('Scenario 2: Multiple metrics created - return all metrics', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-1', 'metric-2', 'metric-3'],
|
|
files: {
|
|
'metric-1': {
|
|
id: 'metric-1',
|
|
file_type: 'metric',
|
|
file_name: 'revenue.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 1' },
|
|
},
|
|
'metric-2': {
|
|
id: 'metric-2',
|
|
file_type: 'metric',
|
|
file_name: 'users.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 2' },
|
|
},
|
|
'metric-3': {
|
|
id: 'metric-3',
|
|
file_type: 'metric',
|
|
file_name: 'growth.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 3' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(3);
|
|
expect(selected).toHaveLength(3);
|
|
expect(messages).toHaveLength(3);
|
|
expect(messages.every((m) => m.type === 'file' && m.file_type === 'metric')).toBe(true);
|
|
});
|
|
|
|
test('Scenario 3: Single metric created - return the single metric', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metric',
|
|
status: 'completed',
|
|
file_ids: ['metric-1'],
|
|
files: {
|
|
'metric-1': {
|
|
id: 'metric-1',
|
|
file_type: 'metric',
|
|
file_name: 'revenue.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'single metric' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(1);
|
|
expect(selected).toHaveLength(1);
|
|
expect(messages).toHaveLength(1);
|
|
const firstMessage = messages[0];
|
|
expect(firstMessage).toBeDefined();
|
|
expect(firstMessage?.type).toBe('file');
|
|
if (firstMessage && firstMessage.type === 'file') {
|
|
expect(firstMessage.file_type).toBe('metric');
|
|
}
|
|
});
|
|
|
|
test('Scenario 4: Metrics and dashboards created - return only dashboards', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-1', 'metric-2'],
|
|
files: {
|
|
'metric-1': {
|
|
id: 'metric-1',
|
|
file_type: 'metric',
|
|
file_name: 'revenue.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 1' },
|
|
},
|
|
'metric-2': {
|
|
id: 'metric-2',
|
|
file_type: 'metric',
|
|
file_name: 'users.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric 2' },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 'reason-2',
|
|
type: 'files',
|
|
title: 'Creating dashboards',
|
|
status: 'completed',
|
|
file_ids: ['dash-1', 'dash-2'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'sales_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard 1' },
|
|
},
|
|
'dash-2': {
|
|
id: 'dash-2',
|
|
file_type: 'dashboard',
|
|
file_name: 'marketing_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard 2' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(4); // 2 metrics + 2 dashboards
|
|
expect(selected).toHaveLength(2); // Only dashboards
|
|
expect(messages).toHaveLength(2);
|
|
expect(messages.every((m) => m.type === 'file' && m.file_type === 'dashboard')).toBe(true);
|
|
expect(messages.map((m) => (m.type === 'file' ? m.file_name : ''))).toEqual([
|
|
'sales_dashboard.yml',
|
|
'marketing_dashboard.yml',
|
|
]);
|
|
});
|
|
|
|
test('Scenario 5: Modified files are also included', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'files',
|
|
title: 'Modifying metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-1-mod', 'metric-2-mod'],
|
|
files: {
|
|
'metric-1-mod': {
|
|
id: 'metric-1-mod',
|
|
file_type: 'metric',
|
|
file_name: 'revenue_modified.yml',
|
|
version_number: 2,
|
|
status: 'completed',
|
|
file: { text: 'modified metric 1' },
|
|
},
|
|
'metric-2-mod': {
|
|
id: 'metric-2-mod',
|
|
file_type: 'metric',
|
|
file_name: 'users_modified.yml',
|
|
version_number: 2,
|
|
status: 'completed',
|
|
file: { text: 'modified metric 2' },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(2);
|
|
expect(selected).toHaveLength(2);
|
|
expect(messages).toHaveLength(2);
|
|
expect(messages.every((m) => m.type === 'file' && m.file_type === 'metric')).toBe(true);
|
|
});
|
|
|
|
test('Scenario 6: No files created - return empty', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'reason-1',
|
|
type: 'text',
|
|
title: 'Thinking',
|
|
status: 'completed',
|
|
message: 'Just thinking, no files created',
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
const selected = selectFilesForResponse(extracted);
|
|
const messages = createFileResponseMessages(selected);
|
|
|
|
expect(extracted).toHaveLength(0);
|
|
expect(selected).toHaveLength(0);
|
|
expect(messages).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|