mirror of https://github.com/buster-so/buster.git
333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { ChatMessageReasoningMessage } from '../../../../../server/src/types/chat-types/chat-message.type';
|
|
import { ChunkProcessor } from '../../../src/utils/database/chunk-processor';
|
|
import { validateArrayAccess } from '../../../src/utils/validation-helpers';
|
|
|
|
describe('ChunkProcessor SQL Reasoning Entry Creation', () => {
|
|
it('should create SQL reasoning entry with statements array', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
|
|
// Test the createReasoningEntry method directly via reflection
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'toolu_01A1rVASAPgSy1RKXBuJBrTh';
|
|
const args = {
|
|
statements: [
|
|
'SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25',
|
|
"SELECT DISTINCT name FROM ont_ont.product_subcategory WHERE name ILIKE '%accessor%' LIMIT 25",
|
|
'SELECT DISTINCT productline FROM ont_ont.product WHERE productline IS NOT NULL LIMIT 25',
|
|
],
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.id).toBe(toolCallId);
|
|
expect(result!.type).toBe('files');
|
|
expect(result!.title).toBe('Executing SQL');
|
|
expect(result!.status).toBe('loading');
|
|
expect((result as any).file_ids).toHaveLength(1);
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
expect((result as any).files?.[fileId]).toBeDefined();
|
|
expect((result as any).files[fileId].file_name).toBe('SQL Statements');
|
|
expect((result as any).files[fileId].file_type).toBe('agent-action');
|
|
|
|
const expectedYaml = `statements:
|
|
- SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25
|
|
- SELECT DISTINCT name FROM ont_ont.product_subcategory WHERE name ILIKE '%accessor%' LIMIT 25
|
|
- SELECT DISTINCT productline FROM ont_ont.product WHERE productline IS NOT NULL LIMIT 25`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
|
|
it('should handle statements as JSON string', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'toolu_01GRLdxzhgpG3YWzDP9CuU2G';
|
|
const args = {
|
|
statements:
|
|
'["SELECT ps.name as subcategory_name FROM ont_ont.product_subcategory ps", "SELECT MAX(year) as max_year FROM ont_ont.product_total_revenue"]',
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('files');
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
const expectedYaml = `statements:
|
|
- SELECT ps.name as subcategory_name FROM ont_ont.product_subcategory ps
|
|
- SELECT MAX(year) as max_year FROM ont_ont.product_total_revenue`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
|
|
it('should handle statements as plain string', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'toolu_012vwyZV9bHefWZMqq97RFZy';
|
|
const args = {
|
|
statements:
|
|
'SELECT year, quarter, COUNT(*) as record_count FROM ont_ont.product_total_revenue',
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('files');
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
const expectedYaml = `statements:
|
|
- SELECT year, quarter, COUNT(*) as record_count FROM ont_ont.product_total_revenue`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
|
|
it('should handle legacy queries format', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'legacy-call-id';
|
|
const args = {
|
|
queries: ['SELECT * FROM table1', { sql: 'SELECT COUNT(*) FROM table2' }],
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('files');
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
const expectedYaml = `statements:
|
|
- SELECT * FROM table1
|
|
- SELECT COUNT(*) FROM table2`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
|
|
it('should handle legacy sql format', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'legacy-sql-call-id';
|
|
const args = {
|
|
sql: 'SELECT * FROM single_table LIMIT 10',
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('files');
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
const expectedYaml = `statements:
|
|
- SELECT * FROM single_table LIMIT 10`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
|
|
it('should return null for non-SQL tools', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const result = createReasoningEntry('tool-id', 'otherTool', { someArg: 'value' });
|
|
|
|
// This should create a generic text entry, not null, but SQL-specific logic shouldn't apply
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('text'); // Generic tool creates text entry
|
|
});
|
|
|
|
it('should return null for invalid SQL args', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const result = createReasoningEntry('tool-id', 'executeSql', { invalidArg: 'value' });
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should handle malformed JSON in statements string gracefully', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
|
|
const toolCallId = 'malformed-json-call';
|
|
const args = {
|
|
statements: '["SELECT * FROM table1", "incomplete json',
|
|
};
|
|
|
|
const result = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.type).toBe('files');
|
|
|
|
const fileId = (result as any).file_ids?.[0] ?? '';
|
|
// Should treat the whole string as a single statement when JSON parsing fails
|
|
const expectedYaml = `statements:
|
|
- ["SELECT * FROM table1", "incomplete json`;
|
|
|
|
expect((result as any).files[fileId].file.text).toBe(expectedYaml);
|
|
});
|
|
});
|
|
|
|
describe('ChunkProcessor SQL Results Integration', () => {
|
|
it('should update SQL file with results after tool completion', () => {
|
|
const chunkProcessor = new ChunkProcessor(null, [], [], []);
|
|
|
|
// Create initial SQL reasoning entry
|
|
const createReasoningEntry = (
|
|
chunkProcessor as unknown as {
|
|
createReasoningEntry: (
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: unknown
|
|
) => ChatMessageReasoningMessage | null;
|
|
}
|
|
).createReasoningEntry.bind(chunkProcessor);
|
|
const toolCallId = 'sql-with-results';
|
|
const args = {
|
|
statements: [
|
|
'SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25',
|
|
'SELECT COUNT(*) FROM ont_ont.invalid_table',
|
|
],
|
|
};
|
|
|
|
const initialEntry = createReasoningEntry(toolCallId, 'executeSql', args);
|
|
|
|
// Add to reasoning history
|
|
const reasoningHistory = (
|
|
chunkProcessor as unknown as {
|
|
state: { reasoningHistory: ChatMessageReasoningMessage[] };
|
|
}
|
|
).state.reasoningHistory;
|
|
reasoningHistory.push(initialEntry!);
|
|
|
|
// Simulate tool result with mixed success/error results
|
|
const toolResult = {
|
|
results: [
|
|
{
|
|
sql: 'SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25',
|
|
status: 'success',
|
|
results: [
|
|
{ name: 'Bikes' },
|
|
{ name: 'Accessories' },
|
|
{ name: 'Clothing' },
|
|
{ name: 'Components' },
|
|
],
|
|
},
|
|
{
|
|
sql: 'SELECT COUNT(*) FROM ont_ont.invalid_table',
|
|
status: 'error',
|
|
error_message: 'PostgreSQL query failed: relation "ont_ont.invalid_table" does not exist',
|
|
},
|
|
],
|
|
};
|
|
|
|
// Call the updateSqlFileWithResults method
|
|
const updateSqlFileWithResults = (
|
|
chunkProcessor as unknown as {
|
|
updateSqlFileWithResults: (toolCallId: string, toolResult: unknown) => void;
|
|
}
|
|
).updateSqlFileWithResults.bind(chunkProcessor);
|
|
updateSqlFileWithResults(toolCallId, toolResult);
|
|
|
|
// Verify the file content was updated with results
|
|
const updatedEntry = validateArrayAccess(
|
|
reasoningHistory,
|
|
0,
|
|
'reasoning history'
|
|
) as ChatMessageReasoningMessage;
|
|
const fileId = (updatedEntry as any).file_ids?.[0] ?? '';
|
|
const fileContent = (updatedEntry as any).files?.[fileId]?.file?.text ?? '';
|
|
|
|
const expectedContent = `statements:
|
|
- SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25
|
|
- SELECT COUNT(*) FROM ont_ont.invalid_table
|
|
|
|
results:
|
|
- status: success
|
|
sql: SELECT DISTINCT name FROM ont_ont.product_category LIMIT 25
|
|
results:
|
|
-
|
|
name: Bikes
|
|
-
|
|
name: Accessories
|
|
-
|
|
name: Clothing
|
|
-
|
|
name: Components
|
|
- status: error
|
|
sql: SELECT COUNT(*) FROM ont_ont.invalid_table
|
|
error_message: |-
|
|
PostgreSQL query failed: relation "ont_ont.invalid_table" does not exist`;
|
|
|
|
expect(fileContent).toBe(expectedContent);
|
|
});
|
|
});
|