Some checks failed
Windows project source guards / test (push) Failing after 33s
Корень ошибки из диагностики 31.10.2026 (0.8.51, Hysteria2 без UDP из МГТС) — в механизме, а не в одном месте: 1. «Какой сервер» имел три протокола: родной пул писал selected_node_id до горячего свитча, Hysteria — до readiness нового sidecar, Amnezia держала выбор в pending. После отказа прежний runtime работал, а выбранным и сохранённым оставался отказавший сервер. 2. Каждый шаг перехода сам писал ошибку в статус — даже вытесненный новым кликом («Не удалось запустить sing-box proxy runtime»), а поздний общий текст затирал точную причину; фаза «error» ставилась при живом соединении. Новые модули: - application/server_selection.py — единственная транзакция выбора для всех протоколов и видов перехода: при живом подключении новый сервер — только цель (pending), выбранным становится в commit() готового runtime; неудача её сбрасывает (откатывать нечего), без подключения выбор фиксируется сразу. - application/transition_outcome.py — шаги сообщают причину (_fail_transition), сильнейшая побеждает (SECURITY > CAUSE > CORE > STEP), публикует один раз _on_transition_runner_finished; вытесненный переход молчит; фаза выводится из того, что работает («Подключено» + «Соединение осталось на «…»»). - diagnostics/transport_failure.py (из предыдущего коммита) даёт причину CAUSE. - signature_service считает желаемый runtime по цели, а не по выбранному. E2E tests/test_app_e2e_hysteria_udp_switch.py на win10: настоящие sing-box и hysteria, рабочий сервер и UDP-«чёрная дыра». v0.8.51: 3/3 FAIL (выбран отказавший сервер; ложная ошибка sing-box; нет причины). Новый код: 3/3 OK. Полный набор Windows: 1831 OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
"""Attach the real selection transaction to fake controllers (not a test module)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import Mock
|
|
|
|
from xray_fluent.application.controller import AppController
|
|
from xray_fluent.application.server_selection import ServerSelection
|
|
|
|
|
|
class SelectionMock(Mock):
|
|
"""``Mock`` whose ``_pending_transport_node_id`` is the real selection's target."""
|
|
|
|
@property
|
|
def _pending_transport_node_id(self):
|
|
return self.selection.pending_id
|
|
|
|
@_pending_transport_node_id.setter
|
|
def _pending_transport_node_id(self, node_id):
|
|
self.selection.pending_id = node_id
|
|
|
|
|
|
def attach_selection(controller: Any, *, bind_methods: bool = False) -> Any:
|
|
"""Give a fake controller the production ServerSelection over its state."""
|
|
|
|
def find(node_id):
|
|
return next((node for node in controller.state.nodes if node.id == node_id), None)
|
|
|
|
controller.selection = ServerSelection(
|
|
lambda: controller.state,
|
|
find_node=find,
|
|
on_changed=lambda node: controller.selection_changed.emit(node),
|
|
save=lambda: controller.schedule_save(),
|
|
log=lambda line: controller._log(line),
|
|
)
|
|
if bind_methods:
|
|
for name in (
|
|
"_commit_pending_transport_selection",
|
|
"_clear_pending_transport_selection",
|
|
"_settle_pending_transport_selection",
|
|
):
|
|
method = getattr(AppController, name)
|
|
setattr(controller, name, Mock(side_effect=lambda *a, _m=method, **k: _m(controller, *a, **k)))
|
|
return controller
|