mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
4.2 KiB
145 lines
4.2 KiB
import { DataSource } from '@prisma/client';
|
|
|
|
import {
|
|
buildAnswer,
|
|
createPreferenceSummaryResponse,
|
|
getUserPreferences,
|
|
isPreferenceRecallQuery,
|
|
resolvePreferenceUpdate
|
|
} from './ai-agent.chat.helpers';
|
|
|
|
describe('AiAgentChatHelpers', () => {
|
|
const originalLlmTimeout = process.env.AI_AGENT_LLM_TIMEOUT_IN_MS;
|
|
|
|
afterEach(() => {
|
|
if (originalLlmTimeout === undefined) {
|
|
delete process.env.AI_AGENT_LLM_TIMEOUT_IN_MS;
|
|
} else {
|
|
process.env.AI_AGENT_LLM_TIMEOUT_IN_MS = originalLlmTimeout;
|
|
}
|
|
});
|
|
|
|
it('returns deterministic fallback when llm generation exceeds timeout', async () => {
|
|
process.env.AI_AGENT_LLM_TIMEOUT_IN_MS = '20';
|
|
|
|
const startedAt = Date.now();
|
|
const answer = await buildAnswer({
|
|
generateText: () => {
|
|
return new Promise<{ text?: string }>(() => undefined);
|
|
},
|
|
languageCode: 'en',
|
|
memory: { turns: [] },
|
|
portfolioAnalysis: {
|
|
allocationSum: 1,
|
|
holdings: [
|
|
{
|
|
allocationInPercentage: 0.6,
|
|
dataSource: DataSource.YAHOO,
|
|
symbol: 'AAPL',
|
|
valueInBaseCurrency: 6000
|
|
},
|
|
{
|
|
allocationInPercentage: 0.4,
|
|
dataSource: DataSource.YAHOO,
|
|
symbol: 'MSFT',
|
|
valueInBaseCurrency: 4000
|
|
}
|
|
],
|
|
holdingsCount: 2,
|
|
totalValueInBaseCurrency: 10000
|
|
},
|
|
query: 'Show my portfolio allocation overview',
|
|
userCurrency: 'USD'
|
|
});
|
|
|
|
expect(Date.now() - startedAt).toBeLessThan(400);
|
|
expect(answer).toContain('Largest long allocations:');
|
|
});
|
|
|
|
it('keeps generated response when answer passes reliability gate', async () => {
|
|
const generatedText =
|
|
'Trim AAPL by 5% and allocate the next 1000 USD toward MSFT and BND. This lowers concentration risk and improves balance.';
|
|
|
|
const answer = await buildAnswer({
|
|
generateText: jest.fn().mockResolvedValue({
|
|
text: generatedText
|
|
}),
|
|
languageCode: 'en',
|
|
memory: { turns: [] },
|
|
query: 'How should I rebalance and invest next?',
|
|
userCurrency: 'USD'
|
|
});
|
|
|
|
expect(answer).toBe(generatedText);
|
|
});
|
|
|
|
it('adds deterministic diversification action guidance when generated answer is unreliable', async () => {
|
|
const answer = await buildAnswer({
|
|
generateText: jest.fn().mockResolvedValue({
|
|
text: 'Diversify.'
|
|
}),
|
|
languageCode: 'en',
|
|
memory: { turns: [] },
|
|
portfolioAnalysis: {
|
|
allocationSum: 1,
|
|
holdings: [
|
|
{
|
|
allocationInPercentage: 0.7,
|
|
dataSource: DataSource.YAHOO,
|
|
symbol: 'AAPL',
|
|
valueInBaseCurrency: 7000
|
|
},
|
|
{
|
|
allocationInPercentage: 0.3,
|
|
dataSource: DataSource.YAHOO,
|
|
symbol: 'MSFT',
|
|
valueInBaseCurrency: 3000
|
|
}
|
|
],
|
|
holdingsCount: 2,
|
|
totalValueInBaseCurrency: 10000
|
|
},
|
|
query: 'help me diversify',
|
|
userCurrency: 'USD'
|
|
});
|
|
|
|
expect(answer).toContain('Next-step allocation:');
|
|
expect(answer).toContain('AAPL');
|
|
});
|
|
|
|
it('parses and persists concise response-style preference updates', () => {
|
|
const result = resolvePreferenceUpdate({
|
|
query: 'Remember to keep responses concise.',
|
|
userPreferences: {}
|
|
});
|
|
|
|
expect(result.shouldPersist).toBe(true);
|
|
expect(result.userPreferences.responseStyle).toBe('concise');
|
|
expect(result.acknowledgement).toContain('Saved preference');
|
|
});
|
|
|
|
it('recognizes preference recall queries and renders deterministic summary', () => {
|
|
expect(isPreferenceRecallQuery('What do you remember about me?')).toBe(true);
|
|
expect(
|
|
createPreferenceSummaryResponse({
|
|
userPreferences: {
|
|
responseStyle: 'concise',
|
|
updatedAt: '2026-02-24T10:00:00.000Z'
|
|
}
|
|
})
|
|
).toContain('response style: concise');
|
|
});
|
|
|
|
it('returns empty preferences for malformed user preference payload', async () => {
|
|
const redisCacheService = {
|
|
get: jest.fn().mockResolvedValue('{bad-json')
|
|
};
|
|
|
|
const result = await getUserPreferences({
|
|
redisCacheService: redisCacheService as never,
|
|
userId: 'user-1'
|
|
});
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|
|
|