Skip to content

Commit d4a9eba

Browse files
committed
ControlMode(test): Cover protocol errors, attach_to, sandbox isolation
1 parent 646c050 commit d4a9eba

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

tests/test_control_mode_regressions.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ class TrailingOutputFixture(t.NamedTuple):
3131
expected_stdout: list[str]
3232

3333

34+
class AttachFixture(t.NamedTuple):
35+
"""Fixture for attach_to behaviours."""
36+
37+
test_id: str
38+
attach_to: str
39+
40+
3441
TRAILING_OUTPUT_CASES = [
3542
pytest.param(
3643
TrailingOutputFixture(
@@ -564,3 +571,36 @@ def test_session_kill_handles_control_eof() -> None:
564571
finally:
565572
with contextlib.suppress(Exception):
566573
server.kill()
574+
575+
576+
@pytest.mark.engines(["control"])
577+
@pytest.mark.parametrize(
578+
"case",
579+
[
580+
AttachFixture(test_id="attach_existing", attach_to="shared_session"),
581+
],
582+
ids=lambda c: c.test_id,
583+
)
584+
def test_attach_to_existing_session(case: AttachFixture) -> None:
585+
"""Control mode attach_to should not create/hide a management session."""
586+
socket_name = f"libtmux_test_{uuid.uuid4().hex[:8]}"
587+
bootstrap = Server(socket_name=socket_name)
588+
try:
589+
# Create the target session via subprocess engine
590+
bootstrap.new_session(
591+
session_name=case.attach_to,
592+
attach=False,
593+
kill_session=True,
594+
)
595+
engine = ControlModeEngine(attach_to=case.attach_to)
596+
server = Server(socket_name=socket_name, engine=engine)
597+
sessions = server.sessions
598+
assert len(sessions) == 1
599+
assert sessions[0].session_name == case.attach_to
600+
601+
attached = server.attached_sessions
602+
assert len(attached) == 1
603+
assert attached[0].session_name == case.attach_to
604+
finally:
605+
with contextlib.suppress(Exception):
606+
bootstrap.kill()

tests/test_control_sandbox.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,14 @@ def test_control_sandbox_smoke(control_sandbox: t.ContextManager[Server]) -> Non
2424
# Run a simple command to ensure control mode path works.
2525
out = server.cmd("display-message", "-p", "hi")
2626
assert out.stdout == ["hi"]
27+
28+
29+
def test_control_sandbox_isolation(control_sandbox: t.ContextManager[Server]) -> None:
30+
"""Sandbox should isolate HOME/TMUX_TMPDIR and use a unique socket."""
31+
with control_sandbox as server:
32+
assert server.socket_name is not None
33+
assert server.socket_name.startswith("libtmux_test")
34+
# Ensure TMUX is unset so the sandbox never reuses a user server
35+
import os
36+
37+
assert "TMUX" not in os.environ

tests/test_engine_protocol.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
NotificationKind,
1313
command_result_to_tmux_cmd,
1414
)
15-
from libtmux._internal.engines.control_protocol import CommandContext, ControlProtocol
15+
from libtmux._internal.engines.control_protocol import (
16+
CommandContext,
17+
ControlProtocol,
18+
ParserState,
19+
)
1620

1721

1822
class NotificationFixture(t.NamedTuple):
@@ -24,6 +28,14 @@ class NotificationFixture(t.NamedTuple):
2428
expected_subset: dict[str, str]
2529

2630

31+
class ProtocolErrorFixture(t.NamedTuple):
32+
"""Fixture for protocol error handling."""
33+
34+
test_id: str
35+
line: str
36+
expected_reason: str
37+
38+
2739
def test_command_result_wraps_tmux_cmd() -> None:
2840
"""CommandResult should adapt cleanly into tmux_cmd wrapper."""
2941
result = CommandResult(
@@ -74,6 +86,31 @@ def test_control_protocol_notifications() -> None:
7486
assert proto.get_stats(restarts=0).dropped_notifications >= 1
7587

7688

89+
PROTOCOL_ERROR_CASES: list[ProtocolErrorFixture] = [
90+
ProtocolErrorFixture(
91+
test_id="unexpected_end",
92+
line="%end 123 1 0",
93+
expected_reason="unexpected %end",
94+
),
95+
ProtocolErrorFixture(
96+
test_id="no_pending_begin",
97+
line="%begin 999 1 0",
98+
expected_reason="no pending command for %begin",
99+
),
100+
]
101+
102+
103+
@pytest.mark.parametrize("case", PROTOCOL_ERROR_CASES, ids=lambda c: c.test_id)
104+
def test_control_protocol_errors(case: ProtocolErrorFixture) -> None:
105+
"""Protocol errors should mark the parser DEAD and record last_error."""
106+
proto = ControlProtocol()
107+
proto.feed_line(case.line)
108+
stats = proto.get_stats(restarts=0)
109+
assert proto.state is ParserState.DEAD
110+
assert stats.last_error is not None
111+
assert case.expected_reason in stats.last_error
112+
113+
77114
NOTIFICATION_FIXTURES: list[NotificationFixture] = [
78115
NotificationFixture(
79116
test_id="layout_change",

0 commit comments

Comments
 (0)