Fix Linux route probe contract and expose K1 verification after Wi-Fi apply

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 13:46:01 +03:00
parent 06d216ac4a
commit e6521e855a
9 changed files with 135 additions and 11 deletions
@@ -88,6 +88,17 @@ test('Bluetooth failure before dispatch asks for discovery without blaming Wi-Fi
assert.equal(api.enrollmentBluetoothFailure(unknown),false);
assert.doesNotMatch(api.enrollmentNotice(unknown),/не были отправлены/);
});
test('applied and restored network states admit read-only verification without credentials',async()=>{
const f=fixture();
const applied={...initial,allowed_actions:['observe-configured-device-network'],connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',attempt_id:'previous',status:'failed',stage:'control-bootstrap-failed',phase:'network_applied',side_effect_status:'applied',public_error_code:'TypeError'}};
assert.equal(api.enrollmentAllowed(applied,'verify'),true);
assert.match(api.enrollmentNotice(applied),/Проверить состояние K1/);
assert.equal(api.enrollmentAllowed({...applied,connection_attempt:null},'verify'),true);
await api.enroll(f.transport,applied,'verify',{device_id:'synthetic-ble'},f.observer);
assert.equal(f.request().action,'verify');
assert.deepEqual(f.request().parameters,{device_id:'synthetic-ble'});
assert.equal(f.counts().posts,1);
});
test('new workflow content scrolls only the containing viewport without stealing focus',()=>{
const original={document:globalThis.document,window:globalThis.window,getComputedStyle:globalThis.getComputedStyle};
const calls=[];let reduced=false;
+1 -1
View File
@@ -11,7 +11,7 @@ import sys
from build_deb import build, VERSION, BRAND_SHA256
ROOT = Path(__file__).resolve().parents[1]
DG_COMMIT = "1bdfc6c24072d38cc1068086ea271c444f2524ad"
DG_COMMIT = "5b882bc3d9b13a86e6c26111ef5ded687ea3a2fc"
def guideline_sources():
+1 -1
View File
@@ -11,7 +11,7 @@ import sys
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.8.4"
VERSION = "0.8.5"
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
from debian import package
@@ -0,0 +1,61 @@
# Applied Wi-Fi and Linux control-path contract R7
The owner UI attempt on R6 confirmed K1 station Wi-Fi with an assigned private
address. The network operation succeeded; its owned read-only control bootstrap
then failed with TypeError. The terminal attempt is network_applied,
side_effect_status=applied, stage=control-bootstrap-failed. This differs from
the preceding pre-dispatch Bluetooth failure. A repeat provisioning write is
not the appropriate next action.
## Reproduced implementation defect
HostWifiAssociationProbe defines observe(interface_name, *, timeout_seconds).
The real service _sample_host_path calls that positional interface contract.
LinuxWifiAssociationProbe instead declared interface_name keyword-only.
Consequently every real Linux service path observation raised TypeError before
examining Ethernet/Wi-Fi or probing the K1 control endpoint. The earlier adapter
test called observe by keyword and therefore missed this consumer boundary.
A new test invokes the actual service host-path and correlated TCP observation
with the real Linux adapter and synthetic OS route/sysfs/TCP endpoints. Before
the fix it reproduces TypeError at facade.py:_sample_host_path. The Linux adapter
now accepts the existing protocol signature. Stable Ethernet reaches a single
TCP probe without nmcli or host association; a route change across that probe
still fails closed. The caller, route fingerprints, supervisor, Wi-Fi protocol,
application authority and MQTT/Rerun profiles are unchanged.
## Operator continuation and messages
The modal previously displayed read-only verification only for
network_outcome_unknown, leaving an applied network without its prescribed
next action. It now exposes Check K1 state whenever the backend permits that
read-only action for the explicitly selected device, including an applied or
restored configured network. No credentials are required and the existing
verification contract remains fenced by runtime/mode/discovery/target.
The ordinary check still submits one explicit provisioning intent; final
Connect still only completes a currently verified dialog.
Messages in the modal use SettingsCard align=center. The canonical Design
Guideline change is 5b882bc3d9b13a86e6c26111ef5ded687ea3a2fc, including registry,
documentation and a catalog example. Centered cards omit an empty body and
center their content on both axes; ordinary settings groups keep their existing
start layout. No local CSS override, fixed height, new colors or decoration.
Node packaging pins this exact DG source.
## Validation and pending physical acceptance
The failing reproduction is retained privately. After the fix: 18 NodeBridge /
package lifecycle checks passed, four applied-network bootstrap lifecycle checks
passed, 23 focused frontend/architecture checks passed and 794 full Core
frontend tests passed. Changed Linux/backend files pass Ruff. DG ui-react build,
catalog typecheck and registry validation passed. Production package and
installation results follow separately.
Node 0.8.5 and K1 0.1.4 are reserved for this source. The next owner UI test must
clear browser cache, select K1 and use read-only Check K1 state to observe the
already applied network. It must prove DeviceInfo/control readiness before
claiming Bridge or live acceptance. Private screenshot, exact operation,
UTC/monotonic manifest and hashed logs are retained under
private/acceptance/k1-node085-20260907-core-ui. No agent CLI BLE/MQTT commands
were sent. Publication of prior installation reports remains blocked by the
earlier automatic review decision; no remote push workaround is used.
@@ -115,14 +115,14 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}
<ResourceRow title="Общая сеть · Bridge" status={busy==='loading'?<ActivityIndicator label="Получаем состояние БК"/>:
<StatusBadge tone={ready?'success':'neutral'}>{ready?'БК доступен':'Подключение недоступно'}</StatusBadge>}/>
</SettingsCard>
{busy!=='loading'&&!ready?<SettingsCard title="Служба подключения устройств на БК недоступна"
{busy!=='loading'&&!ready?<SettingsCard align="center" title="Служба подключения устройств на БК недоступна"
description="Проверьте связь с бортовым компьютером и работу приложения на нём."/>:ready&&<>
<SettingsCard eyebrow="Шаг 01" title="Bluetooth" description="Найдите K1 рядом с бортовым компьютером и выберите его из списка." actions={
<Button disabled={pending||!enrollmentAllowed(state,'scan')} aria-busy={busy==='scan'}
icon={busy==='scan'?<ActivityIndicator size="compact"/>:undefined}
onClick={()=>void run('scan')}>{busy==='scan'?'Ищем K1':'Найти K1'}</Button>}>
<div data-enrollment-reveal="devices">
{scanned&&!state?.candidates?.length&&<SettingsCard title="K1 не найден"
{scanned&&!state?.candidates?.length&&<SettingsCard align="center" title="K1 не найден"
description="Проверьте питание K1 и Bluetooth на бортовом компьютере, затем повторите поиск."/>}
{!!state?.candidates?.length&&<ResourceList aria-label="Найденные устройства K1">
{state.candidates.map(value=><li key={value.id}><ResourceRow title={value.name}
@@ -130,7 +130,7 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}
onClick={()=>{setDevice(value.id);setPassword('');setVerified(null);show('wifi');}}>{device===value.id?'Выбрано':'Выбрать'}</Button>}/></li>)}
</ResourceList>}
</div>
{bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard title={notice}/></div>}
{bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard align="center" title={notice}/></div>}
</SettingsCard>
{selected&&<div data-enrollment-reveal="wifi"><SettingsCard eyebrow="Шаг 02" title="Wi-Fi"
description="Укажите сеть для K1. Проверка передаст настройки сканеру и проверит связь с БК." actions={!confirmed&&
@@ -147,15 +147,15 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}
<Button disabled={pending||!enrollmentAllowed(state,'connect')||!bridgeFormValid(ssid,password)} aria-busy={busy==='connect'}
icon={busy==='connect'?<ActivityIndicator size="compact"/>:undefined}
onClick={()=>void run('connect')}>{busy==='connect'?'Проверяем подключение':'Проверить подключение'}</Button>
{attempt&&attempt.phase==='network_outcome_unknown'&&enrollmentAllowed(state,'verify')&&
{enrollmentAllowed(state,'verify')&&
<Button disabled={pending} aria-busy={busy==='verify'}
icon={busy==='verify'?<ActivityIndicator size="compact"/>:undefined}
onClick={()=>void run('verify')}>{busy==='verify'?'Проверяем состояние':'Проверить состояние K1'}</Button>}
</>}
{notice&&!bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard title={notice}/></div>}
{notice&&!bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard align="center" title={notice}/></div>}
</SettingsCard></div>}
</>}
{notice&&!selected&&!bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard title={notice}/></div>}
{notice&&!selected&&!bluetoothFailure&&<div data-enrollment-reveal="result" role="status"><SettingsCard align="center" title={notice}/></div>}
<ToastStack items={error?[{id:'enrollment-error',tone:'error',title:error,durationMs:null}]:[]} onDismiss={()=>setError('')}/>
</div>,
});
@@ -120,7 +120,7 @@ export function enrollmentNotice(state:EnrollmentState):string {
if(state.command_result?.status==='rejected')return 'Выбранное устройство или сеанс изменились. Обновите сведения и выберите K1 заново.';
if(code&&STATION_WIFI_FAILURE_MESSAGES[code])return STATION_WIFI_FAILURE_MESSAGES[code];
if(state.connected)return 'K1 подключён к БК.';
if(attempt?.phase==='network_applied')return 'Настройки Wi-Fi применены. Связь с K1 пока не подтверждена; проверьте текущее подключение.';
if(attempt?.phase==='network_applied')return 'K1 подключился к Wi-Fi. Связь с БК пока не подтверждена. Нажмите «Проверить состояние K1»; повторно вводить сеть и пароль не нужно.';
if(attempt?.phase==='network_outcome_unknown')return unknownResult;
if(attempt?.status==='failed'||state.command_result?.status==='failed')return 'Подключение не завершено. Проверьте состояние K1 и выбранную сеть.';
return '';
+1 -1
View File
@@ -22,7 +22,7 @@ from credential_install import PROFILE_ID, validate # noqa: E402
from debian import package # noqa: E402
from runtime_payload import files as runtime_files # noqa: E402
VERSION = "0.1.3"
VERSION = "0.1.4"
RESOURCES = (
"plugins/xgrids-k1/profile_loader.py",
"plugins/xgrids-k1/plugin.manifest.json",
@@ -105,7 +105,7 @@ class LinuxWifiAssociationProbe:
self.sys_net = sys_net
self.key = secrets.token_bytes(32)
def observe(self, *, interface_name: str | None, timeout_seconds: float = 30) -> dict:
def observe(self, interface_name: str | None, *, timeout_seconds: float = 30) -> dict:
result = {
"schema_version": 1,
"adapter": "linux-networkmanager",
+52
View File
@@ -351,6 +351,58 @@ def test_wired_board_bridge_does_not_require_host_wifi_association(tmp_path, mon
assert value["reason_code"] is None
@pytest.mark.parametrize("route_changes", [False, True])
def test_wired_board_real_service_correlates_route_and_control_endpoint(
tmp_path, monkeypatch, route_changes,
):
from dataclasses import replace
from k1link.device_plugins.xgrids_k1 import facade, linux_host
sys_net = tmp_path / "sys-net"
(sys_net / "enp1s0").mkdir(parents=True)
probe = linux_host.LinuxWifiAssociationProbe(sys_net)
service = facade.XgridsK1CompatibilityService(
tmp_path / "runtime", host_wifi_association_probe=probe,
)
path = facade.HostPathProbeResult(
available=True, fingerprint="synthetic-route", interface="enp1s0",
source_ipv4="192.168.1.2", route_class="direct", reason_code=None,
)
monkeypatch.setattr(facade, "_inspect_host_path", lambda _target: path)
calls = []
def tcp(_target):
calls.append("tcp")
if route_changes:
monkeypatch.setattr(facade, "_inspect_host_path", lambda _target: replace(
path, fingerprint="new-route", source_ipv4="192.168.1.3",
))
return True
monkeypatch.setattr(facade, "_control_endpoint_reachable", tcp)
def forbidden(*_args, **_kwargs):
raise AssertionError("Wired control bootstrap must not request host Wi-Fi")
monkeypatch.setattr(linux_host, "_run", forbidden)
try:
observed = service._sample_host_path("192.168.1.7")
assert observed.available is True
assert observed.interface == "enp1s0"
assert observed.reason_code is None
assert observed.fingerprint != path.fingerprint
assert service._sample_host_path("192.168.1.7").fingerprint == observed.fingerprint
endpoint = service._probe_control_endpoint("192.168.1.7")
assert calls == ["tcp"]
assert endpoint.reachable is (not route_changes)
assert endpoint.reason_code == (
"host-path-changed-during-tcp-probe" if route_changes else None
)
finally:
service.close()
def test_linux_kernel_route_is_matched_route_not_resolved_host(monkeypatch):
from k1link.device_plugins.xgrids_k1 import linux_host
from k1link.device_plugins.xgrids_k1.facade import _classify_host_route