Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions mempalace/smoke_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass, field
from typing import Iterable

from .version import __version__ # noqa: F401 (kept for parity / introspection)

MIN_VERSION = "3.4.0"

# The full set of tools the server is expected to advertise. Pinned explicitly
Expand Down Expand Up @@ -99,8 +98,10 @@
def _parse_version(v: str) -> list[int]:
parts: list[int] = []
for component in str(v).split("."):
digits = "".join(ch for ch in component if ch.isdigit())
parts.append(int(digits) if digits else 0)
# Take only LEADING digits so a pre-release suffix is the base number,
# not a misread patch: "3.4.0rc1" -> [3, 4, 0], not [3, 4, 1].
m = re.match(r"\d+", component)
parts.append(int(m.group()) if m else 0)
return parts


Expand Down Expand Up @@ -175,14 +176,19 @@ def _mcp_session(
if env:
proc_env.update(env)
payload = "".join(json.dumps(r) + "\n" for r in requests)
proc = subprocess.run(
server_cmd,
input=payload,
capture_output=True,
text=True,
env=proc_env,
timeout=timeout,
)
try:
proc = subprocess.run(
server_cmd,
input=payload,
capture_output=True,
text=True,
env=proc_env,
timeout=timeout,
)
except subprocess.TimeoutExpired:
# A hung server must produce a clean FAIL report (no responses), not
# crash the gate — the gate exists to detect a broken server.
return {}
responses: dict = {}
for line in proc.stdout.splitlines():
line = line.strip()
Expand Down
33 changes: 32 additions & 1 deletion tests/test_smoke_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
(MEMPALACE_PALACE_PATH=tmpdir); the configured palace gains no drawer.
"""

import subprocess

import pytest

# RED: this import fails until mempalace/smoke_upgrade.py exists.
import mempalace.smoke_upgrade as su
from mempalace.smoke_upgrade import (
EXPECTED_TOOLS,
SmokeReport,
Expand Down Expand Up @@ -54,6 +56,35 @@ def test_version_meets_two_component_is_padded():
assert version_meets("3.3", "3.4.0") is False


def test_version_parse_prerelease_suffix_is_not_mis_read_as_patch():
# Polish note (3): a pre-release like "3.4.0rc1" must parse the base
# version, NOT read "0rc1" as patch 1. Leading-digits-per-component.
assert su._parse_version("3.4.0rc1") == [3, 4, 0]
assert version_meets("3.4.0rc1", "3.4.0") is True # rc of 3.4.0 satisfies the floor
assert version_meets("3.3.0rc5", "3.4.0") is False # 3.3.x rc is still below


def test_mcp_session_timeout_yields_empty_not_crash(monkeypatch):
# Polish note (2): a hung server must produce a clean FAIL report, not crash
# the gate. _mcp_session catches subprocess.TimeoutExpired and returns {}.
def _raise_timeout(*a, **k):
raise subprocess.TimeoutExpired(cmd="server", timeout=1)

monkeypatch.setattr(su.subprocess, "run", _raise_timeout)
responses = su._mcp_session([{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}])
assert responses == {} # no responses, no exception


def test_run_smoke_reports_fail_when_server_hangs(monkeypatch):
def _raise_timeout(*a, **k):
raise subprocess.TimeoutExpired(cmd="server", timeout=1)

monkeypatch.setattr(su.subprocess, "run", _raise_timeout)
report = run_smoke(palace_path="/tmp/does-not-matter")
assert isinstance(report, SmokeReport)
assert report.ok is False # FAIL, not a crash


# ── Pure logic: advertised-tool gate (the staleness catch) ───────────────


Expand Down
Loading