RemoteRF 2.2.0__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {remoterf-2.2.0/src/remoterf.egg-info → remoterf-2.2.1}/PKG-INFO +1 -1
  2. {remoterf-2.2.0 → remoterf-2.2.1}/pyproject.toml +1 -1
  3. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/app.py +3 -4
  4. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/direct_path.py +15 -6
  5. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/grpc_client.py +4 -3
  6. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/remoterf_cli.py +25 -11
  7. {remoterf-2.2.0 → remoterf-2.2.1/src/remoterf.egg-info}/PKG-INFO +1 -1
  8. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_cli_login_fresh.py +28 -0
  9. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_gate_i_direct_path.py +32 -0
  10. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_homes.py +3 -2
  11. {remoterf-2.2.0 → remoterf-2.2.1}/LICENSE +0 -0
  12. {remoterf-2.2.0 → remoterf-2.2.1}/README.md +0 -0
  13. {remoterf-2.2.0 → remoterf-2.2.1}/setup.cfg +0 -0
  14. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/__init__.py +0 -0
  15. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/__init__.py +0 -0
  16. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/__init__.py +0 -0
  17. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
  18. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
  19. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  20. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/grpc.proto +0 -0
  21. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
  22. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
  23. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/grpc/v2_codec.py +0 -0
  24. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/tos_notice.txt +0 -0
  25. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/__init__.py +0 -0
  26. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/ansi_codes.py +0 -0
  27. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/api_token.py +0 -0
  28. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/banner.py +0 -0
  29. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/list_string.py +0 -0
  30. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/common/utils/process_arg.py +0 -0
  31. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/config/__init__.py +0 -0
  32. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/config/cert_fetcher.py +0 -0
  33. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/config/config.py +0 -0
  34. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/__init__.py +0 -0
  35. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/acc_login.py +0 -0
  36. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/banner_note.txt +0 -0
  37. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/dynamic_v2_transport.py +0 -0
  38. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/grpc_acc.py +0 -0
  39. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/secure_channel.py +0 -0
  40. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/core/v2_errors.py +0 -0
  41. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/__init__.py +0 -0
  42. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/backend.py +0 -0
  43. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/direct.py +0 -0
  44. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/homes.py +0 -0
  45. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/http.py +0 -0
  46. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/deployment/state.py +0 -0
  47. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/__init__.py +0 -0
  48. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/_virtual.py +0 -0
  49. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
  50. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
  51. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/dynamic_device.py +0 -0
  52. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/dynamic_v2.py +0 -0
  53. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/hackrf/__init__.py +0 -0
  54. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
  55. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
  56. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  57. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
  58. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
  59. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/support/__init__.py +0 -0
  60. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
  61. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/support/uhd.py +0 -0
  62. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
  63. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/support/uhd_v2.py +0 -0
  64. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
  65. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  66. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/usrp/__init__.py +0 -0
  67. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
  68. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
  69. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf/version.py +0 -0
  70. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.egg-info/SOURCES.txt +0 -0
  71. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.egg-info/dependency_links.txt +0 -0
  72. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.egg-info/entry_points.txt +0 -0
  73. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.egg-info/requires.txt +0 -0
  74. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.egg-info/top_level.txt +0 -0
  75. {remoterf-2.2.0 → remoterf-2.2.1}/src/remoterf.py +0 -0
  76. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_banner.py +0 -0
  77. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_config_address.py +0 -0
  78. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_deployment_accounts.py +0 -0
  79. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_direct_path_fixed.py +0 -0
  80. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_direct_path_partner.py +0 -0
  81. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_direct_path_race.py +0 -0
  82. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_dynamic_device_codegen.py +0 -0
  83. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_dynamic_v2.py +0 -0
  84. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_federated_devices.py +0 -0
  85. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_fetch_idl_fallback.py +0 -0
  86. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_finish_prompt.py +0 -0
  87. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_grpc_client_target.py +0 -0
  88. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_native_account_credentials.py +0 -0
  89. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_native_devices.py +0 -0
  90. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_partner_dynamic_v2.py +0 -0
  91. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_pluto_virtual.py +0 -0
  92. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_process_arg.py +0 -0
  93. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_remoterf_cli.py +0 -0
  94. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_ti_mmwave.py +0 -0
  95. {remoterf-2.2.0 → remoterf-2.2.1}/tests/test_upgrade_rescue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoterf"
7
- version = "2.2.0"
7
+ version = "2.2.1"
8
8
  description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -536,10 +536,9 @@ def _federated_reservations_for_range(start_day, end_day):
536
536
 
537
537
  def _federated_reserve_slot(device_id, chosen_day, chosen_slot, slot_start_str, slot_end_str):
538
538
  handle = account.reserve_device(device_id, chosen_slot[0], chosen_slot[1])
539
- printf("Reservation successful for ", (Sty.BOLD, Sty.GREEN), f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
540
- printf("Your device handle -> ", Sty.BOLD, f"{handle}", (Sty.BOLD, Sty.GREEN))
541
- printf("Use it where a token goes, e.g. adi.Pluto(\"" + handle + "\"); it works while you are logged in to this home.", Sty.DEFAULT)
542
- printf("The first call can take ~20 s while the radio powers on.", Sty.GRAY)
539
+ # Worded as a lab account's reservation; the handle goes where a token goes.
540
+ printf("Reservation successful on device ", (Sty.BOLD, Sty.GREEN), f"{str(device_id).rsplit(':', 1)[-1]}", Sty.MAGENTA, " for ", Sty.DEFAULT, f"{chosen_day.strftime('%Y-%m-%d')} {slot_start_str}-{slot_end_str}.", Sty.CYAN)
541
+ printf("Your Token -> ", Sty.BOLD, f"{handle}", (Sty.BOLD, Sty.GREEN))
543
542
  account.install_reserved_driver(handle, device_id)
544
543
 
545
544
 
@@ -50,11 +50,18 @@ import threading
50
50
  import time
51
51
  from typing import Callable, Optional
52
52
 
53
- from aioice import Candidate, Connection
54
- from aioquic.asyncio.protocol import QuicConnectionProtocol
55
- from aioquic.quic import events
56
- from aioquic.quic.configuration import QuicConfiguration
57
- from aioquic.quic.connection import QuicConnection
53
+ try:
54
+ from aioice import Candidate, Connection
55
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
56
+ from aioquic.quic import events
57
+ from aioquic.quic.configuration import QuicConfiguration
58
+ from aioquic.quic.connection import QuicConnection
59
+
60
+ _BROKEN = None
61
+ except Exception as _exc: # noqa: BLE001 - e.g. conda swapped in a cryptography pip's pyOpenSSL cannot use
62
+ Candidate = Connection = QuicConnectionProtocol = QuicConfiguration = QuicConnection = object
63
+ events = None
64
+ _BROKEN = f"{type(_exc).__name__}: {_exc}"
58
65
 
59
66
  from ..common.grpc import grpc_pb2
60
67
  from ..common.utils import map_arg, unmap_arg
@@ -136,7 +143,7 @@ STUN_SECONDS = 1.5 # a STUN server answers in milliseconds or not at all
136
143
 
137
144
 
138
145
  def enabled() -> bool:
139
- return os.getenv("REMOTERF_DIRECT", "1").strip().lower() not in {"0", "off", "no", "false"}
146
+ return _BROKEN is None and os.getenv("REMOTERF_DIRECT", "1").strip().lower() not in {"0", "off", "no", "false"}
140
147
 
141
148
 
142
149
  def wants(function_name: str) -> bool:
@@ -742,6 +749,8 @@ def current(token: Optional[str] = None) -> Optional[DirectPath]:
742
749
 
743
750
  def describe() -> str:
744
751
  """What device calls use right now, for the shell."""
752
+ if _BROKEN is not None:
753
+ return f"relay (direct path unavailable: {_BROKEN}; fix: pip install --force-reinstall remoterf)"
745
754
  if not enabled():
746
755
  return "relay (direct path disabled: REMOTERF_DIRECT=0)"
747
756
  path = _path
@@ -225,15 +225,16 @@ def _device_token(function_name, args):
225
225
  def rpc_client(*, function_name, args, connection=None):
226
226
  global tcp_calls
227
227
  tcp_calls += 1
228
- ref = _federated_ref(function_name, args) if connection is None else None
229
- if ref is not None:
230
- return _finish(_federated_call(ref, function_name, args))
231
228
  wire = os.getenv("REMOTERF_RX_WIRE", "i16").strip().lower()
232
229
  if wire and function_name.endswith(":rx:CALL0"):
233
230
  # Gate I slice 3: c64 halves the samples on the wire, i16 (the default)
234
231
  # quarters them when they are integer IQ; the driver still sees a complex
235
232
  # array. REMOTERF_RX_WIRE="" or "c128" opts out (servers ignore unknown).
233
+ # Also through a HOME, whose relay caps each response at ~768 KiB.
236
234
  args = {**args, "wire": map_arg(wire)}
235
+ ref = _federated_ref(function_name, args) if connection is None else None
236
+ if ref is not None:
237
+ return _finish(_federated_call(ref, function_name, args))
237
238
  if connection is None and direct_path.wants(function_name):
238
239
  path = direct_path.current(token=_device_token(function_name, args))
239
240
  if path is not None:
@@ -312,25 +312,39 @@ def _use_home(name: str, *, register: bool, enrollment_code: str | None = None)
312
312
  )
313
313
 
314
314
 
315
+ def _agree_to_tos() -> bool:
316
+ """Every registration is a fresh agreement, whoever is at the keyboard --
317
+ and starting one forgets the old agreement, so backing out here means
318
+ the next login asks too."""
319
+ from remoterf.config.config import _confirm_tos, forget_tos_agreement, remember_tos_agreement
320
+
321
+ forget_tos_agreement()
322
+ if not _confirm_tos("Do you agree to the RemoteRF Terms of Service? [y/N]: ",
323
+ "Registration cancelled."):
324
+ return False
325
+ remember_tos_agreement()
326
+ return True
327
+
328
+
329
+ def _register_global() -> int:
330
+ """`remoterf -r global`: straight to a Global account, same agreement as -r."""
331
+ print_client_banner(_installed_version(), server="")
332
+ if not _agree_to_tos():
333
+ return 1
334
+ return _use_global(register=True, show_banner=False)
335
+
336
+
315
337
  def _register() -> int:
316
338
  """`remoterf -r`: a deployment enrollment code, or a Global account."""
317
339
  from remoterf_federation_core import ValidationError
318
340
 
319
341
  from remoterf.deployment import homes
320
342
 
321
- from remoterf.config.config import _confirm_tos, forget_tos_agreement, remember_tos_agreement
322
-
323
343
  # The banner opens the session; the target is not known yet, so the shell
324
344
  # below must not print it a second time.
325
345
  print_client_banner(_installed_version(), server="")
326
- # Every registration is a fresh agreement, whoever is at the keyboard --
327
- # and starting one forgets the old agreement, so backing out here means
328
- # the next login asks too.
329
- forget_tos_agreement()
330
- if not _confirm_tos("Do you agree to the RemoteRF Terms of Service? [y/N]: ",
331
- "Registration cancelled."):
346
+ if not _agree_to_tos():
332
347
  return 1
333
- remember_tos_agreement()
334
348
  try:
335
349
  entered = input('Enrollment code (Enter for a RemoteRF Global account): ').strip()
336
350
  except (EOFError, KeyboardInterrupt):
@@ -434,8 +448,8 @@ def main() -> int:
434
448
  return 0
435
449
 
436
450
  if argv[0] in ("--register", "-register", "-r"):
437
- if len(argv) == 2 and argv[1] == "global":
438
- return _use_global(register=True)
451
+ if len(argv) == 2 and argv[1].lstrip("-") == "global":
452
+ return _register_global()
439
453
  if len(argv) > 1:
440
454
  print("ERROR: --register takes no arguments (or 'global')")
441
455
  return 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -52,3 +52,31 @@ def test_register_without_a_code_makes_a_global_account(monkeypatch):
52
52
  assert remoterf_cli._register() == 0
53
53
  use_global.assert_called_once_with(register=True, show_banner=False)
54
54
  shell.assert_not_called()
55
+
56
+
57
+ def test_register_global_asks_for_the_tos_like_register(monkeypatch):
58
+ from remoterf import remoterf_cli
59
+
60
+ for arg in ("global", "-global"):
61
+ monkeypatch.setattr("sys.argv", ["remoterf", "-r", arg])
62
+ with mock.patch("remoterf.config.config._confirm_tos", return_value=True) as confirm, \
63
+ mock.patch("remoterf.config.config.forget_tos_agreement") as forget, \
64
+ mock.patch("remoterf.config.config.remember_tos_agreement") as remember, \
65
+ mock.patch.object(remoterf_cli, "print_client_banner"), \
66
+ mock.patch.object(remoterf_cli, "_use_global", return_value=0) as use_global:
67
+ assert remoterf_cli.main() == 0
68
+ forget.assert_called_once_with()
69
+ confirm.assert_called_once()
70
+ remember.assert_called_once_with()
71
+ use_global.assert_called_once_with(register=True, show_banner=False)
72
+
73
+ # Declining stops before any account is made.
74
+ monkeypatch.setattr("sys.argv", ["remoterf", "-r", "global"])
75
+ with mock.patch("remoterf.config.config._confirm_tos", return_value=False), \
76
+ mock.patch("remoterf.config.config.forget_tos_agreement"), \
77
+ mock.patch("remoterf.config.config.remember_tos_agreement") as remember, \
78
+ mock.patch.object(remoterf_cli, "print_client_banner"), \
79
+ mock.patch.object(remoterf_cli, "_use_global") as use_global:
80
+ assert remoterf_cli.main() == 1
81
+ remember.assert_not_called()
82
+ use_global.assert_not_called()
@@ -248,6 +248,20 @@ class RoutingTests(unittest.TestCase):
248
248
  self.assertEqual(unmap_arg(seen[0][1]["wire"]), "c64")
249
249
  self.assertEqual(unmap_arg(seen[3][1]["wire"]), "i16")
250
250
 
251
+ def test_rx_through_a_home_asks_for_i16_too(self):
252
+ # A Global handle's rx goes through the HOME relay, which caps a response
253
+ # at ~768 KiB: complex128 samples overflow it four times sooner.
254
+ from remoterf.core import grpc_client
255
+
256
+ seen = []
257
+ with mock.patch.object(grpc_client, "_federated_ref", return_value="dep:103"), \
258
+ mock.patch.object(grpc_client, "_federated_call",
259
+ side_effect=lambda ref, name, args: seen.append(dict(args)) or grpc_pb2.GenericRPCResponse()), \
260
+ mock.patch.dict("os.environ"):
261
+ os.environ.pop("REMOTERF_RX_WIRE", None)
262
+ grpc_client.rpc_client(function_name="adalm_pluto:rx:CALL0", args={"a": map_arg("dep:103")})
263
+ self.assertEqual(unmap_arg(seen[0]["wire"]), "i16")
264
+
251
265
  def test_a_path_that_is_not_ready_is_skipped(self):
252
266
  from remoterf.core.grpc_client import rpc_client
253
267
 
@@ -482,3 +496,21 @@ class QuietHandlerTests(unittest.TestCase):
482
496
  finally:
483
497
  loop.close()
484
498
  self.assertEqual([type(c["exception"]) for c in seen], [ValueError])
499
+
500
+
501
+ class BrokenQuicStackTests(unittest.TestCase):
502
+ def test_a_broken_pyopenssl_turns_the_direct_path_off_not_the_client(self):
503
+ # conda can swap in a cryptography the pip-installed pyOpenSSL (aioquic's) cannot use.
504
+ import subprocess
505
+ import sys
506
+ import tempfile
507
+
508
+ with tempfile.TemporaryDirectory() as tmp:
509
+ (Path(tmp) / "OpenSSL").mkdir()
510
+ (Path(tmp) / "OpenSSL" / "__init__.py").write_text("raise AttributeError(\"module 'lib' has no attribute 'GEN_EMAIL'\")\n")
511
+ probe = ("from remoterf.core import direct_path, grpc_client; "
512
+ "print(direct_path.enabled(), direct_path.current(), direct_path.describe())")
513
+ result = subprocess.run([sys.executable, "-c", probe], env={**os.environ, "PYTHONPATH": tmp, "HOME": tmp},
514
+ capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)
515
+ self.assertTrue(result.stdout.startswith("False None relay (direct path unavailable: AttributeError"), result.stderr[-2000:])
516
+
@@ -315,14 +315,15 @@ def test_register_with_a_code_lands_in_the_home_shell(monkeypatch, reachable):
315
315
 
316
316
 
317
317
  def test_a_blank_code_registers_on_global_and_global_has_its_own_spelling(monkeypatch):
318
- answers = iter(["y", ""])
318
+ answers = iter(["y", "", "y"])
319
319
  monkeypatch.setattr("builtins.input", lambda *_: next(answers, ""))
320
320
  used_global = mock.Mock(return_value=0)
321
321
  assert run_cli(["-r"], monkeypatch, _use_global=used_global) == 0
322
322
  used_global.assert_called_once_with(register=True, show_banner=False) # no lab: the account lives on Global
323
323
  used_global.reset_mock()
324
+ # `-r global` asks for the Terms too, then skips the code prompt.
324
325
  assert run_cli(["-r", "global"], monkeypatch, _use_global=used_global) == 0
325
- used_global.assert_called_once_with(register=True)
326
+ used_global.assert_called_once_with(register=True, show_banner=False)
326
327
 
327
328
 
328
329
  def test_declining_the_terms_stops_registration_before_any_code(monkeypatch):
File without changes
File without changes
File without changes
File without changes
File without changes