|
20 | 20 | TAG_READONLY, |
21 | 21 | VALID_SAFETY_LEVELS, |
22 | 22 | _apply_filters, |
23 | | - _get_caller_pane_id, |
24 | 23 | _get_server, |
25 | 24 | _invalidate_server, |
26 | 25 | _resolve_pane, |
@@ -306,23 +305,6 @@ def test_apply_filters( |
306 | 305 | assert len(result) >= 1 |
307 | 306 |
|
308 | 307 |
|
309 | | -# --------------------------------------------------------------------------- |
310 | | -# _get_caller_pane_id / _serialize_pane is_caller tests |
311 | | -# --------------------------------------------------------------------------- |
312 | | - |
313 | | - |
314 | | -def test_get_caller_pane_id_returns_env(monkeypatch: pytest.MonkeyPatch) -> None: |
315 | | - """_get_caller_pane_id returns TMUX_PANE when set.""" |
316 | | - monkeypatch.setenv("TMUX_PANE", "%42") |
317 | | - assert _get_caller_pane_id() == "%42" |
318 | | - |
319 | | - |
320 | | -def test_get_caller_pane_id_returns_none(monkeypatch: pytest.MonkeyPatch) -> None: |
321 | | - """_get_caller_pane_id returns None outside tmux.""" |
322 | | - monkeypatch.delenv("TMUX_PANE", raising=False) |
323 | | - assert _get_caller_pane_id() is None |
324 | | - |
325 | | - |
326 | 308 | # --------------------------------------------------------------------------- |
327 | 309 | # Caller identity parsing tests |
328 | 310 | # --------------------------------------------------------------------------- |
@@ -532,10 +514,16 @@ class SerializePaneCallerFixture(t.NamedTuple): |
532 | 514 |
|
533 | 515 | SERIALIZE_PANE_CALLER_FIXTURES: list[SerializePaneCallerFixture] = [ |
534 | 516 | SerializePaneCallerFixture( |
535 | | - test_id="matching_pane_id", |
| 517 | + # TMUX_PANE is set to the real pane id but TMUX is unset, so the |
| 518 | + # caller's socket cannot be verified. The strict comparator |
| 519 | + # declines to assume same-server: ``False`` not ``True``. |
| 520 | + # Pre-fixup this returned ``True`` via ``_caller_is_on_server``'s |
| 521 | + # conservative-True branch — a cross-socket false positive the |
| 522 | + # informational annotation must not carry. |
| 523 | + test_id="matching_pane_id_no_tmux_env", |
536 | 524 | tmux_pane_env=None, |
537 | 525 | use_real_pane_id=True, |
538 | | - expected_is_caller=True, |
| 526 | + expected_is_caller=False, |
539 | 527 | ), |
540 | 528 | SerializePaneCallerFixture( |
541 | 529 | test_id="non_matching_pane_id", |
@@ -577,6 +565,80 @@ def test_serialize_pane_is_caller( |
577 | 565 | assert data.is_caller is expected_is_caller |
578 | 566 |
|
579 | 567 |
|
| 568 | +def test_serialize_pane_is_caller_false_across_sockets( |
| 569 | + TestServer: type[Server], |
| 570 | + monkeypatch: pytest.MonkeyPatch, |
| 571 | +) -> None: |
| 572 | + """is_caller must not flag a pane on a *different* tmux socket. |
| 573 | +
|
| 574 | + Regression for tmux-python/libtmux-mcp#19. Before the fix, |
| 575 | + ``_serialize_pane`` compared ``pane.pane_id == TMUX_PANE`` without |
| 576 | + any socket check — so a caller inside pane ``%0`` on socket A saw |
| 577 | + ``is_caller=True`` for any pane with id ``%0`` on any other server. |
| 578 | +
|
| 579 | + Two fresh libtmux servers emit matching pane ids (both start at |
| 580 | + ``%0``), so this reproduces the false-positive exactly. Point the |
| 581 | + caller at server A, serialize pane ``%0`` on server B, assert the |
| 582 | + annotation says ``False``. |
| 583 | + """ |
| 584 | + from libtmux_mcp._utils import _effective_socket_path |
| 585 | + |
| 586 | + server_a = TestServer() |
| 587 | + session_a = server_a.new_session(session_name="mcp_issue19_a") |
| 588 | + pane_a = session_a.active_window.active_pane |
| 589 | + assert pane_a is not None and pane_a.pane_id is not None |
| 590 | + |
| 591 | + server_b = TestServer() |
| 592 | + session_b = server_b.new_session(session_name="mcp_issue19_b") |
| 593 | + pane_b = session_b.active_window.active_pane |
| 594 | + assert pane_b is not None and pane_b.pane_id is not None |
| 595 | + |
| 596 | + # Prerequisite: the two freshly-spawned servers emitted matching |
| 597 | + # pane ids. If they didn't (a tmux version quirk), the false |
| 598 | + # positive can't be exercised — skip rather than fail. |
| 599 | + if pane_a.pane_id != pane_b.pane_id: |
| 600 | + pytest.skip( |
| 601 | + f"sibling servers emitted distinct pane ids " |
| 602 | + f"({pane_a.pane_id} vs {pane_b.pane_id}); cannot reproduce issue #19" |
| 603 | + ) |
| 604 | + |
| 605 | + socket_a = _effective_socket_path(server_a) |
| 606 | + assert socket_a is not None |
| 607 | + monkeypatch.setenv("TMUX", f"{socket_a},1,{session_a.session_id or '$0'}") |
| 608 | + monkeypatch.setenv("TMUX_PANE", pane_a.pane_id) |
| 609 | + |
| 610 | + # Pane on the *other* server — must be flagged False even though |
| 611 | + # its pane_id matches TMUX_PANE. |
| 612 | + assert _serialize_pane(pane_b).is_caller is False |
| 613 | + # Sanity: on the caller's own server, same pane_id *is* the caller. |
| 614 | + assert _serialize_pane(pane_a).is_caller is True |
| 615 | + |
| 616 | + |
| 617 | +def test_serialize_pane_is_caller_requires_tmux_env_not_just_pane( |
| 618 | + mcp_pane: Pane, |
| 619 | + monkeypatch: pytest.MonkeyPatch, |
| 620 | +) -> None: |
| 621 | + """``TMUX_PANE`` alone must not declare a caller identity. |
| 622 | +
|
| 623 | + Regression for the subtle cross-socket false positive that |
| 624 | + :func:`_caller_is_on_server`'s "socket_path unset → conservative |
| 625 | + True" branch would otherwise introduce. When the MCP process has |
| 626 | + ``TMUX_PANE`` in its environment but not ``TMUX`` — an unusual but |
| 627 | + possible state an agent harness can produce — the caller's socket |
| 628 | + is unknowable. The strict comparator declines to assert |
| 629 | + ``is_caller=True`` in that case so any pane whose id happens to |
| 630 | + match ``TMUX_PANE`` across *any* server is annotated ``False``, |
| 631 | + not a false positive. Exercises the code path that was left |
| 632 | + un-covered after the direct ``_get_caller_pane_id`` unit tests |
| 633 | + were removed. |
| 634 | + """ |
| 635 | + assert mcp_pane.pane_id is not None |
| 636 | + monkeypatch.setenv("TMUX_PANE", mcp_pane.pane_id) |
| 637 | + monkeypatch.delenv("TMUX", raising=False) |
| 638 | + |
| 639 | + assert _serialize_pane(mcp_pane).is_caller is False |
| 640 | + |
| 641 | + |
580 | 642 | # --------------------------------------------------------------------------- |
581 | 643 | # Annotation and tag constants tests |
582 | 644 | # --------------------------------------------------------------------------- |
|
0 commit comments