Files
herdr-session-switch/tests/test_tmux_integration.py
T
gitea-adminandClaude Fable 5.1 907813867a Batch tmux configuration into single client invocations
configure() ran one tmux client per option and per key binding, and
refresh_labels() one or two per window, on every start/refresh/open: a
no-op refresh with three windows spawned 45 tmux processes. On a phone
each spawn is noticeable.

Add run_batch(), which joins commands with ';' separators (escaping
arguments that end in ';', which tmux would otherwise split) and replays
them one by one if the batch fails so errors stay attributable. Use it
for configure(), window creation, endpoint replacement and label
refreshes. list-windows now also reports the stored short label so
unchanged labels are skipped, and managed_windows() no longer probes
has-session first. The same no-op refresh now spawns 9 processes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 03:02:31 +00:00

434 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
import os
from pathlib import Path
import shutil
import tempfile
import time
import unittest
import uuid
from unittest.mock import patch
from herdr_hub.cli import (
BOOTSTRAP_WINDOW_NAME,
Endpoint,
HubError,
TmuxHub,
WINDOW_SHORT_LABEL_OPTION,
build_parser,
command_control,
)
@unittest.skipUnless(shutil.which("tmux"), "tmux is required")
class TmuxIntegrationTests(unittest.TestCase):
def test_verified_handoff_reconnects_only_originating_client(self) -> None:
self.hub.ensure_server()
original = self.hub.create_window(Endpoint.local("first"), str(self.fake_herdr))
other = self.hub.create_window(Endpoint.local("second"), str(self.fake_herdr))
self.hub.select_window(original.window_id)
def pid(window):
return self.hub.run(["display-message", "-p", "-t", window.window_id, "#{pane_pid}"]).stdout.strip()
original_pid, other_pid = pid(original), pid(other)
def close_popup(run):
self.hub.select_window(other.window_id)
run.__self__.reconnect_requested = True
return 0
with patch("herdr_hub.cli.make_hub", return_value=self.hub), patch(
"herdr_hub.cli.resolve_herdr", return_value=str(self.fake_herdr)
), patch("herdr_hub.cli.curses.wrapper", side_effect=close_popup):
self.assertEqual(command_control(build_parser().parse_args(["control"])), 0)
self.assertNotEqual(pid(original), original_pid)
self.assertEqual(pid(other), other_pid)
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary_directory.cleanup)
root = Path(self.temporary_directory.name)
self.fake_herdr = root / "herdr"
self.fake_herdr.write_text(
"#!/bin/sh\n"
"if [ \"${1:-}\" = \"--session\" ] && [ \"${2:-}\" = \"dead\" ]; then\n"
" printf '%s\\n' 'fake startup failure' >&2\n"
" exit 7\n"
"fi\n"
"exec sleep 30\n",
encoding="utf-8",
)
self.fake_herdr.chmod(0o755)
self.fake_ssh = root / "ssh"
self.fake_ssh.write_text("#!/bin/sh\nexec sleep 30\n", encoding="utf-8")
self.fake_ssh.chmod(0o755)
self.socket_name = f"herdr-hub-test-{os.getpid()}-{uuid.uuid4().hex[:8]}"
self.socket_path = Path(
os.environ.get("TMUX_TMPDIR", f"/tmp/tmux-{os.getuid()}")
) / self.socket_name
self.hub = TmuxHub(
tmux_bin=shutil.which("tmux") or "tmux",
ssh_bin=str(self.fake_ssh),
socket_name=self.socket_name,
session_name="hub",
config_file="/dev/null",
script_path="/dev/null",
)
self.addCleanup(self._stop_hub)
def _stop_hub(self) -> None:
try:
self.hub.stop()
except Exception:
pass
self.socket_path.unlink(missing_ok=True)
def test_control_uses_existing_prefix_and_active_window(self) -> None:
self.hub.ensure_server()
binding = self.hub.run(["list-keys", "-T", "prefix", "h"]).stdout
self.assertIn("display-popup", binding)
self.assertIn("control", binding)
self.assertNotIn("--window", binding)
self.assertNotIn("#{window_id}", binding)
old_binding = self.hub.run(
["list-keys", "-T", "root", "C-g"], check=False
)
self.assertNotEqual(old_binding.returncode, 0)
self.assertEqual(self.hub.run(["show-options", "-gv", "prefix"]).stdout.strip(), "C-a")
self.assertEqual(self.hub.run(["show-options", "-gv", "prefix2"]).stdout.strip(), "None")
self.assertIn("choose", self.hub.run(["list-keys", "-T", "prefix", "g"]).stdout)
def test_help_popup_replaces_status_shortcut_list(self) -> None:
self.hub.ensure_server()
binding = self.hub.run(["list-keys", "-T", "prefix", "?"]).stdout
self.assertIn("display-popup", binding)
self.assertIn(" keys", binding)
status_right = self.hub.run(
["show-options", "-gv", "status-right"]
).stdout
self.assertIn("C-a ?", status_right)
self.assertNotIn("choose", status_right)
self.assertNotIn("refresh", status_right)
self.assertNotIn("C-a R", status_right)
def test_configures_isolated_server_and_adds_without_duplicates(self) -> None:
self.assertTrue(self.hub.ensure_server())
endpoints = [Endpoint.local(), Endpoint.remote("orangepi")]
added = self.hub.add_missing(endpoints, str(self.fake_herdr))
self.assertEqual(len(added), 2)
self.assertEqual(self.hub.add_missing(endpoints, str(self.fake_herdr)), [])
windows = self.hub.managed_windows()
self.assertEqual(
{window.key for window in windows},
{"local:default", "remote:orangepi:default"},
)
remote_window = next(
window for window in windows if window.key == "remote:orangepi:default"
)
self.assertEqual(remote_window.endpoint, Endpoint.remote("orangepi"))
all_names = self.hub.run(
["list-windows", "-t", "hub", "-F", "#{window_name}"]
).stdout.splitlines()
self.assertNotIn(BOOTSTRAP_WINDOW_NAME, all_names)
prefix = self.hub.run(["show-options", "-gv", "prefix"]).stdout.strip()
status = self.hub.run(["show-options", "-gv", "status"]).stdout.strip()
position = self.hub.run(
["show-options", "-gv", "status-position"]
).stdout.strip()
mouse = self.hub.run(["show-options", "-gv", "mouse"]).stdout.strip()
clipboard = self.hub.run(
["show-options", "-sv", "set-clipboard"]
).stdout.strip()
status_right = self.hub.run(
["show-options", "-gv", "status-right"]
).stdout.strip()
expanded_status_left = self.hub.run(
["display-message", "-p", "#{E:status-left}"]
).stdout.strip()
dead_format = self.hub.run(
["show-options", "-wgv", "remain-on-exit-format"]
).stdout.strip()
inactive_window_format = self.hub.run(
["show-options", "-wgv", "window-status-format"]
).stdout.strip()
current_window_format = self.hub.run(
["show-options", "-wgv", "window-status-current-format"]
).stdout.strip()
status_left_binding = self.hub.run(
["list-keys", "-T", "root", "MouseDown1StatusLeft"]
).stdout.strip()
close_binding = self.hub.run(
["list-keys", "-T", "prefix", "q"]
).stdout.strip()
legacy_close_binding = self.hub.run(
["list-keys", "-T", "prefix", "&"], check=False
)
uppercase_close_binding = self.hub.run(
["list-keys", "-T", "prefix", "Q"], check=False
)
self.assertEqual(prefix, "C-a")
self.assertEqual(status, "on")
self.assertEqual(position, "top")
self.assertEqual(mouse, "on")
self.assertEqual(clipboard, "on")
self.assertIn("C-a ?", status_right)
self.assertTrue(expanded_status_left.startswith("#[bold,fg=colour81]"))
self.assertNotIn("C-a R", status_right)
self.assertIn("连接已断开", dead_format)
self.assertIn("Ctrl+A R", dead_format)
self.assertIn("client_width", inactive_window_format)
self.assertIn(WINDOW_SHORT_LABEL_OPTION, inactive_window_format)
self.assertNotIn("#I", inactive_window_format)
self.assertIn("client_width", current_window_format)
self.assertIn(WINDOW_SHORT_LABEL_OPTION, current_window_format)
self.assertIn("display-popup", status_left_binding)
self.assertIn("-EE", status_left_binding)
self.assertIn("confirm-before", close_binding)
self.assertIn(" close --window ", close_binding)
self.assertNotEqual(legacy_close_binding.returncode, 0)
self.assertNotEqual(uppercase_close_binding.returncode, 0)
local_short_label = self.hub.run(
[
"show-options",
"-wv",
"-t",
next(window.window_id for window in windows if window.key == "local:default"),
WINDOW_SHORT_LABEL_OPTION,
]
).stdout.strip()
remote_short_label = self.hub.run(
[
"show-options",
"-wv",
"-t",
remote_window.window_id,
WINDOW_SHORT_LABEL_OPTION,
]
).stdout.strip()
rendered_local_short_label = self.hub.run(
[
"display-message",
"-p",
"-t",
next(window.window_id for window in windows if window.key == "local:default"),
"#{=12:@herdr_hub_short_label}",
]
).stdout.strip()
self.assertEqual(local_short_label, "local")
self.assertEqual(remote_short_label, "orangepi")
self.assertEqual(rendered_local_short_label, "local")
def test_dismissed_window_stays_hidden_until_explicitly_reopened(self) -> None:
self.hub.ensure_server()
endpoints = [Endpoint.local(), Endpoint.local("work")]
self.hub.add_missing(endpoints, str(self.fake_herdr))
default_window = self.hub.window_for_key("local:default")
assert default_window is not None
self.hub.dismiss_window(default_window.window_id)
self.assertEqual(self.hub.dismissed_keys(), {"local:default"})
self.assertIsNone(self.hub.window_for_key("local:default"))
self.assertEqual(self.hub.add_missing(endpoints, str(self.fake_herdr)), [])
reopened = self.hub.activate_endpoint(Endpoint.local(), str(self.fake_herdr))
self.assertEqual(reopened.key, "local:default")
self.assertEqual(self.hub.dismissed_keys(), set())
work_window = self.hub.window_for_key("local:work")
assert work_window is not None
self.hub.dismiss_window(work_window.window_id)
with self.assertRaises(HubError):
self.hub.dismiss_window(reopened.window_id)
def test_dead_window_is_retained_and_can_be_respawned(self) -> None:
self.hub.ensure_server()
window = self.hub.create_window(Endpoint.local("dead"), str(self.fake_herdr))
self.hub.remove_bootstrap()
deadline = time.monotonic() + 3
dead = False
while time.monotonic() < deadline:
current = self.hub.window_for_key("local:dead")
dead = current.dead if current is not None else False
if dead:
break
time.sleep(0.05)
self.assertTrue(dead)
rendered_status = self.hub.run(
[
"display-message",
"-p",
"-t",
window.window_id,
"#{?pane_dead,× #{window_name} [断开],● #{window_name}}",
]
).stdout.strip()
self.assertEqual(rendered_status, f"× {window.name} [断开]")
self.hub.reconnect(window.window_id)
self.assertIsNotNone(self.hub.window_for_key("local:dead"))
def test_startup_verification_reports_immediate_process_failure(self) -> None:
self.hub.ensure_server()
with self.assertRaisesRegex(HubError, "fake startup failure"):
self.hub.activate_endpoint(
Endpoint.local("dead"),
str(self.fake_herdr),
verify_startup=True,
)
failed = self.hub.window_for_key("local:dead")
self.assertIsNotNone(failed)
assert failed is not None
self.assertTrue(failed.dead)
def test_alias_is_applied_immediately_and_can_be_reset(self) -> None:
self.hub.aliases["remote:orangepi:work"] = "客厅开发"
self.hub.ensure_server()
window = self.hub.create_window(
Endpoint.remote("orangepi", "work"),
str(self.fake_herdr),
)
self.hub.remove_bootstrap()
current = self.hub.window_for_key(window.key)
assert current is not None
self.assertEqual(current.name, "客厅开发")
short_label = self.hub.run(
[
"show-options",
"-wv",
"-t",
window.window_id,
WINDOW_SHORT_LABEL_OPTION,
]
).stdout.strip()
self.assertEqual(short_label, "客厅开发")
renamed = self.hub.apply_alias(window.key, "临时名称")
assert renamed is not None
self.assertEqual(renamed.name, "临时名称")
# tmux parses a leading '-' as a flag unless the name is guarded; a
# stored name like this must also survive the label refresh that every
# later start/refresh performs.
dashed = self.hub.apply_alias(window.key, "-dash")
assert dashed is not None
self.assertEqual(dashed.name, "-dash")
self.hub.ensure_server()
refreshed = self.hub.window_for_key(window.key)
assert refreshed is not None
self.assertEqual(refreshed.name, "-dash")
restored = self.hub.apply_alias(window.key, "")
assert restored is not None
self.assertEqual(restored.name, "R orangepi/work")
def test_failed_native_window_can_migrate_to_windows_ssh_transport(self) -> None:
self.hub.ensure_server()
native = self.hub.create_window(
Endpoint.remote("windows"),
str(self.fake_herdr),
)
self.hub.remove_bootstrap()
migrated = self.hub.activate_endpoint(
Endpoint.remote("windows", remote_transport="ssh"),
str(self.fake_herdr),
)
self.assertEqual(migrated.window_id, native.window_id)
assert migrated.endpoint is not None
self.assertEqual(migrated.endpoint.remote_transport, "ssh")
command = self.hub.run(
[
"show-options",
"-wv",
"-t",
native.window_id,
"@herdr_hub_command",
]
).stdout.strip()
self.assertIn(str(self.fake_ssh), command)
self.assertIn("-tt", command)
def test_reconfiguration_batches_tmux_calls_and_escapes_semicolons(self) -> None:
calls: list[list[str]] = []
real_run = self.hub._run # type: ignore[attr-defined]
def counting_run(command, **keywords): # type: ignore[no-untyped-def]
calls.append(list(command))
return real_run(command, **keywords)
self.hub._run = counting_run # type: ignore[attr-defined]
self.hub.ensure_server()
for name in ("a", "b", "c"):
self.hub.create_window(Endpoint.local(name), str(self.fake_herdr))
self.hub.remove_bootstrap()
# A no-op refresh (nothing new discovered, labels unchanged) used to
# spawn one tmux client per option and per window label.
calls.clear()
self.hub.ensure_server()
self.hub.add_missing([], str(self.fake_herdr))
self.hub.managed_windows()
self.assertLessEqual(len(calls), 10, calls)
self.assertEqual(self.hub.run(["show-options", "-gv", "prefix"]).stdout.strip(), "C-a")
self.assertIn("display-popup", self.hub.run(["list-keys", "-T", "prefix", "g"]).stdout)
# tmux treats any argument ending in ';' as a command separator.
self.hub.run_batch(
[
["set-option", "-g", "@herdr_hub_test_one", "value;"],
["set-option", "-g", "@herdr_hub_test_two", "other"],
]
)
self.assertEqual(
self.hub.run(["show-options", "-gv", "@herdr_hub_test_one"]).stdout.strip(),
"value;",
)
self.assertEqual(
self.hub.run(["show-options", "-gv", "@herdr_hub_test_two"]).stdout.strip(),
"other",
)
# A failing command is attributed precisely instead of silently
# skipping the rest of the batch.
with self.assertRaisesRegex(HubError, "no-such-option"):
self.hub.run_batch(
[
["set-option", "-g", "@herdr_hub_test_three", "set"],
["set-option", "-g", "no-such-option", "1"],
]
)
self.assertEqual(
self.hub.run(["show-options", "-gv", "@herdr_hub_test_three"]).stdout.strip(),
"set",
)
def test_concurrent_open_of_the_same_endpoint_creates_one_window(self) -> None:
self.hub.ensure_server()
second_hub = TmuxHub(
tmux_bin=shutil.which("tmux") or "tmux",
ssh_bin=str(self.fake_ssh),
socket_name=self.socket_name,
session_name="hub",
config_file="/dev/null",
script_path="/dev/null",
)
endpoint = Endpoint.local("concurrent")
with ThreadPoolExecutor(max_workers=2) as executor:
windows = list(
executor.map(
lambda hub: hub.activate_endpoint(endpoint, str(self.fake_herdr)),
[self.hub, second_hub],
)
)
self.assertEqual(windows[0].window_id, windows[1].window_id)
self.assertEqual(
[window.key for window in self.hub.managed_windows()],
["local:concurrent"],
)
if __name__ == "__main__":
unittest.main()