@@ -189,6 +189,142 @@ module(`realm-endpoints/${basename(__filename)}`, function () {
189189 ) ;
190190 } ) ;
191191
192+ test ( 'cancels both running and pending jobs when cancelPending is true' , async function ( assert ) {
193+ let concurrencyGroup = `indexing:${ testRealm . url } ` ;
194+
195+ // Create a running job (with active reservation)
196+ let [ { id : runningJobId } ] = ( await dbAdapter . execute ( `INSERT INTO jobs
197+ (args, job_type, concurrency_group, timeout, priority)
198+ VALUES
199+ (
200+ '{"realmURL": "${ testRealm . url } ", "realmUsername":"node-test_realm"}',
201+ 'from-scratch-index',
202+ '${ concurrencyGroup } ',
203+ 180,
204+ 0
205+ ) RETURNING id` ) ) as { id : string } [ ] ;
206+ await dbAdapter . execute ( `INSERT INTO job_reservations
207+ (job_id, locked_until ) VALUES (${ runningJobId } , NOW() + INTERVAL '3 minutes')` ) ;
208+
209+ // Create a pending job (no reservation)
210+ let [ { id : pendingJobId } ] = ( await dbAdapter . execute ( `INSERT INTO jobs
211+ (args, job_type, concurrency_group, timeout, priority)
212+ VALUES
213+ (
214+ '{"realmURL": "${ testRealm . url } ", "realmUsername":"node-test_realm"}',
215+ 'incremental-index',
216+ '${ concurrencyGroup } ',
217+ 180,
218+ 0
219+ ) RETURNING id` ) ) as { id : string } [ ] ;
220+
221+ let response = await request
222+ . post ( '/_cancel-indexing-job' )
223+ . set ( 'Accept' , 'application/json' )
224+ . set (
225+ 'Authorization' ,
226+ `Bearer ${ createJWT ( testRealm , 'writer' , [ 'read' , 'write' ] ) } ` ,
227+ )
228+ . send ( { cancelPending : true } ) ;
229+
230+ assert . strictEqual ( response . status , 204 , 'HTTP 204 response' ) ;
231+
232+ // Running job should be cancelled
233+ let [ runningJob ] = await dbAdapter . execute (
234+ `SELECT status, result, finished_at FROM jobs WHERE id = ${ runningJobId } ` ,
235+ ) ;
236+ assert . strictEqual (
237+ runningJob . status ,
238+ 'rejected' ,
239+ 'running job was canceled' ,
240+ ) ;
241+ assert . deepEqual (
242+ runningJob . result ,
243+ {
244+ status : 418 ,
245+ message : 'User initiated job cancellation' ,
246+ } ,
247+ 'running job result is cancellation payload' ,
248+ ) ;
249+ assert . ok ( runningJob . finished_at , 'running job has finish time' ) ;
250+
251+ // Pending job should ALSO be cancelled
252+ let [ pendingJob ] = await dbAdapter . execute (
253+ `SELECT status, result, finished_at FROM jobs WHERE id = ${ pendingJobId } ` ,
254+ ) ;
255+ assert . strictEqual (
256+ pendingJob . status ,
257+ 'rejected' ,
258+ 'pending job was also canceled when cancelPending is true' ,
259+ ) ;
260+ assert . deepEqual (
261+ pendingJob . result ,
262+ {
263+ status : 418 ,
264+ message : 'User initiated job cancellation' ,
265+ } ,
266+ 'pending job result is cancellation payload' ,
267+ ) ;
268+ assert . ok ( pendingJob . finished_at , 'pending job has finish time' ) ;
269+ } ) ;
270+
271+ test ( 'default behavior (no body) only cancels running jobs, not pending' , async function ( assert ) {
272+ let concurrencyGroup = `indexing:${ testRealm . url } ` ;
273+
274+ let [ { id : runningJobId } ] = ( await dbAdapter . execute ( `INSERT INTO jobs
275+ (args, job_type, concurrency_group, timeout, priority)
276+ VALUES
277+ (
278+ '{"realmURL": "${ testRealm . url } ", "realmUsername":"node-test_realm"}',
279+ 'from-scratch-index',
280+ '${ concurrencyGroup } ',
281+ 180,
282+ 0
283+ ) RETURNING id` ) ) as { id : string } [ ] ;
284+ await dbAdapter . execute ( `INSERT INTO job_reservations
285+ (job_id, locked_until ) VALUES (${ runningJobId } , NOW() + INTERVAL '3 minutes')` ) ;
286+
287+ let [ { id : pendingJobId } ] = ( await dbAdapter . execute ( `INSERT INTO jobs
288+ (args, job_type, concurrency_group, timeout, priority)
289+ VALUES
290+ (
291+ '{"realmURL": "${ testRealm . url } ", "realmUsername":"node-test_realm"}',
292+ 'incremental-index',
293+ '${ concurrencyGroup } ',
294+ 180,
295+ 0
296+ ) RETURNING id` ) ) as { id : string } [ ] ;
297+
298+ // No body — default behavior
299+ let response = await request
300+ . post ( '/_cancel-indexing-job' )
301+ . set ( 'Accept' , 'application/json' )
302+ . set (
303+ 'Authorization' ,
304+ `Bearer ${ createJWT ( testRealm , 'writer' , [ 'read' , 'write' ] ) } ` ,
305+ ) ;
306+
307+ assert . strictEqual ( response . status , 204 , 'HTTP 204 response' ) ;
308+
309+ let [ runningJob ] = await dbAdapter . execute (
310+ `SELECT status FROM jobs WHERE id = ${ runningJobId } ` ,
311+ ) ;
312+ assert . strictEqual (
313+ runningJob . status ,
314+ 'rejected' ,
315+ 'running job canceled' ,
316+ ) ;
317+
318+ let [ pendingJob ] = await dbAdapter . execute (
319+ `SELECT status FROM jobs WHERE id = ${ pendingJobId } ` ,
320+ ) ;
321+ assert . strictEqual (
322+ pendingJob . status ,
323+ 'unfulfilled' ,
324+ 'pending job NOT canceled when cancelPending is not set' ,
325+ ) ;
326+ } ) ;
327+
192328 test ( 'does not treat expired reservations as running jobs' , async function ( assert ) {
193329 let concurrencyGroup = `indexing:${ testRealm . url } ` ;
194330 let [ { id : jobId } ] = ( await dbAdapter . execute ( `INSERT INTO jobs
0 commit comments