Skip to content

test(isolation): 20 reflexive tests fail in the full suite but pass alone — Hypatia.Supervisor is torn down mid-run #643

Description

@hyperpolymath

Context

E2E — Elixir Scanner Pipeline is red on main. With #642 making the output visible, the
cause is now measurable rather than mysterious.

Reproduced locally (Elixir 1.18.3 / OTP 27, mix deps.get clean):

Run Result
mix test (full suite) 1398 tests, 20 failures
mix test test/reflexive_test.exs (alone) 16 tests, 0 failures

Every one of the 20 failures is in test/reflexive_test.exs with the same shape:

** (exit) exited in: GenServer.call(Hypatia.Supervisor, {:terminate_child, Hypatia.SelfDiagnostics}, :infinity)
    ** (EXIT) no process: the process is not alive or there's no process currently
              associated with the given name, possibly because its application isn't started
  test/reflexive_test.exs:29: Hypatia.ReflexiveTest.take_supervised/1

The file passing in isolation is the key datum: this is a test-isolation defect, not a
product bug.
Hypatia.Supervisor is alive at the start of an isolated run and dead by the
time reflexive tests run in the full suite.

Likely mechanism (to be confirmed, not assumed)

take_supervised/1 (test/reflexive_test.exs:29) removes a child from the live application
supervisor so start_supervised!/1 can own a clean instance:

Supervisor.terminate_child(Hypatia.Supervisor, module)
Supervisor.delete_child(Hypatia.Supervisor, module)
start_supervised!(module)
on_exit(fn -> ... Supervisor.start_child(Hypatia.Supervisor, module) end)

Two candidate causes, both consistent with the evidence:

  1. Restore never happens. If a test crashes before on_exit runs, or start_child
    fails (the spec was deleted), the child stays absent and later take_supervised calls
    compound the damage.
  2. max_restarts exceeded. lib/application.ex:90 uses strategy: :one_for_one with
    the default max_restarts: 3, max_seconds: 5. Repeated terminate/restart churn across
    many tests — plus any child that crash-loops — trips the limit and the whole supervisor
    shuts down
    , taking Hypatia.Supervisor with it. This matches "no process ... its
    application isn't started" exactly.

Hypatia.ReflexiveTest is async: false, so ordering is deterministic per seed — the failure
should be reproducible with a fixed --seed.

Changes

Diagnosis first — do not guess at a fix:

  1. Confirm which of the two mechanisms applies: run the full suite with a fixed --seed,
    and assert Process.whereis(Hypatia.Supervisor) in setup to find the first test at
    which it is already nil. Bisect by test file.
  2. Then fix at the right level:
    • If restore-failure: make take_supervised/1 restore idempotently and register the
      on_exit before the mutation, so a crash mid-setup still restores.
    • If max_restarts: stop mutating the live application supervisor from tests at all.
      Preferred shape — start these GenServers under the test's own supervisor via
      start_supervised!/1 with an explicit name:, and leave Hypatia.Supervisor
      untouched. A test should not need to dismantle the application to observe it.
  3. Consider raising max_restarts in lib/application.ex:90 only if it is genuinely
    warranted in production — not as a way to paper over test churn.

Acceptance criteria

  • mix test is 0 failures on a clean checkout, at three different seeds.
  • mix test test/reflexive_test.exs still passes in isolation (16 tests).
  • No test terminates or deletes a child of Hypatia.Supervisor, or if it must, the
    restore is proven by asserting the child is alive again after the suite.
  • E2E — Elixir Scanner Pipeline green on main.

Test plan

Model tier

Sonnet — bounded, single-repo, no new design; the mechanism is narrowed to two
candidates and the acceptance criteria are mechanical. Escalate to Opus only if the
cause turns out to be a genuine supervision-tree design problem rather than test hygiene.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    cx/L3Cross-repo or real code comprehension; strong botrdy/readyUnblocked — a bot can start nowtri/controlSafety triangle — gate it so it cannot regressvfy/testCompletion provable by a test

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions