mirror of https://github.com/buster-so/buster.git
511 lines
16 KiB
TypeScript
511 lines
16 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import type {
|
|
ChatMessageReasoningMessage,
|
|
ChatMessageReasoningMessage_File,
|
|
ChatMessageReasoningMessage_Files,
|
|
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
|
|
// For now, we'll copy the enhanced functions here for testing
|
|
interface ExtractedFile {
|
|
id: string;
|
|
fileType: 'metric' | 'dashboard';
|
|
fileName: string;
|
|
status: 'completed' | 'failed' | 'loading';
|
|
ymlContent?: string;
|
|
}
|
|
|
|
// Test-specific types to simulate error conditions
|
|
type TestReasoningMessage =
|
|
| ChatMessageReasoningMessage
|
|
| {
|
|
id: string;
|
|
type: 'files';
|
|
title: string;
|
|
status: 'completed' | 'failed' | 'loading';
|
|
file_ids: string[];
|
|
files: Record<string, unknown>; // Using unknown to allow test error properties
|
|
};
|
|
|
|
type TestReasoningFile =
|
|
| ChatMessageReasoningMessage_File
|
|
| {
|
|
id: string;
|
|
file_type?: string;
|
|
file_name?: string;
|
|
version_number: number;
|
|
status: 'completed' | 'failed' | 'loading';
|
|
file: { text: string };
|
|
error?: string; // Test-specific error property
|
|
};
|
|
|
|
/**
|
|
* Enhanced extractFilesFromReasoning with failure detection
|
|
* This is a copy of the enhanced function for testing
|
|
*/
|
|
function extractFilesFromReasoning(reasoningHistory: TestReasoningMessage[]): 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] as TestReasoningFile;
|
|
|
|
// 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;
|
|
}
|
|
|
|
describe('Analyst Step - Failed Tool Handling', () => {
|
|
describe('extractFilesFromReasoning - Failure Detection', () => {
|
|
test('should exclude files from failed reasoning entries', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
{
|
|
id: 'failed-entry',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'failed', // Entry itself failed
|
|
file_ids: ['file-1'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'failed_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed', // File marked as completed, but entry failed
|
|
file: {
|
|
text: 'metric: failed_metric',
|
|
},
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should extract no files because the reasoning entry failed
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
|
|
test('should exclude individual failed files from completed reasoning entries', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
{
|
|
id: 'mixed-entry',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed', // Entry completed, but some files failed
|
|
file_ids: ['file-1', 'file-2', 'file-3'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'successful_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: successful' },
|
|
} as TestReasoningFile,
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'failed_metric.yml',
|
|
version_number: 1,
|
|
status: 'failed', // This file failed
|
|
file: { text: 'metric: failed' },
|
|
} as TestReasoningFile,
|
|
'file-3': {
|
|
id: 'file-3',
|
|
file_type: 'metric',
|
|
file_name: 'loading_metric.yml',
|
|
version_number: 1,
|
|
status: 'loading', // This file still loading
|
|
file: { text: 'metric: loading' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the successful file
|
|
expect(extracted).toHaveLength(1);
|
|
const successfulFile = extracted[0];
|
|
expect(successfulFile).toBeDefined();
|
|
expect(successfulFile?.id).toBe('file-1');
|
|
expect(successfulFile?.fileName).toBe('successful_metric.yml');
|
|
expect(successfulFile?.status).toBe('completed');
|
|
});
|
|
|
|
test('should exclude files with error indicators', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
{
|
|
id: 'entry-with-errors',
|
|
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' },
|
|
} as TestReasoningFile,
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'error_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed', // Status says completed...
|
|
error: 'Validation failed', // But has error field
|
|
file: { text: 'metric: error' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the file without error indicators
|
|
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 exclude files missing required properties', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
{
|
|
id: 'incomplete-files',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['file-1', 'file-2', 'file-3'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'complete_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: complete' },
|
|
} as TestReasoningFile,
|
|
'file-2': {
|
|
id: 'file-2',
|
|
// Missing file_type
|
|
file_name: 'missing_type.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: missing_type' },
|
|
} as TestReasoningFile,
|
|
'file-3': {
|
|
id: 'file-3',
|
|
file_type: 'metric',
|
|
// Missing file_name
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: missing_name' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the complete file
|
|
expect(extracted).toHaveLength(1);
|
|
const completeFile = extracted[0];
|
|
expect(completeFile).toBeDefined();
|
|
expect(completeFile?.id).toBe('file-1');
|
|
expect(completeFile?.fileName).toBe('complete_metric.yml');
|
|
});
|
|
|
|
test('should handle reasoning entries with error indicators', () => {
|
|
// Create a test object with error property that simulates failure conditions
|
|
const entryWithError = {
|
|
id: 'entry-with-error-flag',
|
|
type: 'files' as const,
|
|
title: 'Creating metrics',
|
|
status: 'completed' as const, // Status says completed...
|
|
error: 'Something went wrong', // But has error property (simulated)
|
|
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 TestReasoningFile,
|
|
},
|
|
};
|
|
|
|
const reasoningHistory: TestReasoningMessage[] = [entryWithError];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should extract no files because entry has error indicators
|
|
expect(extracted).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle partial failures in dashboard creation', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
{
|
|
id: 'dashboard-entry',
|
|
type: 'files',
|
|
title: 'Creating dashboards',
|
|
status: 'completed',
|
|
file_ids: ['dash-1', 'dash-2'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'successful_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'dashboard: successful' },
|
|
} as TestReasoningFile,
|
|
'dash-2': {
|
|
id: 'dash-2',
|
|
file_type: 'dashboard',
|
|
file_name: 'failed_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'failed',
|
|
file: { text: 'dashboard: failed' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the successful dashboard
|
|
expect(extracted).toHaveLength(1);
|
|
const successfulDashboard = extracted[0];
|
|
expect(successfulDashboard).toBeDefined();
|
|
expect(successfulDashboard?.fileType).toBe('dashboard');
|
|
expect(successfulDashboard?.fileName).toBe('successful_dashboard.yml');
|
|
});
|
|
|
|
test('should handle completely successful scenarios (regression test)', () => {
|
|
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
|
{
|
|
id: 'successful-entry',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['file-1', 'file-2'],
|
|
files: {
|
|
'file-1': {
|
|
id: 'file-1',
|
|
file_type: 'metric',
|
|
file_name: 'metric1.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: 1' },
|
|
},
|
|
'file-2': {
|
|
id: 'file-2',
|
|
file_type: 'metric',
|
|
file_name: 'metric2.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: 2' },
|
|
},
|
|
},
|
|
} as ChatMessageReasoningMessage_Files,
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory as TestReasoningMessage[]);
|
|
|
|
// Should extract both successful files
|
|
expect(extracted).toHaveLength(2);
|
|
expect(extracted.map((f) => f.id)).toEqual(['file-1', 'file-2']);
|
|
expect(extracted.every((f) => f.status === 'completed')).toBe(true);
|
|
});
|
|
|
|
test('should handle complex failure scenarios with mixed types', () => {
|
|
const reasoningHistory: TestReasoningMessage[] = [
|
|
// Successful metrics entry
|
|
{
|
|
id: 'successful-metrics',
|
|
type: 'files',
|
|
title: 'Creating metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-1'],
|
|
files: {
|
|
'metric-1': {
|
|
id: 'metric-1',
|
|
file_type: 'metric',
|
|
file_name: 'good_metric.yml',
|
|
version_number: 1,
|
|
status: 'completed',
|
|
file: { text: 'metric: good' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
// Failed dashboard entry
|
|
{
|
|
id: 'failed-dashboards',
|
|
type: 'files',
|
|
title: 'Creating dashboards',
|
|
status: 'failed',
|
|
file_ids: ['dash-1'],
|
|
files: {
|
|
'dash-1': {
|
|
id: 'dash-1',
|
|
file_type: 'dashboard',
|
|
file_name: 'failed_dashboard.yml',
|
|
version_number: 1,
|
|
status: 'completed', // File shows completed but entry failed
|
|
file: { text: 'dashboard: failed' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
// Mixed success/failure entry
|
|
{
|
|
id: 'mixed-entry',
|
|
type: 'files',
|
|
title: 'Modifying metrics',
|
|
status: 'completed',
|
|
file_ids: ['metric-2', 'metric-3'],
|
|
files: {
|
|
'metric-2': {
|
|
id: 'metric-2',
|
|
file_type: 'metric',
|
|
file_name: 'successful_mod.yml',
|
|
version_number: 2,
|
|
status: 'completed',
|
|
file: { text: 'metric: successful_mod' },
|
|
} as TestReasoningFile,
|
|
'metric-3': {
|
|
id: 'metric-3',
|
|
file_type: 'metric',
|
|
file_name: 'failed_mod.yml',
|
|
version_number: 2,
|
|
status: 'failed',
|
|
file: { text: 'metric: failed_mod' },
|
|
} as TestReasoningFile,
|
|
},
|
|
},
|
|
];
|
|
|
|
const extracted = extractFilesFromReasoning(reasoningHistory);
|
|
|
|
// Should only extract the 2 successful files
|
|
expect(extracted).toHaveLength(2);
|
|
const extractedIds = extracted.map((f) => f.id);
|
|
expect(extractedIds).toEqual(['metric-1', 'metric-2']);
|
|
|
|
// Verify no failed files were extracted
|
|
expect(extractedIds).not.toContain('dash-1'); // From failed entry
|
|
expect(extractedIds).not.toContain('metric-3'); // Failed individual file
|
|
});
|
|
});
|
|
|
|
describe('hasFailureIndicators utility', () => {
|
|
test('should detect failed status', () => {
|
|
const entryWithFailedStatus = {
|
|
id: 'test',
|
|
status: 'failed',
|
|
type: 'files',
|
|
};
|
|
|
|
expect(hasFailureIndicators(entryWithFailedStatus)).toBe(true);
|
|
});
|
|
|
|
test('should detect error field', () => {
|
|
const entryWithError = {
|
|
id: 'test',
|
|
status: 'completed',
|
|
error: 'Something went wrong',
|
|
};
|
|
|
|
expect(hasFailureIndicators(entryWithError)).toBe(true);
|
|
});
|
|
|
|
test('should detect hasError flag', () => {
|
|
const entryWithErrorFlag = {
|
|
id: 'test',
|
|
status: 'completed',
|
|
hasError: true,
|
|
};
|
|
|
|
expect(hasFailureIndicators(entryWithErrorFlag)).toBe(true);
|
|
});
|
|
|
|
test('should NOT detect failed files within entry (handled at file level)', () => {
|
|
const entryWithFailedFile = {
|
|
id: 'test',
|
|
status: 'completed',
|
|
files: {
|
|
'file-1': { status: 'completed' },
|
|
'file-2': { status: 'failed' },
|
|
},
|
|
};
|
|
|
|
// Entry-level function should not reject entries with failed files
|
|
// Individual file failures are handled by hasFileFailureIndicators
|
|
expect(hasFailureIndicators(entryWithFailedFile)).toBe(false);
|
|
|
|
// But individual file failure detection should work
|
|
expect(hasFileFailureIndicators(entryWithFailedFile.files['file-1'])).toBe(false);
|
|
expect(hasFileFailureIndicators(entryWithFailedFile.files['file-2'])).toBe(true);
|
|
});
|
|
|
|
test('should return false for successful entries', () => {
|
|
const successfulEntry = {
|
|
id: 'test',
|
|
status: 'completed',
|
|
files: {
|
|
'file-1': { status: 'completed' },
|
|
'file-2': { status: 'completed' },
|
|
},
|
|
};
|
|
|
|
expect(hasFailureIndicators(successfulEntry)).toBe(false);
|
|
});
|
|
|
|
test('should handle null/undefined gracefully', () => {
|
|
expect(hasFailureIndicators(null)).toBe(false);
|
|
expect(hasFailureIndicators(undefined)).toBe(false);
|
|
expect(hasFailureIndicators({})).toBe(false);
|
|
});
|
|
});
|
|
});
|