msdev 0.14.0__tar.gz → 0.14.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 (107) hide show
  1. {msdev-0.14.0 → msdev-0.14.1}/PKG-INFO +1 -1
  2. {msdev-0.14.0 → msdev-0.14.1}/pyproject.toml +1 -1
  3. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/__init__.py +1 -1
  4. msdev-0.14.1/src/msdev/core/daemon_version.py +26 -0
  5. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/node.py +1 -14
  6. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/transport.py +73 -34
  7. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdevd-linux-amd64 +0 -0
  8. msdev-0.14.1/src/msdev/helpers/msdevd-linux-amd64.sha256 +1 -0
  9. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdevd-linux-arm64 +0 -0
  10. msdev-0.14.1/src/msdev/helpers/msdevd-linux-arm64.sha256 +1 -0
  11. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/skills/msdev-cli/SKILL.md +4 -2
  12. {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/PKG-INFO +1 -1
  13. {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/SOURCES.txt +2 -0
  14. {msdev-0.14.0 → msdev-0.14.1}/tests/test_cli.py +2 -2
  15. {msdev-0.14.0 → msdev-0.14.1}/tests/test_config_bundle.py +3 -3
  16. {msdev-0.14.0 → msdev-0.14.1}/tests/test_go_msdevd_scenario.py +1 -1
  17. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_bootstrap_version_scenario.py +2 -1
  18. msdev-0.14.1/tests/test_stale_daemon_autobootstrap_scenario.py +100 -0
  19. {msdev-0.14.0 → msdev-0.14.1}/tests/test_transport.py +4 -0
  20. msdev-0.14.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
  21. msdev-0.14.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
  22. {msdev-0.14.0 → msdev-0.14.1}/LICENSE +0 -0
  23. {msdev-0.14.0 → msdev-0.14.1}/README.md +0 -0
  24. {msdev-0.14.0 → msdev-0.14.1}/setup.cfg +0 -0
  25. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/cli.py +0 -0
  26. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/__init__.py +0 -0
  27. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/compat.py +0 -0
  28. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/config.py +0 -0
  29. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/config_bundle.py +0 -0
  30. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/container_helper.py +0 -0
  31. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/daemon_deploy.py +0 -0
  32. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/env_transfer.py +0 -0
  33. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/exec_argv.py +0 -0
  34. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/guides.py +0 -0
  35. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/helper_deploy.py +0 -0
  36. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/host_files.py +0 -0
  37. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/host_mutations.py +0 -0
  38. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/init_setup.py +0 -0
  39. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/inventory.py +0 -0
  40. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/limits.py +0 -0
  41. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/local_rpc.py +0 -0
  42. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/multiplex.py +0 -0
  43. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/request_client.py +0 -0
  44. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/resources.py +0 -0
  45. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/__init__.py +0 -0
  46. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/capabilities.py +0 -0
  47. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/context.py +0 -0
  48. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/environment.py +0 -0
  49. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/execution.py +0 -0
  50. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/model.py +0 -0
  51. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/npu.py +0 -0
  52. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/results.py +0 -0
  53. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/sftp_transfer.py +0 -0
  54. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/skill_install.py +0 -0
  55. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_auth.py +0 -0
  56. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_config.py +0 -0
  57. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_tunnel.py +0 -0
  58. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_limits.py +0 -0
  59. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_server.py +0 -0
  60. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_transport.py +0 -0
  61. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/streamproto.py +0 -0
  62. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/daemon.py +0 -0
  63. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
  64. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
  65. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
  66. {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
  67. {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/dependency_links.txt +0 -0
  68. {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/entry_points.txt +0 -0
  69. {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/top_level.txt +0 -0
  70. {msdev-0.14.0 → msdev-0.14.1}/tests/test_cli_only_architecture.py +0 -0
  71. {msdev-0.14.0 → msdev-0.14.1}/tests/test_compat.py +0 -0
  72. {msdev-0.14.0 → msdev-0.14.1}/tests/test_config.py +0 -0
  73. {msdev-0.14.0 → msdev-0.14.1}/tests/test_connect_env_scenario.py +0 -0
  74. {msdev-0.14.0 → msdev-0.14.1}/tests/test_container_helper.py +0 -0
  75. {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon.py +0 -0
  76. {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon_container_stream.py +0 -0
  77. {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon_deploy.py +0 -0
  78. {msdev-0.14.0 → msdev-0.14.1}/tests/test_docker_stream_dispatch.py +0 -0
  79. {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_put_get_scenario.py +0 -0
  80. {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_read_write_scenario.py +0 -0
  81. {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_transfer.py +0 -0
  82. {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_argv.py +0 -0
  83. {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_command_scenario.py +0 -0
  84. {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_default_timeout_scenario.py +0 -0
  85. {msdev-0.14.0 → msdev-0.14.1}/tests/test_helper_deploy.py +0 -0
  86. {msdev-0.14.0 → msdev-0.14.1}/tests/test_init_skill.py +0 -0
  87. {msdev-0.14.0 → msdev-0.14.1}/tests/test_inventory.py +0 -0
  88. {msdev-0.14.0 → msdev-0.14.1}/tests/test_model_cli.py +0 -0
  89. {msdev-0.14.0 → msdev-0.14.1}/tests/test_model_services.py +0 -0
  90. {msdev-0.14.0 → msdev-0.14.1}/tests/test_msdev_cli_skill.py +0 -0
  91. {msdev-0.14.0 → msdev-0.14.1}/tests/test_multiplex.py +0 -0
  92. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_connect_provision_scenario.py +0 -0
  93. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_connect_save_password_scenario.py +0 -0
  94. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_env_config.py +0 -0
  95. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_env_services.py +0 -0
  96. {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_sync_ssh_scenario.py +0 -0
  97. {msdev-0.14.0 → msdev-0.14.1}/tests/test_request_client.py +0 -0
  98. {msdev-0.14.0 → msdev-0.14.1}/tests/test_sftp_transfer_scenario.py +0 -0
  99. {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_auth.py +0 -0
  100. {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_config.py +0 -0
  101. {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_tunnel.py +0 -0
  102. {msdev-0.14.0 → msdev-0.14.1}/tests/test_stream_server.py +0 -0
  103. {msdev-0.14.0 → msdev-0.14.1}/tests/test_stream_transport.py +0 -0
  104. {msdev-0.14.0 → msdev-0.14.1}/tests/test_streamproto.py +0 -0
  105. {msdev-0.14.0 → msdev-0.14.1}/tests/test_target_exec_services.py +0 -0
  106. {msdev-0.14.0 → msdev-0.14.1}/tests/test_target_guides.py +0 -0
  107. {msdev-0.14.0 → msdev-0.14.1}/tests/test_windows_local_scenario.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev
3
- Version: 0.14.0
3
+ Version: 0.14.1
4
4
  Summary: Portable nodes and execution environments for msModelSlim development
5
5
  Author: msModelSlim Agent Contributors
6
6
  License-Expression: MulanPSL-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "msdev"
7
- version = "0.14.0"
7
+ version = "0.14.1"
8
8
  description = "Portable nodes and execution environments for msModelSlim development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """Portable node and execution-environment tooling for msModelSlim."""
2
2
 
3
- __version__ = "0.14.0"
3
+ __version__ = "0.14.1"
@@ -0,0 +1,26 @@
1
+ """Helpers for comparing local CLI and remote msdevd versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .. import __version__
8
+
9
+
10
+ def remote_daemon_version_matches(
11
+ ping: Any,
12
+ *,
13
+ expected: str | None = None,
14
+ ) -> bool:
15
+ """Return True when ping reports the same msdevd version as the local CLI."""
16
+ target = expected if expected is not None else __version__
17
+ if not isinstance(ping, dict):
18
+ return False
19
+ remote = ping.get("version")
20
+ return isinstance(remote, str) and remote == target
21
+
22
+
23
+ def is_incompatible_daemon_rpc_error(exc: BaseException) -> bool:
24
+ """True when the remote daemon rejected a method the current CLI requires."""
25
+ text = str(exc).lower()
26
+ return "unknown rpc method" in text
@@ -6,8 +6,8 @@ from dataclasses import asdict, dataclass
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- from ... import __version__
10
9
  from ..resources import Node
10
+ from ..daemon_version import remote_daemon_version_matches
11
11
  from ..ssh_config import (
12
12
  SshHostSpec,
13
13
  list_ssh_config_hosts,
@@ -19,19 +19,6 @@ from ..transport import DaemonUnavailableError, RpcError
19
19
  from .context import ServiceContext
20
20
 
21
21
 
22
- def remote_daemon_version_matches(
23
- ping: Any,
24
- *,
25
- expected: str | None = None,
26
- ) -> bool:
27
- """Return True when ping reports the same msdevd version as the local CLI."""
28
- target = expected if expected is not None else __version__
29
- if not isinstance(ping, dict):
30
- return False
31
- remote = ping.get("version")
32
- return isinstance(remote, str) and remote == target
33
-
34
-
35
22
  @dataclass(frozen=True)
36
23
  class NodeAddRequest:
37
24
  name: str
@@ -19,6 +19,7 @@ import uuid
19
19
  from pathlib import Path
20
20
  from typing import Any, Callable, Protocol
21
21
 
22
+ from .. import __version__
22
23
  from .compat import (
23
24
  kill_popen_tree,
24
25
  popen_kwargs,
@@ -50,6 +51,10 @@ from .env_transfer import (
50
51
  transfer_timeout_seconds,
51
52
  )
52
53
  from .daemon_deploy import select_daemon_binary
54
+ from .daemon_version import (
55
+ is_incompatible_daemon_rpc_error,
56
+ remote_daemon_version_matches,
57
+ )
53
58
  from .local_rpc import connect_local_rpc
54
59
  from .resources import Environment, Node
55
60
  from .sftp_transfer import sftp_put_file
@@ -649,6 +654,53 @@ class SshRpcTransport:
649
654
  self.control_persist = os.environ.get("MSDEV_SSH_CONTROL_PERSIST", "yes")
650
655
  self._control_master = ssh_control_master_enabled()
651
656
  self._tunnel = LocalForwardTunnel(self)
657
+ self._compatible_daemon = False
658
+
659
+ def _bootstrap_remote_daemon(self, reason: str) -> None:
660
+ print(
661
+ f"[msdev] node {self.node.name!r} msdevd needs update "
662
+ f"({reason}); bootstrapping user daemon...",
663
+ file=sys.stderr,
664
+ )
665
+ try:
666
+ bootstrap_node(self.node)
667
+ except Exception as bootstrap_exc:
668
+ raise RpcError(
669
+ f"automatic bootstrap failed for node {self.node.name!r}: "
670
+ f"{bootstrap_exc}. Retry with `msdev node bootstrap "
671
+ f"{self.node.name}` for detailed diagnostics."
672
+ ) from bootstrap_exc
673
+ self._compatible_daemon = True
674
+ print(
675
+ f"[msdev] msdevd is ready on node {self.node.name!r}; "
676
+ "retrying request.",
677
+ file=sys.stderr,
678
+ )
679
+
680
+ def _ensure_compatible_daemon(self) -> None:
681
+ """Redeploy when remote ping version does not match this CLI."""
682
+ if self._compatible_daemon:
683
+ return
684
+ try:
685
+ ping = self._call_once("ping")
686
+ except DaemonUnavailableError:
687
+ if not self.auto_bootstrap:
688
+ raise
689
+ self._bootstrap_remote_daemon("unavailable")
690
+ ping = self._call_once("ping")
691
+ if remote_daemon_version_matches(ping):
692
+ self._compatible_daemon = True
693
+ return
694
+ remote = ping.get("version") if isinstance(ping, dict) else None
695
+ if not self.auto_bootstrap:
696
+ raise RpcError(
697
+ f"msdevd version mismatch on node {self.node.name!r}: "
698
+ f"remote={remote!r} local={__version__!r}; "
699
+ f"run `msdev node bootstrap {self.node.name}`"
700
+ )
701
+ self._bootstrap_remote_daemon(
702
+ f"version mismatch remote={remote!r} local={__version__!r}"
703
+ )
652
704
 
653
705
  def _rpc_target_label(self) -> str:
654
706
  if self.environment is not None:
@@ -1143,56 +1195,43 @@ esac
1143
1195
  return values
1144
1196
 
1145
1197
  def call(self, method: str, params: dict[str, Any] | None = None) -> Any:
1198
+ if method != "ping":
1199
+ self._ensure_compatible_daemon()
1146
1200
  try:
1147
1201
  return self._call_once(method, params)
1148
- except DaemonUnavailableError as exc:
1202
+ except DaemonUnavailableError:
1149
1203
  if not self.auto_bootstrap:
1150
1204
  raise
1151
- print(
1152
- f"[msdev] node {self.node.name!r} is reachable but msdevd "
1153
- "is unavailable; bootstrapping user daemon...",
1154
- file=sys.stderr,
1155
- )
1156
- try:
1157
- bootstrap_node(self.node)
1158
- except Exception as bootstrap_exc:
1159
- raise RpcError(
1160
- f"automatic bootstrap failed for node {self.node.name!r}: "
1161
- f"{bootstrap_exc}. Retry with `msdev node bootstrap "
1162
- f"{self.node.name}` for detailed diagnostics."
1163
- ) from bootstrap_exc
1164
- print(
1165
- f"[msdev] msdevd is ready on node {self.node.name!r}; "
1166
- "retrying request.",
1167
- file=sys.stderr,
1168
- )
1205
+ self._bootstrap_remote_daemon("unavailable")
1169
1206
  return self._call_once(method, params)
1207
+ except RpcError as exc:
1208
+ if (
1209
+ method != "ping"
1210
+ and self.auto_bootstrap
1211
+ and is_incompatible_daemon_rpc_error(exc)
1212
+ ):
1213
+ self._bootstrap_remote_daemon(str(exc))
1214
+ return self._call_once(method, params)
1215
+ raise
1170
1216
 
1171
1217
  def stream_exec(
1172
1218
  self,
1173
1219
  params: dict[str, Any],
1174
1220
  on_output: Callable[[str, bytes], None],
1175
1221
  ) -> dict[str, Any]:
1222
+ self._ensure_compatible_daemon()
1176
1223
  try:
1177
1224
  return self._stream_exec_once(params, on_output)
1178
- except DaemonUnavailableError as exc:
1225
+ except DaemonUnavailableError:
1179
1226
  if not self.auto_bootstrap:
1180
1227
  raise
1181
- print(
1182
- f"[msdev] node {self.node.name!r} is reachable but msdevd "
1183
- "is unavailable; bootstrapping user daemon...",
1184
- file=sys.stderr,
1185
- )
1186
- try:
1187
- bootstrap_node(self.node)
1188
- except Exception as bootstrap_exc:
1189
- raise RpcError(
1190
- f"automatic bootstrap failed for node "
1191
- f"{self.node.name!r}: {bootstrap_exc}. Retry with "
1192
- f"`msdev node connect {self.node.name}` for "
1193
- "detailed diagnostics."
1194
- ) from bootstrap_exc
1228
+ self._bootstrap_remote_daemon("unavailable")
1195
1229
  return self._stream_exec_once(params, on_output)
1230
+ except RpcError as exc:
1231
+ if self.auto_bootstrap and is_incompatible_daemon_rpc_error(exc):
1232
+ self._bootstrap_remote_daemon(str(exc))
1233
+ return self._stream_exec_once(params, on_output)
1234
+ raise
1196
1235
 
1197
1236
  def _stream_exec_once(
1198
1237
  self,
@@ -0,0 +1 @@
1
+ e397d2c3621ad047df7bc181b2fa7637694fb533fa559a39e3495130d3fbf5fa msdevd-linux-amd64
@@ -0,0 +1 @@
1
+ 86aa45ed2e836e359cc65fe341c39aa84c61bde4f522f086fdecac7026695627 msdevd-linux-arm64
@@ -181,6 +181,8 @@ captures one result. Local cancellation terminates the remote process group.
181
181
 
182
182
  - Ask before destructive or externally visible changes unless authorized.
183
183
  - Track temporary resources and remove only those created by the task.
184
- - If `msdevd` is missing or its version mismatches the local CLI, run
185
- `msdev node bootstrap NODE` (or `msdev node connect NODE`), then retry.
184
+ - If `msdevd` is missing, version-mismatched, or returns
185
+ `unknown RPC method` (for example `env.open`), run
186
+ `msdev node bootstrap NODE` (or retry the command — exec/connect now
187
+ auto-redeploy when `auto_bootstrap` is on), then retry.
186
188
  - If connect fails, report the error; do not bypass msdev with another transport.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev
3
- Version: 0.14.0
3
+ Version: 0.14.1
4
4
  Summary: Portable nodes and execution environments for msModelSlim development
5
5
  Author: msModelSlim Agent Contributors
6
6
  License-Expression: MulanPSL-2.0
@@ -15,6 +15,7 @@ src/msdev/core/config.py
15
15
  src/msdev/core/config_bundle.py
16
16
  src/msdev/core/container_helper.py
17
17
  src/msdev/core/daemon_deploy.py
18
+ src/msdev/core/daemon_version.py
18
19
  src/msdev/core/env_transfer.py
19
20
  src/msdev/core/exec_argv.py
20
21
  src/msdev/core/guides.py
@@ -92,6 +93,7 @@ tests/test_sftp_transfer_scenario.py
92
93
  tests/test_ssh_auth.py
93
94
  tests/test_ssh_config.py
94
95
  tests/test_ssh_tunnel.py
96
+ tests/test_stale_daemon_autobootstrap_scenario.py
95
97
  tests/test_stream_server.py
96
98
  tests/test_stream_transport.py
97
99
  tests/test_streamproto.py
@@ -314,14 +314,14 @@ class CliParserTest(unittest.TestCase):
314
314
  "ready": True,
315
315
  "bootstrapped": True,
316
316
  "reason": "forced",
317
- "daemon": {"ok": True, "version": "0.14.0"},
317
+ "daemon": {"ok": True, "version": "0.14.1"},
318
318
  },
319
319
  (
320
320
  "{\n"
321
321
  ' "bootstrapped": true,\n'
322
322
  ' "daemon": {\n'
323
323
  ' "ok": true,\n'
324
- ' "version": "0.14.0"\n'
324
+ ' "version": "0.14.1"\n'
325
325
  " },\n"
326
326
  ' "ready": true,\n'
327
327
  ' "reason": "forced"\n'
@@ -56,7 +56,7 @@ class ConfigBundleTests(unittest.TestCase):
56
56
  summary = export_bundle(
57
57
  export_dir,
58
58
  config_path=resources,
59
- msdev_version="0.14.0",
59
+ msdev_version="0.14.1",
60
60
  )
61
61
  self.assertFalse(summary["archive"])
62
62
  self.assertTrue((export_dir / BUNDLE_MANIFEST).is_file())
@@ -100,7 +100,7 @@ class ConfigBundleTests(unittest.TestCase):
100
100
  )
101
101
 
102
102
  archive = root / "msdev-config.tar.gz"
103
- export_bundle(archive, config_path=resources, msdev_version="0.14.0")
103
+ export_bundle(archive, config_path=resources, msdev_version="0.14.1")
104
104
  self.assertTrue(archive.is_file())
105
105
 
106
106
  target_resources = root / "imported" / "resources.json"
@@ -126,7 +126,7 @@ class ConfigBundleTests(unittest.TestCase):
126
126
  )
127
127
 
128
128
  bundle_dir = root / "bundle"
129
- export_bundle(bundle_dir, config_path=source_resources, msdev_version="0.14.0")
129
+ export_bundle(bundle_dir, config_path=source_resources, msdev_version="0.14.1")
130
130
  result = import_bundle(bundle_dir, config_path=target_resources)
131
131
  self.assertEqual(result["imported_nodes"], 0)
132
132
  self.assertIn("node:worker", result["skipped"])
@@ -104,7 +104,7 @@ class GoMsdevdScenarioTests(unittest.TestCase):
104
104
  self.assertNotIn("error", response)
105
105
  result = response["result"]
106
106
  self.assertTrue(result["ok"])
107
- self.assertEqual(result["version"], "0.14.0")
107
+ self.assertEqual(result["version"], "0.14.1")
108
108
  self.assertIn("node_id", result)
109
109
 
110
110
  def test_exec(self):
@@ -11,10 +11,11 @@ from pathlib import Path
11
11
  from unittest.mock import Mock, patch
12
12
 
13
13
  from msdev import __version__
14
+ from msdev.core.daemon_version import remote_daemon_version_matches
14
15
  from msdev.cli import build_parser, command_node
15
16
  from msdev.core.resources import EnvironmentStore, Node, NodeStore
16
17
  from msdev.core.services.context import ServiceContext
17
- from msdev.core.services.node import NodeService, remote_daemon_version_matches
18
+ from msdev.core.services.node import NodeService
18
19
 
19
20
 
20
21
  class NodeBootstrapVersionScenarioTests(unittest.TestCase):
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """Scenario: exec against a stale remote msdevd auto-bootstraps.
3
+
4
+ Covers the failure mode where ping still works but required RPCs such as
5
+ ``env.open`` return ``unknown RPC method``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import unittest
11
+ from unittest.mock import patch
12
+
13
+ from msdev import __version__
14
+ from msdev.core.resources import Environment, Node
15
+ from msdev.core.transport import RpcError, SshRpcTransport
16
+
17
+
18
+ class StaleDaemonAutoBootstrapScenarioTests(unittest.TestCase):
19
+ def test_env_open_unknown_method_triggers_bootstrap_then_retry(self) -> None:
20
+ node = Node(name="worker", ssh_host="worker.example")
21
+ env = Environment(name="worker", node="worker")
22
+ transport = SshRpcTransport(node, env, auto_bootstrap=True)
23
+ calls: list[str] = []
24
+
25
+ def fake_once(method, params=None):
26
+ calls.append(method)
27
+ if method == "ping":
28
+ # Version already matches — old binary still lacks env.open.
29
+ return {"ok": True, "version": __version__, "node_id": "n1"}
30
+ if method == "env.open":
31
+ if calls.count("env.open") == 1:
32
+ raise RpcError("KeyError: unknown RPC method: env.open")
33
+ return {"open": True, "ready": True, "name": "worker"}
34
+ raise AssertionError(f"unexpected method {method}")
35
+
36
+ with patch(
37
+ "msdev.core.transport.bootstrap_node",
38
+ return_value={"layout": "static"},
39
+ ) as bootstrap:
40
+ with patch.object(transport, "_call_once", side_effect=fake_once):
41
+ with patch.object(transport, "_ensure_mux"):
42
+ result = transport.call(
43
+ "env.open",
44
+ {"_env": env.to_agent_spec()},
45
+ )
46
+
47
+ self.assertEqual(result["ready"], True)
48
+ bootstrap.assert_called_once_with(node)
49
+ self.assertGreaterEqual(calls.count("env.open"), 2)
50
+
51
+ def test_exec_path_bootstraps_on_version_mismatch_before_env_open(self) -> None:
52
+ node = Node(name="worker", ssh_host="worker.example")
53
+ env = Environment(name="worker", node="worker")
54
+ transport = SshRpcTransport(node, env, auto_bootstrap=True)
55
+ calls: list[str] = []
56
+
57
+ def fake_once(method, params=None):
58
+ calls.append(method)
59
+ if method == "ping":
60
+ return {"ok": True, "version": "0.12.0", "node_id": "old"}
61
+ if method == "env.open":
62
+ return {"open": True, "ready": True, "name": "worker"}
63
+ raise AssertionError(f"unexpected method {method}")
64
+
65
+ with patch(
66
+ "msdev.core.transport.bootstrap_node",
67
+ return_value={"layout": "static"},
68
+ ) as bootstrap:
69
+ with patch.object(transport, "_call_once", side_effect=fake_once):
70
+ with patch.object(transport, "_ensure_mux"):
71
+ transport._ensure_compatible_daemon()
72
+ result = transport.call(
73
+ "env.open",
74
+ {"_env": env.to_agent_spec()},
75
+ )
76
+
77
+ self.assertTrue(result["ready"])
78
+ bootstrap.assert_called_once_with(node)
79
+ self.assertIn("ping", calls)
80
+ self.assertIn("env.open", calls)
81
+
82
+ def test_version_mismatch_without_auto_bootstrap_is_actionable(self) -> None:
83
+ node = Node(
84
+ name="worker",
85
+ ssh_host="worker.example",
86
+ auto_bootstrap=False,
87
+ )
88
+ transport = SshRpcTransport(node, auto_bootstrap=False)
89
+
90
+ with patch.object(
91
+ transport,
92
+ "_call_once",
93
+ return_value={"ok": True, "version": "0.12.0"},
94
+ ):
95
+ with self.assertRaisesRegex(RpcError, "version mismatch"):
96
+ transport._ensure_compatible_daemon()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ unittest.main()
@@ -298,6 +298,7 @@ class PackageSourceTest(unittest.TestCase):
298
298
  node = Node(name="worker", ssh_host="worker")
299
299
  transport = SshRpcTransport(node, auto_bootstrap=False)
300
300
  transport._control_master = False
301
+ transport._compatible_daemon = True
301
302
  transport._tunnel._local_port = 18080
302
303
  chunks = []
303
304
 
@@ -392,6 +393,7 @@ class PackageSourceTest(unittest.TestCase):
392
393
  def test_ssh_transport_streams_exec_events_and_returns_result(self):
393
394
  node = Node(name="worker", ssh_host="worker")
394
395
  transport = SshRpcTransport(node, auto_bootstrap=False)
396
+ transport._compatible_daemon = True
395
397
  chunks = []
396
398
  captured = {}
397
399
 
@@ -453,6 +455,7 @@ class PackageSourceTest(unittest.TestCase):
453
455
  environment=environment,
454
456
  auto_bootstrap=False,
455
457
  )
458
+ transport._compatible_daemon = True
456
459
 
457
460
  def fake_stream(args, **kwargs):
458
461
  return subprocess.CompletedProcess(args, 2, b"", b"")
@@ -478,6 +481,7 @@ class PackageSourceTest(unittest.TestCase):
478
481
  def test_streaming_rpc_rc_127_reports_daemon_unavailable(self):
479
482
  node = Node(name="worker", ssh_host="worker")
480
483
  transport = SshRpcTransport(node, auto_bootstrap=False)
484
+ transport._compatible_daemon = True
481
485
 
482
486
  def fake_stream(args, **kwargs):
483
487
  return subprocess.CompletedProcess(args, 127, b"", b"")
@@ -1 +0,0 @@
1
- 7b3d2dcd31cc752b44289c5b0936569be7df1aef162d07c1b3c00e886cfea884 msdevd-linux-amd64
@@ -1 +0,0 @@
1
- aeb5721e49746058e286ed14621208823ee092372c0a7d772cfdd9924d5b71e1 msdevd-linux-arm64
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes