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.
 
 
 
 
 

272 lines
7.6 KiB

import { AiAgentToolName } from './ai-agent.interfaces';
import {
applyToolExecutionPolicy,
createPolicyRouteResponse,
formatPolicyVerificationDetails
} from './ai-agent.policy.utils';
describe('AiAgentPolicyUtils', () => {
it.each([
'hi',
'hello',
'hey',
'thanks',
'thank you',
'good morning',
'good afternoon',
'good evening'
])('routes greeting-like query "%s" to direct no-tool', (query) => {
const decision = applyToolExecutionPolicy({
plannedTools: ['portfolio_analysis'],
query
});
expect(decision.route).toBe('direct');
expect(decision.blockReason).toBe('no_tool_query');
expect(decision.toolsToExecute).toEqual([]);
});
it.each([
'who are you',
'what are you',
'what can you do',
'how do you work',
'how can i use this',
'help',
'assist me',
'what can you help with'
])('routes assistant capability query "%s" to direct no-tool', (query) => {
const decision = applyToolExecutionPolicy({
plannedTools: [],
query
});
expect(decision.route).toBe('direct');
expect(decision.blockReason).toBe('no_tool_query');
expect(
createPolicyRouteResponse({ policyDecision: decision, query })
).toContain(
'Ghostfolio AI'
);
});
it.each<[string, string]>([
['2+2', '2+2 = 4'],
['what is 5 * 3', '5 * 3 = 15'],
['(2+3)*4', '(2+3)*4 = 20'],
['10 / 4', '10 / 4 = 2.5'],
['7 - 10', '7 - 10 = -3'],
['3.5 + 1.25', '3.5 + 1.25 = 4.75'],
['(8 - 2) / 3', '(8 - 2) / 3 = 2'],
['what is 3*(2+4)?', '3*(2+4) = 18'],
['2 + (3 * (4 - 1))', '2 + (3 * (4 - 1)) = 11'],
['10-3-2', '10-3-2 = 5']
])('returns arithmetic direct response for "%s"', (query, expected) => {
const decision = applyToolExecutionPolicy({
plannedTools: [],
query
});
expect(decision.route).toBe('direct');
expect(
createPolicyRouteResponse({
policyDecision: decision,
query
})
).toBe(expected);
});
it.each(['1/0', '2+*2', '5 % 2'])(
'falls back to capability response for unsupported arithmetic expression "%s"',
(query) => {
const decision = applyToolExecutionPolicy({
plannedTools: [],
query
});
expect(decision.route).toBe('direct');
expect(
createPolicyRouteResponse({
policyDecision: decision,
query
})
).toContain('portfolio analysis');
}
);
it('returns distinct direct no-tool responses for identity and capability prompts', () => {
const identityDecision = applyToolExecutionPolicy({
plannedTools: [],
query: 'who are you?'
});
const capabilityDecision = applyToolExecutionPolicy({
plannedTools: [],
query: 'what can you do?'
});
const identityResponse = createPolicyRouteResponse({
policyDecision: identityDecision,
query: 'who are you?'
});
const capabilityResponse = createPolicyRouteResponse({
policyDecision: capabilityDecision,
query: 'what can you do?'
});
expect(identityResponse).toContain('portfolio copilot');
expect(capabilityResponse).toContain('three modes');
expect(identityResponse).not.toBe(capabilityResponse);
});
it('routes finance read intent with empty planner output to clarify', () => {
const decision = applyToolExecutionPolicy({
plannedTools: [],
query: 'Show portfolio risk and allocation'
});
expect(decision.route).toBe('clarify');
expect(decision.blockReason).toBe('unknown');
expect(createPolicyRouteResponse({ policyDecision: decision })).toContain(
'Which one should I run next?'
);
});
it('routes non-finance empty planner output to direct no-tool', () => {
const decision = applyToolExecutionPolicy({
plannedTools: [],
query: 'Tell me a joke'
});
expect(decision.route).toBe('direct');
expect(decision.blockReason).toBe('no_tool_query');
});
it('deduplicates planned tools while preserving route decisions', () => {
const plannedTools: AiAgentToolName[] = [
'portfolio_analysis',
'portfolio_analysis',
'risk_assessment'
];
const decision = applyToolExecutionPolicy({
plannedTools,
query: 'analyze concentration risk'
});
expect(decision.plannedTools).toEqual([
'portfolio_analysis',
'risk_assessment'
]);
expect(decision.toolsToExecute).toEqual([
'portfolio_analysis',
'risk_assessment'
]);
expect(decision.route).toBe('tools');
});
it.each<{
expectedTools: AiAgentToolName[];
plannedTools: AiAgentToolName[];
query: string;
reason: string;
route?: 'clarify' | 'direct' | 'tools';
}>([
{
expectedTools: ['portfolio_analysis', 'risk_assessment'] as AiAgentToolName[],
plannedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan'
] as AiAgentToolName[],
query: 'review portfolio concentration risk',
reason: 'read-only intent strips rebalance'
},
{
expectedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan'
] as AiAgentToolName[],
plannedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan'
] as AiAgentToolName[],
query: 'invest 2000 and rebalance',
reason: 'action intent preserves rebalance'
},
{
expectedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan',
'market_data_lookup'
] as AiAgentToolName[],
plannedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan',
'market_data_lookup'
] as AiAgentToolName[],
query: 'invest and rebalance after checking market quote for NVDA',
reason: 'action + market intent keeps all planned tools'
},
{
expectedTools: ['stress_test'] as AiAgentToolName[],
plannedTools: ['stress_test'] as AiAgentToolName[],
query: 'run stress scenario read-only',
reason: 'read-only stress execution stays allowed'
}
])(
'applies policy gating: $reason',
({ expectedTools, plannedTools, query, route }) => {
const decision = applyToolExecutionPolicy({
plannedTools,
query
});
if (route) {
expect(decision.route).toBe(route);
} else {
expect(decision.route).toBe('tools');
}
expect(decision.toolsToExecute).toEqual(expectedTools);
}
);
it('marks rebalance-only no-action prompts as clarify with needs_confirmation', () => {
const decision = applyToolExecutionPolicy({
plannedTools: ['rebalance_plan'],
query: 'review concentration profile'
});
expect(decision.route).toBe('clarify');
expect(decision.blockReason).toBe('needs_confirmation');
expect(decision.blockedByPolicy).toBe(true);
expect(decision.toolsToExecute).toEqual([]);
});
it('formats policy verification details with planned and executed tools', () => {
const decision = applyToolExecutionPolicy({
plannedTools: [
'portfolio_analysis',
'risk_assessment',
'rebalance_plan'
],
query: 'review concentration risk'
});
const details = formatPolicyVerificationDetails({
policyDecision: decision
});
expect(details).toContain('route=tools');
expect(details).toContain('blocked_by_policy=true');
expect(details).toContain('block_reason=needs_confirmation');
expect(details).toContain(
'planned_tools=portfolio_analysis, risk_assessment, rebalance_plan'
);
expect(details).toContain(
'executed_tools=portfolio_analysis, risk_assessment'
);
});
});