Skip to content

jcode-tui test suite deadlocks on Windows above 2 threads (lock-order inversion, silently skips tests) #1201

Description

@YalmutairiAisc

cargo test -p jcode-tui --lib does not finish on Windows at the default
thread count. It is not slow, it deadlocks: every worker blocks and the run
hangs indefinitely, so every test after the wedge is silently never executed.

This is on current master (verified at e65e47c31), not a fork-specific
problem. It matters beyond Windows because a hung run reports nothing, so the
suite has been quietly under-reporting rather than failing loudly.

Reproduce

cargo test -p jcode-tui --lib     # hangs forever on Windows
cargo test -p jcode-tui --lib -- --test-threads=2   # completes

Exactly --test-threads many workers wedge. 1 and 2 are clean; 4, 8 and 16 all hang.

Cause

Two process-global test mutexes:

  • jcode_base::storage::lock_test_env() (guards JCODE_HOME / env vars)
  • jcode_tui::tui::ui::render_state_test_lock() (guards global render state)

create_test_app takes the render lock internally, so the de-facto order is
env then render, and six tests follow it. Two take them in the opposite
order, which is a classic AB-BA cycle:

  1. crates/jcode-tui/src/tui/app/tests/scroll_copy_02/part_02.rs:86
    test_alt_shift_i_toggles_inline_images_and_persists takes render, then env.

  2. crates/jcode-tui/src/tui/app/tests/smoothness_benchmark.rs:52
    smoothness_benchmark_simulated_streaming_turn_stays_within_budget takes
    render, then env indirectly via with_reasoning_current_home.

The second is the one that makes this hard to spot: the env acquisition is
inside a helper, so the call site does not look like it touches a lock at all.

A note on reading the thread dump

An earlier pass at this concluded a guard was being leaked from a dead thread,
because a gdb dump shows every worker blocked with no thread holding either
lock
. That reading is wrong, and it is an easy trap: holding a mutex leaves no
stack frame, so a thread that owns one lock while blocking on the other is
indistinguishable from a thread that owns nothing. "No visible holder" is
exactly what an AB-BA cycle looks like.

Both locks also recover from poisoning, so a panicking test cannot leak them.

Fix

Reducing the repro to just the two conflicting tests wedges within ~5 runs and
passes in 0.2s otherwise, which is why this never reproduced on demand.

Making both follow the env -> render order:

before after
full suite, default 16 threads never completes ~50s, 5/5 runs
the 2-test repro at 2 threads hangs within ~5 runs 20/20 clean
serial results 2255 passed / 15 failed 2255 passed / 15 failed

Identical pass/fail counts: this removes a hang, not a test.

The second test does not need the explicit render lock at all, since
create_test_app already takes it.

I also added a source-scanning test that asserts the ordering and names the
file, line and offending call, counting the three env-taking helpers
(with_temp_jcode_home, with_reasoning_current_home,
with_ssh_remote_test_home) as env acquisitions so the indirect form is
caught. It is static rather than runtime because the race is timing-dependent,
so a green run proves very little. Verified it fails by reintroducing the
original inversion.

Availability

The fix is one commit on top of master, on a fork:
YalmutairiAisc/jcode @ fix/windows-overload-and-tests.

I could not open a PR ("An owner of this repository has limited the ability to
open a pull request to users that are collaborators on this repository"), so
filing here instead. Happy to send it as a standalone one-commit PR if you
would like to enable that, or feel free to just take the change directly, it is
a three-line reorder plus the guard test.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomous: clearHands-off: unambiguous bug, obvious fix, no decisions. Don't even look - an agent can fully solve.bugSomething isn't workingtriage: reproducibleClear repro + clear fix path

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions