feat(k1): complete primary acquisition lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 23:03:59 +03:00
parent 9d51080d2e
commit aa3680948f
66 changed files with 6093 additions and 544 deletions
@@ -10,6 +10,9 @@ let xgridsK1Manifest;
let xgridsK1Actions;
let xgridsK1Api;
let lifecycle;
let projectName;
let automaticSourceStart;
let presentation;
before(async () => {
server = await createServer({
@@ -29,6 +32,15 @@ before(async () => {
lifecycle = await server.ssrLoadModule(
"@xgrids-k1/frontend/lifecycle.ts",
);
projectName = await server.ssrLoadModule(
"@xgrids-k1/frontend/projectName.ts",
);
automaticSourceStart = await server.ssrLoadModule(
"@xgrids-k1/frontend/automaticSourceStart.ts",
);
presentation = await server.ssrLoadModule(
"@xgrids-k1/frontend/presentation.ts",
);
({ xgridsK1Api } = await server.ssrLoadModule(
"@xgrids-k1/frontend/api.ts",
));
@@ -279,6 +291,100 @@ test("prepared acquisition resumes without another prepare and remains recoverab
assert.equal(lifecycle.recoverableAcquisition(prepared)?.acquisition_id, "acq-1");
});
test("project name is canonicalized and rejected outside the bounded safe contract", () => {
assert.deepEqual(projectName.validateProjectName(" Mission 01 "), {
value: "Mission 01",
error: null,
});
assert.match(projectName.validateProjectName("line\nbreak").error, /управляющие/);
assert.match(projectName.validateProjectName("\ud800").error, /управляющие/);
assert.match(projectName.validateProjectName("x".repeat(97)).error, /не длиннее 96/);
assert.match(projectName.validateProjectName(" \t ").error, /Введите название/);
});
test("automatic spatial source replaces the old scene only after a successful start", async () => {
const failedEvents = [];
assert.equal(await automaticSourceStart.runAutomaticSpatialSourceStart(
async () => {
failedEvents.push("start");
return false;
},
() => failedEvents.push("activate"),
() => failedEvents.push("open"),
), false);
assert.deepEqual(failedEvents, ["start"]);
const successfulEvents = [];
assert.equal(await automaticSourceStart.runAutomaticSpatialSourceStart(
async () => {
successfulEvents.push("start");
return true;
},
() => successfulEvents.push("activate"),
() => successfulEvents.push("open"),
), true);
assert.deepEqual(successfulEvents, ["start", "activate", "open"]);
});
test("vendor commands fail closed unless the profile and acquisition both enable them", () => {
const capability = {
compatibility: {
vendor_writes_enabled: true,
permitted_mode: "active-control",
},
};
assert.equal(lifecycle.isVendorWriteCapable(capability), true);
assert.equal(lifecycle.isVendorWriteCapable({
compatibility: { vendor_writes_enabled: true, permitted_mode: "read-only" },
}), false);
assert.equal(lifecycle.isSoftwareCommandedAcquisition({
...capability,
acquisition: { control_mode: "operator-manual" },
}), false);
assert.equal(lifecycle.isSoftwareCommandedAcquisition({
...capability,
acquisition: { control_mode: "plugin-commanded" },
}), true);
});
test("device modeling telemetry maps only finite non-negative values", () => {
assert.deepEqual(presentation.deviceTelemetry({
device_elapsed_seconds: 12.5,
device_route_distance_meters: 8.25,
device_speed_meters_per_second: 0.75,
}), {
elapsedSeconds: 12.5,
routeDistanceMeters: 8.25,
speedMetersPerSecond: 0.75,
});
assert.deepEqual(presentation.deviceTelemetry({
device_elapsed_seconds: -1,
device_route_distance_meters: Number.NaN,
device_speed_meters_per_second: Number.POSITIVE_INFINITY,
}), {
elapsedSeconds: null,
routeDistanceMeters: null,
speedMetersPerSecond: null,
});
});
test("spatial K1 action failures have an explicit retry-safe presentation", () => {
assert.equal(presentation.spatialActionFailure(null), null);
assert.equal(presentation.spatialActionFailure(" "), null);
assert.deepEqual(presentation.spatialActionFailure(" stop failed "), {
title: "Действие K1 не выполнено",
detail: "stop failed",
});
assert.equal(lifecycle.shouldRenderSpatialControls({
source_mode: "live",
acquisition: { state: "failed", cleanup_pending: true },
}), true);
assert.equal(lifecycle.shouldRenderSpatialControls({
source_mode: "idle",
acquisition: { state: "failed", cleanup_pending: false },
}), false);
});
test("replay ignores a stale failed live acquisition", () => {
const replay = {
source_mode: "replay",
@@ -343,6 +449,7 @@ test("device mutations send explicit nested compatibility attestation", async ()
idempotency_key: "network-provision:test",
});
await xgridsK1Api.prepareAcquisition({
project_name: "Mission 01",
compatibility_attestation: attestation,
});
} finally {
@@ -355,4 +462,5 @@ test("device mutations send explicit nested compatibility attestation", async ()
assert.deepEqual(provisioning.input.compatibility_attestation, attestation);
assert.equal(provisioning.input.idempotency_key, "network-provision:test");
assert.deepEqual(prepare.input.compatibility_attestation, attestation);
assert.equal(prepare.input.project_name, "Mission 01");
});
@@ -39,7 +39,13 @@ after(async () => {
await server?.close();
});
function syntheticPlugin({ pluginId, modelId, componentKey, connectionView }) {
function syntheticPlugin({
pluginId,
modelId,
componentKey,
connectionView,
spatialControlsView,
}) {
return {
manifest: {
apiVersion: "missioncore.nodedc/v1alpha2",
@@ -69,6 +75,7 @@ function syntheticPlugin({ pluginId, modelId, componentKey, connectionView }) {
},
RuntimeProvider: ({ children }) => children,
connectionViews: { [componentKey]: connectionView },
...(spatialControlsView ? { SpatialControlsView: spatialControlsView } : {}),
};
}
@@ -98,6 +105,35 @@ test("each device plugin contributes its own connection pipeline component", ()
);
});
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), []);
@@ -108,10 +144,22 @@ test("XGRIDS frontend is physically plugin-owned and split by operator pipeline"
"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", () => {
@@ -603,6 +603,25 @@ test("replay coordinator makes rapid saved-session selection latest-request-wins
coordinator.cancel();
assert.equal(third.signal.aborted, true);
assert.equal(third.isCurrent(), false);
assert.equal(third.finish(), true);
assert.equal(third.finish(), false);
});
test("an acquisition guard cancellation keeps settlement ownership", () => {
const coordinator = createObservationReplayCoordinator();
const archiveSwitch = coordinator.begin();
let replacementBlanked = false;
let replacementSettled = false;
// Models the point after onReplayBegin has released the previous viewer.
replacementBlanked = true;
coordinator.cancel();
assert.equal(archiveSwitch.signal.aborted, true);
assert.equal(archiveSwitch.isCurrent(), false);
if (archiveSwitch.finish()) replacementSettled = true;
assert.equal(replacementBlanked, true);
assert.equal(replacementSettled, true);
});
test("switching saved sessions aborts only local polling and never cancels shared backend work", async () => {
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let guard;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
guard = await server.ssrLoadModule("/src/core/runtime/acquisitionGuard.ts");
});
after(async () => {
await server?.close();
});
function runtimeState(acquisitionState, cleanupPending = false) {
return {
phase: "idle",
sourceMode: "idle",
acquisition: acquisitionState === null
? null
: {
acquisitionId: "acq-1",
deviceId: "device-1",
deviceSessionId: "device-session-1",
compatibilityProfileId: "profile-1",
controlMode: "operator-manual",
state: acquisitionState,
stateRevision: 1,
operatorInstructions: [],
cleanupPending,
},
};
}
test("every nonterminal acquisition phase blocks saved and manual source replacement", () => {
for (const state of [
"preparing",
"prepared",
"awaiting_external_start",
"starting",
"acquiring",
"awaiting_external_stop",
"stopping",
"finalizing",
]) {
assert.equal(guard.isSpatialSourceSwitchBlocked(runtimeState(state)), true, state);
}
});
test("terminal or absent acquisitions allow an explicit source selection", () => {
for (const state of ["completed", "failed", "aborted", "interrupted"]) {
assert.equal(guard.isSpatialSourceSwitchBlocked(runtimeState(state)), false, state);
}
assert.equal(guard.isSpatialSourceSwitchBlocked(runtimeState(null)), false);
assert.equal(guard.isSpatialSourceSwitchBlocked(null), false);
});
test("an unknown acquisition state fails closed", () => {
assert.equal(
guard.isSpatialSourceSwitchBlocked(runtimeState("future_vendor_phase")),
true,
);
assert.match(guard.SPATIAL_SOURCE_SWITCH_BLOCKED_REASON, /Завершите текущий приём/);
});
test("terminal acquisition with retained cleanup remains source-switch blocked", () => {
assert.equal(guard.isSpatialSourceSwitchBlocked(runtimeState("failed", true)), true);
assert.equal(guard.isSpatialSourceSwitchBlocked(runtimeState("failed", false)), false);
});
test("saved-session and manual source controls share the acquisition guard", async () => {
const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
const sessionSelectSource = await readFile(
new URL("../src/components/ObservationSessionSelect.tsx", import.meta.url),
"utf8",
);
const observationHookSource = await readFile(
new URL("../src/core/observation/useObservationSessions.ts", import.meta.url),
"utf8",
);
assert.match(appSource, /blockedReason=\{sourceSwitchBlockedReason\}/);
assert.match(appSource, /disabled=\{sourceSwitchBlocked \|\| !sourceDraft\.trim\(\)\}/);
assert.match(appSource, /if \(sourceSwitchBlockedRef\.current\) return;/);
assert.match(sessionSelectSource, /replayEnabled: blockedReason === null/);
assert.match(observationHookSource, /!replayEnabledRef\.current/);
});