Initial release of herdr session switcher

This commit is contained in:
2026-09-02 01:33:38 +00:00
commit 6c2c9672e1
16 changed files with 3400 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Test suite for herdr-hub."""
+136
View File
@@ -0,0 +1,136 @@
from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
import uuid
from herdr_hub.cli import TmuxHub
@unittest.skipUnless(shutil.which("tmux"), "tmux is required")
class CliIntegrationTests(unittest.TestCase):
def test_remote_registry_cli_round_trip(self) -> None:
repository_root = Path(__file__).resolve().parents[1]
launcher = repository_root / "bin" / "herdr-hub"
with tempfile.TemporaryDirectory() as directory:
environment = {
**os.environ,
"HERDR_HUB_REMOTES_FILE": str(Path(directory) / "remotes.json"),
}
add = subprocess.run(
[
str(launcher),
"remote",
"add",
"orangepi",
"--remote-keybindings",
"server",
],
cwd=repository_root,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(add.returncode, 0, add.stderr)
listing = subprocess.run(
[str(launcher), "remote", "list"],
cwd=repository_root,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(listing.stdout.strip(), "orangepi\tserver")
remove = subprocess.run(
[str(launcher), "remote", "remove", "orangepi"],
cwd=repository_root,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(remove.returncode, 0, remove.stderr)
self.assertIn("removed orangepi", remove.stdout)
def test_repository_launcher_refreshes_and_stops_isolated_hub(self) -> None:
repository_root = Path(__file__).resolve().parents[1]
launcher = repository_root / "bin" / "herdr-hub"
socket_name = f"herdr-hub-cli-test-{os.getpid()}-{uuid.uuid4().hex[:8]}"
socket_path = Path(
os.environ.get("TMUX_TMPDIR", f"/tmp/tmux-{os.getuid()}")
) / socket_name
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
proc_root = root / "proc"
proc_root.mkdir()
fake_herdr = root / "herdr"
fake_herdr.write_text(
"#!/bin/sh\n"
"if [ \"${1:-}\" = \"session\" ]; then\n"
" printf '%s\\n' '{\"sessions\":[{\"name\":\"default\",\"running\":true}]}'\n"
" exit 0\n"
"fi\n"
"exec sleep 30\n",
encoding="utf-8",
)
fake_herdr.chmod(0o755)
environment = {
**os.environ,
"HERDR_HUB_HERDR_BIN": str(fake_herdr),
"HERDR_HUB_TMUX_BIN": shutil.which("tmux") or "tmux",
"HERDR_HUB_SOCKET": socket_name,
"HERDR_HUB_SESSION": "hub",
"HERDR_HUB_TMUX_CONFIG": "/dev/null",
"HERDR_HUB_PROC_ROOT": str(proc_root),
}
refresh = subprocess.run(
[str(launcher), "refresh"],
cwd=repository_root,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(refresh.returncode, 0, refresh.stderr)
self.assertIn("added 1 session", refresh.stdout)
hub = TmuxHub(
tmux_bin=shutil.which("tmux") or "tmux",
socket_name=socket_name,
session_name="hub",
config_file="/dev/null",
script_path=str(launcher),
)
def cleanup_hub() -> None:
if hub.exists():
hub.stop()
socket_path.unlink(missing_ok=True)
self.addCleanup(cleanup_hub)
self.assertEqual(
[window.key for window in hub.managed_windows()],
["local:default"],
)
stop = subprocess.run(
[str(launcher), "stop"],
cwd=repository_root,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(stop.returncode, 0, stop.stderr)
self.assertIn("Herdr servers were left running", stop.stdout)
self.assertFalse(hub.exists())
if __name__ == "__main__":
unittest.main()
+173
View File
@@ -0,0 +1,173 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from herdr_hub.cli import (
DEFAULT_SESSION_NAME,
Endpoint,
decode_cmdline,
dedupe_endpoints,
deserialize_endpoint,
discover_local_sessions,
discover_remote_processes,
parse_remote_argv,
serialize_endpoint,
)
class EndpointTests(unittest.TestCase):
def test_local_and_remote_commands(self) -> None:
self.assertEqual(Endpoint.local().command("herdr"), ["herdr"])
self.assertEqual(
Endpoint.local("work").command("/bin/herdr"),
["/bin/herdr", "--session", "work"],
)
self.assertEqual(
Endpoint.remote("orangepi", "work", "server").command("herdr"),
[
"herdr",
"--remote",
"orangepi",
"--remote-keybindings",
"server",
"--session",
"work",
],
)
def test_identity_ignores_remote_keybinding_mode(self) -> None:
local = Endpoint.remote("orangepi", remote_keybindings="local")
server = Endpoint.remote("orangepi", remote_keybindings="server")
self.assertEqual(local.key, server.key)
self.assertEqual(dedupe_endpoints([local, server]), [local])
def test_short_labels_put_the_host_first_and_omit_default(self) -> None:
self.assertEqual(Endpoint.local().short_label, "local")
self.assertEqual(Endpoint.local("work").short_label, "local/work")
self.assertEqual(Endpoint.remote("orangepi").short_label, "orangepi")
self.assertEqual(Endpoint.remote("orangepi", "work").short_label, "orangepi/work")
def test_default_local_sorts_first(self) -> None:
endpoints = dedupe_endpoints(
[
Endpoint.remote("z"),
Endpoint.local("work"),
Endpoint.local(),
Endpoint.remote("a"),
]
)
self.assertEqual(
[endpoint.key for endpoint in endpoints],
["local:default", "local:work", "remote:a:default", "remote:z:default"],
)
def test_endpoint_metadata_and_legacy_command_round_trip(self) -> None:
endpoint = Endpoint.remote("ssh://me@host:2222", "work", "server")
self.assertEqual(deserialize_endpoint(serialize_endpoint(endpoint)), endpoint)
self.assertEqual(
deserialize_endpoint(
"",
"exec /usr/local/bin/herdr --remote orangepi --session work",
),
Endpoint.remote("orangepi", "work"),
)
class RemoteArgvTests(unittest.TestCase):
def test_separate_arguments(self) -> None:
endpoint = parse_remote_argv(
[
"/home/me/.local/bin/herdr",
"--remote",
"orangepi",
"--session",
"work",
"--remote-keybindings",
"server",
]
)
self.assertEqual(endpoint, Endpoint.remote("orangepi", "work", "server"))
def test_equal_arguments_and_arbitrary_order(self) -> None:
endpoint = parse_remote_argv(
["herdr", "--session=work", "--remote=ssh://me@host:2222"]
)
self.assertEqual(endpoint, Endpoint.remote("ssh://me@host:2222", "work"))
def test_default_session(self) -> None:
endpoint = parse_remote_argv(["herdr", "--remote", "orangepi"])
self.assertIsNotNone(endpoint)
assert endpoint is not None
self.assertEqual(endpoint.session, DEFAULT_SESSION_NAME)
def test_non_herdr_and_malformed_commands_are_ignored(self) -> None:
self.assertIsNone(parse_remote_argv(["bash", "-c", "herdr --remote host"]))
self.assertIsNone(parse_remote_argv(["herdr", "--remote"]))
self.assertIsNone(parse_remote_argv(["herdr", "--remote", "-bad"]))
def test_cmdline_is_nul_delimited(self) -> None:
self.assertEqual(
decode_cmdline(b"/bin/herdr\0--remote\0host name\0"),
["/bin/herdr", "--remote", "host name"],
)
class DiscoveryTests(unittest.TestCase):
def test_local_discovery_returns_only_running_sessions(self) -> None:
payload = {
"sessions": [
{"name": "default", "running": True},
{"name": "work", "running": True},
{"name": "old", "running": False},
]
}
def fake_run(*args, **kwargs): # type: ignore[no-untyped-def]
return subprocess.CompletedProcess(args[0], 0, json.dumps(payload), "")
report = discover_local_sessions("herdr", run=fake_run)
self.assertEqual(
[endpoint.key for endpoint in report.endpoints],
["local:default", "local:work"],
)
self.assertEqual(report.warnings, [])
def test_invalid_local_json_is_a_warning(self) -> None:
def fake_run(*args, **kwargs): # type: ignore[no-untyped-def]
return subprocess.CompletedProcess(args[0], 0, "not json", "")
report = discover_local_sessions("herdr", run=fake_run)
self.assertEqual(report.endpoints, [])
self.assertEqual(len(report.warnings), 1)
def test_proc_discovery_reads_exact_argv_and_deduplicates(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
commands = {
"101": ["/usr/bin/herdr", "--remote", "orangepi"],
"102": ["herdr", "--remote=orangepi"],
"103": ["herdr", "--remote", "prod", "--session", "work"],
"104": ["bash", "-c", "herdr --remote ignored"],
}
for process_id, argv in commands.items():
process = root / process_id
process.mkdir()
(process / "cmdline").write_bytes(
b"\0".join(os.fsencode(argument) for argument in argv) + b"\0"
)
report = discover_remote_processes(root, user_id=os.getuid())
self.assertEqual(
[endpoint.key for endpoint in report.endpoints],
["remote:orangepi:default", "remote:prod:work"],
)
if __name__ == "__main__":
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
from __future__ import annotations
from pathlib import Path
import stat
import tempfile
import time
import unittest
from herdr_hub.remotes import (
RegistryError,
RemoteEntry,
RemoteQueryPool,
RemoteRegistry,
RemoteTargetError,
parse_remote_session_result,
remote_session_command,
ssh_destination_arguments,
)
class RemoteRegistryTests(unittest.TestCase):
def test_round_trip_update_remove_and_permissions(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "config" / "remotes.json"
registry = RemoteRegistry(path)
self.assertEqual(registry.load(), [])
self.assertTrue(registry.upsert(RemoteEntry("orangepi")))
self.assertFalse(registry.upsert(RemoteEntry("orangepi")))
self.assertTrue(registry.upsert(RemoteEntry("prod", "server")))
self.assertEqual(
registry.load(),
[RemoteEntry("orangepi"), RemoteEntry("prod", "server")],
)
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600)
self.assertTrue(registry.remove("orangepi"))
self.assertFalse(registry.remove("missing"))
self.assertEqual(registry.load(), [RemoteEntry("prod", "server")])
def test_corrupt_registry_is_not_overwritten(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "remotes.json"
path.write_text("not json", encoding="utf-8")
registry = RemoteRegistry(path)
with self.assertRaises(RegistryError):
registry.upsert(RemoteEntry("orangepi"))
self.assertEqual(path.read_text(encoding="utf-8"), "not json")
class RemoteSshTests(unittest.TestCase):
def test_alias_and_url_targets_become_safe_ssh_arguments(self) -> None:
self.assertEqual(ssh_destination_arguments("orangepi"), ["orangepi"])
self.assertEqual(
ssh_destination_arguments("ssh://me@example.com:2222"),
["-p", "2222", "-l", "me", "example.com"],
)
command = remote_session_command("/usr/bin/ssh", RemoteEntry("host name"))
self.assertEqual(command[0], "/usr/bin/ssh")
self.assertIn("host name", command)
self.assertNotIn("sh", command[:1])
with self.assertRaises(RemoteTargetError):
ssh_destination_arguments("ssh://me:secret@example.com")
with self.assertRaises(RemoteTargetError):
ssh_destination_arguments("ssh://example.com/a/path")
def test_remote_session_json_keeps_running_and_stopped(self) -> None:
result = parse_remote_session_result(
RemoteEntry("orangepi"),
0,
'{"sessions":['
'{"name":"old","running":false},'
'{"name":"default","running":true}]}',
"",
)
self.assertIsNone(result.error)
self.assertEqual(
[(session.name, session.running) for session in result.sessions],
[("default", True), ("old", False)],
)
def test_query_pool_reports_results_and_spawn_failures(self) -> None:
with tempfile.TemporaryDirectory() as directory:
fake_ssh = Path(directory) / "ssh"
fake_ssh.write_text(
"#!/bin/sh\n"
"printf '%s\\n' '{\"sessions\":[{\"name\":\"work\",\"running\":true}]}'\n",
encoding="utf-8",
)
fake_ssh.chmod(0o755)
pool = RemoteQueryPool(str(fake_ssh), query_timeout=1)
self.addCleanup(pool.close)
pool.enqueue(RemoteEntry("orangepi"))
results = []
deadline = time.monotonic() + 2
while not results and time.monotonic() < deadline:
results = pool.poll()
time.sleep(0.01)
self.assertEqual(results[0].sessions[0].name, "work")
missing = RemoteQueryPool(str(Path(directory) / "missing"))
self.addCleanup(missing.close)
missing.enqueue(RemoteEntry("orangepi"))
failure = missing.poll()
self.assertEqual(len(failure), 1)
self.assertIn("cannot start", failure[0].error or "")
def test_query_pool_times_out_without_blocking_other_results(self) -> None:
with tempfile.TemporaryDirectory() as directory:
slow_ssh = Path(directory) / "ssh"
slow_ssh.write_text("#!/bin/sh\nexec sleep 5\n", encoding="utf-8")
slow_ssh.chmod(0o755)
pool = RemoteQueryPool(str(slow_ssh), query_timeout=0.05)
self.addCleanup(pool.close)
pool.enqueue(RemoteEntry("slow"))
time.sleep(0.08)
results = pool.poll()
self.assertEqual(len(results), 1)
self.assertIn("timed out", results[0].error or "")
if __name__ == "__main__":
unittest.main()
+181
View File
@@ -0,0 +1,181 @@
from __future__ import annotations
import os
from pathlib import Path
import pty
import select
import signal
import tempfile
import time
import unittest
from herdr_hub.remotes import RemoteEntry, RemoteQueryResult, RemoteRegistry, RemoteSession
from herdr_hub.selector import (
CatalogWindow,
PickerRow,
SessionSelector,
SelectorResult,
build_main_rows,
filter_rows,
)
class SelectorCatalogTests(unittest.TestCase):
def test_rows_merge_open_running_stopped_and_new_sessions(self) -> None:
windows = [
CatalogWindow(
"@1",
"local:default",
"L default",
False,
True,
kind="local",
session="default",
),
CatalogWindow(
"@2",
"remote:orangepi:work",
"R orangepi/work",
True,
False,
kind="remote",
target="orangepi",
session="work",
),
]
result = RemoteQueryResult(
RemoteEntry("orangepi"),
sessions=(
RemoteSession("default", True),
RemoteSession("old", False),
RemoteSession("work", True),
),
)
rows = build_main_rows(
windows,
[RemoteEntry("orangepi")],
{"orangepi": result},
set(),
local_sessions=["default", "draft"],
)
by_key = {row.key: row for row in rows}
self.assertEqual(by_key["window:@1"].main, "▶ L default")
self.assertEqual(by_key["window:@2"].main, "× R orangepi/work")
self.assertIn("remote:orangepi:default", by_key)
self.assertIn("remote:orangepi:old", by_key)
self.assertIn("new:orangepi", by_key)
self.assertEqual(by_key["local:draft"].main, "○ draft")
self.assertEqual(by_key["close-active"].window_id, "@1")
def test_loading_error_and_filter_rows(self) -> None:
entry = RemoteEntry("prod")
loading = build_main_rows([], [entry], {}, {"prod"})
self.assertTrue(any(row.kind == "loading" for row in loading))
failed = build_main_rows(
[],
[entry],
{"prod": RemoteQueryResult(entry, error="offline")},
set(),
)
self.assertTrue(any(row.kind == "retry" for row in failed))
matches = filter_rows(failed, "offline")
self.assertEqual([row.kind for row in matches], ["retry"])
class SelectorActionTests(unittest.TestCase):
def test_dead_window_reconnect_action_and_new_session_action(self) -> None:
with tempfile.TemporaryDirectory() as directory:
registry = RemoteRegistry(Path(directory) / "remotes.json")
registry.upsert(RemoteEntry("orangepi", "server"))
selector = SessionSelector(
[CatalogWindow("@2", "key", "remote", True, False)],
registry,
str(Path(directory) / "missing-ssh"),
)
self.addCleanup(selector.pool.close)
window_result = selector._activate( # type: ignore[attr-defined]
next(row for row in selector._rows() if row.kind == "window") # type: ignore[attr-defined]
)
self.assertEqual(window_result, SelectorResult.window("@2", dead=True))
close_result = selector._activate( # type: ignore[attr-defined]
PickerRow("close", "close", "close", window_id="@2")
)
self.assertEqual(close_result, SelectorResult.close("@2"))
local_result = selector._activate( # type: ignore[attr-defined]
PickerRow("local", "local", "local", session="draft")
)
self.assertEqual(local_result, SelectorResult.local("draft"))
selector.view = "input-new"
selector.input_target = "orangepi"
new_result = selector._finish_new("work") # type: ignore[attr-defined]
self.assertEqual(
new_result,
SelectorResult.remote("orangepi", "work", "server"),
)
self.assertIsNone(selector._finish_new("bad name")) # type: ignore[attr-defined]
@unittest.skipUnless(hasattr(pty, "fork"), "a Unix PTY is required")
def test_curses_selector_handles_single_click_and_esc(self) -> None:
with tempfile.TemporaryDirectory() as directory:
pid, descriptor = pty.fork()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
selector = SessionSelector(
[],
RemoteRegistry(Path(directory) / "remotes.json"),
"/missing/ssh",
)
try:
result = selector.run()
os._exit(0 if result is None else 2)
except BaseException:
os._exit(3)
status: int | None = None
try:
deadline = time.monotonic() + 3
output = b""
while time.monotonic() < deadline and b"Sessions" not in output:
readable, _, _ = select.select([descriptor], [], [], 0.1)
if readable:
output += os.read(descriptor, 4096)
self.assertIn(b"Sessions", output)
# Empty registries render "+ 添加远端" as the first selectable
# row at terminal row 5. A single SGR mouse click must activate it.
os.write(descriptor, b"\x1b[<0;5;5M\x1b[<0;5;5m")
opened_input = b""
input_deadline = time.monotonic() + 2
while time.monotonic() < input_deadline and b"SSH target" not in opened_input:
readable, _, _ = select.select([descriptor], [], [], 0.1)
if readable:
opened_input += os.read(descriptor, 4096)
self.assertIn(b"SSH target", opened_input)
os.write(descriptor, b"\x1b\x1b")
while time.monotonic() < deadline:
completed, wait_status = os.waitpid(pid, os.WNOHANG)
if completed == pid:
status = wait_status
break
time.sleep(0.05)
self.assertIsNotNone(status, "selector did not exit after Esc")
assert status is not None
self.assertTrue(os.WIFEXITED(status))
self.assertEqual(os.WEXITSTATUS(status), 0)
finally:
try:
os.close(descriptor)
except OSError:
pass
if status is None:
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
os.waitpid(pid, 0)
if __name__ == "__main__":
unittest.main()
+206
View File
@@ -0,0 +1,206 @@
from __future__ import annotations
import os
from pathlib import Path
import shutil
import tempfile
import time
import unittest
import uuid
from herdr_hub.cli import (
BOOTSTRAP_WINDOW_NAME,
Endpoint,
HubError,
TmuxHub,
WINDOW_SHORT_LABEL_OPTION,
)
@unittest.skipUnless(shutil.which("tmux"), "tmux is required")
class TmuxIntegrationTests(unittest.TestCase):
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"
" exit 0\n"
"fi\n"
"exec sleep 30\n",
encoding="utf-8",
)
self.fake_herdr.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",
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_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()
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", "&"]
).stdout.strip()
self.assertEqual(prefix, "C-a")
self.assertEqual(status, "on")
self.assertEqual(position, "top")
self.assertEqual(mouse, "on")
self.assertEqual(clipboard, "on")
self.assertIn("连接已断开", status_right)
self.assertIn("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("confirm-before", close_binding)
self.assertIn(" close --window ", close_binding)
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"))
if __name__ == "__main__":
unittest.main()