1162 lines
50 KiB
JavaScript
1162 lines
50 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import { dirname, join, resolve } from "node:path";
|
||
import { after, before, test } from "node:test";
|
||
|
||
import { createServer } from "vite";
|
||
|
||
const testRoot = dirname(fileURLToPath(import.meta.url));
|
||
const controlStationRoot = resolve(testRoot, "..");
|
||
const repositoryRoot = resolve(controlStationRoot, "../..");
|
||
const coreSourceRoot = join(controlStationRoot, "src");
|
||
const pluginFrontendRoot = join(repositoryRoot, "plugins/xgrids-k1/frontend/src");
|
||
const legacyPluginRoot = join(coreSourceRoot, "device-plugins/xgrids-k1");
|
||
|
||
function sourceFiles(root) {
|
||
return readdirSync(root).flatMap((entry) => {
|
||
const path = join(root, entry);
|
||
if (statSync(path).isDirectory()) return sourceFiles(path);
|
||
return /\.(?:css|ts|tsx)$/.test(entry) ? [path] : [];
|
||
});
|
||
}
|
||
|
||
let server;
|
||
let createDevicePluginRegistry;
|
||
|
||
before(async () => {
|
||
server = await createServer({
|
||
appType: "custom",
|
||
logLevel: "silent",
|
||
server: { middlewareMode: true },
|
||
});
|
||
({ createDevicePluginRegistry } = await server.ssrLoadModule(
|
||
"/src/core/device-plugins/registry.ts",
|
||
));
|
||
});
|
||
|
||
after(async () => {
|
||
await server?.close();
|
||
});
|
||
|
||
function syntheticPlugin({
|
||
pluginId,
|
||
modelId,
|
||
componentKey,
|
||
connectionView,
|
||
spatialControlsView,
|
||
}) {
|
||
return {
|
||
manifest: {
|
||
apiVersion: "missioncore.nodedc/v1alpha2",
|
||
kind: "DevicePlugin",
|
||
metadata: { id: pluginId, version: "1.0.0", displayName: pluginId },
|
||
spec: {
|
||
hostApiRange: "v1alpha2",
|
||
runtime: { backendEntrypoint: `${pluginId}:build`, isolation: "transitional-in-process" },
|
||
permissions: ["device.read"],
|
||
actions: [{ id: "state.read", mutating: false, secretFields: [] }],
|
||
compatibilityProfiles: [{
|
||
profileId: `${modelId}.profile.v1`,
|
||
path: `profiles/${modelId}.json`,
|
||
modelId,
|
||
}],
|
||
models: [{
|
||
id: modelId,
|
||
vendor: pluginId,
|
||
displayName: modelId,
|
||
category: "Sensor",
|
||
description: "Synthetic frontend contribution",
|
||
verified: true,
|
||
capabilities: [{ id: "device.read", label: "Read" }],
|
||
ui: { slot: "device.connection", componentKey },
|
||
}],
|
||
},
|
||
},
|
||
RuntimeProvider: ({ children }) => children,
|
||
connectionViews: { [componentKey]: connectionView },
|
||
...(spatialControlsView ? { SpatialControlsView: spatialControlsView } : {}),
|
||
};
|
||
}
|
||
|
||
test("each device plugin contributes its own connection pipeline component", () => {
|
||
const alphaConnection = () => null;
|
||
const betaConnection = () => null;
|
||
const registry = createDevicePluginRegistry([
|
||
syntheticPlugin({
|
||
pluginId: "synthetic.alpha",
|
||
modelId: "synthetic.alpha.sensor",
|
||
componentKey: "alpha.connection",
|
||
connectionView: alphaConnection,
|
||
}),
|
||
syntheticPlugin({
|
||
pluginId: "synthetic.beta",
|
||
modelId: "synthetic.beta.sensor",
|
||
componentKey: "beta.connection",
|
||
connectionView: betaConnection,
|
||
}),
|
||
]);
|
||
|
||
assert.equal(registry.resolveModel("synthetic.alpha.sensor").ConnectionView, alphaConnection);
|
||
assert.equal(registry.resolveModel("synthetic.beta.sensor").ConnectionView, betaConnection);
|
||
assert.notEqual(
|
||
registry.resolveModel("synthetic.alpha.sensor").ConnectionView,
|
||
registry.resolveModel("synthetic.beta.sensor").ConnectionView,
|
||
);
|
||
});
|
||
|
||
test("selected-model shell leaves the model name to the connection heading", () => {
|
||
const workspace = readFileSync(
|
||
join(coreSourceRoot, "workspaces/DeviceWorkspace.tsx"),
|
||
"utf8",
|
||
);
|
||
const app = readFileSync(join(coreSourceRoot, "App.tsx"), "utf8");
|
||
|
||
const selectedSlot = workspace.slice(workspace.indexOf("const ConnectionView ="));
|
||
assert.match(workspace, /<h3>\{model\.displayName\}<\/h3>/);
|
||
assert.match(selectedSlot, /СЦЕНАРИЙ ПОДКЛЮЧЕНИЯ/);
|
||
assert.match(selectedSlot, /<strong>Модель выбрана<\/strong>/);
|
||
assert.doesNotMatch(selectedSlot, /selection\.model\.displayName/);
|
||
assert.doesNotMatch(
|
||
selectedSlot,
|
||
/selection\.plugin\.manifest\.metadata\.displayName/,
|
||
);
|
||
|
||
const localContour = app.slice(
|
||
app.indexOf('id: "local-contour"'),
|
||
app.indexOf("items={rootWorkspaces", app.indexOf('id: "local-contour"')),
|
||
);
|
||
assert.match(
|
||
localContour,
|
||
/description: selection \? "Подключение" : "Модель не выбрана"/,
|
||
);
|
||
assert.doesNotMatch(localContour, /activeDevice\?\.endpointLabel/);
|
||
assert.doesNotMatch(localContour, /selection\?\.model\.displayName/);
|
||
});
|
||
|
||
test("registry exposes an optional model-scoped spatial controls contribution", () => {
|
||
const connectionView = () => null;
|
||
const spatialControlsView = () => null;
|
||
const registry = createDevicePluginRegistry([
|
||
syntheticPlugin({
|
||
pluginId: "synthetic.spatial",
|
||
modelId: "synthetic.spatial.sensor",
|
||
componentKey: "spatial.connection",
|
||
connectionView,
|
||
spatialControlsView,
|
||
}),
|
||
syntheticPlugin({
|
||
pluginId: "synthetic.connection-only",
|
||
modelId: "synthetic.connection-only.sensor",
|
||
componentKey: "connection-only.connection",
|
||
connectionView,
|
||
}),
|
||
]);
|
||
|
||
assert.equal(
|
||
registry.resolveModel("synthetic.spatial.sensor").SpatialControlsView,
|
||
spatialControlsView,
|
||
);
|
||
assert.equal(
|
||
registry.resolveModel("synthetic.connection-only.sensor").SpatialControlsView,
|
||
undefined,
|
||
);
|
||
});
|
||
|
||
test("XGRIDS frontend is physically plugin-owned and split by operator pipeline", () => {
|
||
if (existsSync(legacyPluginRoot)) {
|
||
assert.deepEqual(readdirSync(legacyPluginRoot), []);
|
||
}
|
||
for (const relativePath of [
|
||
"plugin.ts",
|
||
"runtimeContext.tsx",
|
||
"styles.css",
|
||
"components/K1ProvisioningPipeline.tsx",
|
||
"components/K1AcquisitionPipeline.tsx",
|
||
"components/K1SpatialControls.tsx",
|
||
"components/K1Diagnostics.tsx",
|
||
"physicalCommandConfirmation.ts",
|
||
"projectName.ts",
|
||
]) {
|
||
assert.equal(existsSync(join(pluginFrontendRoot, relativePath)), true, relativePath);
|
||
}
|
||
|
||
const spatialControls = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1SpatialControls.tsx"),
|
||
"utf8",
|
||
);
|
||
assert.match(spatialControls, /shouldRenderSpatialControls\(state\)/);
|
||
assert.match(spatialControls, /cleanup_pending/);
|
||
assert.match(spatialControls, /spatialActionFailure/);
|
||
assert.match(spatialControls, /role="alert"/);
|
||
assert.doesNotMatch(spatialControls, /Повторить остановку/);
|
||
assert.match(spatialControls, /stopLocalReceiver/);
|
||
assert.match(spatialControls, /<ActivityIndicator size="compact"/);
|
||
assert.match(spatialControls, /aria-busy=\{phase\.busy\}/);
|
||
assert.match(spatialControls, /K1 калибруется и готовит облако точек/);
|
||
assert.match(spatialControls, /Первые данные могут появиться через десятки секунд/);
|
||
assert.match(spatialControls, /Не перемещайте устройство/);
|
||
|
||
const styles = readFileSync(join(pluginFrontendRoot, "styles.css"), "utf8");
|
||
assert.doesNotMatch(styles, /@keyframes xgrids-k1-spin/);
|
||
});
|
||
|
||
test("K1 connection surface enforces one scan, local selection, and one Apply", () => {
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const lifecycle = readFileSync(join(pluginFrontendRoot, "lifecycle.ts"), "utf8");
|
||
const submitPrerequisites = lifecycle.slice(
|
||
lifecycle.indexOf("export function canSubmitProvisioningMutation"),
|
||
lifecycle.indexOf("export function canAdmitProvisioningConnection"),
|
||
);
|
||
assert.match(provisioning, /bridge:\s*\{/);
|
||
assert.match(
|
||
provisioning,
|
||
/compatibility_attestation: profileSelectionForConnectionMode\([\s\S]*attemptedConnectionMode/,
|
||
);
|
||
assert.match(provisioning, /scanSecondsRemaining/);
|
||
assert.match(provisioning, /setInterval\(updateCountdown, 250\)/);
|
||
assert.match(provisioning, /Поиск Bluetooth · до \{scanSecondsRemaining \?\? BLE_DISCOVERY_TIMEOUT_SECONDS\} с/);
|
||
assert.match(provisioning, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/);
|
||
assert.doesNotMatch(provisioning, /K1 уже доступен|Сетевой адрес K1 доступен/);
|
||
assert.doesNotMatch(
|
||
provisioning,
|
||
/automaticRecovery|automaticDiscovery|reconnectFallbackAction/,
|
||
);
|
||
assert.match(provisioning, /Переподключиться/);
|
||
assert.match(provisioning, /Подключить новый K1/);
|
||
assert.doesNotMatch(provisioning, /Вернуть прежний K1 и проверить/);
|
||
assert.match(provisioning, /explicitProvisioningDraftMatches/);
|
||
assert.doesNotMatch(
|
||
provisioning,
|
||
/powerConfirmed|powerConfirmationEpoch|resetPowerConfirmation|Питание включено|title="Питание"/,
|
||
);
|
||
assert.doesNotMatch(submitPrerequisites, /power|питани/i);
|
||
assert.match(provisioning, /ПОДКЛЮЧЕНИЕ · ШАГИ 01–02/);
|
||
assert.match(
|
||
provisioning,
|
||
/number="01"[\s\S]*?title="Подключение"/,
|
||
);
|
||
assert.match(provisioning, /number="02"[\s\S]*?title="Сеть"/);
|
||
assert.doesNotMatch(provisioning, /number="03"|showDeviceStep/);
|
||
assert.match(
|
||
provisioning,
|
||
/const backendScanAllowed = scanAllowedByPolicy\s*&& !isBusy\s*&& !networkRecoveryRequired/,
|
||
);
|
||
assert.match(provisioning, /const showNetworkStep = Boolean\([\s\S]*explicitProvisioningDraftRetained/);
|
||
assert.match(
|
||
provisioning,
|
||
/if \(result\.networkIntentCompleted\)[\s\S]*setExplicitProvisioningDraft\(null\)/,
|
||
);
|
||
assert.equal(
|
||
(provisioning.match(/buttonLabel:\s*"Применить"/g) ?? []).length,
|
||
3,
|
||
);
|
||
assert.doesNotMatch(provisioning, /<(?:button|input|select|textarea)\b/);
|
||
assert.doesNotMatch(provisioning, /allow_host_wifi_switch/);
|
||
assert.doesNotMatch(provisioning, /(?:color|background(?:-color)?):\s*(?:#[0-9a-f]{3,8}|rgba?\()/i);
|
||
for (const sharedControl of ["Button", "IconButton", "TextField", "ActivityIndicator", "StatusBadge"]) {
|
||
assert.match(provisioning, new RegExp(`<${sharedControl}\\b`), sharedControl);
|
||
}
|
||
assert.doesNotMatch(
|
||
provisioning,
|
||
/Подключиться к сохранённому|Исходный K1|Проверить связь с K1|Проверить прежнее подключение/,
|
||
);
|
||
});
|
||
|
||
test("K1 click-owned actions are fenced without hidden frontend continuations", () => {
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const search = provisioning.slice(
|
||
provisioning.indexOf("const repeatDeviceScan"),
|
||
provisioning.indexOf("const submitConnect"),
|
||
);
|
||
const apply = provisioning.slice(
|
||
provisioning.indexOf("const submitConnect"),
|
||
provisioning.indexOf("const verifyAppliedNetwork"),
|
||
);
|
||
assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1);
|
||
assert.match(search, /durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/);
|
||
assert.equal((apply.match(/await connect\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(
|
||
apply,
|
||
/scanWithResult\(|verifyConnection\(|candidateRefresh|void submitConnect/,
|
||
);
|
||
assert.match(runtime, /class SnapshotRuntimeActionArbiter/);
|
||
assert.match(runtime, /runtimeActionArbiter\.current\.isCurrent\(actionToken\)/);
|
||
assert.match(runtime, /runtimeActionArbiter\.current\.settle\(actionToken\)/);
|
||
});
|
||
|
||
test("connected presentation uses canonical process copy", () => {
|
||
const connection = readFileSync(
|
||
join(pluginFrontendRoot, "XgridsK1Connection.tsx"),
|
||
"utf8",
|
||
);
|
||
assert.match(
|
||
connection,
|
||
/connectionTopology\?\.status === "active"\s*\? "Подключение установлено"/,
|
||
);
|
||
assert.match(connection, /"Готово к новой сессии\."/);
|
||
assert.match(connection, /<h2>Подключение \{model\.displayName\}<\/h2>/);
|
||
assert.match(
|
||
connection,
|
||
/const operationalPanelsVisible = shouldRenderK1OperationalPanels\(\s*state,\s*controller\.pendingAction,\s*\)/,
|
||
);
|
||
assert.match(
|
||
connection,
|
||
/operationalPanelsVisible \? <K1Metrics controller=\{controller\} \/> : null/,
|
||
);
|
||
assert.match(
|
||
connection,
|
||
/<K1ProvisioningPipeline[\s\S]*?operationalPanelsVisible \? \([\s\S]*?<K1AcquisitionPipeline[\s\S]*?<K1Diagnostics/,
|
||
);
|
||
assert.match(connection, /hasControlAuthority\(state\)/);
|
||
assert.match(connection, /requiresCanonicalStopAfterTerminalLocalFailure\(state\)/);
|
||
assert.match(connection, /state\?\.acquisition\?\.cleanup_pending === true/);
|
||
assert.doesNotMatch(
|
||
connection,
|
||
/Связь с K1 подтверждена|Требуется переподключение к K1|Устройство готово/,
|
||
);
|
||
});
|
||
|
||
test("local connection page owns model naming and process-only shell copy", () => {
|
||
const app = readFileSync(join(coreSourceRoot, "App.tsx"), "utf8");
|
||
const shellPresentation = readFileSync(
|
||
join(coreSourceRoot, "presentation.ts"),
|
||
"utf8",
|
||
);
|
||
const connection = readFileSync(
|
||
join(pluginFrontendRoot, "XgridsK1Connection.tsx"),
|
||
"utf8",
|
||
);
|
||
const operatorError = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1OperatorError.tsx"),
|
||
"utf8",
|
||
);
|
||
const acquisition = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1AcquisitionPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const metrics = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1Metrics.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(connection, /<h2>Подключение \{model\.displayName\}<\/h2>/);
|
||
assert.doesNotMatch(connection, /:\s*message\}/);
|
||
assert.match(
|
||
operatorError,
|
||
/Подключение не завершено\. Автоматического повтора не было/,
|
||
);
|
||
|
||
const deviceHeader = app.slice(
|
||
app.indexOf('activeDefinition.kind === "device" ? ('),
|
||
app.indexOf('activeDefinition.kind === "spatial"', app.indexOf('activeDefinition.kind === "device" ? (')),
|
||
);
|
||
assert.match(deviceHeader, /localConnectionPhaseLabel\(runtime\.state\?\.phase\)/);
|
||
assert.doesNotMatch(deviceHeader, /phaseLabel\(runtime\.state\?\.phase\)/);
|
||
assert.match(
|
||
shellPresentation,
|
||
/phase === "configuring"\) return "Подключение"/,
|
||
);
|
||
assert.match(
|
||
shellPresentation,
|
||
/phase === "connected"\) return "Подключение установлено"/,
|
||
);
|
||
assert.match(shellPresentation, /configuring: "Настройка устройства"/);
|
||
assert.match(shellPresentation, /connected: "Устройство подключено"/);
|
||
|
||
assert.match(acquisition, /hint="Локальный файл записи"/);
|
||
assert.match(acquisition, /Состояние сканирования остаётся неизвестным/);
|
||
assert.doesNotMatch(
|
||
acquisition,
|
||
/Локальный файл исходных данных|Физическое состояние сканера/,
|
||
);
|
||
assert.match(metrics, /Данные потока при этом сохраняются/);
|
||
assert.doesNotMatch(metrics, /Исходные данные при этом сохраняются/);
|
||
});
|
||
|
||
test("K1 START renders an in-button spinner for the complete live orchestration", () => {
|
||
const acquisition = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1AcquisitionPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(acquisition, /pendingAction === "live"[\s\S]*<ActivityIndicator size="compact"/);
|
||
assert.match(acquisition, /aria-busy=\{pendingAction === "live"\}/);
|
||
assert.doesNotMatch(acquisition, /readOnlyConnectionObservationTarget\(state\)/);
|
||
assert.match(acquisition, /appliedTopology\?\.status === "active"\s*&& desiredModeMatchesActive/);
|
||
assert.match(acquisition, /if \(!draftPreparationTarget\) return;/);
|
||
assert.doesNotMatch(acquisition, /START не используется для установки связи/);
|
||
assert.match(acquisition, /Синхронизация…/);
|
||
});
|
||
|
||
test("Connect accepts exact network-applied REST state while START still requires exact ready", () => {
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const exactReady = runtime.slice(
|
||
runtime.indexOf("function hasExactConnectionReady"),
|
||
runtime.indexOf("async function waitForPhysicalReconciliationProof"),
|
||
);
|
||
const connectFlow = runtime.slice(
|
||
runtime.indexOf("const connect = useCallback"),
|
||
runtime.indexOf("const verifyConnection = useCallback"),
|
||
);
|
||
const appliedProof = runtime.slice(
|
||
runtime.indexOf("export function exactAppliedNetworkIntentCompleted"),
|
||
runtime.indexOf("function requireExactReadOnlyVerificationOutcome"),
|
||
);
|
||
|
||
assert.match(exactReady, /state\.desired_connection_mode === connectionMode/);
|
||
assert.match(exactReady, /state\.active_connection_mode === connectionMode/);
|
||
assert.match(exactReady, /application_control_session\?\.state === "connection-ready"/);
|
||
assert.match(exactReady, /currentAppliedConnectionTopology\(state, connectionMode\)\?\.status === "active"/);
|
||
assert.doesNotMatch(connectFlow, /openApplicationControlSession|waitForControlPhase/);
|
||
assert.doesNotMatch(connectFlow, /verifyConnection\(|scanWithResult\(/);
|
||
assert.doesNotMatch(connectFlow, /startAcquisition|startPreparedAcquisition|START acquisition/);
|
||
assert.match(connectFlow, /networkIntentCompleted/);
|
||
assert.match(appliedProof, /attempt\.phase === "network_applied"/);
|
||
assert.match(appliedProof, /operation\.status === "succeeded"/);
|
||
assert.match(appliedProof, /operationPhase === "network_applied"/);
|
||
assert.match(appliedProof, /ledger\.resolution === "target-observed"/);
|
||
assert.match(appliedProof, /deviceNetwork\?\.state === "applied"/);
|
||
assert.doesNotMatch(appliedProof, /control_state/);
|
||
assert.match(
|
||
connectFlow,
|
||
/const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\([\s\S]*?if \(\s*!exactNetworkIntentCompleted\s*&& !hasExactConnectionReady\([\s\S]*?return requireExactConnectionReady\([\s\S]*?nextState = acceptSuccessfulConnectState\(nextState\)/,
|
||
);
|
||
assert.match(provisioning, /Подключение установлено/);
|
||
assert.doesNotMatch(
|
||
provisioning,
|
||
/Подключиться к сохранённому|Проверить связь с K1|Проверить прежнее подключение/,
|
||
);
|
||
assert.match(provisioning, /Переподключиться/);
|
||
assert.match(provisioning, /Подключить новый K1/);
|
||
assert.match(provisioning, /expected_mode_revision: modeAuthority\.desiredModeRevision/);
|
||
assert.match(
|
||
provisioning,
|
||
/expected_discovery_generation: modeAuthority\.discoveryGeneration/,
|
||
);
|
||
assert.doesNotMatch(provisioning, /Настройки сети применены/);
|
||
});
|
||
|
||
test("K1 mode reset is explicit while Scan and Apply keep exact backend CAS", () => {
|
||
const api = readFileSync(join(pluginFrontendRoot, "api.ts"), "utf8");
|
||
const manifest = readFileSync(join(pluginFrontendRoot, "manifest.ts"), "utf8");
|
||
const connection = readFileSync(join(pluginFrontendRoot, "XgridsK1Connection.tsx"), "utf8");
|
||
const runtime = readFileSync(join(pluginFrontendRoot, "useXgridsK1Runtime.ts"), "utf8");
|
||
const acquisition = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1AcquisitionPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(manifest, /connectionModeSelect:[\s\S]*"connection\.mode\.select"/);
|
||
assert.match(api, /interface SelectConnectionModeRequest/);
|
||
assert.match(api, /expected_revision: number/);
|
||
assert.match(api, /reset_scenario\?: true/);
|
||
assert.match(api, /reset_id\?: string/);
|
||
assert.match(api, /expected_mode_revision: number/);
|
||
assert.match(api, /expected_discovery_generation: number/);
|
||
const localModeHandler = connection.slice(
|
||
connection.indexOf("const updateDesiredConnectionMode"),
|
||
connection.indexOf("const sourceTone"),
|
||
);
|
||
assert.match(localModeHandler, /setDesiredConnectionMode\(mode\)/);
|
||
assert.doesNotMatch(localModeHandler, /await|selectConnectionMode\(|refresh\(|connect\(/);
|
||
assert.match(provisioning, /const commitDesiredModeForExplicitAction = useCallback\(async/);
|
||
assert.match(provisioning, /await selectConnectionMode\(\{/);
|
||
assert.match(provisioning, /expected_revision: expectedRevision as number/);
|
||
assert.match(provisioning, /reset_scenario: true/);
|
||
assert.match(provisioning, /const resetId = newOperationId\(\)/);
|
||
assert.match(provisioning, /reset_id: resetId/);
|
||
assert.match(provisioning, /Подключить новый K1/);
|
||
const configurationAnchor = provisioning.slice(
|
||
provisioning.indexOf('<div className="configuration-anchor">'),
|
||
provisioning.indexOf('<div className="wizard-list">'),
|
||
);
|
||
assert.doesNotMatch(configurationAnchor, /Подключить новый K1/);
|
||
assert.match(provisioning, /value=\{connectionMode\}/);
|
||
assert.doesNotMatch(
|
||
provisioning,
|
||
/disabled=\{\s*isBusy\s*\|\|\s*networkRecoveryRequired\s*\|\|\s*physicalRecoveryRequired\s*\|\|\s*connectionRecoveryRequired/,
|
||
);
|
||
assert.match(runtime, /catch \(selectionError\)[\s\S]*xgridsK1Api\.getState\(\)/);
|
||
assert.match(acquisition, /state\?\.active_connection_mode/);
|
||
assert.match(acquisition, /desiredSelectionCommitted/);
|
||
assert.match(acquisition, /configuredConnectionMode !== desiredConnectionMode/);
|
||
assert.doesNotMatch(acquisition, /Выбран другой способ связи/);
|
||
});
|
||
|
||
test("top-right device utility is an explicit pending-aware K1 scenario reset", async () => {
|
||
const { deviceRuntimeUtilityAction } = await server.ssrLoadModule(
|
||
"/src/components/useApplicationPanelActions.ts",
|
||
);
|
||
let resetCalls = 0;
|
||
let refreshCalls = 0;
|
||
const reset = deviceRuntimeUtilityAction({
|
||
refreshRuntime: () => {
|
||
refreshCalls += 1;
|
||
},
|
||
resetConnectionScenario: async () => {
|
||
resetCalls += 1;
|
||
return true;
|
||
},
|
||
connectionScenarioResetting: false,
|
||
});
|
||
|
||
assert.equal(reset.label, "Сбросить подключение");
|
||
assert.equal(reset.icon, "refresh");
|
||
assert.equal(reset.disabled, undefined);
|
||
reset.onClick();
|
||
await Promise.resolve();
|
||
assert.equal(resetCalls, 1);
|
||
assert.equal(refreshCalls, 0);
|
||
|
||
const pending = deviceRuntimeUtilityAction({
|
||
refreshRuntime: () => {
|
||
refreshCalls += 1;
|
||
},
|
||
resetConnectionScenario: async () => true,
|
||
connectionScenarioResetting: true,
|
||
});
|
||
assert.equal(pending.label, "Сбрасываем подключение");
|
||
assert.equal(pending.icon, "activity");
|
||
assert.equal(pending.disabled, true);
|
||
pending.onClick();
|
||
await Promise.resolve();
|
||
assert.equal(resetCalls, 1);
|
||
assert.equal(refreshCalls, 0);
|
||
|
||
const app = readFileSync(join(coreSourceRoot, "App.tsx"), "utf8");
|
||
const contracts = readFileSync(
|
||
join(coreSourceRoot, "core/runtime/contracts.ts"),
|
||
"utf8",
|
||
);
|
||
const runtimeContext = readFileSync(
|
||
join(pluginFrontendRoot, "runtimeContext.tsx"),
|
||
"utf8",
|
||
);
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const connection = readFileSync(
|
||
join(pluginFrontendRoot, "XgridsK1Connection.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(contracts, /resetConnectionScenario\?: \(\) => Promise<boolean>/);
|
||
assert.match(app, /resetConnectionScenario: runtime\.resetConnectionScenario/);
|
||
assert.match(app, /connectionScenarioResetting: runtime\.pendingAction === "mode"/);
|
||
assert.match(
|
||
runtimeContext,
|
||
/resetConnectionScenario: controller\.resetConnectionScenario/,
|
||
);
|
||
|
||
const resetStart = runtime.indexOf("const resetConnectionScenario");
|
||
const resetEnd = runtime.indexOf(
|
||
"const prepareConnectionReconfigurationWithResult",
|
||
resetStart,
|
||
);
|
||
assert.notEqual(resetStart, -1);
|
||
assert.notEqual(resetEnd, -1);
|
||
const resetFlow = runtime.slice(resetStart, resetEnd);
|
||
assert.equal((resetFlow.match(/selectConnectionMode\(/g) ?? []).length, 1);
|
||
assert.match(resetFlow, /connection_mode: DEFAULT_CONNECTION_MODE/);
|
||
assert.match(resetFlow, /expected_revision: expectedRevision as number/);
|
||
assert.match(resetFlow, /reset_scenario: true/);
|
||
assert.match(resetFlow, /reset_id: newOperationId\(\)/);
|
||
assert.doesNotMatch(
|
||
resetFlow,
|
||
/refresh\(|getState\(|scan|verify|connect\(|prepare|start|stop|camera/i,
|
||
);
|
||
|
||
const selectStart = runtime.indexOf("const selectConnectionMode");
|
||
const selectEnd = runtime.indexOf("const resetConnectionScenario", selectStart);
|
||
const selectFlow = runtime.slice(selectStart, selectEnd);
|
||
assert.match(selectFlow, /expected_snapshot_runtime_id: expectedSnapshotRuntimeId\(\)/);
|
||
assert.match(selectFlow, /supersedePending: request\.reset_scenario === true/);
|
||
|
||
const runStart = runtime.indexOf("const run = useCallback");
|
||
const runEnd = runtime.indexOf("const scanWithResult", runStart);
|
||
const runFlow = runtime.slice(runStart, runEnd);
|
||
assert.match(runFlow, /setPendingAction\(action\)/);
|
||
assert.match(runFlow, /setPresentedErrorCorrelation\(null\)/);
|
||
assert.match(runFlow, /setError\(null\)/);
|
||
assert.match(runFlow, /setErrorDiagnostic\(null\)/);
|
||
assert.match(runFlow, /setPendingAction\(null\)/);
|
||
|
||
const draftFenceStart = provisioning.indexOf("const nextFence = localProvisioningDraftFenceKey");
|
||
assert.notEqual(draftFenceStart, -1);
|
||
const draftFence = provisioning.slice(draftFenceStart, draftFenceStart + 2_700);
|
||
assert.match(draftFence, /reconfigurationRevision/);
|
||
assert.match(draftFence, /setSelectedDeviceId\(""\)/);
|
||
assert.match(draftFence, /setSelectedDeviceSnapshot\(null\)/);
|
||
assert.match(draftFence, /setExplicitProvisioningDraft\(null\)/);
|
||
assert.match(draftFence, /setSsid\(""\)/);
|
||
assert.match(draftFence, /setPassword\(""\)/);
|
||
assert.match(draftFence, /setCandidateUnavailableMessage\(null\)/);
|
||
assert.match(draftFence, /resetSearchPresentation\(\)/);
|
||
assert.match(
|
||
provisioning,
|
||
/const hydratedScenarioResetPresentationKey = useRef<string \| null>\(null\)/,
|
||
);
|
||
assert.match(
|
||
provisioning,
|
||
/hydratedScenarioResetPresentationKey\.current = scenarioResetPresentationKey/,
|
||
);
|
||
assert.match(provisioning, /setConnectionAttemptPresentation\(null\)/);
|
||
assert.match(
|
||
provisioning,
|
||
/const modeResetInFlight = pendingAction === "mode" \|\| modeResetPending !== null/,
|
||
);
|
||
assert.match(provisioning, /if \(modeResetInFlight\) return/);
|
||
assert.equal((provisioning.match(/disabled=\{modeResetInFlight\}/g) ?? []).length, 1);
|
||
assert.match(provisioning, /disabled=\{isBusy \|\| modeResetInFlight\}/);
|
||
|
||
assert.match(connection, /const hydratedScenarioResetKey = useRef<string \| null>\(null\)/);
|
||
assert.match(
|
||
connection,
|
||
/scenarioReset\.revision === state\?\.desired_connection_mode_revision/,
|
||
);
|
||
assert.match(connection, /scenarioReset\.desired_mode === backendDesiredMode/);
|
||
assert.match(connection, /desiredModeLocallyDirty\.current = false/);
|
||
assert.match(connection, /setDesiredConnectionMode\(backendDesiredMode\)/);
|
||
});
|
||
|
||
test("background polling stays read-only while backend state reconciliation retires terminal control", () => {
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
|
||
const refreshFlow = runtime.slice(
|
||
runtime.indexOf("const refresh = useCallback"),
|
||
runtime.indexOf("const run = useCallback"),
|
||
);
|
||
assert.match(runtime, /refresh\(false\)/);
|
||
assert.match(runtime, /refresh\(true\)/);
|
||
assert.doesNotMatch(runtime, /terminalControlCleanupInFlight/);
|
||
assert.doesNotMatch(
|
||
refreshFlow,
|
||
/closeApplicationControlSession|startAcquisition|stopAcquisition|networkProvision/,
|
||
);
|
||
assert.match(refreshFlow, /xgridsK1Api\.getState\(\)/);
|
||
});
|
||
|
||
test("automatic K1 live start opens the selected delivered camera despite an older saved layout", () => {
|
||
const app = readFileSync(join(coreSourceRoot, "App.tsx"), "utf8");
|
||
const layout = readFileSync(
|
||
join(coreSourceRoot, "core/observation/useObservationLayout.ts"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(app, /observationLayout\.activateAutomaticDefaults\(\)/);
|
||
assert.match(layout, /const activateAutomaticDefaults = useCallback/);
|
||
assert.match(layout, /restoredLayoutAuthorityRef\.current = false/);
|
||
assert.match(layout, /sources\.filter\(canOpenByDefault\)/);
|
||
assert.match(layout, /source\.capabilities\.overlay/);
|
||
});
|
||
|
||
test("K1 connect errors preserve the result and expose explicit recovery", () => {
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const connectStart = runtime.indexOf("const connect = useCallback(");
|
||
const connectEnd = runtime.indexOf("const verifyConnection = useCallback(", connectStart);
|
||
assert.notEqual(connectStart, -1);
|
||
assert.notEqual(connectEnd, -1);
|
||
const connectFlow = runtime.slice(connectStart, connectEnd);
|
||
|
||
assert.match(connectFlow, /observeProvisioningRequest\(/);
|
||
assert.match(connectFlow, /operation\?\.status !== "succeeded"/);
|
||
assert.match(connectFlow, /networkProvisionFailureMessage\(/);
|
||
assert.doesNotMatch(
|
||
connectFlow,
|
||
/требует ручной проверки|измените параметры только после проверки устройства|Сохранён тот же ключ/,
|
||
);
|
||
const networkFlow = readFileSync(join(pluginFrontendRoot, "networkProvisioning.ts"), "utf8");
|
||
assert.match(networkFlow, /Проверьте состояние K1 через «Переподключиться»/);
|
||
assert.match(networkFlow, /новое подключение через поиск Bluetooth/);
|
||
assert.equal((networkFlow.match(/await context\.send\(/g) ?? []).length, 1);
|
||
const verifyStart = runtime.indexOf("const verifyConnection = useCallback(");
|
||
const verifyEnd = runtime.indexOf("const probeConfiguredEndpoint = useCallback(", verifyStart);
|
||
const verifyFlow = runtime.slice(verifyStart, verifyEnd);
|
||
assert.match(verifyFlow, /failedOperation\?\.status === "succeeded"/);
|
||
assert.doesNotMatch(verifyFlow, /xgridsK1Api\.verifyConnection\([^)]*\)[\s\S]*xgridsK1Api\.verifyConnection/);
|
||
assert.doesNotMatch(runtime, /automatic.?retry\s*:\s*true/);
|
||
});
|
||
|
||
test("K1 provisioning keeps the operator draft separate from the backend lease", () => {
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const selection = provisioning.slice(
|
||
provisioning.indexOf("const selectFreshDevice"),
|
||
provisioning.indexOf("const chooseAnother"),
|
||
);
|
||
const apply = provisioning.slice(
|
||
provisioning.indexOf("const submitConnect"),
|
||
provisioning.indexOf("const verifyAppliedNetwork"),
|
||
);
|
||
assert.match(provisioning, /explicitProvisioningDraftRetained/);
|
||
assert.match(provisioning, /localProvisioningDraftFenceKey/);
|
||
assert.match(provisioning, /provisioningIntentKey\(null\)/);
|
||
assert.match(provisioning, /const \[selectedDeviceSnapshot, setSelectedDeviceSnapshot\]/);
|
||
assert.doesNotMatch(selection, /await|scanWithResult\(|verifyConnection\(|connect\(/);
|
||
assert.match(selection, /requestExplicitProvisioning\(device\.device_id/);
|
||
assert.equal((apply.match(/await connect\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(apply, /scanWithResult\(|verifyConnection\(|candidateRefresh/);
|
||
assert.match(provisioning, /scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/);
|
||
assert.match(
|
||
provisioning,
|
||
/onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/,
|
||
);
|
||
});
|
||
|
||
test("K1 plugin layout follows its contribution width and contains long topology text", () => {
|
||
const styles = readFileSync(join(pluginFrontendRoot, "styles.css"), "utf8");
|
||
const baseGrid = styles.slice(
|
||
styles.indexOf(".device-workspace__grid"),
|
||
styles.indexOf(".device-workspace__side"),
|
||
);
|
||
const splitThreshold = styles.match(
|
||
/@container xgrids-k1 \(min-width:\s*([0-9.]+)rem\)/,
|
||
);
|
||
const splitColumns = styles.match(
|
||
/grid-template-columns:\s*minmax\(([0-9.]+)rem,\s*0\.8fr\)\s*minmax\(([0-9.]+)rem,\s*1\.2fr\)/,
|
||
);
|
||
|
||
assert.match(styles, /container:\s*xgrids-k1\s*\/\s*inline-size/);
|
||
assert.match(styles, /container:\s*k1-connection-panel\s*\/\s*inline-size/);
|
||
assert.match(styles, /container:\s*k1-session-panel\s*\/\s*inline-size/);
|
||
assert.match(styles, /@container xgrids-k1 \(min-width:\s*78rem\)/);
|
||
assert.match(
|
||
styles,
|
||
/grid-template-columns:\s*minmax\(32rem,\s*0\.8fr\)\s*minmax\(38rem,\s*1\.2fr\)/,
|
||
);
|
||
assert.match(styles, /@container k1-connection-panel \(max-width:\s*48rem\)/);
|
||
assert.match(styles, /@container k1-session-panel \(max-width:\s*48rem\)/);
|
||
assert.match(styles, /@container xgrids-k1 \(max-width:\s*48rem\)/);
|
||
assert.match(styles, /overflow-wrap:\s*anywhere/);
|
||
assert.match(baseGrid, /grid-template-columns:\s*minmax\(0,\s*1fr\)/);
|
||
assert.ok(splitThreshold);
|
||
assert.ok(splitColumns);
|
||
const splitThresholdPixels = Number(splitThreshold[1]) * 16;
|
||
const minimumSplitPixels = (Number(splitColumns[1]) + Number(splitColumns[2]) + 0.85) * 16;
|
||
assert.equal(splitThresholdPixels, 1248);
|
||
assert.ok(minimumSplitPixels < splitThresholdPixels);
|
||
assert.ok(390 < splitThresholdPixels);
|
||
assert.ok(760 < splitThresholdPixels);
|
||
assert.ok(1280 > splitThresholdPixels);
|
||
assert.match(
|
||
styles,
|
||
/\.wizard-list,[\s\S]*?\.wizard-step,[\s\S]*?\.session-form,[\s\S]*?\.device-row,[\s\S]*?\.detail-list\s*\{[^}]*min-width:\s*0[^}]*max-width:\s*100%/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/> \*\s*\{[^}]*min-width:\s*0[^}]*max-width:\s*100%/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.metrics-grid > \*,[\s\S]*?\.device-workspace__grid > \*,[\s\S]*?\.diagnostics-grid > \*\s*\{[^}]*min-width:\s*0[^}]*max-width:\s*100%/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.device-row code,[\s\S]*?\.detail-row code\s*\{[^}]*overflow-wrap:\s*anywhere[^}]*white-space:\s*normal/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.detail-row dd\s*\{[^}]*overflow-wrap:\s*anywhere[^}]*text-overflow:\s*clip[^}]*white-space:\s*normal/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.error-banner__actions > \.nodedc-button\s*\{[^}]*min-width:\s*0[^}]*max-width:\s*100%[^}]*overflow-wrap:\s*anywhere/,
|
||
);
|
||
assert.doesNotMatch(
|
||
styles,
|
||
/\.connection-panel,[\s\S]*?\.session-panel\s*\{[^}]*overflow:\s*(?:clip|hidden)/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/@container k1-connection-panel \(max-width:\s*48rem\)[\s\S]*?\.wizard-step__content > header[^}]*flex-wrap:\s*wrap/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/@container k1-connection-panel \(max-width:\s*48rem\)[\s\S]*?\.retained-recovery-target > div[^}]*flex-direction:\s*column/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/@container k1-session-panel \(max-width:\s*48rem\)[\s\S]*?\.panel-heading[^}]*flex-wrap:\s*wrap/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.connection-summary__value > \.nodedc-status,[\s\S]*?white-space:\s*normal/,
|
||
);
|
||
assert.doesNotMatch(styles, /\.nodedc-checker(?:__copy|__label)?\s*\{/);
|
||
assert.match(
|
||
styles,
|
||
/\.workspace-lead__status > span,[\s\S]*?\.nodedc-field__description,[\s\S]*?\.retained-recovery-target small,[\s\S]*?\.session-footer p\s*\{[^}]*overflow-wrap:\s*anywhere/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/@container xgrids-k1 \(max-width:\s*32rem\)[\s\S]*?\.error-banner__actions\s*\{[^}]*align-items:\s*stretch[^}]*flex-direction:\s*column/,
|
||
);
|
||
assert.doesNotMatch(styles, /@media \(max-width:\s*(?:1280|1480)px\)/);
|
||
assert.doesNotMatch(styles, /device-recovery-choice/);
|
||
});
|
||
|
||
test("Mission Core shell protects the device workspace before the shared mobile breakpoint", () => {
|
||
const responsive = readFileSync(join(coreSourceRoot, "styles/responsive.css"), "utf8");
|
||
|
||
assert.match(
|
||
responsive,
|
||
/@media \(min-width:\s*761px\) and \(max-width:\s*929px\)/,
|
||
);
|
||
assert.match(
|
||
responsive,
|
||
/\.nodedc-app-shell__navigation,\s*\.nodedc-app-shell__content\s*\{[^}]*left:\s*var\(--nodedc-app-page-pad\)[^}]*width:\s*auto/s,
|
||
);
|
||
assert.match(
|
||
responsive,
|
||
/\[data-content-open="true"\] \.nodedc-app-shell__navigation\s*\{[^}]*opacity:\s*0[^}]*pointer-events:\s*none/s,
|
||
);
|
||
assert.match(
|
||
responsive,
|
||
/@media \(max-width:\s*760px\)[\s\S]*?\.nodedc-application-panel__head,\s*\.nodedc-application-panel__body\s*\{[^}]*width:\s*auto[^}]*min-width:\s*0[^}]*max-width:\s*100%/,
|
||
);
|
||
assert.match(
|
||
responsive,
|
||
/\.nodedc-application-panel__head\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s*auto/,
|
||
);
|
||
});
|
||
|
||
test("K1 frontend state models durable mutation and connection supervision facts", () => {
|
||
const api = readFileSync(join(pluginFrontendRoot, "api.ts"), "utf8");
|
||
const lifecycle = readFileSync(join(pluginFrontendRoot, "lifecycle.ts"), "utf8");
|
||
|
||
assert.match(api, /interface XgridsNetworkMutationLedger/);
|
||
assert.match(api, /scope:\s*"durable-ledger"/);
|
||
assert.match(api, /network_mutation_ledger\?: XgridsNetworkMutationLedger \| null/);
|
||
assert.match(api, /handle_retained\?: boolean/);
|
||
assert.match(api, /gatt_validated_recently\?: boolean/);
|
||
assert.match(api, /interface XgridsConnectionSupervisor/);
|
||
assert.match(api, /interface XgridsConnectionSupervisorDeviceNetwork/);
|
||
assert.match(api, /device_network: XgridsConnectionSupervisorDeviceNetwork/);
|
||
assert.match(api, /missioncore\.k1-connection-supervisor\/v1/);
|
||
assert.match(api, /connection_supervisor\?: XgridsConnectionSupervisor \| null/);
|
||
assert.match(api, /interface XgridsConnectionPolicy/);
|
||
assert.match(api, /missioncore\.xgrids-k1-connection-policy\/v1/);
|
||
assert.match(api, /connection_policy\?: XgridsConnectionPolicy \| null/);
|
||
assert.match(api, /interface XgridsSemanticTopologyStore/);
|
||
assert.match(api, /configured_offline_evidence: boolean/);
|
||
assert.match(api, /live_connection_authority: false/);
|
||
assert.match(api, /semantic_topology_store\?: XgridsSemanticTopologyStore \| null/);
|
||
|
||
const reconciliation = lifecycle.slice(
|
||
lifecycle.indexOf("export function readOnlyVerificationClearedReconciliation"),
|
||
lifecycle.indexOf("export function provisioningCandidateById"),
|
||
);
|
||
assert.match(reconciliation, /nextState\?\.network_mutation_ledger/);
|
||
assert.match(reconciliation, /nextLedger\.status === "resolved"/);
|
||
assert.match(reconciliation, /nextLedger\.operation_id === previousOperationId/);
|
||
assert.doesNotMatch(reconciliation, /snapshot_runtime_id/);
|
||
});
|
||
|
||
test("Bridge device and network reconfiguration stays backend-owned and CAS-fenced", () => {
|
||
const api = readFileSync(join(pluginFrontendRoot, "api.ts"), "utf8");
|
||
const manifest = readFileSync(join(pluginFrontendRoot, "manifest.ts"), "utf8");
|
||
const lifecycle = readFileSync(join(pluginFrontendRoot, "lifecycle.ts"), "utf8");
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const prepare = provisioning.slice(
|
||
provisioning.indexOf("const prepareReconfiguration"),
|
||
provisioning.indexOf("const changeDesiredConnectionMode"),
|
||
);
|
||
const selection = provisioning.slice(
|
||
provisioning.indexOf("const selectFreshDevice"),
|
||
provisioning.indexOf("const chooseAnother"),
|
||
);
|
||
assert.match(manifest, /connectionReconfigurePrepare:[\s\S]*"connection\.reconfigure\.prepare"/);
|
||
assert.match(api, /expected_reconfiguration_revision: number/);
|
||
assert.match(api, /expected_reconfiguration_intent_id/);
|
||
assert.match(prepare, /prepareConnectionReconfigurationWithResult\(\{/);
|
||
assert.doesNotMatch(prepare, /scanWithResult\(|verifyConnection\(|connect\(/);
|
||
assert.doesNotMatch(selection, /await|scanWithResult\(|verifyConnection\(|connect\(/);
|
||
assert.match(provisioning, /showNetworkStep = Boolean\([\s\S]*changeNetworkDialogue/);
|
||
assert.match(provisioning, /localProvisioningDraftFenceKey/);
|
||
assert.match(lifecycle, /reconfigurationAllowsFreshDevice/);
|
||
assert.match(runtime, /prepareConnectionReconfiguration/);
|
||
});
|
||
|
||
test("shared runtime exposes only a reachable K1 endpoint as active", () => {
|
||
const runtimeContext = readFileSync(
|
||
join(pluginFrontendRoot, "runtimeContext.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(
|
||
runtimeContext,
|
||
/endpointLabel: activeConnectionEndpointLabel\(state\)/,
|
||
);
|
||
assert.doesNotMatch(runtimeContext, /endpointLabel: state\.k1_ip/);
|
||
});
|
||
|
||
test("every K1 connection and acquisition action is fenced to the accepted backend runtime", () => {
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(runtime, /latestState\.current\?\.snapshot_runtime_id/);
|
||
assert.equal(
|
||
[...runtime.matchAll(
|
||
/expected_snapshot_runtime_id: expectedSnapshotRuntimeId\(\)/g,
|
||
)].length,
|
||
14,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/mode:\s*"graceful",[\s\S]*?expected_snapshot_runtime_id: checkpoint\.snapshotRuntimeId/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/xgridsK1Api\.scanBle\([\s\S]*?expected_snapshot_runtime_id/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/xgridsK1Api\.verifyConnection\([\s\S]*?expected_snapshot_runtime_id/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/xgridsK1Api\.connect\([\s\S]*?expected_snapshot_runtime_id/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/xgridsK1Api\.retireUnavailablePhysicalCommand\([\s\S]*?expected_snapshot_runtime_id:\s*exactSnapshotRuntimeId/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/run\("retire",[\s\S]*?surfaceErrors: false[\s\S]*?await refresh\(false\)/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/const exactSnapshotRuntimeId = actionSnapshotRuntimeId\.trim\(\)[\s\S]*?isSnapshotRuntimeCurrent\(exactSnapshotRuntimeId\)[\s\S]*?xgridsK1Api\.reopenRetiredPhysicalReconciliation\([\s\S]*?expected_snapshot_runtime_id: exactSnapshotRuntimeId/,
|
||
);
|
||
for (const action of [
|
||
"openApplicationControlSession",
|
||
"enterApplicationWorkspace",
|
||
"closeApplicationControlSession",
|
||
"prepareAcquisition",
|
||
"startAcquisition",
|
||
"abortAcquisition",
|
||
"reconcilePhysicalCommand",
|
||
]) {
|
||
assert.match(
|
||
runtime,
|
||
new RegExp(`xgridsK1Api\\.${action}\\([\\s\\S]*?expected_snapshot_runtime_id`),
|
||
action,
|
||
);
|
||
}
|
||
assert.equal(
|
||
[...runtime.matchAll(
|
||
/expected_snapshot_runtime_id: actionSnapshotRuntimeId/g,
|
||
)].length,
|
||
4,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/stopSessionCompatibility\(\{[\s\S]*?expected_snapshot_runtime_id/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/run\([\s\S]*?"reopen"[\s\S]*?surfaceErrors: false, supersedePending: true[\s\S]*?await refresh\(false\)/,
|
||
);
|
||
assert.match(
|
||
runtime,
|
||
/const actionSnapshotRuntimeId =\s*options\.expectedSnapshotRuntimeId\?\.trim\(\) \|\| null[\s\S]*?expected_snapshot_runtime_id:\s*actionSnapshotRuntimeId \?\? expectedSnapshotRuntimeId\(\)/,
|
||
);
|
||
});
|
||
|
||
test("fresh Scan treats an exact prior K1 as one local Select action", () => {
|
||
const provisioning = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const lifecycle = readFileSync(join(pluginFrontendRoot, "lifecycle.ts"), "utf8");
|
||
const runtime = readFileSync(
|
||
join(pluginFrontendRoot, "useXgridsK1Runtime.ts"),
|
||
"utf8",
|
||
);
|
||
const selection = provisioning.slice(
|
||
provisioning.indexOf("const selectFreshDevice"),
|
||
provisioning.indexOf("const chooseAnother"),
|
||
);
|
||
const apply = provisioning.slice(
|
||
provisioning.indexOf("const submitConnect"),
|
||
provisioning.indexOf("const verifyAppliedNetwork"),
|
||
);
|
||
assert.match(selection, /requestExplicitProvisioning\(device\.device_id/);
|
||
assert.match(selection, /const actionableDevices = devices\.filter\(candidateSelectionAllowed\)/);
|
||
assert.doesNotMatch(
|
||
selection,
|
||
/physicallyRetired|retiredPhysical|await|verifyConnection\(|retireUnavailable|reopenRetired/,
|
||
);
|
||
const resultRows = provisioning.slice(
|
||
provisioning.indexOf('<div className="device-list" aria-label="Результаты Bluetooth">'),
|
||
provisioning.indexOf('<div className="empty-state" role="status">'),
|
||
);
|
||
assert.match(resultRows, /actionLabel="Выбрать"/);
|
||
assert.match(resultRows, /onSelect=\{\(\) => selectCandidate\(device\)\}/);
|
||
assert.doesNotMatch(resultRows, /Переподключиться|reopen|verifyConnection/);
|
||
assert.doesNotMatch(provisioning, /recoverRetiredPhysicalCandidate/);
|
||
assert.doesNotMatch(apply, /verifyConnection\(|retireUnavailable|reopenRetired/);
|
||
assert.match(provisioning, /Переподключиться/);
|
||
assert.match(provisioning, /Подключить новый K1/);
|
||
assert.doesNotMatch(provisioning, /Вернуть прежний K1 и проверить/);
|
||
assert.match(runtime, /reopenRetiredPhysicalReconciliation/);
|
||
assert.match(runtime, /retireUnavailablePhysicalCommand/);
|
||
assert.match(lifecycle, /retiredPhysicalReopenAuthority/);
|
||
});
|
||
|
||
test("K1 orchestration accepts backend recovery but still requires exact topology before physical START", () => {
|
||
const acquisition = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1AcquisitionPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const diagnostics = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1Diagnostics.tsx"),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(acquisition, /currentAppliedConnectionTopology\(state\)/);
|
||
assert.ok(
|
||
[...acquisition.matchAll(/!connectionConfigured/g)].length >= 2,
|
||
"handler and button must both reject absent or configured-offline topology",
|
||
);
|
||
assert.match(acquisition, /desiredModeMatchesActive/);
|
||
assert.match(acquisition, /modeSwitchRequired/);
|
||
assert.doesNotMatch(acquisition, /Выбран другой способ связи/);
|
||
assert.match(acquisition, /prepareCanonicalAcquisition/);
|
||
assert.match(acquisition, /startPreparedAcquisition/);
|
||
assert.match(acquisition, /operatorActionPhysicalAcceptance\(\)/);
|
||
assert.doesNotMatch(acquisition, /K1PhysicalCommandConfirmation/);
|
||
assert.match(acquisition, /connectionPolicyAllows\(state, "start-acquisition"\)/);
|
||
assert.match(acquisition, /canIssueCanonicalStop\(state, physicalStopIntentSpent\)/);
|
||
assert.match(acquisition, /physicalStopInFlight \|\| physicalStopExecutable/);
|
||
assert.match(acquisition, /connectionPolicyAllows\(state, "stop-local-receiver"\)/);
|
||
assert.match(acquisition, /if \(finalStartTarget\)/);
|
||
assert.match(acquisition, /if \(physicalStopExecutable\)/);
|
||
assert.match(acquisition, /if \(!physicalStartAllowed\) return;/);
|
||
assert.doesNotMatch(acquisition, /physicalStopAllowed/);
|
||
const replaySubmit = acquisition.slice(
|
||
acquisition.indexOf("const submitReplay ="),
|
||
acquisition.indexOf("return (", acquisition.indexOf("const submitReplay =")),
|
||
);
|
||
assert.doesNotMatch(
|
||
replaySubmit,
|
||
/connectionPolicyAllows|physicalStartAllowed|physicalStopExecutable/,
|
||
);
|
||
assert.match(acquisition, /void stopLocalReceiver\(\);/);
|
||
assert.match(acquisition, /onClick=\{\(\) => void abort\(\)\}/);
|
||
assert.doesNotMatch(acquisition, /acknowledge-data-loss/);
|
||
assert.doesNotMatch(acquisition, /PHYSICAL_ACCEPTANCE/);
|
||
assert.doesNotMatch(acquisition, /!state\?\.k1_ip/);
|
||
assert.match(diagnostics, /activeConnectionEndpointLabel\(state\)/);
|
||
assert.match(diagnostics, /Адрес конфигурации/);
|
||
assert.match(diagnostics, /связь не подтверждена/);
|
||
assert.doesNotMatch(diagnostics, /state\?\.k1_ip/);
|
||
});
|
||
|
||
test("one explicit K1 action performs START or STOP without a redundant checklist modal", () => {
|
||
const acquisition = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1AcquisitionPipeline.tsx"),
|
||
"utf8",
|
||
);
|
||
const spatial = readFileSync(
|
||
join(pluginFrontendRoot, "components/K1SpatialControls.tsx"),
|
||
"utf8",
|
||
);
|
||
const confirmation = readFileSync(
|
||
join(pluginFrontendRoot, "physicalCommandConfirmation.ts"),
|
||
"utf8",
|
||
);
|
||
const styles = readFileSync(join(pluginFrontendRoot, "styles.css"), "utf8");
|
||
|
||
assert.equal(
|
||
existsSync(join(pluginFrontendRoot, "components/K1PhysicalCommandConfirmation.tsx")),
|
||
false,
|
||
);
|
||
assert.doesNotMatch(acquisition, /K1PhysicalCommandConfirmation|ConfirmationModal/);
|
||
assert.doesNotMatch(spatial, /K1PhysicalCommandConfirmation|ConfirmationModal/);
|
||
assert.match(acquisition, /operatorActionPhysicalAcceptance\(\)/);
|
||
assert.match(acquisition, /await submitFinalStart\(\)/);
|
||
assert.match(spatial, /stop\(operatorActionPhysicalAcceptance\(\)\)/);
|
||
assert.match(spatial, /physicalStopExecutable/);
|
||
assert.match(spatial, /canIssueCanonicalStop\(state, physicalStopIntentSpent\)/);
|
||
assert.match(spatial, /physicalStopInFlight \|\| physicalStopExecutable/);
|
||
assert.match(spatial, /connectionPolicyAllows\(state, "stop-local-receiver"\)/);
|
||
assert.doesNotMatch(spatial, /Физическая остановка K1 недоступна/);
|
||
assert.match(spatial, /onClick=\{\(\) => void stopLocalReceiver\(\)\}/);
|
||
assert.match(spatial, /Завершить локальный приём/);
|
||
assert.doesNotMatch(spatial, /Повторить остановку/);
|
||
assert.match(acquisition, /canIssueCanonicalStop\(state, physicalStopIntentSpent\)/);
|
||
assert.match(acquisition, /stopLocalReceiver/);
|
||
assert.match(acquisition, /Повторная команда устройству не отправляется/);
|
||
assert.doesNotMatch(spatial, /acknowledge-data-loss/);
|
||
assert.match(confirmation, /operatorActionPhysicalAcceptance/);
|
||
assert.match(confirmation, /operator_present:\s*true/);
|
||
assert.match(confirmation, /acquisition\.state !== "prepared"/);
|
||
assert.match(confirmation, /control\.state !== "project-ready"/);
|
||
assert.match(confirmation, /latest_device_session_state/);
|
||
assert.match(confirmation, /acquisition_start_allowed !== true/);
|
||
assert.doesNotMatch(styles, /xgrids-k1-physical-confirmation/);
|
||
});
|
||
|
||
test("generic Control Station has one composition import and no K1 implementation knowledge", () => {
|
||
const compositionPath = join(coreSourceRoot, "composition/devicePlugins.ts");
|
||
const composition = readFileSync(compositionPath, "utf8");
|
||
assert.match(composition, /from "@xgrids-k1\/frontend\/plugin"/);
|
||
|
||
for (const path of sourceFiles(coreSourceRoot)) {
|
||
if (path === compositionPath) continue;
|
||
const source = readFileSync(path, "utf8");
|
||
assert.doesNotMatch(source, /xgrids|lixel|\bk1\b/i, path);
|
||
}
|
||
|
||
for (const path of sourceFiles(pluginFrontendRoot)) {
|
||
const source = readFileSync(path, "utf8");
|
||
assert.doesNotMatch(source, /apps\/control-station|\.\.\/\.\.\/core|\.\.\/\.\.\/components/, path);
|
||
}
|
||
});
|