import { describe, expect, it } from 'vitest'; import { metricRecommendations } from '../../src/lib/metrics-insights.js'; import type { MapMetrics } from '../../src/lib/metrics.js'; const policy = { id: 't5-policy', mode: 'create-destroy' as const, minimum: 1, maximum: 3, spares: 1, pausedReason: null }; const map = (values: Partial = {}): MapMetrics => ({ game: 't5', map: 'Moon', matches: { started: 0, withRecordedPlayers: 0, averageGroupSize: null, solo: 0, soloShare: null }, observed: 10, active: 5, idle: 5, unknown: 0, playerHours: 10, noEmpty: 0, availabilityObserved: 5, availabilityUnknown: 0, noEmptyAtMaximum: 0, noEmptyBelowMaximum: 0, policy, ...values }); describe('autoscaling policy signals', () => { it('compares map availability with the current maximum without needing player limits', () => { const [result] = metricRecommendations([ map({ observed: 100, active: 50, idle: 50, noEmpty: 0.1, availabilityObserved: 4, noEmptyAtMaximum: 0.1 }) ]); expect(result).toMatchObject({ kind: 'maximum', tentative: false, href: '/autoscaling#t5-policy', action: 'Review policy' }); expect(result.evidence).toContain('2.5%'); expect(result.evidence).toContain('At or above the current maximum (3)'); expect(result.policySummary).toBe('Current policy: 1–3 lobbies, 1 spare'); }); it('reviews the spare target and create time for no-empty time below the maximum', () => { const [result] = metricRecommendations([ map({ noEmpty: 1, noEmptyBelowMaximum: 0.9, noEmptyAtMaximum: 0.1 }) ]); expect(result.kind).toBe('spares'); expect(result.reason).toContain('below the current maximum of 3'); expect(result.reason).toContain('raising the spare target from 1'); expect(result.title).toContain('create time'); expect(result.evidence).toContain('below it: 0.9 h'); }); it('uses the larger no-empty duration and favors maximum review on a tie', () => { const [result] = metricRecommendations([ map({ noEmpty: 1, noEmptyBelowMaximum: 0.5, noEmptyAtMaximum: 0.5 }) ]); expect(result.kind).toBe('maximum'); expect(result.reason).toContain('At least half'); }); it('uses the two-percent threshold without treating occupied time as demand', () => { expect( metricRecommendations([ map({ active: 9.99, idle: 0.01, noEmpty: 0.099, noEmptyAtMaximum: 0.099 }) ])[0].kind ).toBe('watch'); expect(metricRecommendations([map({ noEmpty: 0.1, noEmptyAtMaximum: 0.1 })])[0].kind).toBe( 'maximum' ); expect(metricRecommendations([map({ active: 0.1, idle: 9.9 })])[0].kind).toBe('watch'); }); it('marks a tiny recorded sample as tentative', () => { const [result] = metricRecommendations([ map({ observed: 1 / 120, active: 1 / 120, idle: 0, noEmpty: 1 / 120, availabilityObserved: 1 / 120, noEmptyAtMaximum: 1 / 120 }) ]); expect(result.kind).toBe('maximum'); expect(result.tentative).toBe(true); expect(result.evidence).toContain('30 s'); }); it('measures evidence coverage with map availability time', () => { const [result] = metricRecommendations([ map({ noEmpty: 1, noEmptyBelowMaximum: 1, availabilityObserved: 5, availabilityUnknown: 2, observed: 100 }) ]); expect(result.kind).toBe('spares'); expect(result.tentative).toBe(true); expect( metricRecommendations([map({ availabilityObserved: 4, availabilityUnknown: 1 })])[0] .tentative ).toBe(false); }); it('suggests adding a policy for a map without one', () => { const [result] = metricRecommendations([map({ policy: null })]); expect(result).toMatchObject({ kind: 'policy', title: 'Consider adding an autoscaling policy', href: '/autoscaling', action: 'Add policy', tentative: false }); }); it('keeps mixed no-empty evidence tentative when most lobby occupancy is unknown', () => { const [result] = metricRecommendations([ map({ observed: 2, active: 2, idle: 0, unknown: 18, availabilityObserved: 2, availabilityUnknown: 0, noEmpty: 2, noEmptyAtMaximum: 2 }) ]); expect(result.kind).toBe('maximum'); expect(result.tentative).toBe(true); expect(result.evidence).toContain('18 h of lobby time has unknown occupancy'); }); it('reviews paused or disabled policies before suggesting limit changes', () => { const values = { noEmpty: 1, noEmptyAtMaximum: 1 }; const [paused] = metricRecommendations([ map({ ...values, policy: { ...policy, pausedReason: 'Review a failed host command.' } }) ]); expect(paused.kind).toBe('policy'); expect(paused.title).toBe('Review and resume the autoscaling policy'); expect(paused.reason).toContain('Review a failed host command.'); const [disabled] = metricRecommendations([ map({ ...values, policy: { ...policy, mode: 'off' } }) ]); expect(disabled.kind).toBe('policy'); expect(disabled.title).toBe('Review and enable the autoscaling policy'); }); it('does not confuse all-unknown occupancy with an idle fleet or a policy signal', () => { const [result] = metricRecommendations([ map({ observed: 0, active: 0, idle: 0, unknown: 10, playerHours: 0, availabilityObserved: 0, availabilityUnknown: 5, policy: null }) ]); expect(result.kind).toBe('reporting'); expect(result.title).toBe('Check occupancy reporting'); expect(result.tentative).toBe(true); }); it('distinguishes a zero-duration sample from failed occupancy reporting', () => { const [result] = metricRecommendations([ map({ observed: 0, active: 0, idle: 0, playerHours: 0, availabilityObserved: 0 }) ]); expect(result.title).toBe('Let observations accumulate'); expect(result.reason).toContain('no elapsed availability observations'); }); it('keeps advice scoped to input maps and sorts policy actions before quiet findings', () => { const results = metricRecommendations([ map({ map: 'Moon' }), map({ game: 't6', map: 'Town', noEmpty: 1, noEmptyBelowMaximum: 1 }) ]); expect(results.map(({ game, map, kind }) => ({ game, map, kind }))).toEqual([ { game: 't6', map: 'Town', kind: 'spares' }, { game: 't5', map: 'Moon', kind: 'watch' } ]); expect(metricRecommendations([])).toEqual([]); }); });