Skip to content

test: prevent tmux server leaks in endpoint safety fixtures - #55

Merged
MrGTV-love merged 2 commits into
mainfrom
fm/fm-test-endpoint-safety-tmux-leak
Oct 8, 2026
Merged

MrGTV-love merged 2 commits into
mainfrom
fm/fm-test-endpoint-safety-tmux-leak

Conversation

@MrGTV-love

Copy link
Copy Markdown
Owner

Intent

I would like all bugs to be fixed so tomorrow can be focused entirely to Vernant and not problems preventning Vernant from getting built.

Context: the close-failure case in the fm-teardown-endpoint-safety test left its dedicated tmux server running (pid 2011 since 2026-10-07 11:54; its socket file deleted). A leaked tmux server owned by this user made fm-control relaunch --reconcile-only refuse with endpoint absence unproved, which blocked lane d6 worker recovery for Vernant PRs 3099 and 3096 overnight. PR #46 (missing tmux endpoint recovery despite unrelated servers) has since landed and d6 recovery then succeeded, so the remaining defect is the test fixture leak itself.

What Changed

  • Clean up isolated tmux servers using case-relative socket paths to avoid macOS Unix socket path limits.
  • Fail the test and report the socket if a server still responds after cleanup.
  • Consolidate exit and signal handling through a shared cleanup function that preserves the exit status unless a leak is detected.

Risk Assessment

✅ Low: The change is narrowly scoped to fixture cleanup, uses the same relative socket addressing as server creation, and preserves failure status while detecting servers that remain responsive after cleanup.

Testing

Reproduced the baseline leak and exercised the changed cleanup against real isolated tmux servers with 281–308-character socket paths. Normal execution, failure, and all handled signals cleaned up correctly; deliberately blocked cleanup produced the named leak diagnostic and exit status 1. An unrelated isolated server remained untouched. CLI and process-state evidence was captured, and all disposable servers and fixtures were removed. No full suite, linters, or other pipeline phases were run.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Run endpoint-safety tests under an overlong socket path without leaving tmux servers ✅ pass live normal-long-path-transcript.txt: exit status 0, no tracked server or initial-pane survivors, and no temporary fixture entries.
Fail the close-failure fixture and reap its server before deleting the socket directory ✅ pass live baseline-long-path-leak-transcript.txt reproduced a surviving server and shell after directory removal; failed-case-exit-transcript.txt confirmed no survivors with the changed cleanup and preserved ex…
Interrupt the close-failure fixture with INT, TERM, HUP, or QUIT and clean up while preserving signal status ✅ pass live cleanup-live-summary.json: INT=130, TERM=143, HUP=129, QUIT=131; every interrupted run left no tracked server or initial-pane survivors and no fixture entries.
Refuse the cleanup kill and report the still-running server as a test failure ✅ pass live unreapable-server-diagnostic-transcript.txt and successful-run-leak-guard-transcript.txt: real servers remained responsive after the deliberately refused kill, the diagnostic named the socket, and bot…
Clean up test fixtures without disturbing an unrelated tmux server ✅ pass live cleanup-live-summary.json: the independent validation-sentinel server remained alive and retained its session throughout the cleanup scenarios; it was explicitly stopped afterward.
Evidence: Baseline reproduction: server survives after socket removal

Source: Baseline reproduction: server survives after socket removal

Command: TMPDIR=<worktree-local long path> bash tests/.endpoint-safety-baseline.sh
Fault injection: failure

STDOUT
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target

STDERR

OBSERVED PROCESS AND FILESYSTEM STATE
{
  "label": "baseline-long-path-leak",
  "mode": "failure",
  "exit_status": 73,
  "expected_status": 73,
  "duration_seconds": 58.94,
  "server_and_initial_pane_pids": [
    22863,
    22889,
    40165,
    40185
  ],
  "surviving_pids_after_exit": [
    40165,
    40185
  ],
  "temporary_entries_after_exit": [],
  "socket_path_lengths": [
    289
  ],
  "leak_diagnostic": false,
  "expected_survivor_observed": true,
  "survivors_after_safety_cleanup": []
}
Evidence: Normal endpoint-safety execution and cleanup

Source: Normal endpoint-safety execution and cleanup

Command: TMPDIR=<worktree-local long path> bash tests/fm-teardown-endpoint-safety.test.sh
Fault injection: normal

STDOUT
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target
ok - fm-teardown: a close that genuinely failed refuses and keeps the record naming the surviving endpoint, and the same teardown finishes once the close works
ok - fm-teardown: --force continues past a close it could not make while still reporting it, and the same case refuses without --force
ok - fm-teardown: a close re-read that could not run refuses, while a definitively absent session or server still completes silently
ok - fm-teardown: forced secondmate cleanup still refuses on a child endpoint close that failed
ok - forced recursive descendant cleanup leaves an uninspected Orca backend copy intact
ok - fm-teardown: an Orca close its missing CLI never attempted refuses even under --force, keeping the record naming the terminal
ok - fm-teardown: an already-exited endpoint, and a server that is already gone, still complete cleanup silently
ok - Treehouse locking resolves a bare local origin against its source project, matching the provisioned clone
ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset
ok - fm-teardown: a pool slot held by another firstmate home is never returned
ok - fm-teardown: a task that solely holds its slot still returns it
ok - fm-teardown: a pool slot claimed by another task is left alone while the task's own cleanup finishes
ok - fm-teardown: a task's own slot claim, and an unclaimed slot, both still tear down
ok - fm-teardown: an exact recorded endpoint still tears down after changing cwd outside its worktree
ok - Treehouse project locking anchors at the local root for main-home, local-secondmate, and remote-seeded layouts
ok - fm-teardown: a remote-seeded secondmate home returns its own uncontested pool slot
ok - fm-teardown: slot ownership across a remote-seeded home and its local child still refuses
ok - Treehouse project locking still serializes two homes across the remote-seeded boundary

STDERR

OBSERVED PROCESS AND FILESYSTEM STATE
{
  "label": "normal-long-path",
  "mode": "normal",
  "exit_status": 0,
  "expected_status": 0,
  "duration_seconds": 97.26,
  "server_and_initial_pane_pids": [
    2448,
    2474,
    12446,
    12474,
    34558,
    34574,
    57201,
    57225,
    74030,
    74045,
    83594,
    83607,
    86608,
    86624
  ],
  "surviving_pids_after_exit": [],
  "temporary_entries_after_exit": [],
  "socket_path_lengths": [
    281,
    282,
    289,
    290,
    295,
    299
  ],
  "leak_diagnostic": false
}
Evidence: Failed-case cleanup preserves exit status

Source: Failed-case cleanup preserves exit status

Command: TMPDIR=<worktree-local long path> bash tests/fm-teardown-endpoint-safety.test.sh
Fault injection: failure

STDOUT
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target

STDERR

OBSERVED PROCESS AND FILESYSTEM STATE
{
  "label": "failed-case-exit",
  "mode": "failure",
  "exit_status": 73,
  "expected_status": 73,
  "duration_seconds": 34.87,
  "server_and_initial_pane_pids": [
    5200,
    5248,
    44949,
    44970
  ],
  "surviving_pids_after_exit": [],
  "temporary_entries_after_exit": [],
  "socket_path_lengths": [
    282
  ],
  "leak_diagnostic": false
}
Evidence: Live scenario outcomes, signal statuses, and unrelated-server isolation

Source: Live scenario outcomes, signal statuses, and unrelated-server isolation

{
  "scenarios": [
    {
      "label": "baseline-long-path-leak",
      "mode": "failure",
      "exit_status": 73,
      "expected_status": 73,
      "duration_seconds": 58.94,
      "server_and_initial_pane_pids": [
        22863,
        22889,
        40165,
        40185
      ],
      "surviving_pids_after_exit": [
        40165,
        40185
      ],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        289
      ],
      "leak_diagnostic": false,
      "expected_survivor_observed": true,
      "survivors_after_safety_cleanup": []
    },
    {
      "label": "normal-long-path",
      "mode": "normal",
      "exit_status": 0,
      "expected_status": 0,
      "duration_seconds": 97.26,
      "server_and_initial_pane_pids": [
        2448,
        2474,
        12446,
        12474,
        34558,
        34574,
        57201,
        57225,
        74030,
        74045,
        83594,
        83607,
        86608,
        86624
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        281,
        282,
        289,
        290,
        295,
        299
      ],
      "leak_diagnostic": false
    },
    {
      "label": "failed-case-exit",
      "mode": "failure",
      "exit_status": 73,
      "expected_status": 73,
      "duration_seconds": 34.87,
      "server_and_initial_pane_pids": [
        5200,
        5248,
        44949,
        44970
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        282
      ],
      "leak_diagnostic": false
    },
    {
      "label": "interrupted-int",
      "mode": "signal-INT",
      "exit_status": 130,
      "expected_status": 130,
      "duration_seconds": 35.79,
      "server_and_initial_pane_pids": [
        88204,
        88216,
        99830,
        99858
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        281
      ],
      "leak_diagnostic": false
    },
    {
      "label": "interrupted-term",
      "mode": "signal-TERM",
      "exit_status": 143,
      "expected_status": 143,
      "duration_seconds": 34.77,
      "server_and_initial_pane_pids": [
        3549,
        3567,
        28965,
        29016
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        282
      ],
      "leak_diagnostic": false
    },
    {
      "label": "interrupted-hup",
      "mode": "signal-HUP",
      "exit_status": 129,
      "expected_status": 129,
      "duration_seconds": 34.82,
      "server_and_initial_pane_pids": [
        13651,
        13675,
        42963,
        42999
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        281
      ],
      "leak_diagnostic": false
    },
    {
      "label": "interrupted-quit",
      "mode": "signal-QUIT",
      "exit_status": 131,
      "expected_status": 131,
      "duration_seconds": 44.16,
      "server_and_initial_pane_pids": [
        76043,
        76052,
        87722,
        87744
      ],
      "surviving_pids_after_exit": [],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        282
      ],
      "leak_diagnostic": false
    },
    {
      "label": "unreapable-server-diagnostic",
      "mode": "unreapable",
      "exit_status": 1,
      "expected_status": 1,
      "duration_seconds": 66.17,
      "server_and_initial_pane_pids": [
        3660,
        3674,
        88461,
        88514
      ],
      "surviving_pids_after_exit": [
        3660,
        3674
      ],
      "temporary_entries_after_exit": [],
      "socket_path_lengths": [
        294
      ],
      "leak_diagnostic": true,
      "expected_survivor_observed": true,
      "survivors_after_safety_cleanup": []
    }
  ],
  "unrelated_server": {
    "unrelated_server_pid": 70340,
    "sessions_after_all_scenarios": "validation-sentinel",
    "survived_all_fixture_cleanup": true
  }
}
Evidence: Blocked cleanup overrides an existing failure with leak status

Source: Blocked cleanup overrides an existing failure with leak status

Command: TMPDIR=<worktree-local long path> bash tests/fm-teardown-endpoint-safety.test.sh
Fault injection: unreapable

STDOUT
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target

STDERR
not ok - isolated tmux server outlived the test: ~/.no-mistakes/worktrees/32d18ed9638d/01M4DVMRC7TRN3SWV67P4B0VHR/.endpoint-validation/unreapable-server-diagnostic/long-isolated-temporary-directory-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/fm-teardown-endpoint-safety.JraFfj/close-failure/dedicated.sock

OBSERVED PROCESS AND FILESYSTEM STATE
{
  "label": "unreapable-server-diagnostic",
  "mode": "unreapable",
  "exit_status": 1,
  "expected_status": 1,
  "duration_seconds": 66.17,
  "server_and_initial_pane_pids": [
    3660,
    3674,
    88461,
    88514
  ],
  "surviving_pids_after_exit": [
    3660,
    3674
  ],
  "temporary_entries_after_exit": [],
  "socket_path_lengths": [
    294
  ],
  "leak_diagnostic": true,
  "expected_survivor_observed": true,
  "survivors_after_safety_cleanup": []
}
Evidence: Leak guard fails an otherwise successful test run

Source: Leak guard fails an otherwise successful test run

Command: TMPDIR=<worktree-local long path> bash tests/fm-teardown-endpoint-safety.test.sh
Fault injection: unreapable-success

STDOUT
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target
ok - fm-teardown: a close that genuinely failed refuses and keeps the record naming the surviving endpoint, and the same teardown finishes once the close works
ok - fm-teardown: --force continues past a close it could not make while still reporting it, and the same case refuses without --force
ok - fm-teardown: a close re-read that could not run refuses, while a definitively absent session or server still completes silently
ok - fm-teardown: forced secondmate cleanup still refuses on a child endpoint close that failed
ok - forced recursive descendant cleanup leaves an uninspected Orca backend copy intact
ok - fm-teardown: an Orca close its missing CLI never attempted refuses even under --force, keeping the record naming the terminal
ok - fm-teardown: an already-exited endpoint, and a server that is already gone, still complete cleanup silently
ok - Treehouse locking resolves a bare local origin against its source project, matching the provisioned clone
ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset
ok - fm-teardown: a pool slot held by another firstmate home is never returned
ok - fm-teardown: a task that solely holds its slot still returns it
ok - fm-teardown: a pool slot claimed by another task is left alone while the task's own cleanup finishes
ok - fm-teardown: a task's own slot claim, and an unclaimed slot, both still tear down
ok - fm-teardown: an exact recorded endpoint still tears down after changing cwd outside its worktree
ok - Treehouse project locking anchors at the local root for main-home, local-secondmate, and remote-seeded layouts
ok - fm-teardown: a remote-seeded secondmate home returns its own uncontested pool slot
ok - fm-teardown: slot ownership across a remote-seeded home and its local child still refuses
ok - Treehouse project locking still serializes two homes across the remote-seeded boundary

STDERR
not ok - isolated tmux server outlived the test: ~/.no-mistakes/worktrees/32d18ed9638d/01M4DVMRC7TRN3SWV67P4B0VHR/.endpoint-validation/successful-run-leak-guard/long-isolated-temporary-directory-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/fm-teardown-endpoint-safety.gsrKAQ/close-failure/dedicated.sock

OBSERVED PROCESS AND FILESYSTEM STATE
{
  "label": "successful-run-leak-guard",
  "mode": "unreapable-success",
  "exit_status": 1,
  "expected_status": 1,
  "duration_seconds": 150.34,
  "server_and_initial_pane_pids": [
    5404,
    5431,
    15362,
    15401,
    23738,
    23753,
    41651,
    41661,
    52754,
    52772,
    68991,
    69019,
    86870,
    86900
  ],
  "surviving_pids_after_exit": [
    68991,
    69019
  ],
  "temporary_entries_after_exit": [],
  "socket_path_lengths": [
    290,
    291,
    298,
    299,
    304,
    308
  ],
  "leak_diagnostic": true,
  "expected_survivor_observed": true,
  "survivors_after_safety_cleanup": []
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Run endpoint-safety tests under an overlong socket path without leaving tmux servers ✅ pass live normal-long-path-transcript.txt: exit status 0, no tracked server or initial-pane survivors, and no temporary fixture entries.
Fail the close-failure fixture and reap its server before deleting the socket directory ✅ pass live baseline-long-path-leak-transcript.txt reproduced a surviving server and shell after directory removal; failed-case-exit-transcript.txt confirmed no survivors with the changed cleanup and preserved ex…
Interrupt the close-failure fixture with INT, TERM, HUP, or QUIT and clean up while preserving signal status ✅ pass live cleanup-live-summary.json: INT=130, TERM=143, HUP=129, QUIT=131; every interrupted run left no tracked server or initial-pane survivors and no fixture entries.
Refuse the cleanup kill and report the still-running server as a test failure ✅ pass live unreapable-server-diagnostic-transcript.txt and successful-run-leak-guard-transcript.txt: real servers remained responsive after the deliberately refused kill, the diagnostic named the socket, and bot…
Clean up test fixtures without disturbing an unrelated tmux server ✅ pass live cleanup-live-summary.json: the independent validation-sentinel server remained alive and retained its session throughout the cleanup scenarios; it was explicitly stopped afterward.
  • git diff 32fbe8edbbd9d7c75990c3c259b7fb4fbf80de6c 34f7083f65477509da473e00d1eb0ab62e10e378 to scope the change.
  • python3 .endpoint-cleanup-validation.py using disposable worktree-local fixtures and real tmux servers: baseline reproduction, normal endpoint-safety test execution, injected failure, INT/TERM/HUP/QUIT interruptions, refused cleanup, and an unrelated isolated server.
  • run_case(&#39;successful-run-leak-guard&#39;, &#39;unreapable-success&#39;, ROOT / &#39;tests/fm-teardown-endpoint-safety.test.sh&#39;, 1) to verify that a surviving server changes an otherwise successful test run to exit status 1.
  • Recorded CLI transcripts, tmux operations, server and initial-pane PIDs, process survival, and fixture-directory removal; terminated deliberately leaked servers by their recorded disposable PIDs.
  • Removed the disposable driver, baseline script, and fixture directories; confirmed no transient validation files remained.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…when one outlives it

The teardown endpoint-safety cleanup reached each case socket by absolute path. Under the default macOS per-user TMPDIR that path exceeds the unix socket limit, so tmux refused it and kill-server silently did nothing, leaking the close-failure server whenever a case failed or was interrupted. Reach the socket by its relative name from the case directory, and exit nonzero naming any server that still answers after the reap.
@MrGTV-love
MrGTV-love merged commit 9da1b33 into main Oct 8, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant