Стабилизация переключения Bridge и Quick Connect

Безопасно восстанавливает управляющее подключение и локальные checkpoint без повторных команд сканеру.

Добавляет PCAP/Bridge guardrails и регрессионные проверки одношагового переподключения.

Известный дефект: после второго подключения интерфейс не присоединяется к новой генерации preview правой камеры. В живой Quick Connect-сессии STOP был принят, но READY не подтвердился до таймаута; автоматический повтор STOP запрещён.
This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 00:15:28 +03:00
parent 1001a31638
commit 0752e5c6bf
12 changed files with 3481 additions and 234 deletions
@@ -17,6 +17,7 @@ let physicalRecoveryConnectionDetail;
let shouldRenderK1OperationalPanels;
let K1ProvisioningPipeline;
let RuntimeActionFenceTestContext;
let LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS;
let emptySearchPresentation;
let emptyProvisioningAttemptPresentation;
let emptyReadOnlyReconnectPresentation;
@@ -127,6 +128,7 @@ before(async () => {
({
K1ProvisioningPipeline,
RuntimeActionFenceTestContext,
LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS,
emptySearchPresentation,
emptyProvisioningAttemptPresentation,
emptyReadOnlyReconnectPresentation,
@@ -3245,6 +3247,7 @@ test("replay and local-only stop branches use bounded process copy", () => {
const stopState = runtimeState();
stopState.compatibility.vendor_writes_enabled = false;
stopState.acquisition.control_mode = "operator-manual";
stopState.connection_policy = {
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
facts: { retained_context_is_presence: false },
@@ -3272,7 +3275,68 @@ test("replay and local-only stop branches use bounded process copy", () => {
assertCanonicalConnectionCopy(stopMarkup);
});
test("physical STOP is fail-closed and both acquisition surfaces fall back to local cleanup", () => {
test("software-commanded calibration exposes no teardown until canonical STOP is safe", () => {
const state = runtimeState();
state.snapshot_runtime_id = "runtime-calibration-001";
state.snapshot_revision = 31;
state.phase = "starting_live";
state.source_mode = "live";
state.acquisition.state = "awaiting_external_start";
state.acquisition.state_revision = 3;
state.application_control_session = {
session_generation: 5,
state_revision: 7,
state: "device-initializing",
can_stop: false,
control_socket_open: true,
};
state.connection_policy = {
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
facts: { retained_context_is_presence: false },
allowed_actions: ["stop-local-receiver"],
actions: {
"stop-acquisition": {
allowed: false,
reason_codes: ["supervisor-action-not-allowed"],
target_source: "connection-supervisor",
required_transport_ref: null,
required_connection_mode: null,
requires_live_gatt_validation: false,
automatic_retry: false,
},
"stop-local-receiver": {
allowed: true,
reason_codes: [],
target_source: "local-runtime",
required_transport_ref: null,
required_connection_mode: null,
requires_live_gatt_validation: false,
automatic_retry: false,
},
},
};
const pipelineMarkup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
controller: acquisitionController(state),
desiredConnectionMode: "bridge",
openSpatialScene() {},
activateAutomaticSpatialSource() {},
}));
const spatialMarkup = renderToStaticMarkup(createElement(K1SpatialControlsView, {
controller: acquisitionController(state),
}));
assert.match(pipelineMarkup, /Кнопка остановки появится только после подтверждённого SCANNING/);
assert.match(spatialMarkup, /K1 калибруется и готовит облако точек/);
for (const markup of [pipelineMarkup, spatialMarkup]) {
assert.equal(buttonMarkupWithText(markup, "Завершить локальный приём").length, 0);
assert.equal(buttonMarkupWithText(markup, "Аварийно завершить локальный приём").length, 0);
assert.equal(buttonMarkupWithText(markup, "Остановить устройство и запись").length, 0);
assert.equal(buttonMarkupWithText(markup, "Отменить запуск до START").length, 0);
}
});
test("physical STOP is fail-closed across both acquisition surfaces", () => {
const state = runtimeState();
state.snapshot_runtime_id = "runtime-stop-001";
state.snapshot_revision = 40;
@@ -3442,13 +3506,15 @@ test("physical STOP is fail-closed and both acquisition surfaces fall back to lo
},
));
assert.doesNotMatch(dismissedFailureMarkup, /Остановить устройство и запись/);
assert.match(dismissedFailureMarkup, /Повторная команда устройству не отправляется/);
const localButtons = buttonMarkupWithText(
dismissedFailureMarkup,
"Завершить локальный приём",
);
assert.equal(localButtons.length, 1);
assert.doesNotMatch(localButtons[0], /\bdisabled(?:=|\s|>)/);
assert.equal(localButtons.length, 0);
assert.match(
dismissedFailureMarkup,
/Повторная команда и локальное завершение заблокированы/,
);
assertCanonicalConnectionCopy(dismissedFailureMarkup);
const classifiedStopState = structuredClone(state);
@@ -3485,7 +3551,11 @@ test("physical STOP is fail-closed and both acquisition surfaces fall back to lo
activateAutomaticSpatialSource() {},
},
));
assert.match(localCleanupPendingMarkup, /Завершение локального приёма…/);
assert.doesNotMatch(localCleanupPendingMarkup, /Завершение локального приёма…/);
assert.match(
localCleanupPendingMarkup,
/Повторная команда и локальное завершение заблокированы/,
);
assert.doesNotMatch(localCleanupPendingMarkup, /Остановка устройства…/);
const physicalStopPendingMarkup = renderToStaticMarkup(createElement(
@@ -5802,6 +5872,78 @@ test("physical recovery new-device action performs only an explicit scenario res
assert.equal(reopenCalls, 0);
});
test("topology switch stays silent for two seconds and then uses only the inline loader", async () => {
const state = terminalConnectionRecoveryState();
state.connection_attempt = null;
let resolveReset;
const resetSettlement = new Promise((resolve) => {
resolveReset = resolve;
});
const resetRequests = [];
const controller = {
...provisioningController(state),
selectConnectionMode: (request) => {
resetRequests.push(request);
return resetSettlement;
},
};
const props = {
controller,
desiredMode: "bridge",
onDesiredModeChange: () => undefined,
};
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
const timers = [];
globalThis.setTimeout = (callback, delay, ...args) => {
const timer = { callback, delay, args, cleared: false };
timers.push(timer);
return timer;
};
globalThis.clearTimeout = (timer) => {
timer.cleared = true;
};
const harness = createStatefulProvisioningHarness(props);
try {
let tree = harness.render();
harness.flushEffects();
const modeSelect = elementByProp(tree, "label", "Способ подключения");
assert.ok(modeSelect);
modeSelect.props.onChange("quick-connect");
assert.equal(resetRequests.length, 1);
tree = harness.render();
harness.flushEffects();
let markup = renderToStaticMarkup(tree);
assert.doesNotMatch(markup, /Переключаем способ подключения/);
assert.doesNotMatch(markup, /Завершение прежней локальной границы/);
const progressTimer = timers.find(
(timer) => timer.delay === LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS,
);
assert.ok(progressTimer);
assert.equal(progressTimer.cleared, false);
progressTimer.callback(...progressTimer.args);
tree = harness.render();
markup = renderToStaticMarkup(tree);
assert.match(markup, /Переключаем способ подключения/);
assert.match(markup, /connection-action-progress/);
assert.doesNotMatch(markup, /dialog|modal|window/i);
resolveReset(true);
await Promise.resolve();
await Promise.resolve();
tree = harness.render();
harness.flushEffects();
markup = renderToStaticMarkup(tree);
assert.doesNotMatch(markup, /Переключаем способ подключения/);
} finally {
harness.dispose();
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
}
});
test("physical retirement dispatch refuses a rendered runtime A after runtime B is current", async () => {
let currentRuntimeId = "runtime-B";
let mutationRequests = 0;
@@ -6360,7 +6502,7 @@ test("unknown and cold durable recovery offer one reconnect and one new-device p
...provisioningController(coldQuickState),
getConnectionRecoveryObservationTarget: () => coldQuickTarget,
},
desiredMode: "bridge",
desiredMode: "quick-connect",
});
const coldQuickReconnect = buttonMarkupWithText(
coldQuickMarkup,
@@ -6374,6 +6516,32 @@ test("unknown and cold durable recovery offer one reconnect and one new-device p
);
assert.match(coldQuickMarkup, /Quick Connect/);
const quickTargetInsideBridgeMarkup = renderProvisioning({
controller: {
...provisioningController(coldQuickState),
getConnectionRecoveryObservationTarget: () => coldQuickTarget,
},
desiredMode: "bridge",
});
assert.equal(
buttonMarkupWithText(
quickTargetInsideBridgeMarkup,
"Переподключиться",
).length,
0,
);
assert.equal(
buttonMarkupWithText(
quickTargetInsideBridgeMarkup,
"Найти по Bluetooth",
).length,
1,
);
assert.doesNotMatch(
quickTargetInsideBridgeMarkup,
/Сохранённое подключение/,
);
const unrelatedMarkup = renderProvisioningWithAttempt({
controller: {
...controller,
@@ -6385,6 +6553,41 @@ test("unknown and cold durable recovery offer one reconnect and one new-device p
assert.match(unrelatedMarkup, /Сохранённое подключение/);
});
test("saved Bridge reconnect stays hidden in a fresh Quick Connect draft", () => {
const state = terminalConnectionRecoveryState();
state.connection_attempt = null;
const bridgeTarget = recommendedConnectionRecoveryObservationTarget(state);
assert.equal(bridgeTarget?.connectionMode, "bridge");
const controller = {
...provisioningController(state),
getConnectionRecoveryObservationTarget: () => bridgeTarget,
};
const quickConnectMarkup = renderProvisioning({
controller,
desiredMode: "quick-connect",
});
assert.equal(
buttonMarkupWithText(quickConnectMarkup, "Переподключиться").length,
0,
);
assert.equal(
buttonMarkupWithText(quickConnectMarkup, "Найти по Bluetooth").length,
1,
);
assert.doesNotMatch(quickConnectMarkup, /Сохранённое подключение/);
const bridgeMarkup = renderProvisioning({
controller,
desiredMode: "bridge",
});
assert.equal(
buttonMarkupWithText(bridgeMarkup, "Переподключиться").length,
1,
);
assert.match(bridgeMarkup, /Сохранённое подключение/);
});
test("read-only reconnect keeps its card and loader across a transient projection", async () => {
const initialState = terminalConnectionRecoveryState();
initialState.connection_attempt = null;
+9
View File
@@ -96,6 +96,15 @@ Core Quick Connect intent emits one AP-enable frame even when the baseline mode
already says `WIFI_AP`, then polls for the byte-51 ready flag for at most 15
seconds. It never retries the device write automatically.
For that exact explicit intent, a completed write-with-response followed by
the same canonical `WIFI_AP / 192.168.56.1 / ready` observation is an
idempotent ensure-target success: the K1 AP was already ready, and the host may
continue into the bounded exact-SSID CoreWLAN association. This exception
requires the durable ledger to record `write_confirmed=true`. An unconfirmed
dispatch, a timeout, or an unchanged read-only observation without that exact
confirmed write remains ambiguous and cannot authorize host association or an
automatic retry.
Static review of the original client also established a lifecycle requirement:
LixelGO keeps the same BLE manager connected after AP-ready and invokes native
Wi-Fi association from that live session. Mission Core now retains the same
@@ -163,6 +163,13 @@ contract coverage but remains a distinct physical acceptance gate; it must not
be reported as field-accepted until one redacted live run records both sides of
the transition.
Quick Connect Apply is an exact idempotent ensure-target operation when the
selected K1 already reports the canonical AP-ready state. Mission Core accepts
that unchanged state only after the one reviewed write-with-response is
durably confirmed, then performs the normal exact device-SSID host association.
An unconfirmed or merely read-only unchanged observation remains fail-closed;
this rule neither retries the BLE write nor weakens Bridge admission.
The corrected host boundary derives a non-secret, device-scoped profile ID from
the selected SSID. The reviewed client contains per-device `WiFiAP_SSID` and
`WiFiAP_Password` fields, but the 2026-07-20 review of the exact official K1
+10
View File
@@ -1036,6 +1036,16 @@ export interface XgridsK1State {
network_write_performed: false;
automatic_scan: false;
operation_sequence: number;
timing?: {
schema_version: "missioncore.xgrids-k1-connection-scenario-reset-timing/v1";
started_at_utc: string;
completed_at_utc: string;
total_ms: number;
lifecycle_boundary_wait_ms: number;
monitor_quiescence_wait_ms: number;
local_retirement_ms: number;
intent_commit_ms: number;
};
} | null;
connection_scenario_reset_pending?: {
reset_id: string;
@@ -207,9 +207,16 @@ export function K1AcquisitionPipeline({
&& canIssueCanonicalStop(state, physicalStopIntentSpent),
);
const physicalStopPresented = physicalStopInFlight || physicalStopExecutable;
const protectedSoftwareCommandedTransition = Boolean(
activeAcquisition
&& activeAcquisition.control_mode === "plugin-commanded"
&& activeAcquisition.state !== "prepared"
&& !isTerminalAcquisitionState(activeAcquisition.state),
);
const localReceiverStopExecutable = Boolean(
connectionPolicyAllows(state, "stop-local-receiver")
&& preparedAcquisition === null,
&& preparedAcquisition === null
&& !protectedSoftwareCommandedTransition,
);
const terminalPhysicalStopPending = terminalPhysicalStopObserved
&& physicalStopInFlight;
@@ -306,6 +313,10 @@ export function K1AcquisitionPipeline({
: "Управляющая команда устройству сейчас недоступна. Завершите только разрешённый сервером локальный приём или выполните read-only восстановление."
: terminalReadOnlyRecovery
? "Управляющие действия сейчас не разрешены. Дождитесь нового подтверждённого состояния или выполните read-only восстановление."
: protectedSoftwareCommandedTransition && !physicalStopPresented
? physicalStopIntentSpent
? "Команда остановки уже была принята интерфейсом. Повторная команда и локальное завершение заблокированы до нового подтверждённого состояния K1."
: "K1 выполняет переход к сканированию. Кнопка остановки появится только после подтверждённого SCANNING."
: physicalStopGuidance
? `${physicalStopGuidance.reason} ${physicalStopGuidance.nextAction}`
: gracefulStopTarget && !physicalStopPresented && physicalStopIntentSpent
@@ -572,7 +583,7 @@ export function K1AcquisitionPipeline({
: physicalStopPresented ? recoveredPhysicalStop ? "Остановить сканирование" : "Остановить устройство и запись" : state?.source_mode === "replay" ? "Остановить повтор" : preparedAcquisition ? "Завершить подготовленный приём" : "Завершить локальный приём"}
</Button>
) : null}
{activeAcquisition ? (
{activeAcquisition && !protectedSoftwareCommandedTransition ? (
<Button variant="ghost" disabled={isBusy} onClick={() => void abort()}>
{pendingAction === "abort" ? "Прерываем локальную операцию…" : preparedAcquisition ? "Отменить подготовку" : "Аварийно завершить локальный приём"}
</Button>
@@ -100,6 +100,31 @@ export function emptyReadOnlyReconnectPresentation():
return null;
}
export const LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS = 2_000;
/**
* Keep short local lifecycle settlements visually silent. Safety interlocks
* still take effect immediately; only the progress presentation is deferred.
*/
export function useDelayedLocalOperationProgress(
active: boolean,
delayMilliseconds = LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS,
): boolean {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!active) {
setVisible(false);
return undefined;
}
const timer = globalThis.setTimeout(
() => setVisible(true),
delayMilliseconds,
);
return () => globalThis.clearTimeout(timer);
}, [active, delayMilliseconds]);
return active && visible;
}
export interface PhysicalReopenPresentation {
key: string;
snapshotRuntimeId: string;
@@ -1240,6 +1265,9 @@ export function K1ProvisioningPipeline({
targetMode: ConnectionMode;
} | null>(null);
const modeResetInFlight = pendingAction === "mode" || modeResetPending !== null;
const modeResetProgressVisible = useDelayedLocalOperationProgress(
modeResetInFlight,
);
const [selectedDeviceSnapshot, setSelectedDeviceSnapshot] = useState<BleDevice | null>(null);
const [ssid, setSsid] = useState("");
const [password, setPassword] = useState("");
@@ -1657,7 +1685,8 @@ export function K1ProvisioningPipeline({
? readOnlyReconnectPresentation
: null;
const connectionRecoveryVerificationPending =
currentReadOnlyReconnectPresentation?.kind === "connection";
currentReadOnlyReconnectPresentation?.kind === "connection"
&& currentReadOnlyReconnectPresentation.connectionMode === connectionMode;
const physicalRecoveryVerificationPending =
currentReadOnlyReconnectPresentation?.kind === "physical";
const provisioningMutationBusy = Boolean(
@@ -1796,7 +1825,7 @@ export function K1ProvisioningPipeline({
state,
connectionMode,
);
const connectionRecoveryTarget = !physicalRecoveryRequired
const recommendedConnectionRecoveryTarget = !physicalRecoveryRequired
&& !unresolvedAppliedAttempt
&& !selectedModeConnected
&& !reconfigurationActive
@@ -1804,6 +1833,13 @@ export function K1ProvisioningPipeline({
&& connectionRecoveryObservationAllowed
? recommendedConnectionRecoveryObservationTarget(state)
: null;
// A durable recovery target remains available when the operator returns to
// its mode, but it must never leak a Bridge reconnect action into a freshly
// selected Quick Connect draft (or vice versa).
const connectionRecoveryTarget =
recommendedConnectionRecoveryTarget?.connectionMode === connectionMode
? recommendedConnectionRecoveryTarget
: null;
const connectionRecoveryKey = connectionRecoveryEscapeKey({
snapshotRuntimeId,
attempt: connectionRecoveryAttempt,
@@ -3162,10 +3198,16 @@ export function K1ProvisioningPipeline({
disabled={modeResetInFlight}
variant="split"
/>
{modeResetInFlight ? (
<p className="safety-note" role="status">
Завершение прежней локальной границы. Новый поиск не начнётся автоматически.
</p>
{modeResetProgressVisible ? (
<div
className="connection-action-progress"
role="status"
aria-live="polite"
aria-busy="true"
>
<ActivityIndicator size="compact" />
<strong>Переключаем способ подключения</strong>
</div>
) : null}
{activeScenarioReset ? (
<p className="safety-note" role="status">
@@ -197,6 +197,7 @@ export function K1SpatialControlsView({
&& canIssueCanonicalStop(state, physicalStopIntentSpent),
);
const physicalStopPresented = physicalStopInFlight || physicalStopExecutable;
const pluginCommandedAcquisition = acquisition?.control_mode === "plugin-commanded";
const stopping = ["awaiting_external_stop", "stopping", "finalizing"].includes(
acquisition?.state ?? "",
);
@@ -327,7 +328,10 @@ export function K1SpatialControlsView({
</span>
</Button>
) : null}
{!physicalStopPresented && localReceiverStopAllowed && !stopping ? (
{!physicalStopPresented
&& localReceiverStopAllowed
&& !pluginCommandedAcquisition
&& !stopping ? (
<Button
size="compact"
variant="ghost"
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Fail closed if Quick Connect work drifts from K1 protocol or Bridge baselines.
This checker performs local filesystem, Git, and synthetic-test validation only.
It never opens BLE, CoreWLAN, MQTT, RTSP, or a socket to the scanner.
"""
from __future__ import annotations
import hashlib
import os
import stat
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
BASELINE_COMMIT = "1001a316385a742114523c9996c43034898c115d"
@dataclass(frozen=True, slots=True)
class CaptureOracle:
relative_path: str
size_bytes: int
sha256: str
CAPTURE_ORACLES = (
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T151239Z_lixelgo-clean-cycle_d547/"
"captures/iphone-network.pcapng",
126_962_848,
"e6e98e22156ba2ce041c5f6ff32a3116446aaae8b617c90c16b3f00de8d5100f",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T151239Z_lixelgo-clean-cycle_d547/"
"captures/iphone-network.pcap",
116_785_317,
"45ebaebbba8c8f62ebc405eb0f7cdc496d84dcdf80c01decba61c56b4514a26f",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T152407Z_lixelgo-controls-stop_9be0/"
"captures/iphone-network.pcapng",
254_161_132,
"59133299bb1c4dc2157e50401ddef4c131acd2196d7d2e2ccddd5c51b95e57cb",
),
CaptureOracle(
"sessions/iphone-k1-observation/"
"20260716T152407Z_lixelgo-controls-stop_9be0/"
"captures/iphone-network.pcap",
234_691_027,
"6e267f0ab9b213e5dde732b94cf3f4b2f32fcf66f5569bcb511a13c952883606",
),
)
FROZEN_PATHS = (
"src/k1link/device_plugins/xgrids_k1/protocol",
"src/k1link/device_plugins/xgrids_k1/ble/ap_activation.py",
"src/k1link/device_plugins/xgrids_k1/ble/wifi_provisioning.py",
"src/k1link/device_plugins/xgrids_k1/connection_supervisor.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_coordinator.py",
"src/k1link/device_plugins/xgrids_k1/physical_command_ledger.py",
"src/k1link/device_plugins/xgrids_k1/mqtt",
"src/k1link/device_plugins/xgrids_k1/camera.py",
"src/k1link/device_plugins/xgrids_k1/viewer/runtime.py",
"src/k1link/device_plugins/xgrids_k1/archive.py",
)
PYTEST_TARGETS = (
"tests/test_xgrids_application_bootstrap.py",
(
"tests/test_xgrids_application_mqtt.py::"
"test_unanswered_optional_status_does_not_steal_same_identity_required_refresh"
),
(
"tests/test_xgrids_application_mqtt.py::"
"test_already_arrived_optional_status_is_consumed_before_required_refresh"
),
(
"tests/test_xgrids_application_mqtt.py::"
"test_unbound_optional_status_remains_distinct_from_bound_required_refresh"
),
(
"tests/test_xgrids_application_session.py::"
"test_canonical_stages_require_operator_events_but_device_standby_does_not"
),
(
"tests/test_connection_supervisor.py::"
"test_quick_to_bridge_revokes_authority_with_same_target_and_host_epoch"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_applied_bridge_target_requires_explicit_wifi_client_mode"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_post_dispatch_bridge_accepts_exact_fw302_network_name_when_baseline_is_same"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_post_dispatch_bridge_rejects_another_fw302_network_name"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_bridge_read_only_verify_never_shortcuts_fresh_ble_status"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_durable_restart_same_bridge_baseline_cannot_resolve_ambiguous_write"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_to_bridge_requires_new_explicit_scan_instead_of_retained_session"
),
(
"tests/test_xgrids_acquisition_lifecycle.py::"
"test_quick_to_bridge_does_not_retrieve_old_handle_without_new_scan"
),
)
def fail(message: str) -> None:
raise RuntimeError(message)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
while chunk := source.read(4 * 1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def verify_capture_oracles() -> None:
for oracle in CAPTURE_ORACLES:
path = REPOSITORY_ROOT / oracle.relative_path
try:
metadata = path.lstat()
except FileNotFoundError:
fail(f"capture oracle is missing: {oracle.relative_path}")
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
fail(f"capture oracle is not a regular non-symlink file: {oracle.relative_path}")
if stat.S_IMODE(metadata.st_mode) != 0o600:
fail(f"capture oracle mode is not 0600: {oracle.relative_path}")
if metadata.st_size != oracle.size_bytes:
fail(
"capture oracle size changed: "
f"{oracle.relative_path} ({metadata.st_size} != {oracle.size_bytes})"
)
observed_sha256 = sha256_file(path)
if observed_sha256 != oracle.sha256:
fail(
"capture oracle SHA-256 changed: "
f"{oracle.relative_path} ({observed_sha256} != {oracle.sha256})"
)
print(f"[k1-guardrail] PASS capture oracles: {len(CAPTURE_ORACLES)}")
def git_output(*args: str) -> str:
result = subprocess.run(
("git", *args),
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def verify_frozen_contour() -> None:
git_output("cat-file", "-e", f"{BASELINE_COMMIT}^{{commit}}")
changed = git_output("diff", "--name-only", BASELINE_COMMIT, "--", *FROZEN_PATHS)
untracked = git_output(
"ls-files",
"--others",
"--exclude-standard",
"--",
*FROZEN_PATHS,
)
drift = tuple(line for line in (changed, untracked) if line)
if drift:
fail("frozen K1 protocol/Bridge contour changed:\n" + "\n".join(drift))
print(
"[k1-guardrail] PASS frozen protocol/Bridge contour: "
f"baseline {BASELINE_COMMIT[:7]}"
)
def run_checked(command: tuple[str, ...]) -> None:
print("[k1-guardrail] RUN " + " ".join(command), flush=True)
subprocess.run(command, cwd=REPOSITORY_ROOT, check=True)
def verify_synthetic_contracts() -> None:
python = REPOSITORY_ROOT / ".venv" / "bin" / "python"
ruff = REPOSITORY_ROOT / ".venv" / "bin" / "ruff"
if not python.is_file():
fail("repository Python runtime is missing: .venv/bin/python")
if not ruff.is_file():
fail("repository Ruff runtime is missing: .venv/bin/ruff")
run_checked((str(python), "-m", "pytest", "-q", *PYTEST_TARGETS))
run_checked(
(
str(ruff),
"check",
"scripts/check_k1_quick_connect_guardrails.py",
"src/k1link/device_plugins/xgrids_k1/facade.py",
"tests/test_xgrids_acquisition_lifecycle.py",
)
)
run_checked(("git", "diff", "--check"))
print("[k1-guardrail] PASS synthetic protocol/Bridge sentinels")
def main() -> int:
os.chdir(REPOSITORY_ROOT)
try:
verify_capture_oracles()
verify_frozen_contour()
verify_synthetic_contracts()
except (OSError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f"[k1-guardrail] FAIL {exc}", file=sys.stderr)
return 1
print("[k1-guardrail] PASS all checks; no scanner I/O was performed")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -3045,16 +3045,25 @@ def _require_prepared_resolved_start_standby(
raise ActiveAcquisitionRecoveryCheckpointTransitionError(
"resolved START origin does not bind the exact prepared acquisition"
)
if (
cessation_status_proof.binding.runtime_instance_id
recovery_binding = cessation_status_proof.binding
same_runtime = (
recovery_binding.runtime_instance_id
== checkpoint.prepared_binding.runtime_instance_id
or cessation_status_proof.binding.control_session_id
)
if (
recovery_binding.control_session_id
== checkpoint.prepared_binding.control_session_id
or cessation_status_proof.evidence_session_id
== checkpoint.original_evidence_session_id
or (
same_runtime
and recovery_binding.producer_generation
<= checkpoint.prepared_binding.producer_generation
)
):
raise ActiveAcquisitionRecoveryCheckpointTransitionError(
"restart standby settlement requires new runtime, control and evidence sessions"
"standby settlement requires a new control/evidence boundary; "
"same-runtime recovery also requires a newer producer generation"
)
exact_terminal_lineage = bool(
cessation_physical_proof.operation_id
@@ -3474,7 +3483,7 @@ def _require_active_reconciled_standby_shape(
or (
same_runtime
and recovery_binding.producer_generation
== failed_binding.producer_generation
<= failed_binding.producer_generation
)
):
raise ActiveAcquisitionRecoveryCheckpointTransitionError(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1134,6 +1134,44 @@ def test_cease_prepared_resolved_start_standby_rejects_inexact_restart_proofs(
replace(status, mqtt_retained=True)
def test_cease_prepared_resolved_start_standby_accepts_fresh_same_runtime_boundary(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A new verified control generation is sufficient without a process restart."""
prepared_binding = _binding()
store = _store(tmp_path, monkeypatch)
_prepare(store, prepared_binding)
kwargs = _cease_prepared_resolved_start_standby_kwargs(
prepared_binding,
terminal_state="ready",
)
status = kwargs["cessation_status_proof"]
physical = kwargs["cessation_physical_proof"]
same_runtime_binding = replace(
status.binding,
runtime_instance_id=prepared_binding.runtime_instance_id,
producer_generation=prepared_binding.producer_generation + 1,
)
kwargs["cessation_status_proof"] = replace(
status,
binding=same_runtime_binding,
)
kwargs["cessation_physical_proof"] = replace(
physical,
binding=same_runtime_binding,
)
ceased = store.cease_prepared_resolved_start_standby(**kwargs)
assert ceased.state == "ceased"
assert ceased.current_binding == same_runtime_binding
assert ceased.current_evidence_session_id == "evidence-restarted"
assert ceased.activated_at_utc is None
assert ceased.first_published_pcl_proof is None
@pytest.mark.parametrize("terminal_state", ("ready", "scan_over"))
def test_cease_active_reconciled_standby_atomically_opens_and_closes_gap(
tmp_path: Path,