fix(test): register the supervisor restore BEFORE mutating (#643)#644
Merged
Conversation
`take_supervised/1` removed a child from the live application supervisor and
only THEN registered the `on_exit` that puts it back:
Supervisor.terminate_child(Hypatia.Supervisor, module)
Supervisor.delete_child(Hypatia.Supervisor, module)
start_supervised!(module) # <- if this raises...
on_exit(fn -> ...restore... end) # <- ...this never registers
If anything between the delete and the on_exit fails, the child is deleted
from Hypatia.Supervisor and NEVER restored — for the whole rest of the suite.
Every later test needing that module then fails too. That is why one root
failure cascaded into ~20, and why the failing set changed with the seed.
Fix: register the restore first (on_exit still runs when setup raises), and
make it idempotent — clear any stale spec, re-add, and inspect the return
rather than discarding it, so a restore failure is loud instead of silently
poisoning everything downstream.
PROOF of the mechanism (load-independent, by injecting a raise after the
delete and asking the supervisor what children it has):
[[PROOF]] OLD -> child present after raise: false <- lost forever
[[PROOF]] NEW -> restore registered, runs on_exit
Verified:
mix test test/reflexive_test.exs -> 16 tests, 0 failures
mix test reflexive_test + safety_test -> 42 tests, 0 failures
reflexive_test followed by an intactness assertion
(all four modules still children of Hypatia.Supervisor
with live pids) -> 17 tests, 0 failures
SCOPE — deliberately honest: this fixes the cascade, NOT the whole suite.
`mix test` remains red for other reasons. Whole-suite A/B across seeds was
inconclusive here because reflexive_test (and others) use `:timer.sleep/1`
for synchronisation, so counts swing with machine load — this box was busy.
The remaining order- and timing-dependent failures are separate work and are
tracked in #643, which this commit updates rather than closes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hyperpolymath
marked this pull request as ready for review
July 21, 2026 12:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Partial fix for #643 — kills the cascade, not the whole red suite. Scope is stated
plainly at the bottom.
The bug
take_supervised/1(test/reflexive_test.exs:28) removed a child from the liveapplication supervisor and only then registered the
on_exitthat restores it:If anything between the delete and the
on_exitfails, the child is gone fromHypatia.Supervisorpermanently, for the rest of the suite. Every later test needingthat module then fails too.
That explains both puzzling observations: one root failure cascading into ~20, and the
failing set changing with the seed.
Proof of mechanism
Injected a raise after the delete and asked the supervisor what children it actually has —
load-independent, unlike whole-suite counts:
Changes
on_exitcallbacks still run when setup raises, so therestore becomes unconditional.
restore_supervised/1and make it idempotent — safe whether the spec isabsent, present-but-stopped, or already running.
Supervisor.start_child/2instead of discarding it, andraise on an unexpected result. A silent restore failure is precisely what made the
original cascade invisible.
Verification
mix test test/reflexive_test.exsreflexive_test+safety_testtogetherreflexive_test+ intactness assertion (all 4 modules still children ofHypatia.Supervisor, live pids)Scope — what this does NOT fix
mix testis still red for other reasons, and I am not claiming otherwise.A whole-suite A/B across seeds came out inconclusive on this machine:
reflexive_test(and others) synchronise with
:timer.sleep/1, so failure counts swing with machine load,and this box was busy building containers. Counts moved in both directions across seeds for
reasons unrelated to this change, so I am not presenting them as evidence either way — the
injected-raise proof above is the load-independent evidence.
Remaining work, tracked in #643 (which this updates rather than closes):
dashboard,root_hygiene,workflow_audit,metrics,recipe_health… vary by seed):timer.sleep/1-based synchronisation, which should become deterministic waits🤖 Generated with Claude Code