fix(k1): expose exact supervisor recovery verify
This commit is contained in:
@@ -5260,6 +5260,90 @@ test("an unresolved physical command has one explicit server-bound read-only rec
|
|||||||
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
|
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible", () => {
|
||||||
|
const state = durableTopologyState();
|
||||||
|
const recoveryTransport = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||||||
|
state.snapshot_runtime_id = "runtime-terminal-control-physical-recovery";
|
||||||
|
state.semantic_topology_store.record.transport_ref = recoveryTransport;
|
||||||
|
state.physical_command = {
|
||||||
|
status: "unresolved",
|
||||||
|
reason_code: "physical-command-reconciliation-required",
|
||||||
|
requires_reconciliation: true,
|
||||||
|
resolved_active_recovery_required: false,
|
||||||
|
reconciliation_ready: true,
|
||||||
|
record: {
|
||||||
|
revision: 583,
|
||||||
|
operation_id: "old-start-operation",
|
||||||
|
action: "start",
|
||||||
|
stage: "observing",
|
||||||
|
resolution: null,
|
||||||
|
connection: {
|
||||||
|
transport_ref: recoveryTransport,
|
||||||
|
connection_mode: "bridge",
|
||||||
|
target_ipv4: "192.168.68.52",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const deniedObservation = (targetSource) => ({
|
||||||
|
allowed: false,
|
||||||
|
reason_codes: ["control-session-not-admissible-for-network-change"],
|
||||||
|
target_source: targetSource,
|
||||||
|
required_transport_ref: recoveryTransport,
|
||||||
|
required_connection_mode: "bridge",
|
||||||
|
requires_live_gatt_validation: true,
|
||||||
|
automatic_retry: false,
|
||||||
|
});
|
||||||
|
state.connection_policy = {
|
||||||
|
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||||||
|
facts: { retained_context_is_presence: false, retired_transport_refs: [] },
|
||||||
|
recommended_action: "scan-ble",
|
||||||
|
allowed_actions: [
|
||||||
|
"scan-ble",
|
||||||
|
"inspect-configured-endpoint",
|
||||||
|
"verify-control-device-info",
|
||||||
|
],
|
||||||
|
actions: {
|
||||||
|
"observe-fresh-device-network": deniedObservation("fresh-scan"),
|
||||||
|
"observe-current-device-network": deniedObservation(
|
||||||
|
"retained-current-process",
|
||||||
|
),
|
||||||
|
"observe-configured-device-network": deniedObservation(
|
||||||
|
"durable-configured-state",
|
||||||
|
),
|
||||||
|
"verify-control-device-info": {
|
||||||
|
allowed: true,
|
||||||
|
reason_codes: [],
|
||||||
|
target_source: "connection-supervisor",
|
||||||
|
required_transport_ref: null,
|
||||||
|
requires_live_gatt_validation: false,
|
||||||
|
automatic_retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(readOnlyConnectionObservationTarget(state), {
|
||||||
|
action: "verify-control-device-info",
|
||||||
|
deviceId: recoveryTransport,
|
||||||
|
connectionMode: "bridge",
|
||||||
|
source: "durable-configured-state",
|
||||||
|
serverBound: true,
|
||||||
|
expectedDiscoveryGeneration: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||||||
|
controller: provisioningController(state),
|
||||||
|
desiredMode: "bridge",
|
||||||
|
}));
|
||||||
|
const reconnect = buttonMarkupWithText(markup, "Переподключиться");
|
||||||
|
assert.equal(reconnect.length, 1);
|
||||||
|
assert.doesNotMatch(reconnect[0], /\bdisabled(?:=|\s|>)/);
|
||||||
|
assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1);
|
||||||
|
|
||||||
|
const mismatched = structuredClone(state);
|
||||||
|
mismatched.semantic_topology_store.record.transport_ref = "another-k1";
|
||||||
|
assert.equal(readOnlyConnectionObservationTarget(mismatched), null);
|
||||||
|
});
|
||||||
|
|
||||||
test("unavailable physical replacement stays CAS-protected behind the simple new-device path", () => {
|
test("unavailable physical replacement stays CAS-protected behind the simple new-device path", () => {
|
||||||
const state = durableTopologyState();
|
const state = durableTopologyState();
|
||||||
const transportRef = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
const transportRef = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||||||
|
|||||||
@@ -1096,7 +1096,7 @@ export function readOnlyPhysicalRecoveryBinding(
|
|||||||
state: XgridsK1State | null | undefined,
|
state: XgridsK1State | null | undefined,
|
||||||
): ReadOnlyPhysicalRecoveryBinding | null {
|
): ReadOnlyPhysicalRecoveryBinding | null {
|
||||||
if (!requiresReadOnlyPhysicalRecovery(state)) return null;
|
if (!requiresReadOnlyPhysicalRecovery(state)) return null;
|
||||||
const actions: ReadOnlyConnectionObservationAction[] = [
|
const actions: ReadOnlyNetworkObservationAction[] = [
|
||||||
"observe-fresh-device-network",
|
"observe-fresh-device-network",
|
||||||
"observe-current-device-network",
|
"observe-current-device-network",
|
||||||
"observe-configured-device-network",
|
"observe-configured-device-network",
|
||||||
@@ -1124,13 +1124,17 @@ export function canSelectConnectionMode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReadOnlyConnectionObservationAction = Extract<
|
type ReadOnlyNetworkObservationAction = Extract<
|
||||||
XgridsConnectionPolicyAction,
|
XgridsConnectionPolicyAction,
|
||||||
| "observe-fresh-device-network"
|
| "observe-fresh-device-network"
|
||||||
| "observe-current-device-network"
|
| "observe-current-device-network"
|
||||||
| "observe-configured-device-network"
|
| "observe-configured-device-network"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ReadOnlyConnectionObservationAction =
|
||||||
|
| ReadOnlyNetworkObservationAction
|
||||||
|
| Extract<XgridsConnectionPolicyAction, "verify-control-device-info">;
|
||||||
|
|
||||||
export type ReadOnlyConnectionObservationSource =
|
export type ReadOnlyConnectionObservationSource =
|
||||||
| "fresh-scan"
|
| "fresh-scan"
|
||||||
| "retained-current-process"
|
| "retained-current-process"
|
||||||
@@ -1153,7 +1157,7 @@ function isConnectionMode(value: unknown): value is XgridsConnectionMode {
|
|||||||
|
|
||||||
function exactPolicyObservationTarget(
|
function exactPolicyObservationTarget(
|
||||||
state: XgridsK1State | null | undefined,
|
state: XgridsK1State | null | undefined,
|
||||||
action: ReadOnlyConnectionObservationAction,
|
action: ReadOnlyNetworkObservationAction,
|
||||||
source: ReadOnlyConnectionObservationSource,
|
source: ReadOnlyConnectionObservationSource,
|
||||||
): ReadOnlyConnectionObservationTarget | null {
|
): ReadOnlyConnectionObservationTarget | null {
|
||||||
if (!connectionPolicyAllows(state, action)) return null;
|
if (!connectionPolicyAllows(state, action)) return null;
|
||||||
@@ -1189,7 +1193,7 @@ function exactPolicyObservationTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY:
|
const SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY:
|
||||||
ReadonlyArray<ReadOnlyConnectionObservationAction> = [
|
ReadonlyArray<ReadOnlyNetworkObservationAction> = [
|
||||||
"observe-current-device-network",
|
"observe-current-device-network",
|
||||||
"observe-configured-device-network",
|
"observe-configured-device-network",
|
||||||
"observe-fresh-device-network",
|
"observe-fresh-device-network",
|
||||||
@@ -1206,17 +1210,17 @@ export function recommendedConnectionRecoveryObservationTarget(
|
|||||||
): ReadOnlyConnectionObservationTarget | null {
|
): ReadOnlyConnectionObservationTarget | null {
|
||||||
const recommended = state?.connection_policy?.recommended_action;
|
const recommended = state?.connection_policy?.recommended_action;
|
||||||
const orderedActions = SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.includes(
|
const orderedActions = SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.includes(
|
||||||
recommended as ReadOnlyConnectionObservationAction,
|
recommended as ReadOnlyNetworkObservationAction,
|
||||||
)
|
)
|
||||||
? [
|
? [
|
||||||
recommended as ReadOnlyConnectionObservationAction,
|
recommended as ReadOnlyNetworkObservationAction,
|
||||||
...SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.filter(
|
...SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.filter(
|
||||||
(action) => action !== recommended,
|
(action) => action !== recommended,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY;
|
: SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY;
|
||||||
const sources: Record<
|
const sources: Record<
|
||||||
ReadOnlyConnectionObservationAction,
|
ReadOnlyNetworkObservationAction,
|
||||||
ReadOnlyConnectionObservationSource
|
ReadOnlyConnectionObservationSource
|
||||||
> = {
|
> = {
|
||||||
"observe-current-device-network": "retained-current-process",
|
"observe-current-device-network": "retained-current-process",
|
||||||
@@ -1230,6 +1234,44 @@ export function recommendedConnectionRecoveryObservationTarget(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A terminal control session may block the network-observation actions while
|
||||||
|
* the supervisor still explicitly allows its narrower DeviceInfo Verify.
|
||||||
|
* Reuse that permission only when the physical ledger and durable topology
|
||||||
|
* independently pin the same K1 and mode; browser selection is never used.
|
||||||
|
*/
|
||||||
|
function exactControlVerificationTarget(
|
||||||
|
state: XgridsK1State | null | undefined,
|
||||||
|
): ReadOnlyConnectionObservationTarget | null {
|
||||||
|
if (
|
||||||
|
!requiresReadOnlyPhysicalRecovery(state)
|
||||||
|
|| !connectionPolicyAllows(state, "verify-control-device-info")
|
||||||
|
) return null;
|
||||||
|
|
||||||
|
const binding = readOnlyPhysicalRecoveryBinding(state);
|
||||||
|
const semanticStore = state?.semantic_topology_store;
|
||||||
|
const durable = semanticStore?.record;
|
||||||
|
if (
|
||||||
|
!binding
|
||||||
|
|| semanticStore?.status !== "available"
|
||||||
|
|| semanticStore.configured_offline_evidence !== true
|
||||||
|
|| semanticStore.live_connection_authority !== false
|
||||||
|
|| durable?.schema_version !== "missioncore.xgrids-k1-semantic-topology/v1"
|
||||||
|
|| transportRefEquivalenceKey(durable.transport_ref)
|
||||||
|
!== transportRefEquivalenceKey(binding.deviceId)
|
||||||
|
|| durable.connection_mode !== binding.connectionMode
|
||||||
|
) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "verify-control-device-info",
|
||||||
|
deviceId: binding.deviceId,
|
||||||
|
connectionMode: binding.connectionMode,
|
||||||
|
source: "durable-configured-state",
|
||||||
|
serverBound: true,
|
||||||
|
expectedDiscoveryGeneration: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the one read-only BLE target authorized by the server policy.
|
* Resolve the one read-only BLE target authorized by the server policy.
|
||||||
* Unresolved writes never fall back to a browser selection or dropdown mode:
|
* Unresolved writes never fall back to a browser selection or dropdown mode:
|
||||||
@@ -1285,7 +1327,7 @@ export function readOnlyConnectionObservationTarget(
|
|||||||
state,
|
state,
|
||||||
"observe-configured-device-network",
|
"observe-configured-device-network",
|
||||||
"durable-configured-state",
|
"durable-configured-state",
|
||||||
);
|
) ?? exactControlVerificationTarget(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user