Dismiss WiFi boot prompt when mobile session opens

This commit is contained in:
2026-10-09 12:26:55 +08:00
parent a277da0f96
commit c0e26b97fe
16 changed files with 250 additions and 18 deletions
@@ -1,4 +1,5 @@
import base64
import threading
from io import BytesIO
from pathlib import Path
@@ -1160,6 +1161,13 @@ def test_wifi_settings_api_reveals_password_only_on_dedicated_get(tmp_path):
"control_url": "http://192.168.9.93:8080/",
},
)
startup_activation = threading.Event()
activate = backend.activate
def record_activation(*args, **kwargs):
result = activate(*args, **kwargs)
startup_activation.set()
return result
backend.activate = record_activation
app = create_app(
project_root=tmp_path,
driver_kind="mock",
@@ -1174,6 +1182,7 @@ def test_wifi_settings_api_reveals_password_only_on_dedicated_get(tmp_path):
assert initial.json()["active"]["control_url"] == "http://192.168.9.93:8080/"
status_network = client.get("/api/status").json()["network"]
assert "password" not in status_network["saved"]
assert startup_activation.wait(5), "boot network activation did not complete"
startup_activation_count = len(backend.activations)
payload = {
@@ -4,9 +4,11 @@ from app.mobile.protocol import Handshake, ProtocolError, fragments
from app.mobile.session import SessionManager
def connection(clock=lambda: 0):
def connection(clock=lambda: 0, *, manager=None, on_session_open=None):
calls = []
manager = SessionManager(lambda method, params: calls.append(method) or {}, lambda: {"device_id": "test"}, clock=clock)
if manager is None:
manager = SessionManager(lambda method, params: calls.append(method) or {}, lambda: {"device_id": "test"},
clock=clock, on_session_open=on_session_open)
assert manager.connect('a')
assert not manager.connect('b')
client = Handshake()
@@ -69,6 +71,42 @@ def test_negotiated_receive_mtu_resets_on_disconnect():
@pytest.mark.parametrize('mtu', [0, 22, 518, True, '247'])
def test_invalid_receive_mtu_never_opens_session(mtu):
manager, cipher, _ = connection()
opened = []
manager, cipher, _ = connection(on_session_open=lambda: opened.append(True))
assert not request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机', 'receive_mtu': mtu})['ok']
assert not manager.opened
assert opened == []
def test_open_notification_only_after_valid_encrypted_session():
opened = []
manager, cipher, _ = connection(on_session_open=lambda: opened.append(manager.status()['connected']))
assert opened == [] # Neither the link nor the handshake is a session.
assert not request(manager, cipher, 1, '1', 'session.open', {'client_name': ''})['ok']
assert opened == []
assert request(manager, cipher, 2, '2', 'session.open', {'client_name': '手机'})['ok']
assert opened == [True]
assert not request(manager, cipher, 3, '3', 'session.open', {'client_name': '手机'})['ok']
request(manager, cipher, 4, '4', 'session.ping')
manager.disconnect('a')
assert opened == [True]
def test_failed_handshake_never_notifies():
opened = []
manager = SessionManager(lambda *args: {}, lambda: {}, on_session_open=lambda: opened.append(True))
assert manager.connect('a')
with pytest.raises(ProtocolError):
for part in fragments(1, 0, b'invalid-handshake', 247):
manager.accept('a', part)
assert not manager.opened
assert opened == []
def test_notification_failure_does_not_break_open_session(caplog):
def failed_notification():
raise OSError('test notification failure')
manager, cipher, _ = connection(on_session_open=failed_notification)
assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})['ok']
assert request(manager, cipher, 2, '2', 'session.ping')['ok']
assert 'session-open notification failed' in caplog.text
+76 -1
View File
@@ -1,8 +1,11 @@
import json
import time
import threading
from pathlib import Path
import pytest
from PIL import Image
from fastapi.testclient import TestClient
from app.config.store import ConfigStore
from app.display.service import DisplayService
@@ -10,6 +13,8 @@ from app.display.wifi_indicator import render_wifi_indicator
from app.network.manager import MockNetworkManager, NmcliNetworkManager
from app.network.service import WifiNetworkService, validate_wifi_settings
from app.network.store import WifiConfigError, WifiConfigStore
from app.main import create_app
from test_mobile_session import connection, request
@pytest.mark.parametrize('raw_dns', ['192.0.2.1,192.0.2.2', '192.0.2.1\n192.0.2.2', '192.0.2.1, 192.0.2.2\n192.0.2.1'])
@@ -287,9 +292,79 @@ def test_dismissed_network_prompt_does_not_reappear_after_disconnect(tmp_path):
ipv4_address=None,
control_url=None,
)
time.sleep(0.02)
service._refresh_indicator()
assert wifi_store.session["dismissed"] is True
assert display.get_status()["state"]["wifi_indicator_active"] is False
finally:
service.close()
display.close()
@pytest.mark.parametrize('prompt_state', ['before_delay', 'connected', 'disconnected'])
def test_mobile_open_dismisses_prompt_and_preserves_user_frame(tmp_path, prompt_state):
backend = MockNetworkManager(saved_profile(), active_profile())
app = create_app(project_root=tmp_path, driver_kind='mock', startup_boot_id='boot-a', network_backend=backend)
network = app.state.network_service
display = app.state.display_service
try:
image = Image.new('RGB', (64, 64), (12, 34, 56))
display.show_image(image, notify_activity=False)
before = display.get_status()
config_before = display.config_store.config
assert not TestClient(app).get('/api/status').json()['network']['prompt']['dismissed_for_boot']
if prompt_state != 'before_delay':
backend.active = active_profile(connected=prompt_state == 'connected')
network._refresh_indicator()
assert display.get_status()['state']['wifi_indicator_active']
manager, cipher, _ = connection(manager=app.state.mobile_sessions)
assert not network.store.session['dismissed']
assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})['ok']
manager.disconnect('a')
for connected in [False, True, False]:
backend.active = active_profile(connected=connected)
network._refresh_indicator()
assert not display.get_status()['state']['wifi_indicator_active']
assert network.get_cached_status()['prompt']['dismissed_for_boot']
assert display.get_current_frame().tobytes() == image.tobytes()
after = display.get_status()
for key in ['revision', 'brightness', 'orientation', 'mode', 'current_content']:
assert after['state'][key] == before['state'][key]
assert display.config_store.config == config_before
same = WifiConfigStore(network.store.path.parent, network.store.session_path.parent, boot_id='boot-a')
assert same.session['dismissed']
next_boot = WifiConfigStore(network.store.path.parent, network.store.session_path.parent, boot_id='boot-b')
assert not next_boot.session['dismissed']
finally:
network.close()
display.close()
@pytest.mark.parametrize('connected', [False, True])
def test_late_network_read_cannot_restore_dismissed_prompt(tmp_path, connected):
config = ConfigStore(project_root=tmp_path)
display = DisplayService(config, driver_kind='mock', startup_boot_id='boot')
backend = MockNetworkManager(saved_profile(), active_profile(connected=connected))
store = WifiConfigStore(config.data_dir, config.runtime_dir, boot_id='boot')
service = WifiNetworkService(store, backend, display)
entered, release = threading.Event(), threading.Event()
read_active = backend.read_active
def delayed_read():
entered.set()
assert release.wait(5)
return read_active()
backend.read_active = delayed_read
thread = threading.Thread(target=service._refresh_indicator)
try:
thread.start()
assert entered.wait(5)
service.dismiss_for_boot()
release.set()
thread.join(5)
assert not thread.is_alive()
assert store.session['dismissed']
assert not display.get_status()['state']['wifi_indicator_active']
finally:
release.set()
thread.join(5)
service.close()
display.close()