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.
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/PKG-INFO +1 -1
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/daemon.py +3 -3
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/main.py +47 -9
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/runtime_evidence.py +23 -11
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/pyproject.toml +1 -1
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_auth_and_runtime_commands.py +96 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/README.md +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/setup.cfg +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.47.2 → forgexa_cli-1.47.3}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.47.
|
|
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.
|
|
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":
|
|
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":
|
|
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
|
|
1021
|
-
"""
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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"][:
|
|
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"]) >
|
|
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"][:
|
|
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
|
-
|
|
738
|
-
|
|
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
|
|
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"]) >
|
|
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
|
|
@@ -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
|
|
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
|