buster/packages/ai/tests/utils/memory/workflow-message-flow.test.ts

286 lines
9.7 KiB
TypeScript

import type {
CoreAssistantMessage,
CoreMessage,
CoreToolMessage,
ToolCallPart,
ToolResultPart,
} from 'ai';
import { describe, expect, test } from 'vitest';
import { extractMessageHistory } from '../../../src/utils/memory/message-history';
import { validateArrayAccess } from '../../../src/utils/validation-helpers';
describe('Workflow Message Flow', () => {
test('should maintain sequential order when passing messages between steps', () => {
// Simulate what comes out of think-and-prep step
const thinkAndPrepOutput: CoreMessage[] = [
{
role: 'user',
content: 'Who is my top customer?',
},
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'toolu_1',
toolName: 'sequentialThinking',
args: { thought: 'Analyzing request...' },
},
],
} as CoreAssistantMessage,
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: 'toolu_1',
toolName: 'sequentialThinking',
result: { success: true },
},
],
} as CoreToolMessage,
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'toolu_2',
toolName: 'submitThoughts',
args: {},
},
],
} as CoreAssistantMessage,
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: 'toolu_2',
toolName: 'submitThoughts',
result: {},
},
],
} as CoreToolMessage,
];
// This is what gets passed to analyst step
const messagesForAnalyst = extractMessageHistory(thinkAndPrepOutput);
// Should maintain exact order
expect(messagesForAnalyst).toEqual(thinkAndPrepOutput);
expect(messagesForAnalyst).toHaveLength(5);
// Simulate analyst adding more messages
const analystMessages: CoreMessage[] = [
...messagesForAnalyst,
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'toolu_3',
toolName: 'executeSql',
args: { statements: ['SELECT * FROM customers'] },
},
],
} as CoreAssistantMessage,
{
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: 'toolu_3',
toolName: 'executeSql',
result: { results: [] },
},
],
} as CoreToolMessage,
];
// Extract complete history
const completeHistory = extractMessageHistory(analystMessages);
// Should have all messages in correct order
expect(completeHistory).toHaveLength(7);
// Verify the pattern
const msg0 = validateArrayAccess(completeHistory, 0, 'complete history');
const msg1 = validateArrayAccess(completeHistory, 1, 'complete history');
const msg2 = validateArrayAccess(completeHistory, 2, 'complete history');
const msg3 = validateArrayAccess(completeHistory, 3, 'complete history');
const msg4 = validateArrayAccess(completeHistory, 4, 'complete history');
const msg5 = validateArrayAccess(completeHistory, 5, 'complete history');
const msg6 = validateArrayAccess(completeHistory, 6, 'complete history');
expect(msg0.role).toBe('user');
expect(msg1.role).toBe('assistant');
expect(msg2.role).toBe('tool');
expect(msg3.role).toBe('assistant');
expect(msg4.role).toBe('tool');
expect(msg5.role).toBe('assistant');
expect(msg6.role).toBe('tool');
});
test('should handle follow-up conversation with existing history', () => {
// First conversation saved in database
const firstConversation: CoreMessage[] = [
{ role: 'user', content: 'Who is my top customer?' },
{
role: 'assistant',
content: [{ type: 'tool-call', toolCallId: 't1', toolName: 'sql', args: {} }],
},
{
role: 'tool',
content: [
{ type: 'tool-result', toolCallId: 't1', toolName: 'sql', result: { customer: 'ACME' } },
],
},
{
role: 'assistant',
content: 'Your top customer is ACME.',
},
];
// User asks follow-up
const followUpPrompt = 'What about their revenue?';
// Build messages for new workflow run
const messagesWithHistory: CoreMessage[] = [
...firstConversation,
{ role: 'user', content: followUpPrompt },
];
// Extract for processing
const extracted = extractMessageHistory(messagesWithHistory);
// Should maintain conversation flow
expect(extracted).toHaveLength(5);
const extractedMsg0 = validateArrayAccess(extracted, 0, 'extracted messages');
const extractedMsg3 = validateArrayAccess(extracted, 3, 'extracted messages');
const extractedMsg4 = validateArrayAccess(extracted, 4, 'extracted messages');
expect(extractedMsg0.content).toBe('Who is my top customer?');
expect(extractedMsg3.content).toBe('Your top customer is ACME.');
expect(extractedMsg4.content).toBe('What about their revenue?');
});
test('should handle edge case: bundled messages from AI SDK', () => {
// If AI SDK returns bundled messages (bug scenario)
const bundledFromSDK: CoreMessage[] = [
{ role: 'user', content: 'Analyze sales data' },
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: 'id1', toolName: 'think', args: {} },
{ type: 'tool-call', toolCallId: 'id2', toolName: 'analyze', args: {} },
{ type: 'tool-call', toolCallId: 'id3', toolName: 'report', args: {} },
],
},
{
role: 'tool',
content: [{ type: 'tool-result', toolCallId: 'id1', toolName: 'think', result: {} }],
},
{
role: 'tool',
content: [{ type: 'tool-result', toolCallId: 'id2', toolName: 'analyze', result: {} }],
},
{
role: 'tool',
content: [{ type: 'tool-result', toolCallId: 'id3', toolName: 'report', result: {} }],
},
];
// extractMessageHistory should fix this
const fixed = extractMessageHistory(bundledFromSDK);
// Should be properly interleaved
expect(fixed).toHaveLength(7);
// Check sequential pattern
const roles = fixed.map((m) => m.role);
expect(roles).toEqual(['user', 'assistant', 'tool', 'assistant', 'tool', 'assistant', 'tool']);
// Verify tool calls are properly paired with results
const fixedMsg1 = validateArrayAccess(fixed, 1, 'fixed messages');
const fixedMsg2 = validateArrayAccess(fixed, 2, 'fixed messages');
const fixedMsg3 = validateArrayAccess(fixed, 3, 'fixed messages');
const fixedMsg4 = validateArrayAccess(fixed, 4, 'fixed messages');
const fixedMsg5 = validateArrayAccess(fixed, 5, 'fixed messages');
const fixedMsg6 = validateArrayAccess(fixed, 6, 'fixed messages');
if (fixedMsg1.role === 'assistant' && Array.isArray(fixedMsg1.content)) {
const content1 = validateArrayAccess(fixedMsg1.content, 0, 'assistant content');
if ('toolCallId' in content1) {
expect(content1.toolCallId).toBe('id1');
}
}
if (fixedMsg2.role === 'tool' && Array.isArray(fixedMsg2.content)) {
const content2 = validateArrayAccess(fixedMsg2.content, 0, 'tool content');
if ('toolCallId' in content2) {
expect(content2.toolCallId).toBe('id1');
}
}
if (fixedMsg3.role === 'assistant' && Array.isArray(fixedMsg3.content)) {
const content3 = validateArrayAccess(fixedMsg3.content, 0, 'assistant content');
if ('toolCallId' in content3) {
expect(content3.toolCallId).toBe('id2');
}
}
if (fixedMsg4.role === 'tool' && Array.isArray(fixedMsg4.content)) {
const content4 = validateArrayAccess(fixedMsg4.content, 0, 'tool content');
if ('toolCallId' in content4) {
expect(content4.toolCallId).toBe('id2');
}
}
if (fixedMsg5.role === 'assistant' && Array.isArray(fixedMsg5.content)) {
const content5 = validateArrayAccess(fixedMsg5.content, 0, 'assistant content');
if ('toolCallId' in content5) {
expect(content5.toolCallId).toBe('id3');
}
}
if (fixedMsg6.role === 'tool' && Array.isArray(fixedMsg6.content)) {
const content6 = validateArrayAccess(fixedMsg6.content, 0, 'tool content');
if ('toolCallId' in content6) {
expect(content6.toolCallId).toBe('id3');
}
}
});
test('should preserve message metadata (IDs, timestamps, etc)', () => {
const messagesWithMetadata = [
{
role: 'user',
content: 'Test',
// @ts-ignore - additional metadata
timestamp: '2024-01-01T00:00:00Z',
},
{
// @ts-ignore - additional metadata
id: 'unique-id-123',
role: 'assistant',
content: [{ type: 'tool-call', toolCallId: 'tool-id', toolName: 'test', args: {} }],
// @ts-ignore - additional metadata
model: 'claude-3',
},
{
// @ts-ignore - additional metadata
id: 'tool-result-id',
role: 'tool',
content: [{ type: 'tool-result', toolCallId: 'tool-id', toolName: 'test', result: {} }],
},
] as CoreMessage[];
const extracted = extractMessageHistory(messagesWithMetadata);
// Metadata should be preserved
const extractedMsg0 = validateArrayAccess(extracted, 0, 'extracted metadata messages');
const extractedMsg1 = validateArrayAccess(extracted, 1, 'extracted metadata messages');
const extractedMsg2 = validateArrayAccess(extracted, 2, 'extracted metadata messages');
expect(extractedMsg0).toHaveProperty('timestamp');
expect(extractedMsg1).toHaveProperty('id', 'unique-id-123');
expect(extractedMsg1).toHaveProperty('model');
expect(extractedMsg2).toHaveProperty('id', 'tool-result-id');
});
});