From 1c7d0c7f3ea3aaa5bdd0262e4553f80464ef23bb Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 12 Aug 2026 15:45:23 -0400 Subject: [PATCH] Match Trio's startup error exactly Compare the complete canonical `Nursery.start()` protocol error before re-surfacing ambient cancellation. Preserve child-owned `RuntimeError` objects whose messages only resemble Trio's wording. Cover the colliding prefix and assert the original error remains the exception group's sole leaf. Review: PR #479 (goodboy) https://github.com/goodboy/tractor/pull/479 (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- tests/trionics/test_taskc.py | 17 +++++++++-------- tractor/trionics/_taskc.py | 5 ++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/trionics/test_taskc.py b/tests/trionics/test_taskc.py index 203e0e4d..be4f6745 100644 --- a/tests/trionics/test_taskc.py +++ b/tests/trionics/test_taskc.py @@ -224,10 +224,10 @@ def test_genuine_startup_rte_still_raised( @pytest.mark.parametrize( 'rte_arg', [ - # a bare `'started' in args[0]` substring match - # would (wrongly) demote this one to a `Cancelled` - # under ambient cancellation. + # Broad substring matches would wrongly demote either + # child-owned error to `Cancelled` under cancellation. 'never got started!', + 'child exited without calling user hook', # non-`str` first-arg edge; must not `TypeError` # inside the wrapper's msg-match guard. 1234, @@ -242,9 +242,12 @@ def test_childs_own_rte_never_demoted_to_cancel( first arg), raised under ambient cancellation must NOT be demoted to a `trio.Cancelled` by the exact-msg-match guard inside `start_or_cancel()`; the real error must - always propagate to the caller unchanged. + always propagate to the caller as the sole exception-group + leaf, preserving object identity. ''' + child_rte = RuntimeError(rte_arg) + async def cancels_cs_then_raises( task_status: TaskStatus[None] = ( trio.TASK_STATUS_IGNORED @@ -255,7 +258,7 @@ def test_childs_own_rte_never_demoted_to_cancel( # deterministically dies with ITS error while the # caller is under effective cancellation. cs.cancel() - raise RuntimeError(rte_arg) + raise child_rte cs = trio.CancelScope() @@ -270,9 +273,7 @@ def test_childs_own_rte_never_demoted_to_cancel( with pytest.raises(ExceptionGroup) as excinfo: trio.run(main) - rte = excinfo.value.exceptions[0] - assert isinstance(rte, RuntimeError) - assert rte.args[0] == rte_arg + assert excinfo.value.exceptions == (child_rte,) def test_started_value_and_args_passthru(): diff --git a/tractor/trionics/_taskc.py b/tractor/trionics/_taskc.py index 7cb5fff6..2079179b 100644 --- a/tractor/trionics/_taskc.py +++ b/tractor/trionics/_taskc.py @@ -349,7 +349,10 @@ async def start_or_cancel( # demote it to a `Cancelled`, losing the real error. The # `isinstance` guard also avoids a `TypeError` when # `rte.args[0]` isn't a `str`. - 'child exited without calling' in rte.args[0] + rte.args[0] == ( + 'child exited without calling ' + 'task_status.started()' + ) ): # re-raises the in-flight `trio.Cancelled` IFF we're # under effective cancellation; else a cheap no-op and