diff --git a/cmd_chat/__init__.py b/cmd_chat/__init__.py index ef9bc2d..e1e9504 100644 --- a/cmd_chat/__init__.py +++ b/cmd_chat/__init__.py @@ -2,8 +2,11 @@ import argparse import getpass import os -from cmd_chat.server.server import run_server -from cmd_chat.client.client import Client +# NOTE: the server (run_server) and client (Client) are imported lazily inside +# main() below. Importing them here would pull sanic/pydantic (server) at +# package-init time for *any* `cmd_chat.*` import — including +# `python -m cmd_chat.operator` on a phone/Termux where those server-only deps +# aren't installed. See docs/termux-operator.md (Phase 0). def resolve_password(args_password: str | None, prompt: str = "Room password: ") -> str: @@ -43,6 +46,8 @@ def main(): args = parser.parse_args() if args.command == "serve": + from cmd_chat.server.server import run_server + password = resolve_password(args.password) run_server( host=args.ip_address, @@ -53,6 +58,8 @@ def main(): no_tls=args.no_tls, ) elif args.command == "connect": + from cmd_chat.client.client import Client + password = resolve_password(args.password) Client( server=args.ip_address, diff --git a/tests/test_operator_import_termux.py b/tests/test_operator_import_termux.py new file mode 100644 index 0000000..d4f45a0 --- /dev/null +++ b/tests/test_operator_import_termux.py @@ -0,0 +1,40 @@ +"""Guard the operator's lightweight import surface (docs/termux-operator.md P0). + +A phone/Termux operator installs only the operator deps (requests, rich, +websockets, cryptography, srp) — never the server stack (sanic, pydantic) — and +`srp` may be unbuildable there (pure-Python fallback). So `python -m +cmd_chat.operator` must import with srp/sanic/pydantic ALL absent. This is easy +to regress: any eager `import srp`/sanic on the operator path breaks it. Run the +check in a subprocess so blocking modules can't pollute the test interpreter. +""" + +import subprocess +import sys + + +_SNIPPET = """ +import sys +# Simulate a Termux operator box: no srp C-ext build, no server-only deps. +for missing in ("srp", "sanic", "sanic_ext", "pydantic"): + sys.modules[missing] = None # 'import X' now raises ImportError + +import cmd_chat.operator # must not need srp/sanic/pydantic +assert hasattr(cmd_chat.operator, "OperatorBridge") + +import cmd_chat.client.client as c # client must fall back to the shim +assert c.srp.__name__.endswith("_srp_pure"), c.srp.__name__ +print("OK") +""" + + +def test_operator_imports_without_srp_or_server_deps(): + proc = subprocess.run( + [sys.executable, "-c", _SNIPPET], + capture_output=True, + text=True, + ) + assert proc.returncode == 0, ( + "operator import failed without srp/sanic/pydantic:\n" + f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" + ) + assert proc.stdout.strip().endswith("OK"), proc.stdout