@@ -255,16 +255,15 @@ void testOutOfOrderScopeCloseReleasesImmediately() {
255255 Context contextD = root ().with (CONTINUATION_KEY , "D" );
256256 try (ContextScope scopeR = continuation .resume ()) {
257257 assertEquals (contextC , current ());
258- ContextScope scopeD = contextD .attach (); // attaching D fires detach:C, attach:D
259- assertEquals (contextD , current ());
258+ try ( ContextScope scopeD = contextD .attach ()) { // attaching D fires detach:C, attach:D
259+ assertEquals (contextD , current ());
260260
261- // close the resume scope out-of-order while D is still nested on top;
262- // release fires immediately, but detach:C does not (C is not current)
263- scopeR .close ();
264- assertEquals (asList ("attach:C" , "detach:C" , "attach:D" , "release:C" ), events );
265- assertEquals (contextD , current ()); // D is still current
266-
267- scopeD .close (); // unwind D normally, restores C
261+ // close the resume scope out-of-order while D is still nested on top;
262+ // release fires immediately, but detach:C does not (C is not current)
263+ scopeR .close ();
264+ assertEquals (asList ("attach:C" , "detach:C" , "attach:D" , "release:C" ), events );
265+ assertEquals (contextD , current ()); // D is still current
266+ } // scopeD closes here: unwind D normally, restores C
268267 assertEquals (
269268 asList ("attach:C" , "detach:C" , "attach:D" , "release:C" , "detach:D" , "attach:C" ), events );
270269 } // try-with-resources closes scopeR again; no second release, C unwinds to root
@@ -292,14 +291,13 @@ void testHoldWithOutOfOrderScopeCloseFiresReleaseOnExplicitRelease() {
292291 Context contextD = root ().with (CONTINUATION_KEY , "D" );
293292 try (ContextScope scopeR = continuation .resume ()) {
294293 assertEquals (contextC , current ());
295- ContextScope scopeD = contextD .attach (); // detach:C, attach:D
296- assertEquals (contextD , current ());
297-
298- scopeR .close (); // out-of-order close while D is still on top; hold prevents auto-release
299- assertEquals (asList ("attach:C" , "detach:C" , "attach:D" ), events );
300- assertEquals (contextD , current ());
294+ try (ContextScope scopeD = contextD .attach ()) { // detach:C, attach:D
295+ assertEquals (contextD , current ());
301296
302- scopeD .close (); // unwind D, restores C
297+ scopeR .close (); // out-of-order close while D is still on top; hold prevents auto-release
298+ assertEquals (asList ("attach:C" , "detach:C" , "attach:D" ), events );
299+ assertEquals (contextD , current ());
300+ } // scopeD closes here: unwind D, restores C
303301 } // TWR closes scopeR again (now in-order); detach:C, no release yet (hold is active)
304302
305303 assertEquals (root (), current ());
@@ -312,6 +310,46 @@ void testHoldWithOutOfOrderScopeCloseFiresReleaseOnExplicitRelease() {
312310 events );
313311 }
314312
313+ @ Test
314+ void testMultipleHoldCallsAreIdempotent () {
315+ // Calling hold() more than once should not require more than one explicit release().
316+ List <String > events = new ArrayList <>();
317+ ContextManager .register (trackingListener (events ));
318+ Context context = root ().with (CONTINUATION_KEY , "value" );
319+ ContextContinuation continuation ;
320+ try (ContextScope scope = context .attach ()) {
321+ continuation = context .capture ();
322+ continuation .hold ();
323+ continuation .hold (); // second hold must be a no-op
324+ }
325+ // One explicit release() is enough — no extra releases needed for the second hold().
326+ continuation .release ();
327+ assertEquals (asList ("attach" , "capture" , "detach" , "release" ), events );
328+ continuation .release (); // still idempotent after the final release
329+ assertEquals (asList ("attach" , "capture" , "detach" , "release" ), events );
330+ }
331+
332+ @ Test
333+ void testHoldAfterReleaseIsIgnored () {
334+ // hold() on an already-released continuation must not resurrect it.
335+ List <String > events = new ArrayList <>();
336+ ContextManager .register (trackingListener (events ));
337+ Context context = root ().with (CONTINUATION_KEY , "value" );
338+ ContextContinuation continuation ;
339+ try (ContextScope scope = context .attach ()) {
340+ continuation = context .capture ();
341+ }
342+ continuation .release ();
343+ assertEquals (asList ("attach" , "capture" , "detach" , "release" ), events );
344+ continuation .hold (); // must be silently ignored
345+ // resume() after release is already a noop, even with the spurious hold()
346+ try (ContextScope scope = continuation .resume ()) {
347+ assertEquals (root (), current ());
348+ }
349+ continuation .release (); // must not fire a second release event
350+ assertEquals (asList ("attach" , "capture" , "detach" , "release" ), events );
351+ }
352+
315353 @ Test
316354 void testHoldAllowsMultipleReleaseCalls () {
317355 List <String > events = new ArrayList <>();
0 commit comments