@@ -173,7 +173,7 @@ export function testBackend(options: TestBackendOptions): void {
173173 expect ( created . concurrencyLimit ) . toBeNull ( ) ;
174174 } ) ;
175175
176- test ( "rejects mismatched workflow concurrency metadata pairs " , async ( ) => {
176+ test ( "rejects key-only workflow concurrency metadata" , async ( ) => {
177177 const base = {
178178 workflowName : randomUUID ( ) ,
179179 version : null ,
@@ -193,19 +193,30 @@ export function testBackend(options: TestBackendOptions): void {
193193 await expect (
194194 Promise . resolve ( ) . then ( ( ) => backend . createWorkflowRun ( keyOnly ) ) ,
195195 ) . rejects . toThrow (
196- 'Invalid workflow concurrency metadata: "concurrencyKey" and " concurrencyLimit" must both be null or both be set .' ,
196+ 'Invalid workflow concurrency metadata: "concurrencyLimit" must be set when "concurrencyKey" is provided .' ,
197197 ) ;
198+ } ) ;
199+
200+ test ( "accepts limit-only workflow concurrency metadata as default bucket" , async ( ) => {
201+ const base = {
202+ workflowName : randomUUID ( ) ,
203+ version : null ,
204+ idempotencyKey : null ,
205+ input : null ,
206+ config : { } ,
207+ context : null ,
208+ availableAt : null ,
209+ deadlineAt : null ,
210+ } ;
198211
199212 const limitOnly = {
200213 ...base ,
201214 concurrencyKey : null ,
202215 concurrencyLimit : 1 ,
203216 } ;
204- await expect (
205- Promise . resolve ( ) . then ( ( ) => backend . createWorkflowRun ( limitOnly ) ) ,
206- ) . rejects . toThrow (
207- 'Invalid workflow concurrency metadata: "concurrencyKey" and "concurrencyLimit" must both be null or both be set.' ,
208- ) ;
217+ const created = await backend . createWorkflowRun ( limitOnly ) ;
218+ expect ( created . concurrencyKey ) . toBeNull ( ) ;
219+ expect ( created . concurrencyLimit ) . toBe ( 1 ) ;
209220 } ) ;
210221
211222 test ( "rejects invalid workflow concurrency limit values" , async ( ) => {
@@ -289,6 +300,35 @@ export function testBackend(options: TestBackendOptions): void {
289300 await teardown ( backend ) ;
290301 } ) ;
291302
303+ test ( "rejects mixed concurrency limits for the same active default bucket" , async ( ) => {
304+ const backend = await setup ( ) ;
305+ const workflowName = randomUUID ( ) ;
306+ const version = "v1" ;
307+
308+ await createPendingWorkflowRun ( backend , {
309+ workflowName,
310+ version,
311+ concurrencyLimit : 1 ,
312+ } ) ;
313+
314+ await expect (
315+ backend . createWorkflowRun ( {
316+ workflowName,
317+ version,
318+ idempotencyKey : null ,
319+ concurrencyKey : null ,
320+ concurrencyLimit : 2 ,
321+ input : null ,
322+ config : { } ,
323+ context : null ,
324+ availableAt : null ,
325+ deadlineAt : null ,
326+ } ) ,
327+ ) . rejects . toThrow ( CONCURRENCY_LIMIT_MISMATCH_ERROR ) ;
328+
329+ await teardown ( backend ) ;
330+ } ) ;
331+
292332 test ( "allows changing concurrency limit after terminal runs leave active states" , async ( ) => {
293333 const backend = await setup ( ) ;
294334 const workflowName = randomUUID ( ) ;
@@ -1034,6 +1074,40 @@ export function testBackend(options: TestBackendOptions): void {
10341074 await teardown ( backend ) ;
10351075 } ) ;
10361076
1077+ test ( "enforces concurrency limit for default workflow-version bucket when key is omitted" , async ( ) => {
1078+ const backend = await setup ( ) ;
1079+ const workflowName = randomUUID ( ) ;
1080+ const version = "v1" ;
1081+ const concurrencyLimit = 1 ;
1082+
1083+ await createPendingWorkflowRun ( backend , {
1084+ workflowName,
1085+ version,
1086+ concurrencyLimit,
1087+ } ) ;
1088+ await createPendingWorkflowRun ( backend , {
1089+ workflowName,
1090+ version,
1091+ concurrencyLimit,
1092+ } ) ;
1093+
1094+ const firstClaimed = await backend . claimWorkflowRun ( {
1095+ workerId : randomUUID ( ) ,
1096+ leaseDurationMs : 100 ,
1097+ } ) ;
1098+ expect ( firstClaimed ) . not . toBeNull ( ) ;
1099+ expect ( firstClaimed ?. concurrencyKey ) . toBeNull ( ) ;
1100+ expect ( firstClaimed ?. concurrencyLimit ) . toBe ( concurrencyLimit ) ;
1101+
1102+ const blocked = await backend . claimWorkflowRun ( {
1103+ workerId : randomUUID ( ) ,
1104+ leaseDurationMs : 100 ,
1105+ } ) ;
1106+ expect ( blocked ) . toBeNull ( ) ;
1107+
1108+ await teardown ( backend ) ;
1109+ } ) ;
1110+
10371111 test ( "supports limits greater than one" , async ( ) => {
10381112 const backend = await setup ( ) ;
10391113 const workflowName = randomUUID ( ) ;
@@ -1210,6 +1284,37 @@ export function testBackend(options: TestBackendOptions): void {
12101284 await teardown ( backend ) ;
12111285 } ) ;
12121286
1287+ test ( "allows claims for different versions in default bucket when key is omitted" , async ( ) => {
1288+ const backend = await setup ( ) ;
1289+ const workflowName = randomUUID ( ) ;
1290+
1291+ await createPendingWorkflowRun ( backend , {
1292+ workflowName,
1293+ version : "v1" ,
1294+ concurrencyLimit : 1 ,
1295+ } ) ;
1296+ await createPendingWorkflowRun ( backend , {
1297+ workflowName,
1298+ version : "v2" ,
1299+ concurrencyLimit : 1 ,
1300+ } ) ;
1301+
1302+ const firstClaimed = await backend . claimWorkflowRun ( {
1303+ workerId : randomUUID ( ) ,
1304+ leaseDurationMs : 100 ,
1305+ } ) ;
1306+ const secondClaimed = await backend . claimWorkflowRun ( {
1307+ workerId : randomUUID ( ) ,
1308+ leaseDurationMs : 100 ,
1309+ } ) ;
1310+
1311+ expect ( firstClaimed ) . not . toBeNull ( ) ;
1312+ expect ( secondClaimed ) . not . toBeNull ( ) ;
1313+ expect ( secondClaimed ?. id ) . not . toBe ( firstClaimed ?. id ) ;
1314+
1315+ await teardown ( backend ) ;
1316+ } ) ;
1317+
12131318 test ( "allows claims after the active lease expires" , async ( ) => {
12141319 const backend = await setup ( ) ;
12151320 const workflowName = randomUUID ( ) ;
0 commit comments