@@ -35,6 +35,120 @@ function buildApp(developerSemaphore: DeveloperSemaphore) {
3535 return app ;
3636}
3737
38+ // ---------------------------------------------------------------------------
39+ // GET /api/admin/metrics — per-developer concurrency stats overview
40+ // ---------------------------------------------------------------------------
41+
42+ describe ( 'GET /api/admin/metrics' , ( ) => {
43+ let developerSemaphore : DeveloperSemaphore ;
44+
45+ beforeEach ( ( ) => {
46+ developerSemaphore = new DeveloperSemaphore ( 5 , 1000 ) ;
47+ } ) ;
48+
49+ afterEach ( ( ) => {
50+ developerSemaphore . clear ( ) ;
51+ } ) ;
52+
53+ it ( 'returns empty stats when no developers are active' , async ( ) => {
54+ const app = buildApp ( developerSemaphore ) ;
55+
56+ const res = await request ( app )
57+ . get ( '/api/admin/metrics' )
58+ . set ( 'x-admin-api-key' , ADMIN_KEY ) ;
59+
60+ expect ( res . status ) . toBe ( 200 ) ;
61+ expect ( res . body . data ) . toMatchObject ( {
62+ totalActive : 0 ,
63+ activeDeveloperCount : 0 ,
64+ perDeveloper : [ ] ,
65+ campaign : 'GrantFox FWC26' ,
66+ } ) ;
67+ expect ( res . body . data . maxConcurrencyPerDeveloper ) . toBe ( 5 ) ;
68+ } ) ;
69+
70+ it ( 'returns per-developer stats with correct utilization' , async ( ) => {
71+ const app = buildApp ( developerSemaphore ) ;
72+
73+ await developerSemaphore . withSlot ( 'dev_abc' , async ( ) => {
74+ await developerSemaphore . withSlot ( 'dev_abc' , async ( ) => {
75+ await developerSemaphore . withSlot ( 'dev_def' , async ( ) => {
76+ const res = await request ( app )
77+ . get ( '/api/admin/metrics' )
78+ . set ( 'x-admin-api-key' , ADMIN_KEY ) ;
79+
80+ expect ( res . status ) . toBe ( 200 ) ;
81+ expect ( res . body . data . totalActive ) . toBe ( 3 ) ;
82+ expect ( res . body . data . activeDeveloperCount ) . toBe ( 2 ) ;
83+ expect ( res . body . data . perDeveloper ) . toHaveLength ( 2 ) ;
84+ expect ( res . body . data . perDeveloper [ 0 ] ) . toMatchObject ( {
85+ developerId : 'dev_abc' ,
86+ activeCount : 2 ,
87+ atLimit : false ,
88+ utilizationPercent : 40 ,
89+ } ) ;
90+ expect ( res . body . data . perDeveloper [ 1 ] ) . toMatchObject ( {
91+ developerId : 'dev_def' ,
92+ activeCount : 1 ,
93+ atLimit : false ,
94+ utilizationPercent : 20 ,
95+ } ) ;
96+ } ) ;
97+ } ) ;
98+ } ) ;
99+ } ) ;
100+
101+ it ( 'reports atLimit and 100% utilization when exactly at the ceiling' , async ( ) => {
102+ const sem = new DeveloperSemaphore ( 1 , 1000 ) ;
103+ const app = buildApp ( sem ) ;
104+
105+ await sem . withSlot ( 'dev_maxed' , async ( ) => {
106+ const res = await request ( app )
107+ . get ( '/api/admin/metrics' )
108+ . set ( 'x-admin-api-key' , ADMIN_KEY ) ;
109+
110+ expect ( res . status ) . toBe ( 200 ) ;
111+ expect ( res . body . data . perDeveloper [ 0 ] ) . toMatchObject ( {
112+ developerId : 'dev_maxed' ,
113+ activeCount : 1 ,
114+ atLimit : true ,
115+ utilizationPercent : 100 ,
116+ } ) ;
117+ } ) ;
118+ sem . clear ( ) ;
119+ } ) ;
120+
121+ it ( 'sorts per-developer results by active count descending' , async ( ) => {
122+ const app = buildApp ( developerSemaphore ) ;
123+
124+ await developerSemaphore . withSlot ( 'dev_light' , async ( ) => {
125+ await developerSemaphore . withSlot ( 'dev_heavy' , async ( ) => {
126+ await developerSemaphore . withSlot ( 'dev_heavy' , async ( ) => {
127+ await developerSemaphore . withSlot ( 'dev_heavy' , async ( ) => {
128+ const res = await request ( app )
129+ . get ( '/api/admin/metrics' )
130+ . set ( 'x-admin-api-key' , ADMIN_KEY ) ;
131+
132+ expect ( res . status ) . toBe ( 200 ) ;
133+ expect ( res . body . data . perDeveloper [ 0 ] . developerId ) . toBe ( 'dev_heavy' ) ;
134+ expect ( res . body . data . perDeveloper [ 0 ] . activeCount ) . toBe ( 3 ) ;
135+ expect ( res . body . data . perDeveloper [ 1 ] . developerId ) . toBe ( 'dev_light' ) ;
136+ expect ( res . body . data . perDeveloper [ 1 ] . activeCount ) . toBe ( 1 ) ;
137+ } ) ;
138+ } ) ;
139+ } ) ;
140+ } ) ;
141+ } ) ;
142+
143+ it ( 'requires admin authentication — 401 without credentials' , async ( ) => {
144+ const app = buildApp ( developerSemaphore ) ;
145+
146+ const res = await request ( app ) . get ( '/api/admin/metrics' ) ;
147+
148+ expect ( res . status ) . toBe ( 401 ) ;
149+ } ) ;
150+ } ) ;
151+
38152// ---------------------------------------------------------------------------
39153// GET /api/admin/metrics/concurrency — collection endpoint
40154// ---------------------------------------------------------------------------
0 commit comments