forgexa-cli 1.47.2__tar.gz → 1.47.3__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 (28) hide show
  1. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/daemon.py +3 -3
  4. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/main.py +47 -9
  5. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/runtime_evidence.py +23 -11
  6. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/pyproject.toml +1 -1
  8. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_auth_and_runtime_commands.py +96 -0
  9. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/README.md +0 -0
  10. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/_build_config.py +0 -0
  11. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/_local_bind.py +0 -0
  12. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/agent_core.py +0 -0
  13. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/autoupgrade.py +0 -0
  14. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/py.typed +0 -0
  15. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/setup.cfg +0 -0
  21. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_autoupgrade.py +0 -0
  22. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_check_command.py +0 -0
  23. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  24. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_local_bind_commands.py +0 -0
  25. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_runtime_credentials.py +0 -0
  26. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_session_credentials.py +0 -0
  27. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_silent_install.py +0 -0
  28. {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.47.2
3
+ Version: 1.47.3
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.47.2"
2
+ __version__ = "1.47.3"
@@ -1075,7 +1075,7 @@ except (ImportError, ModuleNotFoundError):
1075
1075
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1076
1076
  # Kept in sync with pyproject.toml version via bump-version.sh.
1077
1077
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1078
- DAEMON_VERSION = "1.47.2"
1078
+ DAEMON_VERSION = "1.47.3"
1079
1079
 
1080
1080
 
1081
1081
  def _detect_client_type() -> str:
@@ -7908,7 +7908,7 @@ class ServerConnection:
7908
7908
  "capabilities": {
7909
7909
  "platform": platform.platform(),
7910
7910
  "python_version": platform.python_version(),
7911
- "local_workspace_bindings": {"protocol_version": 2},
7911
+ "local_workspace_bindings": {"protocol_version": 3},
7912
7912
  },
7913
7913
  "server_url": self.server_url,
7914
7914
  "expires_in_days": 90,
@@ -8306,7 +8306,7 @@ class ServerConnection:
8306
8306
  "capabilities": {
8307
8307
  "platform": platform.platform(),
8308
8308
  "python_version": platform.python_version(),
8309
- "local_workspace_bindings": {"protocol_version": 2},
8309
+ "local_workspace_bindings": {"protocol_version": 3},
8310
8310
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
8311
8311
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
8312
8312
  },
@@ -1017,16 +1017,29 @@ def _version_is_newer(candidate: str, reference: str) -> bool:
1017
1017
  return False
1018
1018
 
1019
1019
 
1020
- def _run_update_check() -> None:
1021
- """Fetch the latest published CLI version and refresh the local cache."""
1020
+ def _latest_published_cli_version() -> str:
1021
+ """Return the current forgexa-cli version from the official PyPI API."""
1022
1022
  try:
1023
1023
  import httpx
1024
1024
 
1025
1025
  with httpx.Client(timeout=5) as client:
1026
- resp = client.get("https://pypi.org/pypi/forgexa-cli/json")
1027
- resp.raise_for_status()
1028
- latest = str(resp.json()["info"]["version"])
1029
- _write_update_cache(latest)
1026
+ response = client.get("https://pypi.org/pypi/forgexa-cli/json")
1027
+ response.raise_for_status()
1028
+ version = _normalize_target_version(str(response.json()["info"]["version"]))
1029
+ except Exception as exc:
1030
+ raise RuntimeError(
1031
+ "Could not determine the latest forgexa-cli release from PyPI. "
1032
+ "Check access to pypi.org or specify --target-version."
1033
+ ) from exc
1034
+ if version is None:
1035
+ raise RuntimeError("PyPI did not return a forgexa-cli version.")
1036
+ return version
1037
+
1038
+
1039
+ def _run_update_check() -> None:
1040
+ """Fetch the latest published CLI version and refresh the local cache."""
1041
+ try:
1042
+ _write_update_cache(_latest_published_cli_version())
1030
1043
  except Exception:
1031
1044
  pass # Network errors are silently ignored
1032
1045
 
@@ -1581,13 +1594,15 @@ def _run_windows_upgrade_worker(payload: str) -> None:
1581
1594
  parent_pid = int(data["parent_pid"])
1582
1595
  launcher_paths = data.get("launcher_paths", [])
1583
1596
  daemon_stopped = bool(data.get("daemon_stopped"))
1584
- target_version = str(data.get("target_version") or "latest")
1597
+ target_version = str(data.get("target_version") or "").strip()
1585
1598
  except (KeyError, TypeError, ValueError, json.JSONDecodeError):
1586
1599
  return
1587
1600
  if not isinstance(command, list) or not all(isinstance(part, str) for part in command):
1588
1601
  return
1589
1602
  if not isinstance(launcher_paths, list) or not all(isinstance(path, str) for path in launcher_paths):
1590
1603
  return
1604
+ if not target_version:
1605
+ return
1591
1606
 
1592
1607
  try:
1593
1608
  log_path = _upgrade_log_path()
@@ -1608,6 +1623,13 @@ def _run_windows_upgrade_worker(payload: str) -> None:
1608
1623
  if result.returncode != 0:
1609
1624
  log_file.write(f"Upgrade failed (returncode={result.returncode}).\n")
1610
1625
  return
1626
+ installed_version = _installed_cli_version()
1627
+ if installed_version != target_version:
1628
+ log_file.write(
1629
+ f"Upgrade failed: expected {target_version}, "
1630
+ f"installed {installed_version or 'unknown'}.\n"
1631
+ )
1632
+ return
1611
1633
  log_file.write("Upgrade complete.\n")
1612
1634
  if daemon_stopped:
1613
1635
  restarted, server_url = _start_daemon_after_upgrade()
@@ -3446,12 +3468,14 @@ def cmd_version(args: argparse.Namespace) -> None:
3446
3468
  def cmd_upgrade(args: argparse.Namespace) -> None:
3447
3469
  try:
3448
3470
  target_version = _normalize_target_version(getattr(args, "target_version", None))
3471
+ if target_version is None:
3472
+ target_version = _latest_published_cli_version()
3449
3473
  plan = _build_upgrade_plan(target_version)
3450
3474
  except (RuntimeError, ValueError) as exc:
3451
3475
  print(str(exc), file=sys.stderr)
3452
3476
  sys.exit(1)
3453
3477
 
3454
- if target_version and target_version == plan.current_version:
3478
+ if target_version == plan.current_version:
3455
3479
  print(f"forgexa-cli {plan.current_version} is already installed.")
3456
3480
  return
3457
3481
 
@@ -3470,7 +3494,7 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
3470
3494
  print(f"Stopped local daemon (PID {daemon_pid}) before upgrade.")
3471
3495
 
3472
3496
  print(f"Current version : {plan.current_version}")
3473
- print(f"Target version : {target_version or 'latest'}")
3497
+ print(f"Target version : {target_version}")
3474
3498
  print(f"Upgrade mode : {plan.installer}")
3475
3499
  print(f"Running : {shlex.join(plan.command)}")
3476
3500
 
@@ -3494,6 +3518,20 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
3494
3518
  sys.exit(result.returncode or 1)
3495
3519
 
3496
3520
  installed_version = _installed_cli_version() or "unknown"
3521
+ if installed_version != target_version:
3522
+ print(
3523
+ f"Upgrade did not install the requested version {target_version}; "
3524
+ f"installed version is {installed_version}. "
3525
+ "The configured package index, proxy, or constraint may be stale.",
3526
+ file=sys.stderr,
3527
+ )
3528
+ if daemon_stopped:
3529
+ print(
3530
+ "The local daemon was stopped before the upgrade attempt. "
3531
+ "Restart it manually after resolving the package source issue.",
3532
+ file=sys.stderr,
3533
+ )
3534
+ sys.exit(1)
3497
3535
  print(f"Upgrade complete. Installed version: {installed_version}")
3498
3536
  if daemon_stopped:
3499
3537
  restarted, server_url = _start_daemon_after_upgrade()
@@ -14,7 +14,6 @@ import xml.etree.ElementTree as ElementTree
14
14
  from pathlib import Path
15
15
  from typing import Any
16
16
 
17
-
18
17
  RUNTIME_EVIDENCE_PLAN_VERSION = 1
19
18
  TEST_COVERAGE_CHECK_KIND = "test_coverage"
20
19
  STATIC_CHECK_KIND = "static_check"
@@ -25,6 +24,7 @@ LOCAL_EVIDENCE_SUMMARY_VERSION = 1
25
24
  _DEFAULT_TIMEOUT_SECONDS = 600
26
25
  _MAX_FAILED_TESTS = 20
27
26
  _MAX_LINT_ISSUES = 50
27
+ _MAX_ACCEPTANCE_CRITERIA_COVERAGE_ITEMS = 200
28
28
  _MAX_OUTPUT_CHARS = 100_000
29
29
  _MAX_TEST_CASE_RESULTS = 200
30
30
  _SHELL_METACHARACTERS = (";", "`", "$", ">", "<", "&", "|", "\n", "\r", "\x00")
@@ -53,6 +53,13 @@ _SAFE_ACCEPTANCE_REFERENCE = re.compile(r"^AC-(\d+)$", re.IGNORECASE)
53
53
  _CANONICAL_TEST_CASE_ID = re.compile(r"^TC-(?:00[1-9]|0[1-9]\d|[1-9]\d{2})$", re.IGNORECASE)
54
54
  _TEST_CASE_MARKER = re.compile(r"(?<![A-Za-z0-9_-])TC-(\d+)(?![A-Za-z0-9_-])", re.IGNORECASE)
55
55
  _SAFE_PYTHON_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
56
+ _COVERAGE_DECLARED_OUTCOMES = frozenset({
57
+ "covered",
58
+ "partial",
59
+ "uncovered",
60
+ "unverifiable",
61
+ "not_applicable",
62
+ })
56
63
  _MYPY_DIAGNOSTIC = re.compile(
57
64
  r"^(?P<file>[^:\n]+):(?P<line>[1-9]\d*)(?::(?P<column>[1-9]\d*))?: "
58
65
  r"error: (?P<message>.+?)(?:\s+\[(?P<code>[A-Za-z0-9_-]+)\])?\s*$"
@@ -711,7 +718,7 @@ def parse_acceptance_criteria_coverage_artifacts(
711
718
  return _acceptance_criteria_coverage_failure("acceptance_criteria_json_invalid_shape")
712
719
 
713
720
  test_case_intents: dict[str, str] = {}
714
- for raw_test_case in test_cases_document["test_cases"][:_MAX_LINT_ISSUES]:
721
+ for raw_test_case in test_cases_document["test_cases"][:_MAX_ACCEPTANCE_CRITERIA_COVERAGE_ITEMS]:
715
722
  if not isinstance(raw_test_case, dict):
716
723
  return _acceptance_criteria_coverage_failure("test_case_invalid")
717
724
  test_case_id = raw_test_case.get("id")
@@ -725,24 +732,28 @@ def parse_acceptance_criteria_coverage_artifacts(
725
732
  ):
726
733
  return _acceptance_criteria_coverage_failure("test_case_invalid")
727
734
  test_case_intents[test_case_id.strip()] = intent_id.strip()
728
- if len(test_cases_document["test_cases"]) > _MAX_LINT_ISSUES:
735
+ if len(test_cases_document["test_cases"]) > _MAX_ACCEPTANCE_CRITERIA_COVERAGE_ITEMS:
729
736
  return _acceptance_criteria_coverage_failure("test_case_limit_exceeded")
730
737
 
731
738
  coverage_links: list[dict[str, Any]] = []
732
739
  seen_acceptance_ids: set[str] = set()
733
- for raw_criterion in coverage_matrix_document["acceptance_criteria"][:_MAX_LINT_ISSUES]:
740
+ for raw_criterion in coverage_matrix_document["acceptance_criteria"][:_MAX_ACCEPTANCE_CRITERIA_COVERAGE_ITEMS]:
734
741
  if not isinstance(raw_criterion, dict):
735
742
  return _acceptance_criteria_coverage_failure("coverage_matrix_invalid")
736
743
  acceptance_id = _canonical_acceptance_criteria_reference(raw_criterion.get("id"))
737
- status = raw_criterion.get("status")
738
- covered_by = raw_criterion.get("covered_by")
744
+ raw_status = raw_criterion.get("status")
745
+ declared_outcome = raw_status.strip().lower() if isinstance(raw_status, str) else ""
746
+ covered_by = raw_criterion.get("covered_by", [])
739
747
  if (
740
748
  acceptance_id is None
741
749
  or acceptance_id in seen_acceptance_ids
742
- or not isinstance(status, str)
743
- or status.strip().lower() != "covered"
744
750
  or not isinstance(covered_by, list)
745
- or not covered_by
751
+ or declared_outcome not in _COVERAGE_DECLARED_OUTCOMES
752
+ or (declared_outcome == "covered" and not covered_by)
753
+ or (
754
+ declared_outcome in {"uncovered", "unverifiable", "not_applicable"}
755
+ and covered_by
756
+ )
746
757
  ):
747
758
  return _acceptance_criteria_coverage_failure("coverage_matrix_invalid")
748
759
  test_case_ids: list[str] = []
@@ -760,8 +771,9 @@ def parse_acceptance_criteria_coverage_artifacts(
760
771
  "acceptance_criteria_ref": acceptance_id,
761
772
  "test_case_ids": test_case_ids,
762
773
  "test_intent_ids": [test_case_intents[test_case_id] for test_case_id in test_case_ids],
774
+ "declared_outcome": declared_outcome,
763
775
  })
764
- if len(coverage_matrix_document["acceptance_criteria"]) > _MAX_LINT_ISSUES:
776
+ if len(coverage_matrix_document["acceptance_criteria"]) > _MAX_ACCEPTANCE_CRITERIA_COVERAGE_ITEMS:
765
777
  return _acceptance_criteria_coverage_failure("coverage_matrix_limit_exceeded")
766
778
  return {
767
779
  "status": "completed",
@@ -1449,4 +1461,4 @@ def _security_score(high: int, medium: int, low: int) -> float:
1449
1461
  return 0.8
1450
1462
  if low > 0:
1451
1463
  return max(0.85, 1.0 - low * 0.03)
1452
- return 1.0
1464
+ return 1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.47.2
3
+ Version: 1.47.3
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.47.2"
3
+ version = "1.47.3"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
@@ -730,6 +730,66 @@ def test_cmd_upgrade_runs_planned_command(monkeypatch: pytest.MonkeyPatch) -> No
730
730
  assert "Upgrade complete. Installed version: 1.12.3" in output
731
731
 
732
732
 
733
+ def test_cmd_upgrade_resolves_latest_to_an_explicit_pypi_version(
734
+ monkeypatch: pytest.MonkeyPatch,
735
+ ) -> None:
736
+ plan = main.UpgradePlan(
737
+ installer="pipx",
738
+ command=["pipx", "install", "--force", "forgexa-cli==1.12.3"],
739
+ current_version="1.12.2",
740
+ target_version="1.12.3",
741
+ )
742
+ calls: list[list[str]] = []
743
+
744
+ def build_upgrade_plan(target_version: str | None) -> main.UpgradePlan:
745
+ assert target_version == "1.12.3"
746
+ return plan
747
+
748
+ monkeypatch.setattr(main, "_latest_published_cli_version", lambda: "1.12.3")
749
+ monkeypatch.setattr(main, "_build_upgrade_plan", build_upgrade_plan)
750
+ monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (False, None))
751
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.3")
752
+ monkeypatch.setattr(main, "_start_daemon_after_upgrade", lambda: (False, None))
753
+ monkeypatch.setattr(
754
+ main.subprocess,
755
+ "run",
756
+ lambda command: calls.append(command) or SimpleNamespace(returncode=0),
757
+ )
758
+
759
+ buffer = io.StringIO()
760
+ with contextlib.redirect_stdout(buffer):
761
+ main.cmd_upgrade(SimpleNamespace(target_version=None))
762
+
763
+ assert calls == [["pipx", "install", "--force", "forgexa-cli==1.12.3"]]
764
+ assert "Target version : 1.12.3" in buffer.getvalue()
765
+
766
+
767
+ def test_cmd_upgrade_rejects_success_when_target_version_is_not_installed(
768
+ monkeypatch: pytest.MonkeyPatch,
769
+ capsys: pytest.CaptureFixture[str],
770
+ ) -> None:
771
+ plan = main.UpgradePlan(
772
+ installer="pipx",
773
+ command=["pipx", "install", "--force", "forgexa-cli==1.12.3"],
774
+ current_version="1.12.2",
775
+ target_version="1.12.3",
776
+ )
777
+ restart_calls: list[bool] = []
778
+
779
+ monkeypatch.setattr(main, "_build_upgrade_plan", lambda target: plan)
780
+ monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
781
+ monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (True, 4242))
782
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.2")
783
+ monkeypatch.setattr(main, "_start_daemon_after_upgrade", lambda: restart_calls.append(True))
784
+ monkeypatch.setattr(main.subprocess, "run", lambda command: SimpleNamespace(returncode=0))
785
+
786
+ with pytest.raises(SystemExit, match="1"):
787
+ main.cmd_upgrade(SimpleNamespace(target_version="1.12.3"))
788
+
789
+ assert restart_calls == []
790
+ assert "did not install the requested version 1.12.3" in capsys.readouterr().err
791
+
792
+
733
793
  def test_cmd_upgrade_defers_windows_install_until_cli_exits(
734
794
  monkeypatch: pytest.MonkeyPatch,
735
795
  ) -> None:
@@ -812,6 +872,7 @@ def test_windows_upgrade_worker_waits_before_running_pip(
812
872
  "run",
813
873
  lambda command, **kwargs: events.append("run") or SimpleNamespace(returncode=0),
814
874
  )
875
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.3")
815
876
 
816
877
  main._run_windows_upgrade_worker(
817
878
  json.dumps(
@@ -838,6 +899,7 @@ def test_windows_upgrade_worker_restarts_stopped_daemon_after_success(
838
899
  monkeypatch.setattr(main, "_upgrade_log_path", lambda: log_path)
839
900
  monkeypatch.setattr(main, "_wait_for_windows_upgrade_release", lambda *args: True)
840
901
  monkeypatch.setattr(main.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0))
902
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.3")
841
903
  monkeypatch.setattr(
842
904
  main,
843
905
  "_start_daemon_after_upgrade",
@@ -860,6 +922,38 @@ def test_windows_upgrade_worker_restarts_stopped_daemon_after_success(
860
922
  assert "Daemon restarted in background (server=https://api.example.com)." in log_path.read_text()
861
923
 
862
924
 
925
+ def test_windows_upgrade_worker_does_not_restart_when_version_is_not_installed(
926
+ tmp_path: Path,
927
+ monkeypatch: pytest.MonkeyPatch,
928
+ ) -> None:
929
+ log_path = tmp_path / "upgrade.log"
930
+ restart_calls: list[bool] = []
931
+ monkeypatch.setattr(main, "_upgrade_log_path", lambda: log_path)
932
+ monkeypatch.setattr(main, "_wait_for_windows_upgrade_release", lambda *args: True)
933
+ monkeypatch.setattr(main.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0))
934
+ monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.12.2")
935
+ monkeypatch.setattr(
936
+ main,
937
+ "_start_daemon_after_upgrade",
938
+ lambda: restart_calls.append(True) or (True, "https://api.example.com"),
939
+ )
940
+
941
+ main._run_windows_upgrade_worker(
942
+ json.dumps(
943
+ {
944
+ "command": ["python", "-m", "pip", "install", "--upgrade", "forgexa-cli"],
945
+ "parent_pid": 4242,
946
+ "launcher_paths": [],
947
+ "daemon_stopped": True,
948
+ "target_version": "1.12.3",
949
+ }
950
+ )
951
+ )
952
+
953
+ assert restart_calls == []
954
+ assert "Upgrade failed: expected 1.12.3, installed 1.12.2." in log_path.read_text()
955
+
956
+
863
957
  def test_cmd_upgrade_restarts_background_daemon_after_success(
864
958
  monkeypatch: pytest.MonkeyPatch,
865
959
  ) -> None:
@@ -873,6 +967,7 @@ def test_cmd_upgrade_restarts_background_daemon_after_success(
873
967
 
874
968
  monkeypatch.setattr(main, "_build_upgrade_plan", lambda target: plan)
875
969
  monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
970
+ monkeypatch.setattr(main, "_latest_published_cli_version", lambda: "1.16.4")
876
971
  monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (True, 4242))
877
972
  monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.16.4")
878
973
  monkeypatch.setattr(main.subprocess, "run", lambda cmd: SimpleNamespace(returncode=0))
@@ -905,6 +1000,7 @@ def test_cmd_upgrade_keeps_manual_restart_hint_when_auto_restart_skips(
905
1000
 
906
1001
  monkeypatch.setattr(main, "_build_upgrade_plan", lambda target: plan)
907
1002
  monkeypatch.setattr(main, "_normalize_target_version", lambda value: value)
1003
+ monkeypatch.setattr(main, "_latest_published_cli_version", lambda: "1.16.4")
908
1004
  monkeypatch.setattr(main, "_stop_daemon_if_running", lambda: (True, 4242))
909
1005
  monkeypatch.setattr(main, "_installed_cli_version", lambda: "1.16.4")
910
1006
  monkeypatch.setattr(main.subprocess, "run", lambda cmd: SimpleNamespace(returncode=0))
File without changes
File without changes