@@ -222,6 +222,167 @@ describe("ConcurrencyManager", () => {
222222 ) ;
223223 } ) ;
224224
225+ describe ( "getAvailableCapacity" , ( ) => {
226+ redisTest (
227+ "should return available capacity for single group" ,
228+ { timeout : 10000 } ,
229+ async ( { redisOptions } ) => {
230+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
231+
232+ const manager = new ConcurrencyManager ( {
233+ redis : redisOptions ,
234+ keys,
235+ groups : [
236+ {
237+ name : "tenant" ,
238+ extractGroupId : ( q ) => q . tenantId ,
239+ getLimit : async ( ) => 10 ,
240+ defaultLimit : 10 ,
241+ } ,
242+ ] ,
243+ } ) ;
244+
245+ const queue : QueueDescriptor = {
246+ id : "queue-1" ,
247+ tenantId : "t1" ,
248+ metadata : { } ,
249+ } ;
250+
251+ // Initial capacity should be full
252+ let capacity = await manager . getAvailableCapacity ( queue ) ;
253+ expect ( capacity ) . toBe ( 10 ) ;
254+
255+ // Reserve 3 slots
256+ await manager . reserve ( queue , "msg-1" ) ;
257+ await manager . reserve ( queue , "msg-2" ) ;
258+ await manager . reserve ( queue , "msg-3" ) ;
259+
260+ // Capacity should be reduced
261+ capacity = await manager . getAvailableCapacity ( queue ) ;
262+ expect ( capacity ) . toBe ( 7 ) ;
263+
264+ await manager . close ( ) ;
265+ }
266+ ) ;
267+
268+ redisTest (
269+ "should return minimum capacity across multiple groups" ,
270+ { timeout : 10000 } ,
271+ async ( { redisOptions } ) => {
272+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
273+
274+ const manager = new ConcurrencyManager ( {
275+ redis : redisOptions ,
276+ keys,
277+ groups : [
278+ {
279+ name : "tenant" ,
280+ extractGroupId : ( q ) => q . tenantId ,
281+ getLimit : async ( ) => 5 ,
282+ defaultLimit : 5 ,
283+ } ,
284+ {
285+ name : "organization" ,
286+ extractGroupId : ( q ) => ( q . metadata . orgId as string ) ?? "default" ,
287+ getLimit : async ( ) => 20 ,
288+ defaultLimit : 20 ,
289+ } ,
290+ ] ,
291+ } ) ;
292+
293+ const queue : QueueDescriptor = {
294+ id : "queue-1" ,
295+ tenantId : "t1" ,
296+ metadata : { orgId : "org1" } ,
297+ } ;
298+
299+ // Initial capacity should be minimum (5 for tenant, 20 for org)
300+ let capacity = await manager . getAvailableCapacity ( queue ) ;
301+ expect ( capacity ) . toBe ( 5 ) ;
302+
303+ // Reserve 3 slots
304+ await manager . reserve ( queue , "msg-1" ) ;
305+ await manager . reserve ( queue , "msg-2" ) ;
306+ await manager . reserve ( queue , "msg-3" ) ;
307+
308+ // Now tenant has 2 left, org has 17 left - minimum is 2
309+ capacity = await manager . getAvailableCapacity ( queue ) ;
310+ expect ( capacity ) . toBe ( 2 ) ;
311+
312+ await manager . close ( ) ;
313+ }
314+ ) ;
315+
316+ redisTest (
317+ "should return 0 when any group is at capacity" ,
318+ { timeout : 10000 } ,
319+ async ( { redisOptions } ) => {
320+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
321+
322+ const manager = new ConcurrencyManager ( {
323+ redis : redisOptions ,
324+ keys,
325+ groups : [
326+ {
327+ name : "tenant" ,
328+ extractGroupId : ( q ) => q . tenantId ,
329+ getLimit : async ( ) => 3 ,
330+ defaultLimit : 3 ,
331+ } ,
332+ {
333+ name : "organization" ,
334+ extractGroupId : ( q ) => ( q . metadata . orgId as string ) ?? "default" ,
335+ getLimit : async ( ) => 10 ,
336+ defaultLimit : 10 ,
337+ } ,
338+ ] ,
339+ } ) ;
340+
341+ const queue : QueueDescriptor = {
342+ id : "queue-1" ,
343+ tenantId : "t1" ,
344+ metadata : { orgId : "org1" } ,
345+ } ;
346+
347+ // Fill up tenant capacity
348+ await manager . reserve ( queue , "msg-1" ) ;
349+ await manager . reserve ( queue , "msg-2" ) ;
350+ await manager . reserve ( queue , "msg-3" ) ;
351+
352+ // Tenant is at 3/3, org is at 3/10
353+ const capacity = await manager . getAvailableCapacity ( queue ) ;
354+ expect ( capacity ) . toBe ( 0 ) ;
355+
356+ await manager . close ( ) ;
357+ }
358+ ) ;
359+
360+ redisTest (
361+ "should return 0 when no groups are configured" ,
362+ { timeout : 10000 } ,
363+ async ( { redisOptions } ) => {
364+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
365+
366+ const manager = new ConcurrencyManager ( {
367+ redis : redisOptions ,
368+ keys,
369+ groups : [ ] ,
370+ } ) ;
371+
372+ const queue : QueueDescriptor = {
373+ id : "queue-1" ,
374+ tenantId : "t1" ,
375+ metadata : { } ,
376+ } ;
377+
378+ const capacity = await manager . getAvailableCapacity ( queue ) ;
379+ expect ( capacity ) . toBe ( 0 ) ;
380+
381+ await manager . close ( ) ;
382+ }
383+ ) ;
384+ } ) ;
385+
225386 describe ( "atomic reservation" , ( ) => {
226387 redisTest (
227388 "should atomically reserve across groups" ,
0 commit comments