fix(cutter): attribute the host application version

Identify the Cutter host through Qt applicationVersion rather than the embedded Rizin module-version report. Preserve unknown host metadata without backend fallback and reuse the existing per-plugin lifetime observation.

Validated 13 Python protocol tests, 66 related CLI/MCP/provider tests, full static/docs gates, and official Linux Cutter 2.5.0 Qt5 public workflows. Native discovery and both Evidence version fields match the independently observed host version; source paths and cleanup remain preserved. The named-tool workflow serialization proxy drops from 8767 to 6912 o200k tokens; this is not model billing.

Fixes #4498.
This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-16 23:33:13 +08:00
1 parent 2c1dee16df
commit 9a82046b07
2 files changed
+45 -4

No files matched your search

+4 -3
View File
@@ -28,9 +28,9 @@ if os.name == "nt":
import cutter
try:
from PySide6.QtCore import QObject, Signal
from PySide6.QtCore import QCoreApplication, QObject, Signal
except ImportError:
from PySide2.QtCore import QObject, Signal
from PySide2.QtCore import QCoreApplication, QObject, Signal
MAX_FRAME_BYTES = 16 * 1024 * 1024
@@ -326,7 +326,8 @@ class _RequestDispatcher(QObject):
self._receipt_lock = threading.Lock()
self.current_file = self._read_current_file()
try:
self.cutter_version = str(cutter.core().getVersionInformation())
value = QCoreApplication.applicationVersion()
self.cutter_version = value if isinstance(value, str) and value else None
except Exception:
self.cutter_version = None
+41 -1
View File
@@ -24,6 +24,7 @@ def _install_host_stubs():
qtcore = types.ModuleType("PySide6.QtCore")
qtcore.QObject = type("QObject", (), {})
qtcore.Signal = lambda *_args: types.SimpleNamespace(connect=lambda _handler: None)
qtcore.QCoreApplication = types.SimpleNamespace(applicationVersion=lambda: "Cutter test host")
pyside = types.ModuleType("PySide6")
pyside.QtCore = qtcore
sys.modules["PySide6"] = pyside
@@ -229,7 +230,6 @@ class DocumentIdentityTest(unittest.TestCase):
configuration = {"file.path": first}
core = types.SimpleNamespace(
getConfig=configuration.__getitem__,
getVersionInformation=lambda: "backend fixture",
)
commands = []
with patch.object(bridge.cutter, "core", lambda: core, create=True), patch.object(
@@ -280,6 +280,46 @@ class DocumentIdentityTest(unittest.TestCase):
self.assertIsNone(bridge._RequestDispatcher._read_current_file())
class HostVersionTest(unittest.TestCase):
def test_host_version_reaches_status_and_command_without_backend_attribution(self):
backend_reads = []
core = types.SimpleNamespace(
getConfig=lambda _key: None,
getVersionInformation=lambda: backend_reads.append(True) or "Rizin 0.9.1",
)
application = sys.modules["PySide6.QtCore"].QCoreApplication
with patch.object(bridge.cutter, "core", lambda: core, create=True), patch.object(
application, "applicationVersion", return_value="2.5.0-HEAD-fixture"
), patch.object(bridge.cutter, "cmd", lambda _command: "observed", create=True):
dispatcher = bridge._RequestDispatcher()
self.assertEqual(dispatcher.cutter_version, "2.5.0-HEAD-fixture")
for values in [
{"kind": "status"},
{"kind": "command", "request_id": str(uuid.uuid4()), "expected_generation": 0, "command": "iI"},
]:
request = {**values, "_event": threading.Event(), "_cancelled": threading.Event()}
reply = {}
dispatcher._handle(request, reply)
self.assertTrue(request["_event"].is_set())
self.assertEqual(reply["cutter_version"], "2.5.0-HEAD-fixture")
self.assertEqual(backend_reads, [])
def test_unavailable_host_version_stays_unknown_without_backend_fallback(self):
core = types.SimpleNamespace(
getConfig=lambda _key: None,
getVersionInformation=lambda: "Rizin 0.9.1",
)
application = sys.modules["PySide6.QtCore"].QCoreApplication
with patch.object(bridge.cutter, "core", lambda: core, create=True):
for value in ["", None, object()]:
with self.subTest(value=type(value).__name__), patch.object(
application, "applicationVersion", return_value=value
):
self.assertIsNone(bridge._RequestDispatcher().cutter_version)
with patch.object(application, "applicationVersion", side_effect=RuntimeError("host unavailable")):
self.assertIsNone(bridge._RequestDispatcher().cutter_version)
class ProtocolProducerTest(unittest.TestCase):
def test_rejects_legacy_peers_before_ui_dispatch(self):
with socket.create_connection(("127.0.0.1", self.server.port), timeout=2) as connection: