miravo 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ProtocolName } from "@miravo/shared";
|
|
3
|
-
import {
|
|
4
|
-
type ApplyDesiredState,
|
|
5
|
-
type ApplyOptions,
|
|
6
|
-
buildApplyPlan,
|
|
7
|
-
type DesiredDevice,
|
|
8
|
-
executePlan,
|
|
9
|
-
} from "./le-apply.ts";
|
|
10
|
-
import type { LEDevice, LEDeviceHub } from "./le-devicehub.ts";
|
|
11
|
-
import type { LEDigitalTwins } from "./le-digital-twins.ts";
|
|
12
|
-
|
|
13
|
-
interface TestStaticAttributeWrite {
|
|
14
|
-
readonly ModelID?: string;
|
|
15
|
-
readonly InstanceID?: string;
|
|
16
|
-
readonly Key: string;
|
|
17
|
-
readonly Value: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface TestInstanceStaticAttributeUpdate {
|
|
21
|
-
readonly ID: string;
|
|
22
|
-
readonly ModelID?: string | null;
|
|
23
|
-
readonly InstanceID?: string | null;
|
|
24
|
-
readonly Key: string;
|
|
25
|
-
readonly Value: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function createDesiredState(deviceName: string): ApplyDesiredState {
|
|
29
|
-
const device: DesiredDevice = {
|
|
30
|
-
instanceId: "pump-001",
|
|
31
|
-
modelName: "pump",
|
|
32
|
-
protocol: "mqtt" satisfies ProtocolName,
|
|
33
|
-
deviceName,
|
|
34
|
-
browsePath: "area/line/pump-001",
|
|
35
|
-
endpoint: { host: "127.0.0.1", port: 1883 },
|
|
36
|
-
properties: [],
|
|
37
|
-
tags: [
|
|
38
|
-
{
|
|
39
|
-
name: "rpm",
|
|
40
|
-
tagName: "rpm",
|
|
41
|
-
valueType: "float32",
|
|
42
|
-
properties: [],
|
|
43
|
-
unsPath: "area/line/pump-001/rpm",
|
|
44
|
-
access: "read",
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
return { devices: [device], models: [], instances: [] };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createApplyOptions(scope: readonly ("devices" | "models" | "instances")[]): ApplyOptions {
|
|
52
|
-
return {
|
|
53
|
-
templateId: "smart-factory",
|
|
54
|
-
targetHost: "le.example.local",
|
|
55
|
-
sourceHostname: "miravo.local",
|
|
56
|
-
scope,
|
|
57
|
-
filters: {},
|
|
58
|
-
ifUnmanaged: "skip",
|
|
59
|
-
dryRun: true,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function createHubMock(devices: readonly LEDevice[]): LEDeviceHub {
|
|
64
|
-
return {
|
|
65
|
-
async listDevices() {
|
|
66
|
-
return devices;
|
|
67
|
-
},
|
|
68
|
-
async resolveDriverByName() {
|
|
69
|
-
return { ID: "driver-1", Name: "MQTT Client" };
|
|
70
|
-
},
|
|
71
|
-
} as unknown as LEDeviceHub;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createDigitalTwinsMock(): LEDigitalTwins {
|
|
75
|
-
return {
|
|
76
|
-
async listModels() {
|
|
77
|
-
return [];
|
|
78
|
-
},
|
|
79
|
-
async listInstances() {
|
|
80
|
-
return [];
|
|
81
|
-
},
|
|
82
|
-
} as unknown as LEDigitalTwins;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function createUnmanagedDevice(name: string): LEDevice {
|
|
86
|
-
return {
|
|
87
|
-
ID: "dev-1",
|
|
88
|
-
Name: name,
|
|
89
|
-
DriverID: "driver-1",
|
|
90
|
-
Properties: [],
|
|
91
|
-
MetaData: [{ Name: "owner", Value: "external" }],
|
|
92
|
-
Description: "External device",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function createManagedDevice(name: string, protocol: ProtocolName): LEDevice {
|
|
97
|
-
return {
|
|
98
|
-
ID: "dev-1",
|
|
99
|
-
Name: name,
|
|
100
|
-
DriverID: `driver-${protocol}`,
|
|
101
|
-
Properties: [],
|
|
102
|
-
MetaData: [
|
|
103
|
-
{ Name: "miravo_managed", Value: "true" },
|
|
104
|
-
{ Name: "miravo_template", Value: "smart-factory" },
|
|
105
|
-
{ Name: "miravo_instance", Value: "pump-001" },
|
|
106
|
-
{ Name: "miravo_model", Value: "pump" },
|
|
107
|
-
{ Name: "miravo_protocol", Value: protocol },
|
|
108
|
-
],
|
|
109
|
-
Description: "Miravo managed device",
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
describe("buildApplyPlan protocol change handling", () => {
|
|
114
|
-
test("emits replace when managed device protocol changes", async () => {
|
|
115
|
-
const desired: ApplyDesiredState = {
|
|
116
|
-
devices: [
|
|
117
|
-
{
|
|
118
|
-
instanceId: "pump-001",
|
|
119
|
-
modelName: "pump",
|
|
120
|
-
protocol: "mqtt" satisfies ProtocolName,
|
|
121
|
-
deviceName: "pump-001",
|
|
122
|
-
browsePath: "area/line/pump-001",
|
|
123
|
-
endpoint: { host: "127.0.0.1", port: 1883 },
|
|
124
|
-
properties: [],
|
|
125
|
-
tags: [],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
models: [],
|
|
129
|
-
instances: [],
|
|
130
|
-
};
|
|
131
|
-
const opts = createApplyOptions(["devices"]);
|
|
132
|
-
const hub = createHubMock([createManagedDevice("pump-001", "opcua")]);
|
|
133
|
-
const dt = createDigitalTwinsMock();
|
|
134
|
-
|
|
135
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
136
|
-
|
|
137
|
-
const entry = plan.entries.find((e) => e.resourceType === "device");
|
|
138
|
-
expect(entry?.action).toBe("replace");
|
|
139
|
-
expect(entry?.protocol).toBe("mqtt");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("emits update when managed device protocol is unchanged", async () => {
|
|
143
|
-
const desired = createDesiredState("pump-001"); // mqtt
|
|
144
|
-
const opts = createApplyOptions(["devices"]);
|
|
145
|
-
const hub = createHubMock([createManagedDevice("pump-001", "mqtt")]);
|
|
146
|
-
const dt = createDigitalTwinsMock();
|
|
147
|
-
|
|
148
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
149
|
-
|
|
150
|
-
const entry = plan.entries.find((e) => e.resourceType === "device");
|
|
151
|
-
expect(entry?.action).toBe("update");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("blocks replace when new protocol driver is not found on LE", async () => {
|
|
155
|
-
const desired: ApplyDesiredState = {
|
|
156
|
-
devices: [
|
|
157
|
-
{
|
|
158
|
-
instanceId: "pump-001",
|
|
159
|
-
modelName: "pump",
|
|
160
|
-
protocol: "mqtt" satisfies ProtocolName,
|
|
161
|
-
deviceName: "pump-001",
|
|
162
|
-
browsePath: "area/line/pump-001",
|
|
163
|
-
endpoint: { host: "127.0.0.1", port: 1883 },
|
|
164
|
-
properties: [],
|
|
165
|
-
tags: [],
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
models: [],
|
|
169
|
-
instances: [],
|
|
170
|
-
};
|
|
171
|
-
const opts = createApplyOptions(["devices"]);
|
|
172
|
-
const hub: LEDeviceHub = {
|
|
173
|
-
async listDevices() {
|
|
174
|
-
return [createManagedDevice("pump-001", "opcua")];
|
|
175
|
-
},
|
|
176
|
-
async resolveDriverByName() {
|
|
177
|
-
throw new Error("driver not found");
|
|
178
|
-
},
|
|
179
|
-
} as unknown as LEDeviceHub;
|
|
180
|
-
const dt = createDigitalTwinsMock();
|
|
181
|
-
|
|
182
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
183
|
-
|
|
184
|
-
const entry = plan.entries.find((e) => e.resourceType === "device");
|
|
185
|
-
expect(entry?.action).toBe("blocked");
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe("buildApplyPlan unmanaged device handling", () => {
|
|
190
|
-
test("skips device planning when unmanaged device is skipped with --if-unmanaged=skip", async () => {
|
|
191
|
-
const desired = createDesiredState("pump-001");
|
|
192
|
-
const opts = createApplyOptions(["devices"]);
|
|
193
|
-
const hub = createHubMock([createUnmanagedDevice("pump-001")]);
|
|
194
|
-
const dt = createDigitalTwinsMock();
|
|
195
|
-
|
|
196
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
197
|
-
|
|
198
|
-
expect(plan.entries.find((entry) => entry.resourceType === "device")).toBeUndefined();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("blocks device for unmanaged device when policy is fail", async () => {
|
|
202
|
-
const desired = createDesiredState("pump-001");
|
|
203
|
-
const opts = { ...createApplyOptions(["devices"]), ifUnmanaged: "fail" as const };
|
|
204
|
-
const hub = createHubMock([createUnmanagedDevice("pump-001")]);
|
|
205
|
-
const dt = createDigitalTwinsMock();
|
|
206
|
-
|
|
207
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
208
|
-
|
|
209
|
-
expect(plan.entries).toHaveLength(1);
|
|
210
|
-
expect(plan.entries[0]).toMatchObject({
|
|
211
|
-
action: "blocked",
|
|
212
|
-
resourceType: "device",
|
|
213
|
-
name: "pump-001",
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe("buildApplyPlan model handling", () => {
|
|
219
|
-
test("blocks model update when LE has duplicate model names", async () => {
|
|
220
|
-
const desired: ApplyDesiredState = {
|
|
221
|
-
devices: [],
|
|
222
|
-
models: [{ name: "AC-INDUCTION-MOTOR", leDtJson: { mock: true } }],
|
|
223
|
-
instances: [],
|
|
224
|
-
};
|
|
225
|
-
const opts = createApplyOptions(["models"]);
|
|
226
|
-
const hub = createHubMock([]);
|
|
227
|
-
const dt: LEDigitalTwins = {
|
|
228
|
-
async listModels() {
|
|
229
|
-
return [
|
|
230
|
-
{
|
|
231
|
-
ID: "model-1",
|
|
232
|
-
Name: "AC-INDUCTION-MOTOR",
|
|
233
|
-
Description: "",
|
|
234
|
-
Version: 1,
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
ID: "model-2",
|
|
238
|
-
Name: "AC-INDUCTION-MOTOR",
|
|
239
|
-
Description: "",
|
|
240
|
-
Version: 1,
|
|
241
|
-
},
|
|
242
|
-
];
|
|
243
|
-
},
|
|
244
|
-
async listInstances() {
|
|
245
|
-
return [];
|
|
246
|
-
},
|
|
247
|
-
} as unknown as LEDigitalTwins;
|
|
248
|
-
|
|
249
|
-
const { plan } = await buildApplyPlan(hub, dt, desired, opts);
|
|
250
|
-
expect(plan.entries).toHaveLength(1);
|
|
251
|
-
expect(plan.entries[0]).toMatchObject({
|
|
252
|
-
action: "blocked",
|
|
253
|
-
resourceType: "model",
|
|
254
|
-
name: "AC-INDUCTION-MOTOR",
|
|
255
|
-
});
|
|
256
|
-
expect(plan.entries[0]?.blockReason).toContain("duplicate DT models");
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe("executePlan model idempotency", () => {
|
|
261
|
-
test("model create resolves markers in imported DT JSON", async () => {
|
|
262
|
-
const desired: ApplyDesiredState = {
|
|
263
|
-
devices: [],
|
|
264
|
-
models: [
|
|
265
|
-
{
|
|
266
|
-
name: "PUMP",
|
|
267
|
-
leDtJson: [
|
|
268
|
-
{
|
|
269
|
-
staticAttributes: [
|
|
270
|
-
{ key: "miravo_managed", value: "false" },
|
|
271
|
-
{ key: "miravo_template", value: "__miravo_placeholder__" },
|
|
272
|
-
{ key: "miravo_model", value: "__miravo_placeholder__" },
|
|
273
|
-
{ key: "miravo_instance", value: "__miravo_placeholder__" },
|
|
274
|
-
],
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
instances: [],
|
|
280
|
-
};
|
|
281
|
-
const opts = createApplyOptions(["models"]);
|
|
282
|
-
const hub = createHubMock([]);
|
|
283
|
-
const importedPayloads: unknown[] = [];
|
|
284
|
-
|
|
285
|
-
const dt: LEDigitalTwins = {
|
|
286
|
-
async listModels() {
|
|
287
|
-
return [];
|
|
288
|
-
},
|
|
289
|
-
async listInstances() {
|
|
290
|
-
return [];
|
|
291
|
-
},
|
|
292
|
-
async importModel(payload: unknown) {
|
|
293
|
-
importedPayloads.push(payload);
|
|
294
|
-
return true;
|
|
295
|
-
},
|
|
296
|
-
} as unknown as LEDigitalTwins;
|
|
297
|
-
|
|
298
|
-
const { plan, snapshot } = await buildApplyPlan(hub, dt, desired, opts);
|
|
299
|
-
expect(plan.entries[0]?.action).toBe("create");
|
|
300
|
-
|
|
301
|
-
const result = await executePlan(hub, dt, plan, desired, opts, snapshot);
|
|
302
|
-
|
|
303
|
-
expect(result.applied).toBe(1);
|
|
304
|
-
expect(result.failed).toBe(0);
|
|
305
|
-
expect(importedPayloads).toHaveLength(1);
|
|
306
|
-
expect(importedPayloads[0]).toEqual([
|
|
307
|
-
{
|
|
308
|
-
staticAttributes: [
|
|
309
|
-
{ key: "miravo_managed", value: "true" },
|
|
310
|
-
{ key: "miravo_template", value: "smart-factory" },
|
|
311
|
-
{ key: "miravo_model", value: "PUMP" },
|
|
312
|
-
{ key: "miravo_instance", value: "__miravo_placeholder__" },
|
|
313
|
-
],
|
|
314
|
-
},
|
|
315
|
-
]);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("model update skips import when model already exists (prevents duplicates)", async () => {
|
|
319
|
-
const desired: ApplyDesiredState = {
|
|
320
|
-
devices: [],
|
|
321
|
-
models: [{ name: "PUMP", leDtJson: { model: true } }],
|
|
322
|
-
instances: [],
|
|
323
|
-
};
|
|
324
|
-
const opts = createApplyOptions(["models"]);
|
|
325
|
-
const hub = createHubMock([]);
|
|
326
|
-
let importCalled = false;
|
|
327
|
-
|
|
328
|
-
const dt: LEDigitalTwins = {
|
|
329
|
-
async listModels() {
|
|
330
|
-
return [{ ID: "existing-model-1", Name: "PUMP", Description: "", Version: 1 }];
|
|
331
|
-
},
|
|
332
|
-
async listInstances() {
|
|
333
|
-
return [];
|
|
334
|
-
},
|
|
335
|
-
async importModel() {
|
|
336
|
-
importCalled = true;
|
|
337
|
-
return true;
|
|
338
|
-
},
|
|
339
|
-
} as unknown as LEDigitalTwins;
|
|
340
|
-
|
|
341
|
-
const { plan, snapshot } = await buildApplyPlan(hub, dt, desired, opts);
|
|
342
|
-
expect(plan.entries[0]?.action).toBe("update");
|
|
343
|
-
|
|
344
|
-
const result = await executePlan(hub, dt, plan, desired, opts, snapshot);
|
|
345
|
-
|
|
346
|
-
expect(result.applied).toBe(1);
|
|
347
|
-
expect(result.failed).toBe(0);
|
|
348
|
-
expect(importCalled).toBe(false);
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
describe("executePlan marker failure accounting", () => {
|
|
353
|
-
test("updates instance markers through transactional instance update", async () => {
|
|
354
|
-
const desired: ApplyDesiredState = {
|
|
355
|
-
devices: [],
|
|
356
|
-
models: [
|
|
357
|
-
{
|
|
358
|
-
name: "HOT-WATER-BOILER",
|
|
359
|
-
leDtJson: { model: "payload" },
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
instances: [
|
|
363
|
-
{
|
|
364
|
-
instanceId: "boiler-qa-001",
|
|
365
|
-
modelName: "hot-water-boiler",
|
|
366
|
-
dtInstanceName: "boiler-qa-001",
|
|
367
|
-
dtModelName: "HOT-WATER-BOILER",
|
|
368
|
-
boundDeviceName: "boiler-qa-001",
|
|
369
|
-
browsePath: "area/line/boiler-qa-001",
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
};
|
|
373
|
-
const opts = createApplyOptions(["models", "instances"]);
|
|
374
|
-
const hub = createHubMock([]);
|
|
375
|
-
const staticAttrCreates: TestStaticAttributeWrite[][] = [];
|
|
376
|
-
const instanceStaticAttrUpdates: {
|
|
377
|
-
instanceId: string;
|
|
378
|
-
attrs: TestInstanceStaticAttributeUpdate[];
|
|
379
|
-
}[] = [];
|
|
380
|
-
|
|
381
|
-
const dt: LEDigitalTwins = {
|
|
382
|
-
async listModels() {
|
|
383
|
-
return [
|
|
384
|
-
{
|
|
385
|
-
ID: "model-created",
|
|
386
|
-
Name: "HOT-WATER-BOILER",
|
|
387
|
-
Description: "",
|
|
388
|
-
Version: 1,
|
|
389
|
-
},
|
|
390
|
-
];
|
|
391
|
-
},
|
|
392
|
-
async listInstances() {
|
|
393
|
-
return [];
|
|
394
|
-
},
|
|
395
|
-
async importModel() {
|
|
396
|
-
return true;
|
|
397
|
-
},
|
|
398
|
-
async createStaticAttributes(attrs: readonly TestStaticAttributeWrite[]) {
|
|
399
|
-
staticAttrCreates.push([...attrs]);
|
|
400
|
-
return [];
|
|
401
|
-
},
|
|
402
|
-
async listStaticAttributes(opts: { modelId?: string; instanceId?: string }) {
|
|
403
|
-
if (opts.instanceId === "inst-created") {
|
|
404
|
-
return [
|
|
405
|
-
{
|
|
406
|
-
ID: "attr-managed",
|
|
407
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
408
|
-
InstanceID: "inst-created",
|
|
409
|
-
Key: "miravo_managed",
|
|
410
|
-
Value: "true",
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
ID: "attr-template",
|
|
414
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
415
|
-
InstanceID: "inst-created",
|
|
416
|
-
Key: "miravo_template",
|
|
417
|
-
Value: "__miravo_placeholder__",
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
ID: "attr-model",
|
|
421
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
422
|
-
InstanceID: "inst-created",
|
|
423
|
-
Key: "miravo_model",
|
|
424
|
-
Value: "__miravo_placeholder__",
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
ID: "attr-instance",
|
|
428
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
429
|
-
InstanceID: "inst-created",
|
|
430
|
-
Key: "miravo_instance",
|
|
431
|
-
Value: "__miravo_placeholder__",
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
ID: "attr-uns-path",
|
|
435
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
436
|
-
InstanceID: "inst-created",
|
|
437
|
-
Key: "uns_path",
|
|
438
|
-
Value: "__miravo_placeholder__",
|
|
439
|
-
},
|
|
440
|
-
];
|
|
441
|
-
}
|
|
442
|
-
return [];
|
|
443
|
-
},
|
|
444
|
-
async createInstance(input: {
|
|
445
|
-
ModelID: string;
|
|
446
|
-
Name: string;
|
|
447
|
-
Topic: string;
|
|
448
|
-
Interval: number;
|
|
449
|
-
FlatHierarchy?: boolean;
|
|
450
|
-
}) {
|
|
451
|
-
return {
|
|
452
|
-
ID: "inst-created",
|
|
453
|
-
ModelID: input.ModelID,
|
|
454
|
-
Name: input.Name,
|
|
455
|
-
Topic: input.Topic,
|
|
456
|
-
Interval: input.Interval,
|
|
457
|
-
FlatHierarchy: null,
|
|
458
|
-
Version: 1,
|
|
459
|
-
};
|
|
460
|
-
},
|
|
461
|
-
async listParameters() {
|
|
462
|
-
return [{ ID: "param-device", OwnerID: "inst-created", Name: "device", Value: "" }];
|
|
463
|
-
},
|
|
464
|
-
async updateInstanceStaticAttributes(
|
|
465
|
-
instanceId: string,
|
|
466
|
-
attrs: readonly TestInstanceStaticAttributeUpdate[],
|
|
467
|
-
) {
|
|
468
|
-
instanceStaticAttrUpdates.push({ instanceId, attrs: [...attrs] });
|
|
469
|
-
},
|
|
470
|
-
async setInstanceParameters() {},
|
|
471
|
-
} as unknown as LEDigitalTwins;
|
|
472
|
-
|
|
473
|
-
const { plan, snapshot } = await buildApplyPlan(hub, dt, desired, opts);
|
|
474
|
-
const result = await executePlan(hub, dt, plan, desired, opts, snapshot);
|
|
475
|
-
|
|
476
|
-
expect(result.applied).toBe(3);
|
|
477
|
-
expect(result.failed).toBe(0);
|
|
478
|
-
expect(staticAttrCreates).toHaveLength(0);
|
|
479
|
-
expect(instanceStaticAttrUpdates).toHaveLength(1);
|
|
480
|
-
expect(instanceStaticAttrUpdates[0]?.instanceId).toBe("inst-created");
|
|
481
|
-
expect(instanceStaticAttrUpdates[0]?.attrs).toEqual([
|
|
482
|
-
{
|
|
483
|
-
ID: "attr-managed",
|
|
484
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
485
|
-
InstanceID: "inst-created",
|
|
486
|
-
Key: "miravo_managed",
|
|
487
|
-
Value: "true",
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
ID: "attr-template",
|
|
491
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
492
|
-
InstanceID: "inst-created",
|
|
493
|
-
Key: "miravo_template",
|
|
494
|
-
Value: "smart-factory",
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
ID: "attr-model",
|
|
498
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
499
|
-
InstanceID: "inst-created",
|
|
500
|
-
Key: "miravo_model",
|
|
501
|
-
Value: "hot-water-boiler",
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
ID: "attr-instance",
|
|
505
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
506
|
-
InstanceID: "inst-created",
|
|
507
|
-
Key: "miravo_instance",
|
|
508
|
-
Value: "boiler-qa-001",
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
ID: "attr-uns-path",
|
|
512
|
-
ModelID: "00000000-0000-0000-0000-000000000000",
|
|
513
|
-
InstanceID: "inst-created",
|
|
514
|
-
Key: "uns_path",
|
|
515
|
-
Value: "area/line/boiler-qa-001",
|
|
516
|
-
},
|
|
517
|
-
]);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test("returns partial failure when instance placeholder attrs are missing", async () => {
|
|
521
|
-
const desired: ApplyDesiredState = {
|
|
522
|
-
devices: [],
|
|
523
|
-
models: [{ name: "HOT-WATER-BOILER", leDtJson: { model: "payload" } }],
|
|
524
|
-
instances: [
|
|
525
|
-
{
|
|
526
|
-
instanceId: "boiler-qa-001",
|
|
527
|
-
modelName: "hot-water-boiler",
|
|
528
|
-
dtInstanceName: "boiler-qa-001",
|
|
529
|
-
dtModelName: "HOT-WATER-BOILER",
|
|
530
|
-
boundDeviceName: "boiler-qa-001",
|
|
531
|
-
browsePath: "area/line/boiler-qa-001",
|
|
532
|
-
},
|
|
533
|
-
],
|
|
534
|
-
};
|
|
535
|
-
const opts = createApplyOptions(["models", "instances"]);
|
|
536
|
-
const hub = createHubMock([]);
|
|
537
|
-
|
|
538
|
-
const dt: LEDigitalTwins = {
|
|
539
|
-
async listModels() {
|
|
540
|
-
return [
|
|
541
|
-
{
|
|
542
|
-
ID: "model-created",
|
|
543
|
-
Name: "HOT-WATER-BOILER",
|
|
544
|
-
Description: "",
|
|
545
|
-
Version: 1,
|
|
546
|
-
},
|
|
547
|
-
];
|
|
548
|
-
},
|
|
549
|
-
async listInstances() {
|
|
550
|
-
return [];
|
|
551
|
-
},
|
|
552
|
-
async importModel() {
|
|
553
|
-
return true;
|
|
554
|
-
},
|
|
555
|
-
async createStaticAttributes() {
|
|
556
|
-
return [];
|
|
557
|
-
},
|
|
558
|
-
async listStaticAttributes() {
|
|
559
|
-
return [];
|
|
560
|
-
},
|
|
561
|
-
async createInstance(input: {
|
|
562
|
-
ModelID: string;
|
|
563
|
-
Name: string;
|
|
564
|
-
Topic: string;
|
|
565
|
-
Interval: number;
|
|
566
|
-
FlatHierarchy?: boolean;
|
|
567
|
-
}) {
|
|
568
|
-
return {
|
|
569
|
-
ID: "inst-created",
|
|
570
|
-
ModelID: input.ModelID,
|
|
571
|
-
Name: input.Name,
|
|
572
|
-
Topic: input.Topic,
|
|
573
|
-
Interval: input.Interval,
|
|
574
|
-
FlatHierarchy: null,
|
|
575
|
-
Version: 1,
|
|
576
|
-
};
|
|
577
|
-
},
|
|
578
|
-
async listParameters() {
|
|
579
|
-
return [{ ID: "param-device", OwnerID: "inst-created", Name: "device", Value: "" }];
|
|
580
|
-
},
|
|
581
|
-
async setInstanceParameters() {},
|
|
582
|
-
} as unknown as LEDigitalTwins;
|
|
583
|
-
|
|
584
|
-
const { plan, snapshot } = await buildApplyPlan(hub, dt, desired, opts);
|
|
585
|
-
const result = await executePlan(hub, dt, plan, desired, opts, snapshot);
|
|
586
|
-
|
|
587
|
-
expect(result.applied).toBe(3);
|
|
588
|
-
expect(result.failed).toBe(1);
|
|
589
|
-
expect(result.error).toBeUndefined();
|
|
590
|
-
});
|
|
591
|
-
});
|