同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ def test_status_config_and_display_api(tmp_path):
"effective": False,
"current_governors": {},
"restore_governors": {},
"last_error": None,
"last_error": "当前内核未提供 CPU 调频策略;请检查候选内核是否回退",
}
assert status.json()["state"]["display_test"] == {
"active": False,
@@ -49,6 +49,13 @@ def test_check_profile_accepts_walnutpi_contract(tmp_path):
]
def test_bluetooth_is_not_a_display_startup_blocker(tmp_path):
def runner(args):
assert not any('bluetooth' in part for part in args)
return healthy_runner(args)
assert all(check.ok for check in check_profile(make_paths(tmp_path), runner=runner))
def test_service_preflight_does_not_require_default_route(tmp_path):
def runner(args):
assert tuple(args) != ("ip", "route", "show", "default")
@@ -1,7 +1,14 @@
import hashlib
from pathlib import Path
import subprocess
import pytest
from app.system import kernel_recovery as kr
from app.ota.manager import OtaManager
from app.ota.state import write_json, write_last_result
from app.ota.versioning import SoftwareVersion
from scripts.ota_worker import UpdateFailed, verify_kernel_capability_retained
from scripts.axp313a_kernel_health import (
HealthError,
check_cpufreq,
@@ -11,6 +18,159 @@ from scripts.axp313a_kernel_health import (
from scripts.render_dual_kernel_boot import BootScriptError, render
def make_recovery(tmp_path: Path, monkeypatch) -> kr.KernelRecovery:
host = tmp_path / "host"
data = tmp_path / "data"
boot = host / "boot"
boot.mkdir(parents=True)
files = {
"Image-matrix-axp313a1": b"candidate-image",
"sun50i-h616-walnutpi-1b-matrix-axp313a1.dtb": b"candidate-sd-dtb",
"sun50i-h616-walnutpi-1b-emmc-matrix-axp313a1.dtb": b"candidate-emmc-dtb",
}
monkeypatch.setattr(kr, "BOOT_HASHES", {name: hashlib.sha256(value).hexdigest() for name, value in files.items()})
for name, value in files.items():
(boot / name).write_bytes(value)
for name in ("Image", "sun50i-h616-walnutpi-1b.dtb", "sun50i-h616-walnutpi-1b-emmc.dtb",
"boot.cmd.matrix-original", "boot.scr.matrix-original", "matrix-original.SHA256SUMS"):
(boot / name).write_bytes(b"original")
(boot / "boot.cmd").write_text(
"matrix-kernel-good matrix-kernel-good matrix_kernel_candidate=1",
encoding="utf-8",
)
(boot / "boot.scr").write_bytes(b"matrix_kernel_candidate=1 Image-matrix-axp313a1")
health = host / "etc/systemd/system/matrix-axp313a-health.service"
health.parent.mkdir(parents=True)
health.write_text("ConditionKernelCommandLine=matrix_kernel_candidate=1", encoding="utf-8")
script = host / "opt/matrix-screen-controller-system/axp313a_kernel_health.py"
script.parent.mkdir(parents=True)
script.write_text("health", encoding="utf-8")
enabled = host / "etc/systemd/system/multi-user.target.wants/matrix-axp313a-health.service"
enabled.parent.mkdir(parents=True)
enabled.write_text("enabled", encoding="utf-8")
modules = host / "lib/modules" / kr.CANDIDATE_RELEASE
(modules / "kernel").mkdir(parents=True)
(modules / "modules.dep").write_text("index", encoding="ascii")
module = modules / "kernel/example.ko"
module.write_bytes(b"module")
digest = hashlib.sha256(b"kernel/example.ko\0" + hashlib.sha256(b"module").digest()).hexdigest()
monkeypatch.setattr(kr, "MODULE_COUNT", 1)
monkeypatch.setattr(kr, "MODULE_DIGEST", digest)
release = host / "proc/sys/kernel/osrelease"
release.parent.mkdir(parents=True)
release.write_text("6.1.31", encoding="ascii")
boot_id = host / "proc/sys/kernel/random/boot_id"
boot_id.parent.mkdir(parents=True)
boot_id.write_text("old-boot", encoding="ascii")
return kr.KernelRecovery(data, host_root=host)
def test_candidate_validation_and_single_recovery_attempt(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.verify_candidate() is None
assert recovery.prepare_attempt("1.2.0:time") is True
assert recovery.prepare_attempt("1.2.0:time") is False
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args, 1))
recovery.arm_and_reboot(reboot=False)
assert recovery.status()["state"] == "pending"
with pytest.raises(RuntimeError, match="no unused"):
recovery.arm_and_reboot(reboot=False)
recovery.host("/proc/sys/kernel/random/boot_id").write_text("new-boot", encoding="ascii")
recovery.host("/proc/sys/kernel/osrelease").write_text(kr.CANDIDATE_RELEASE, encoding="ascii")
policy = recovery.host("/sys/devices/system/cpu/cpufreq/policy0")
policy.mkdir(parents=True)
(policy / "scaling_governor").write_text("schedutil", encoding="ascii")
(policy / "scaling_available_governors").write_text("schedutil performance", encoding="ascii")
recovery.finalize_after_boot(wait_seconds=1)
assert recovery.status()["state"] == "succeeded"
def test_recovery_failure_and_damaged_candidate_do_not_retry(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
recovery.host("/boot/Image-matrix-axp313a1").write_bytes(b"damaged")
assert recovery.prepare_attempt("1.2.0:time") is False
assert recovery.status()["state"] == "requires_package"
assert recovery.prepare_attempt("1.2.1:later") is False
other = make_recovery(tmp_path / "other", monkeypatch)
assert other.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args, 1))
other.arm_and_reboot(reboot=False)
other.host("/proc/sys/kernel/random/boot_id").write_text("new-boot", encoding="ascii")
other.finalize_after_boot(wait_seconds=1)
assert other.status()["state"] == "failed"
assert other.prepare_attempt("1.2.1:later") is False
def test_scheduled_recovery_that_never_runs_becomes_persistent_failure(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.time, "time", lambda: recovery.read_record()["recorded_at"] + 181)
assert recovery.status()["state"] == "failed"
assert recovery.read_record()["state"] == "failed"
assert recovery.prepare_attempt("1.2.1:later") is False
def test_delayed_recovery_task_does_not_reboot(tmp_path, monkeypatch):
recovery = make_recovery(tmp_path, monkeypatch)
assert recovery.prepare_attempt("1.2.0:time") is True
monkeypatch.setattr(kr.time, "time", lambda: recovery.read_record()["recorded_at"] + 181)
monkeypatch.setattr(kr.subprocess, "run", lambda *args, **kwargs: pytest.fail("late task ran a command"))
recovery.arm_and_reboot()
assert recovery.read_record()["state"] == "failed"
def test_ota_rejects_new_cpufreq_loss_and_keeps_preexisting_degradation():
before = {"release": kr.CANDIDATE_RELEASE, "available": True, "marker": True,
"policies": {"policy0": {"current": "schedutil", "performance": True}}}
after = {**before, "policies": {}, "available": False}
off = {"requested": False, "available": True, "effective": False,
"current_governors": {"policy0": "schedutil"}}
with pytest.raises(UpdateFailed, match="lost the validated"):
verify_kernel_capability_retained(before, after, off, off)
verify_kernel_capability_retained({**before, "available": False}, after, off, off)
with pytest.raises(UpdateFailed, match="governors"):
verify_kernel_capability_retained(before, before, off, {**off, "current_governors": {"policy0": "performance"}})
def test_legacy_worker_completion_schedules_lightweight_recovery(tmp_path, monkeypatch):
class Display:
def start_ota_indicator(self, _path):
pass
def stop_ota_indicator(self):
pass
class Recovery:
def __init__(self):
self.attempts = []
def status(self):
return {"state": "ready", "reason": ""}
def prepare_attempt(self, result_id):
self.attempts.append(result_id)
return len(self.attempts) == 1
recovery = Recovery()
manager = OtaManager(
code_root=tmp_path / "code", data_root=tmp_path / "data", runtime_root=tmp_path / "run",
software_version=SoftwareVersion.parse("1.2.0"), display=Display(),
)
monkeypatch.setattr(manager, "_production_recovery", lambda: recovery)
calls = []
monkeypatch.setattr("app.ota.manager.subprocess.run", lambda command, **kwargs: calls.append(command))
write_json(manager.status_path, {"schema_version": 1, "active": True, "job": {"id": "old-worker"}})
assert manager.resume_or_start_monitor(manager.schedule_kernel_recovery_after_ota)
write_last_result(manager.data_root, {"status": "success", "target_version": "1.2.0",
"installed_at": "2026-09-26T08:00:00Z"})
write_json(manager.status_path, {"schema_version": 1, "active": False, "job": {"id": "old-worker"}})
manager._monitor_thread.join(timeout=3)
assert recovery.attempts == ["1.2.0:2026-09-26T08:00:00Z"]
assert len(calls) == 1
assert "systemd-run" in calls[0]
BOOT_SOURCE = '''# DO NOT EDIT THIS FILE
setenv docker_optimizations "on"
setenv bootargs "root=/dev/mmcblk1p2"
@@ -0,0 +1,167 @@
import pytest
from fastapi.testclient import TestClient
from app.main import create_app
from app.mobile.session import RpcError
def test_identity_persistence_and_rename(tmp_path):
app = create_app(project_root=tmp_path, driver_kind='mock')
mobile = app.state.mobile_control
before = mobile.identity()
renamed = mobile.dispatch('device.rename', {'name': '客厅小屏幕'})
assert renamed['device_id'] == before['device_id']
assert renamed['device_name'] == '客厅小屏幕'
other = create_app(project_root=tmp_path, driver_kind='mock')
assert other.state.mobile_control.identity() == renamed
def test_web_change_invalidates_mobile_revision(tmp_path):
app = create_app(project_root=tmp_path, driver_kind='mock')
mobile = app.state.mobile_control
settings = mobile.dispatch('settings.get', {})
client = TestClient(app)
assert client.put('/api/config', json={'brightness': 37}).status_code == 200
with pytest.raises(RpcError, match='CONFLICT'):
mobile.dispatch('settings.patch', {'expected_revision': settings['revision'], 'changes': {'brightness': 20}})
revision = mobile.settings()['revision']
mobile.dispatch('settings.patch', {'expected_revision': revision, 'changes': {'brightness': 25}})
assert client.get('/api/config').json()['brightness'] == 25
def test_library_and_frame_readback(tmp_path):
app = create_app(project_root=tmp_path, driver_kind='mock')
mobile = app.state.mobile_control
first = mobile.dispatch('library.list', {'limit': 1})
assert len(first['items']) == 1 and first['next_cursor']
second = mobile.dispatch('library.list', {'cursor': first['next_cursor'], 'limit': 1})
assert second['items'][0]['id'] != first['items'][0]['id']
item = first['items'][0]
assert mobile.dispatch('library.thumbnail', item)['mime'] == 'image/png'
mobile.dispatch('content.play', item)
frame = mobile.dispatch('frame.get', {})
assert mobile.dispatch('frame.get', {'known_revision': frame['frame_revision']})['unchanged']
with pytest.raises(RpcError, match='CONFLICT'):
mobile.dispatch('content.play', {**item, 'revision': 'old'})
def test_forbidden_operation_and_settings_fields(tmp_path):
mobile = create_app(project_root=tmp_path, driver_kind='mock').state.mobile_control
with pytest.raises(RpcError, match='UNSUPPORTED_CAPABILITY'):
mobile.dispatch('shell.run', {'command': 'anything'})
with pytest.raises(RpcError, match='BAD_REQUEST'):
mobile.dispatch('settings.patch', {'expected_revision': mobile.settings()['revision'], 'changes': {'low_voltage_protection_enabled': False}})
def test_wifi_open_network_and_password_not_returned(tmp_path):
mobile = create_app(project_root=tmp_path, driver_kind='mock').state.mobile_control
revision = mobile.dispatch('wifi.get', {})['revision']
result = mobile.dispatch('wifi.set', dict(expected_revision=revision, ssid='Test Open', security='open',
password_action='none', ipv4_mode='dhcp', dns_servers=[], activation='next_boot'))
assert mobile.dispatch('task.get', {'task_id': result['task_id']})['state'] == 'succeeded'
wifi = mobile.dispatch('wifi.get', {})
assert 'password' not in wifi['saved']
assert not wifi['saved']['password_configured']
assert wifi['saved']['security'] == 'open'
assert wifi['revision'] != revision
mobile.dispatch('wifi.set', dict(expected_revision=wifi['revision'], ssid='Test Private', security='wpa-psk',
password_action='replace', password='public-test-fixture', ipv4_mode='dhcp', activation='next_boot'))
wifi = mobile.dispatch('wifi.get', {})
assert 'password' not in wifi['saved'] and wifi['saved']['password_configured']
def test_scan_escaped_ssid_deduplicates_and_marks_unsupported():
from app.network.manager import NmcliNetworkManager
class Stub(NmcliNetworkManager):
def _run(self, args, *, timeout=10):
assert timeout == 15 and '--rescan' in args
return '*:Test\\:WiFi:WPA2:42\n:Test\\:WiFi:WPA2:81\n:Open:--:37\n:EAP:WPA2 802.1X:80'
rows = Stub().scan()
assert rows[0] == dict(ssid='Test:WiFi', security='wpa-psk', signal_percent=81, connected=True)
assert rows[1]['security'] == 'unsupported'
assert rows[2]['security'] == 'open'
def test_mobile_default_and_animation_controls_share_web_state(tmp_path):
app = create_app(project_root=tmp_path, driver_kind='mock')
with TestClient(app) as client:
mobile = app.state.mobile_control
items = mobile.dispatch('library.list', {})['items']
animation = next(item for item in items if item['type'] == 'animation' and item['playable'])
mobile.dispatch('content.default.set', animation)
assert mobile.dispatch('content.default.get', {})['id'] == animation['id']
assert client.get('/api/display/default-content').json()['id'] == animation['id']
playback = client.get('/api/status').json()['state']['animation_playback']
controlled = mobile.dispatch('playback.patch', dict(
session_id=playback['session_id'], paused=True, position_ms=0, speed=1.5))
assert controlled['animation_playback']['paused']
current = client.get('/api/status').json()['state']['animation_playback']
assert current['position_ms'] == 0 and current['speed'] == 1.5
mobile.dispatch('content.play', animation)
with pytest.raises(RpcError, match='CONFLICT'):
mobile.dispatch('playback.patch', dict(session_id=playback['session_id'], paused=False))
def test_mobile_library_uses_persistent_mixed_order_and_invalidates_cursor(tmp_path):
from app.demo_library import demo_template
app = create_app(project_root=tmp_path, driver_kind='mock')
mobile = app.state.mobile_control
templates, animations = mobile.templates, mobile.animations
import base64
t = templates.create('user-template', {'version': 1, 'width': 64, 'height': 64, 'pixelRgb': base64.b64encode(bytes(64*64*3)).decode('ascii'), 'elements': []})
# Empty animations are valid library items, even though not playable.
a = animations.create('user-animation')
defaults = [{'type': 'template', 'id': t['id']}, {'type': 'animation', 'id': a['id']}]
store = app.state.library_order_store
current = store.get(defaults)
first = mobile.dispatch('library.list', {'limit': 1})
order = list(reversed(defaults))
store.update(order, default_items=defaults, expected_revision=current['revision'])
items = mobile.dispatch('library.list', {})['items']
assert [{'type': i['type'], 'id': i['id']} for i in items if not i['is_demo']] == order
with pytest.raises(RpcError, match='CONFLICT'):
mobile.dispatch('library.list', {'cursor': first['next_cursor']})
@pytest.mark.parametrize('reason,expected', [(53, 'NETWORK_UNAVAILABLE'), (5, 'IP_CONFIG_FAILED'), (11, 'UNKNOWN'), (7, 'UNKNOWN')])
def test_wifi_failure_classification_does_not_guess_authentication(monkeypatch, reason, expected):
import subprocess
from app.network.manager import NmcliNetworkManager, NetworkManagerError
class Backend(NmcliNetworkManager):
def _run(self, args, *, timeout=10):
if 'up' in args:
raise NetworkManagerError('sanitized')
return str(reason) + ' (reason)'
monkeypatch.setattr(subprocess, 'run', lambda *a, **k: subprocess.CompletedProcess(a, 0, '', ''))
with pytest.raises(NetworkManagerError) as error:
Backend().activate('public-test-id')
assert error.value.error_code == expected
def test_wifi_auth_requires_explicit_wrong_key_event(monkeypatch):
import subprocess
from app.network.manager import NmcliNetworkManager, NetworkManagerError
class Backend(NmcliNetworkManager):
def _run(self, args, *, timeout=10):
if 'up' in args: raise NetworkManagerError('sanitized')
return '7 (no-secrets)'
monkeypatch.setattr(subprocess, 'run', lambda *a, **k: subprocess.CompletedProcess(a, 0,
'wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=0 ssid="fixture" auth_failures=1 duration=10 reason=WRONG_KEY', ''))
with pytest.raises(NetworkManagerError) as error:
Backend().activate('public-test-id')
assert error.value.error_code == 'AUTH_FAILED'
assert str(error.value) == 'WiFi activation failed'
@pytest.mark.parametrize('failure', ['AUTH_FAILED', 'NETWORK_UNAVAILABLE', 'IP_CONFIG_FAILED', 'UNKNOWN'])
def test_task_returns_only_sanitized_failure_code(tmp_path, failure):
from app.network.manager import NetworkManagerError
mobile = create_app(project_root=tmp_path, driver_kind='mock').state.mobile_control
def fail(*args, **kwargs):
raise NetworkManagerError('private backend detail must not escape', error_code=failure)
mobile.dispatch('wifi.set', dict(expected_revision=mobile.wifi()['revision'], ssid='Test Open', security='open', password_action='none', ipv4_mode='dhcp', dns_servers=[], activation='next_boot'))
mobile.network.backend.activate = fail
task = 'public-test-task'
mobile._activate(task)
result = mobile.dispatch('task.get', {'task_id': task})
assert result == {'state': 'failed', 'stage': 'finished', 'error_code': failure}
assert 'private' not in str(result)
@@ -0,0 +1,88 @@
import json
from pathlib import Path
import pytest
from app.mobile.protocol import Handshake, ProtocolError, Reassembler, fragments, json_object
from cryptography.hazmat.primitives.asymmetric import ec
@pytest.mark.parametrize("mtu", [23, 64, 247, 517])
def test_fragments_roundtrip_and_replay(mtu):
data = ("奇妙小屏幕" * 200).encode()
parts = list(fragments(3, 0, data, mtu))
receiver = Reassembler()
for part in parts[:-1]:
assert receiver.accept(part) is None
assert receiver.accept(parts[-1]) == (3, data)
with pytest.raises(ProtocolError):
receiver.accept(parts[0])
def test_fragment_reordering_and_deadline():
parts = list(fragments(1, 0, b"hello world", 23))
with pytest.raises(ProtocolError):
Reassembler().accept(parts[1])
now = [0.0]
receiver = Reassembler(lambda: now[0])
receiver.accept(parts[0])
now[0] = 15
with pytest.raises(ProtocolError, match="TIMEOUT"):
receiver.accept(parts[1])
def pair():
client, server = Handshake(), Handshake()
return client.finish(server.hello, server=False), server.finish(client.hello, server=True)
def test_bidirectional_encryption_and_replay():
client, server = pair()
wire = client.encrypt('你好'.encode())
assert server.decrypt(wire) == '你好'.encode()
assert client.decrypt(server.encrypt(b'reply')) == b'reply'
with pytest.raises(ProtocolError):
server.decrypt(wire)
with pytest.raises(ProtocolError):
server.decrypt(client.encrypt(b'next'))
def test_tamper_direction_and_connection_isolation():
client, server = pair()
wire = client.encrypt(b'command')
with pytest.raises(ProtocolError):
client.decrypt(wire)
with pytest.raises(ProtocolError):
server.decrypt(wire[:-1] + bytes([wire[-1] ^ 1]))
_, other = pair()
with pytest.raises(ProtocolError):
other.decrypt(wire)
def test_invalid_hello_and_json():
handshake = Handshake()
hello = json.loads(handshake.hello)
hello['protocol_major'] = 2
with pytest.raises(ProtocolError, match='UNSUPPORTED_VERSION'):
handshake.finish(json.dumps(hello).encode(), server=True)
for raw in (b'{"a":1,"a":2}', b'{"a":NaN}', b'[]', b'\xff'):
with pytest.raises(ProtocolError):
json_object(raw)
def test_shared_golden_vector():
path = Path(__file__).resolve().parents[2] / '移动端相关内容/安卓app/安卓程序源代码/sharedCore/src/jvmTest/resources/protocol-v1.json'
vector = json.loads(path.read_text(encoding='utf-8'))
def raw(key):
return bytes.fromhex(vector[key])
client = Handshake(private_key=ec.derive_private_key(int(vector['client_scalar']), ec.SECP256R1()), random_bytes=raw('client_random'))
server = Handshake(private_key=ec.derive_private_key(int(vector['server_scalar']), ec.SECP256R1()), random_bytes=raw('server_random'))
assert client.hello == raw('client_hello')
assert server.hello == raw('server_hello')
cc, sc = client.finish(server.hello, server=False), server.finish(client.hello, server=True)
assert cc.transcript == raw('transcript')
assert cc.encrypt(raw('request')) == raw('client_record')
assert sc.encrypt(raw('response')) == raw('server_record')
assert sc.decrypt(raw('client_record')) == raw('request')
assert cc.decrypt(raw('server_record')) == raw('response')
assert [p.hex() for p in fragments(3, 0, raw('client_record'), 23)] == vector['fragments_mtu23']
@@ -0,0 +1,74 @@
import json
import pytest
from app.mobile.protocol import Handshake, ProtocolError, fragments
from app.mobile.session import SessionManager
def connection(clock=lambda: 0):
calls = []
manager = SessionManager(lambda method, params: calls.append(method) or {}, lambda: {"device_id": "test"}, clock=clock)
assert manager.connect('a')
assert not manager.connect('b')
client = Handshake()
result = None
for part in fragments(1, 0, client.hello, 247):
result = manager.accept('a', part)
cipher = client.finish(result[1], server=False)
return manager, cipher, calls
def request(manager, cipher, mid, rid, method, params=None):
raw = json.dumps(dict(id=rid, method=method, params=params or {})).encode()
for part in fragments(3, mid, cipher.encrypt(raw), 247):
result = manager.accept('a', part)
return json.loads(cipher.decrypt(result[1]))
def test_no_control_before_open():
manager, cipher, calls = connection()
with pytest.raises(ProtocolError):
request(manager, cipher, 1, '1', 'settings.patch')
assert calls == []
def test_single_owner_duplicate_and_disconnect():
manager, cipher, calls = connection()
assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})['ok']
assert manager.status()['client_name'] == '手机'
assert request(manager, cipher, 2, '2', 'content.play')['ok']
assert not request(manager, cipher, 3, '2', 'content.play')['ok']
assert calls == ['content.play']
manager.disconnect('b')
assert manager.status()['connected']
manager.disconnect('a')
assert not manager.status()['connected']
assert manager.connect('b')
def test_handshake_and_idle_deadlines():
now = [0]
manager, cipher, _ = connection(lambda: now[0])
now[0] = 10
assert manager.expired()
manager, cipher, _ = connection(lambda: now[0])
request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机'})
now[0] = 29
assert not manager.expired()
now[0] = 30
assert manager.expired()
def test_negotiated_receive_mtu_resets_on_disconnect():
manager, cipher, _ = connection()
assert manager.peer_mtu == 23
assert request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机', 'receive_mtu': 247})['ok']
assert manager.peer_mtu == 247
manager.disconnect('a')
assert manager.peer_mtu == 23
@pytest.mark.parametrize('mtu', [0, 22, 518, True, '247'])
def test_invalid_receive_mtu_never_opens_session(mtu):
manager, cipher, _ = connection()
assert not request(manager, cipher, 1, '1', 'session.open', {'client_name': '手机', 'receive_mtu': mtu})['ok']
assert not manager.opened
@@ -133,6 +133,31 @@ def test_failed_install_restores_system_bytes_and_running_state(host):
assert ['systemctl', 'start', c.FRP] in host
def test_mobile_rollback_restores_files_masks_and_running_state(host):
journal, record = make_journal()
record['mobile_services'] = {
'aw859-bluetooth.service': {'enabled_state': 'masked', 'active': False},
'bluetooth.service': {'enabled_state': 'enabled', 'active': True},
}
record['files']['bluetooth-original'] = None
for name in c.MOBILE_FILES:
c.FILES[name].write_bytes(b'changed-by-mobile-install')
c.restore_components(journal, record)
assert not c.FILES['bluetooth-original'].exists()
for name in set(c.MOBILE_FILES) - {'bluetooth-original'}:
assert c.FILES[name].read_bytes() == ('old-' + name).encode()
assert ['systemctl', 'mask', 'aw859-bluetooth.service'] in host
assert ['systemctl', 'enable', 'bluetooth.service'] in host
assert ['systemctl', 'start', 'bluetooth.service'] in host
assert ['systemctl', 'start', 'aw859-bluetooth.service'] not in host
def test_legacy_component_journal_does_not_touch_bluetooth(host):
journal, record = make_journal()
c.restore_components(journal, record)
assert not any(unit in args for args in host for unit in c.MOBILE_SERVICES)
def test_power_loss_between_data_renames_recovers_original(host):
journal, record = make_journal()
(c.DATA/'user-content').write_bytes(b'unchanged')
@@ -181,6 +206,35 @@ def test_normal_migration_does_not_touch_components(host):
assert host == []
def test_mobile_only_patch_enters_durable_component_transaction(host, monkeypatch):
from types import SimpleNamespace
source = c.RELEASES / '1.1.2-mobilepatch'
source.mkdir()
(source / 'VERSION').write_text('1.1.2', encoding='utf-8')
(source / 'scripts').mkdir()
(source / 'scripts/install_mobile_bluetooth.sh').write_text('# test fixture', encoding='utf-8')
candidate = c.DATA.with_name('matrix-screen-controller.ota.mobilepatch')
candidate.mkdir()
c.RUNTIME.mkdir(parents=True, exist_ok=True)
(c.RUNTIME / 'ota-request.json').write_text(json.dumps({
'job_id': 'mobilepatch', 'current_version': '1.1.1', 'target_version': '1.1.2'
}), encoding='utf-8')
monkeypatch.setattr(c.os, 'geteuid', lambda: 0, raising=False)
monkeypatch.setattr(c.os, 'uname', lambda: SimpleNamespace(machine='aarch64'), raising=False)
monkeypatch.setattr(c, 'check_installed', lambda *args: None)
monkeypatch.setattr(c, 'protect_runtime', lambda: None)
monkeypatch.setattr(c, 'mirror_permissions', lambda *args: None)
# Host test verifies transaction ordering; POSIX durability is covered by
# real Linux lifecycle checks, not Windows read-only descriptor fsync.
monkeypatch.setattr(c.os, 'fsync', lambda *args: None)
c.begin(source, candidate)
record = json.loads((c.DATA / c.JOURNAL / 'state.json').read_text(encoding='utf-8'))
assert set(record['mobile_services']) == set(c.MOBILE_SERVICES)
assert (candidate / c.JOURNAL / 'state.json').is_file()
assert ['/bin/sh', str(source / 'scripts/install_mobile_bluetooth.sh')] in host
assert not any('install_frpc_system.sh' in str(arg) for args in host for arg in args)
def release_project(tmp_path, monkeypatch):
from scripts import export_release as exporter
source = tmp_path / 'software'
@@ -114,7 +114,7 @@ def test_unavailable_cpufreq_is_reported_without_fake_success(tmp_path):
"effective": False,
"current_governors": {},
"restore_governors": {},
"last_error": None,
"last_error": "当前内核未提供 CPU 调频策略;请检查候选内核是否回退",
}
with pytest.raises(PerformanceModeError, match="unavailable"):
manager.transact(True, lambda _enabled: None)
@@ -1,6 +1,7 @@
from __future__ import annotations
import importlib.util
import json
from pathlib import Path, PurePosixPath
import pytest
@@ -110,3 +111,30 @@ def test_repository_contract_has_example_and_ignored_private_file():
assert hygiene.EXAMPLE_CREDENTIAL in paths
assert hygiene.PRIVATE_CREDENTIAL not in paths
assert hygiene._check_repository_contract(paths, staged=False) == []
def test_mobile_markers_are_checked_even_without_board_credentials(tmp_path, monkeypatch):
monkeypatch.setattr(hygiene, "WORKSPACE_ROOT", tmp_path)
private = tmp_path.joinpath(*hygiene.PRIVATE_MOBILE_REGISTRATION.parts)
private.parent.mkdir(parents=True)
private.write_text(json.dumps({"devices": [{"serial": "FAKE-SERIAL-PRIVATE-123"}]}), encoding="utf-8")
markers = hygiene._private_value_markers()
assert "FAKE-SERIAL-PRIVATE-123" in markers
assert hygiene._text_issues(PurePosixPath("docs/report.md"), "FAKE-SERIAL-PRIVATE-123", markers)
def test_corrupt_mobile_registration_fails_closed(tmp_path, monkeypatch):
monkeypatch.setattr(hygiene, "WORKSPACE_ROOT", tmp_path)
private = tmp_path.joinpath(*hygiene.PRIVATE_MOBILE_REGISTRATION.parts)
private.parent.mkdir(parents=True)
private.write_text("broken", encoding="utf-8")
with pytest.raises(hygiene.HygieneError, match="移动端真实登记损坏"):
hygiene._private_value_markers()
def test_mobile_private_and_signing_files_are_rejected():
findings = hygiene.audit_paths(
[hygiene.PRIVATE_MOBILE_REGISTRATION, PurePosixPath("keys/debug.keystore")],
scan_binary=False,
)
assert len(findings) == 2
+15 -1
View File
@@ -7,11 +7,25 @@ import pytest
from app.config.store import ConfigStore
from app.display.service import DisplayService
from app.display.wifi_indicator import render_wifi_indicator
from app.network.manager import MockNetworkManager
from app.network.manager import MockNetworkManager, NmcliNetworkManager
from app.network.service import WifiNetworkService, validate_wifi_settings
from app.network.store import WifiConfigError, WifiConfigStore
@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'])
def test_nmcli_multiple_dns_can_roundtrip_without_password(monkeypatch, raw_dns):
backend = NmcliNetworkManager()
values = {'802-11-wireless.ssid': 'test-network', 'ipv4.method': 'manual',
'ipv4.addresses': '192.0.2.20/24', 'ipv4.gateway': '192.0.2.1',
'ipv4.dns': raw_dns, '802-11-wireless-security.key-mgmt': 'wpa-psk'}
monkeypatch.setattr(backend, '_connection_value', lambda uuid, field, **kwargs: values.get(field, ''))
saved = backend.read_saved('test-profile')
assert saved['dns_servers'] == ['192.0.2.1', '192.0.2.2']
checked = validate_wifi_settings(saved, previous=saved)
assert checked['dns_servers'] == saved['dns_servers']
assert checked['password'] is None
UUID = "12345678-1234-5678-1234-567812345678"