181 lines
6.0 KiB
JavaScript
181 lines
6.0 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("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",
|
|
"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.match(spatialControls, /Повторить остановку/);
|
|
});
|
|
|
|
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);
|
|
}
|
|
});
|