import { IssuePriority } from './enums'; import { portfolioHealth, projectHealth } from './health'; import { ProjectInsights } from './insights'; const issue = (key: string) => ({ key }) as never; const insights = (over: Partial = {}): ProjectInsights => ({ risks: { overdue: [], overdueCount: 0, dueSoon: [], dueSoonCount: 0, blocked: [], blockedCount: 0, stuck: [], stuckCount: 0, stuckDays: 5, }, quality: { openBugsByPriority: [], bugWeeks: [], qaByArea: [], escaped: [], }, releases: [], bottlenecks: [], ...over, }) as ProjectInsights; describe('project health', () => { it('is good with nothing to say', () => { expect(projectHealth(insights())).toEqual({ level: 'GOOD', signals: [] }); }); it('turns to attention, then risk, with the cause and the tickets behind it', () => { const base = insights(); const stuck = projectHealth({ ...base, risks: { ...base.risks, stuck: [issue('WEB-1')], stuckCount: 1 }, }); expect(stuck.level).toBe('ATTENTION'); expect(stuck.signals[0]).toMatchObject({ key: 'stuck', title: '1 ticket lleva 5 días o más sin moverse', keys: ['WEB-1'], }); const bad = projectHealth({ ...base, risks: { ...base.risks, overdueCount: 4, overdue: [] }, quality: { ...base.quality, openBugsByPriority: [{ priority: IssuePriority.CRITICAL, count: 1 }], qaByArea: [{ area: null, returned: 3, passed: 3 }], }, releases: [ { releaseId: 'r', name: 'v2.0', targetDate: '2026-11-15', total: 10, finished: 2, likelyDate: '2026-12-01', safeDate: null, verdict: 'LATE', }, ], bottlenecks: [ { status: 'IN_PROGRESS', averageDays: 1, samples: 3, open: 9, wipLimit: 5, overLimit: true, }, ], }); expect(bad.level).toBe('RISK'); expect(bad.signals.map((s) => [s.key, s.level])).toEqual([ ['overdue', 'RISK'], ['bugs', 'RISK'], ['qa', 'RISK'], ['releases', 'RISK'], ['wip', 'ATTENTION'], ]); expect(bad.signals[3].title).toContain('v2.0 no llega a su fecha'); }); it('does not read a trend into a handful of QA verdicts', () => { const base = insights(); expect( projectHealth({ ...base, quality: { ...base.quality, qaByArea: [{ area: null, returned: 2, passed: 1 }], }, }).level, ).toBe('GOOD'); }); }); describe('portfolio health', () => { it('reads the light from the counts and the forecasts', () => { const project = { stuck: 0, overdue: 1, blocked: 0, releases: [{ name: 'v2.0', verdict: 'LATE' }], } as never; expect(portfolioHealth(project)).toEqual({ level: 'RISK', causes: ['1 vencido', 'v2.0 llega tarde'], }); expect( portfolioHealth({ stuck: 0, overdue: 0, blocked: 0, releases: [], } as never), ).toEqual({ level: 'GOOD', causes: [] }); }); });