Skip to content

Commit ea916fa

Browse files
committed
Merge pull request #1118
2 parents ce553d3 + 81b2f57 commit ea916fa

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { classifyAtRiskCommitments } from '@/utils/classification';
3+
import type { Commitment } from '@/lib/types/domain';
4+
5+
const base: Commitment = {
6+
id: 'c1',
7+
type: 'Balanced',
8+
status: 'Active',
9+
asset: 'XLM',
10+
amount: '1000',
11+
};
12+
13+
describe('classifyAtRiskCommitments', () => {
14+
it('returns empty array when no commitments are at risk', () => {
15+
const safe: Commitment = {
16+
...base,
17+
complianceScore: 90,
18+
daysRemaining: 30,
19+
status: 'Active',
20+
};
21+
const result = classifyAtRiskCommitments([safe], null);
22+
expect(result).toHaveLength(0);
23+
});
24+
25+
it('classifies low compliance score (< 70) as low_compliance', () => {
26+
const commitment: Commitment = { ...base, complianceScore: 69 };
27+
const result = classifyAtRiskCommitments([commitment], null);
28+
expect(result).toHaveLength(1);
29+
expect(result[0].riskCategories).toContain('low_compliance');
30+
});
31+
32+
it('does not classify compliance score of exactly 70 as low_compliance', () => {
33+
const commitment: Commitment = { ...base, complianceScore: 70 };
34+
const result = classifyAtRiskCommitments([commitment], null);
35+
expect(result).toHaveLength(0);
36+
});
37+
38+
it('classifies daysRemaining <= 7 as maturing_soon', () => {
39+
const atBoundary: Commitment = { ...base, daysRemaining: 7 };
40+
const justOver: Commitment = { ...base, id: 'c2', daysRemaining: 8 };
41+
const resultBoundary = classifyAtRiskCommitments([atBoundary], null);
42+
const resultOver = classifyAtRiskCommitments([justOver], null);
43+
44+
expect(resultBoundary).toHaveLength(1);
45+
expect(resultBoundary[0].riskCategories).toContain('maturing_soon');
46+
expect(resultOver).toHaveLength(0);
47+
});
48+
49+
it('classifies Violated status as action_required', () => {
50+
const commitment: Commitment = { ...base, status: 'Violated' };
51+
const result = classifyAtRiskCommitments([commitment], null);
52+
expect(result).toHaveLength(1);
53+
expect(result[0].riskCategories).toContain('action_required');
54+
});
55+
56+
it('classifies drawdown >= 80% of maxLoss as action_required', () => {
57+
const atThreshold: Commitment = {
58+
...base,
59+
currentDrawdown: '80',
60+
maxLoss: '100',
61+
};
62+
const belowThreshold: Commitment = {
63+
...base,
64+
id: 'c2',
65+
currentDrawdown: '79',
66+
maxLoss: '100',
67+
};
68+
const atResult = classifyAtRiskCommitments([atThreshold], null);
69+
const belowResult = classifyAtRiskCommitments([belowThreshold], null);
70+
71+
expect(atResult).toHaveLength(1);
72+
expect(atResult[0].riskCategories).toContain('action_required');
73+
expect(belowResult).toHaveLength(0);
74+
});
75+
76+
it('handles non-numeric drawdown/maxLoss gracefully', () => {
77+
const commitment: Commitment = {
78+
...base,
79+
currentDrawdown: 'N/A',
80+
maxLoss: '100',
81+
};
82+
const result = classifyAtRiskCommitments([commitment], null);
83+
expect(result).toHaveLength(0);
84+
});
85+
86+
it('assigns multiple risk categories when multiple conditions are met', () => {
87+
const commitment: Commitment = {
88+
...base,
89+
complianceScore: 50,
90+
daysRemaining: 3,
91+
status: 'Violated',
92+
};
93+
const result = classifyAtRiskCommitments([commitment], null);
94+
expect(result).toHaveLength(1);
95+
expect(result[0].riskCategories).toContain('low_compliance');
96+
expect(result[0].riskCategories).toContain('maturing_soon');
97+
expect(result[0].riskCategories).toContain('action_required');
98+
});
99+
100+
it('filters out commitments with no risk categories', () => {
101+
const commitments: Commitment[] = [
102+
{ ...base, id: 'c1', complianceScore: 80, daysRemaining: 30, status: 'Active' },
103+
{ ...base, id: 'c2', complianceScore: 50 },
104+
{ ...base, id: 'c3', daysRemaining: 5 },
105+
];
106+
const result = classifyAtRiskCommitments(commitments, null);
107+
expect(result).toHaveLength(2);
108+
expect(result.map((r) => r.id)).toEqual(['c2', 'c3']);
109+
});
110+
111+
it('returns empty array for empty input', () => {
112+
const result = classifyAtRiskCommitments([], null);
113+
expect(result).toHaveLength(0);
114+
});
115+
116+
it('returns exact risk category tokens (pin styling contract)', () => {
117+
const low: Commitment = { ...base, complianceScore: 0 };
118+
const soon: Commitment = { ...base, id: 'c2', daysRemaining: 0 };
119+
const action: Commitment = { ...base, id: 'c3', status: 'Violated' };
120+
121+
const [lowResult] = classifyAtRiskCommitments([low], null);
122+
const [soonResult] = classifyAtRiskCommitments([soon], null);
123+
const [actionResult] = classifyAtRiskCommitments([action], null);
124+
125+
expect(lowResult.riskCategories[0]).toBe('low_compliance');
126+
expect(soonResult.riskCategories[0]).toBe('maturing_soon');
127+
expect(actionResult.riskCategories[0]).toBe('action_required');
128+
});
129+
});

0 commit comments

Comments
 (0)