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.
Files changed (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,314 +0,0 @@
1
- /**
2
- * E2E test: concurrent OPC UA client connections, reads, clean + unclean disconnect,
3
- * and reconnect. Validates the session-pool hardening changes (MAX_SESSIONS, HELLO_TIMEOUT).
4
- */
5
- import { describe, expect, test } from "bun:test";
6
- import type { EventEmitter } from "node:events";
7
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
8
- import { createServer } from "node:net";
9
- import { tmpdir } from "node:os";
10
- import { join, resolve } from "node:path";
11
- import type { ClientSession } from "node-opcua-client";
12
- import { AttributeIds, MessageSecurityMode, OPCUAClient, SecurityPolicy } from "node-opcua-client";
13
- import { stringify as stringifyYAML } from "yaml";
14
-
15
- async function canBindLocalTcp(): Promise<boolean> {
16
- const probe = createServer();
17
- return new Promise((res) => {
18
- const cleanup = () => {
19
- try {
20
- probe.close();
21
- } catch {
22
- /* ignore */
23
- }
24
- };
25
- (probe as unknown as EventEmitter).once("error", () => {
26
- cleanup();
27
- res(false);
28
- });
29
- (probe as unknown as EventEmitter).once("listening", () => {
30
- cleanup();
31
- res(true);
32
- });
33
- probe.listen(0, "127.0.0.1");
34
- });
35
- }
36
-
37
- const TCP_AVAILABLE = await canBindLocalTcp();
38
- const describeIfTcp = TCP_AVAILABLE ? describe : describe.skip;
39
-
40
- interface RuntimeHandle {
41
- readonly process: Bun.Subprocess<"ignore", "pipe", "pipe">;
42
- readonly tmpDir: string;
43
- readonly opcuaPort: number;
44
- readonly instancePath: string;
45
- }
46
-
47
- async function startRuntime(): Promise<RuntimeHandle> {
48
- const tmpDir = await mkdtemp(join(tmpdir(), "miravo-opcua-e2e-"));
49
- const modelsDir = join(tmpDir, "models");
50
- const templatePath = join(tmpDir, "e2e.miravo.yaml");
51
- const modelPath = join(modelsDir, "e2e-pump.twin.yaml");
52
- const readyFile = join(tmpDir, "ready.json");
53
-
54
- await mkdir(modelsDir, { recursive: true });
55
-
56
- await Bun.write(
57
- modelPath,
58
- stringifyYAML({
59
- schema_version: "2",
60
- model: {
61
- name: "e2e-pump",
62
- namespace: "com.miravo.testing.e2e-pump",
63
- description: "Minimal pump for OPC UA concurrent-client e2e test.",
64
- },
65
- parameters: {},
66
- methods: {},
67
- members: {
68
- speed: {
69
- kind: "variable",
70
- data_type: "Double",
71
- access: "read",
72
- unit: "rpm",
73
- generator: { type: "sine", center: 500, amplitude: 100, period: 10 },
74
- },
75
- status: {
76
- kind: "property",
77
- data_type: "String",
78
- access: "read",
79
- default: "running",
80
- },
81
- },
82
- faults: [],
83
- }),
84
- );
85
-
86
- await Bun.write(
87
- templatePath,
88
- stringifyYAML({
89
- kind: "MiravoTemplate",
90
- version: "1",
91
- name: "E2E Template",
92
- namespace: { prefix: "e2e" },
93
- areas: [
94
- {
95
- name: "plant",
96
- lines: [{ name: "line1", twins: [{ model: "e2e-pump", count: 1, variation: 0 }] }],
97
- },
98
- ],
99
- settings: { seed: 1, tick_interval_ms: 100, time_multiplier: 1 },
100
- }),
101
- );
102
-
103
- const proc = Bun.spawn({
104
- cmd: [
105
- "bun",
106
- "run",
107
- resolve(import.meta.dir, "executable-methods-runner.ts"),
108
- "--template",
109
- templatePath,
110
- "--models-dir",
111
- modelsDir,
112
- "--ready-file",
113
- readyFile,
114
- ],
115
- cwd: resolve("."),
116
- stdin: "ignore",
117
- stdout: "pipe",
118
- stderr: "pipe",
119
- });
120
-
121
- try {
122
- const ready = await Promise.race([
123
- (async () => {
124
- const deadline = Date.now() + 20_000;
125
- while (Date.now() < deadline) {
126
- if (!(await Bun.file(readyFile).exists())) {
127
- await Bun.sleep(50);
128
- continue;
129
- }
130
- return (await Bun.file(readyFile).json()) as {
131
- readonly opcuaEndpointUrl: string;
132
- readonly instancePath: string;
133
- };
134
- }
135
- throw new Error("Runtime did not write ready file within 20 s");
136
- })(),
137
- proc.exited.then((code) => {
138
- throw new Error(`Runtime exited early with code ${code}`);
139
- }),
140
- ]);
141
-
142
- const match = ready.opcuaEndpointUrl.match(/:(\d+)\//);
143
- const opcuaPort = match?.[1] ? Number.parseInt(match[1], 10) : 0;
144
- return { process: proc, tmpDir, opcuaPort, instancePath: ready.instancePath };
145
- } catch (error) {
146
- proc.kill("SIGTERM");
147
- await proc.exited;
148
- const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
149
- const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
150
- await rm(tmpDir, { recursive: true, force: true });
151
- throw new Error(
152
- [
153
- `Failed to start runtime: ${error instanceof Error ? error.message : String(error)}`,
154
- stdout.trim() ? `stdout:\n${stdout.trim()}` : "",
155
- stderr.trim() ? `stderr:\n${stderr.trim()}` : "",
156
- ]
157
- .filter(Boolean)
158
- .join("\n\n"),
159
- );
160
- }
161
- }
162
-
163
- async function stopRuntime(handle: RuntimeHandle): Promise<void> {
164
- handle.process.kill("SIGTERM");
165
- await Promise.race([
166
- handle.process.exited,
167
- Bun.sleep(8_000).then(() => handle.process.kill("SIGKILL")),
168
- ]);
169
- await rm(handle.tmpDir, { recursive: true, force: true });
170
- }
171
-
172
- function makeClient(): OPCUAClient {
173
- return OPCUAClient.create({
174
- securityMode: MessageSecurityMode.None,
175
- securityPolicy: SecurityPolicy.None,
176
- endpointMustExist: false,
177
- connectionStrategy: { maxRetry: 0, initialDelay: 0 },
178
- });
179
- }
180
-
181
- /**
182
- * Creates a client that will NOT send CloseSession on disconnect.
183
- * `keepPendingSessionsOnDisconnect: true` skips `_close_pending_sessions()`,
184
- * so the server retains the session slot until its 30 s watchdog fires.
185
- * Use this to simulate unclean client crashes.
186
- */
187
- function makeZombieClient(): OPCUAClient {
188
- return OPCUAClient.create({
189
- securityMode: MessageSecurityMode.None,
190
- securityPolicy: SecurityPolicy.None,
191
- endpointMustExist: false,
192
- connectionStrategy: { maxRetry: 0, initialDelay: 0 },
193
- keepPendingSessionsOnDisconnect: true,
194
- });
195
- }
196
-
197
- async function connectClient(
198
- port: number,
199
- ): Promise<{ client: OPCUAClient; session: ClientSession }> {
200
- const client = makeClient();
201
- await client.connect(`opc.tcp://127.0.0.1:${port}`);
202
- const session = await client.createSession();
203
- return { client, session };
204
- }
205
-
206
- async function closeClient(client: OPCUAClient, session: ClientSession): Promise<void> {
207
- await session.close().catch(() => {});
208
- await client.disconnect().catch(() => {});
209
- }
210
-
211
- async function readSpeed(session: ClientSession, instancePath: string): Promise<number> {
212
- const dv = await session.read({
213
- nodeId: `ns=1;s=${instancePath}/speed`,
214
- attributeId: AttributeIds.Value,
215
- });
216
- const v = dv.value.value;
217
- if (typeof v !== "number") throw new Error(`Expected number, got ${typeof v}`);
218
- return v;
219
- }
220
-
221
- describeIfTcp("OPC UA concurrent-client e2e", () => {
222
- let runtime: RuntimeHandle | undefined;
223
-
224
- test("setup: start miravo runtime with OPC UA", async () => {
225
- runtime = await startRuntime();
226
- expect(runtime.opcuaPort).toBeGreaterThan(0);
227
- }, 30_000);
228
-
229
- test("20 clients connect concurrently and all read a value", async () => {
230
- if (!runtime) return;
231
- const rt = runtime;
232
- const N = 20;
233
- const handles = await Promise.all(Array.from({ length: N }, () => connectClient(rt.opcuaPort)));
234
- try {
235
- const values = await Promise.all(
236
- handles.map(({ session }) => readSpeed(session, rt.instancePath)),
237
- );
238
- expect(values).toHaveLength(N);
239
- for (const v of values) {
240
- expect(typeof v).toBe("number");
241
- expect(Number.isFinite(v)).toBe(true);
242
- }
243
- console.log(` ✓ ${N} concurrent clients read speed; sample: ${values[0]?.toFixed(2)}`);
244
- } finally {
245
- await Promise.all(handles.map(({ client, session }) => closeClient(client, session)));
246
- }
247
- }, 30_000);
248
-
249
- test("client disconnects cleanly and reconnects — reads succeed after reconnect", async () => {
250
- if (!runtime) return;
251
- const { client: c1, session: s1 } = await connectClient(runtime.opcuaPort);
252
- const before = await readSpeed(s1, runtime.instancePath);
253
- await closeClient(c1, s1);
254
- console.log(` ✓ Client read ${before.toFixed(2)} before clean disconnect`);
255
-
256
- const { client: c2, session: s2 } = await connectClient(runtime.opcuaPort);
257
- try {
258
- const after = await readSpeed(s2, runtime.instancePath);
259
- expect(typeof after).toBe("number");
260
- console.log(` ✓ Reconnected client read ${after.toFixed(2)}`);
261
- } finally {
262
- await closeClient(c2, s2);
263
- }
264
- }, 20_000);
265
-
266
- test("unclean disconnect stress: 15 zombie sessions exhaust old default (10) — new client still connects", async () => {
267
- if (!runtime) return;
268
- const rt = runtime;
269
- // node-opcua default maxSessions is 10; our fix sets it to 500.
270
- // makeZombieClient() uses keepPendingSessionsOnDisconnect:true so client.disconnect()
271
- // skips CloseSession — the server retains each session slot for ~30 s (session watchdog).
272
- // With the old default, the 11th slot is unavailable → connectClient() throws
273
- // BadTooManySessions → test fails.
274
- const ZOMBIE_COUNT = 15; // intentionally > old default of 10
275
-
276
- const zombies = await Promise.all(
277
- Array.from({ length: ZOMBIE_COUNT }, async () => {
278
- const client = makeZombieClient();
279
- await client.connect(`opc.tcp://127.0.0.1:${rt.opcuaPort}`);
280
- const session = await client.createSession();
281
- return { client, session };
282
- }),
283
- );
284
- // Disconnect without CloseSession — server retains sessions, consuming pool slots.
285
- await Promise.all(zombies.map(({ client }) => client.disconnect().catch(() => {})));
286
- console.log(` ✓ ${ZOMBIE_COUNT} zombie sessions dropped (linger on server for ~30 s)`);
287
-
288
- const { client: fresh, session: freshSession } = await connectClient(runtime.opcuaPort);
289
- try {
290
- const v = await readSpeed(freshSession, runtime.instancePath);
291
- expect(typeof v).toBe("number");
292
- console.log(
293
- ` ✓ Client #${ZOMBIE_COUNT + 1} connected and read ${v.toFixed(2)} while ${ZOMBIE_COUNT} zombie sessions hold server slots`,
294
- );
295
- } finally {
296
- await closeClient(fresh, freshSession);
297
- }
298
- }, 30_000);
299
-
300
- test("10 sequential connect/read/disconnect cycles complete without error", async () => {
301
- if (!runtime) return;
302
- for (let i = 0; i < 10; i++) {
303
- const { client, session } = await connectClient(runtime.opcuaPort);
304
- const v = await readSpeed(session, runtime.instancePath);
305
- expect(typeof v).toBe("number");
306
- await closeClient(client, session);
307
- }
308
- console.log(" ✓ 10 sequential connect-read-disconnect cycles completed");
309
- }, 30_000);
310
-
311
- test("teardown: stop miravo runtime", async () => {
312
- if (runtime) await stopRuntime(runtime);
313
- }, 15_000);
314
- });
@@ -1,249 +0,0 @@
1
- import type { EngineCommand } from "@miravo/shared";
2
- import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
3
- import type { Command } from "commander";
4
- import { colors } from "consola/utils";
5
- import { executeAdminCommand, getManifest, resolveAdminUrl } from "../lib/admin-client.ts";
6
- import { parsePositiveInt, parseTimeoutSeconds } from "../lib/parse.ts";
7
- import {
8
- engineLog,
9
- handleCommandError,
10
- handleCommandResult,
11
- modbusLog,
12
- writeOutput,
13
- } from "../logger.ts";
14
- import { formatTable } from "../output.ts";
15
-
16
- interface AdapterOptions {
17
- readonly port?: number;
18
- readonly host?: string;
19
- readonly json?: boolean;
20
- readonly adminUrl?: string;
21
- readonly timeout?: number;
22
- }
23
-
24
- export function registerAdapterCommand(program: Command): void {
25
- const adapter = program
26
- .command("adapter")
27
- .description("Manage protocol adapters (MQTT, OPC UA, Modbus)");
28
-
29
- adapter
30
- .command("enable <name>")
31
- .description("Enable a protocol adapter (mqtt, opcua, modbus)")
32
- .option("-p, --port <number>", "Override adapter port", parsePositiveInt)
33
- .option("--host <host>", "Override adapter bind host")
34
- .option("--json", "Machine-readable JSON output")
35
- .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
36
- .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
37
- .addHelpText(
38
- "after",
39
- `
40
- Examples:
41
- $ miravo adapter enable mqtt Enable MQTT adapter
42
- $ miravo adapter enable opcua -p 4841 Enable OPC UA on custom port`,
43
- )
44
- .action(async (name: string, options: AdapterOptions) => {
45
- try {
46
- const adminUrl = resolveAdminUrl(options.adminUrl);
47
- const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
48
- const config: Record<string, unknown> = {};
49
- if (options.port !== undefined) config["port"] = options.port;
50
- if (options.host !== undefined) config["host"] = options.host;
51
- const command: EngineCommand = {
52
- type: "enableAdapter",
53
- adapter: name,
54
- ...(Object.keys(config).length > 0 ? { config } : {}),
55
- };
56
- const result = await executeAdminCommand(adminUrl, command, timeoutMs);
57
- handleCommandResult(result, options.json);
58
- } catch (error) {
59
- handleCommandError(error);
60
- }
61
- });
62
-
63
- adapter
64
- .command("disable <name>")
65
- .description("Disable a running protocol adapter (mqtt, opcua, modbus)")
66
- .option("--json", "Machine-readable JSON output")
67
- .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
68
- .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
69
- .addHelpText(
70
- "after",
71
- `
72
- Examples:
73
- $ miravo adapter disable mqtt Disable MQTT adapter`,
74
- )
75
- .action(async (name: string, options: AdapterOptions) => {
76
- try {
77
- const adminUrl = resolveAdminUrl(options.adminUrl);
78
- const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
79
- const command: EngineCommand = { type: "disableAdapter", adapter: name };
80
- const result = await executeAdminCommand(adminUrl, command, timeoutMs);
81
- handleCommandResult(result, options.json);
82
- } catch (error) {
83
- handleCommandError(error);
84
- }
85
- });
86
-
87
- // Generic adapter introspection via unified manifest
88
- const inspectAction = async (name: string | undefined, options: AdapterOptions) => {
89
- try {
90
- const adapterName = name ?? "modbus";
91
- const adminUrl = resolveAdminUrl(options.adminUrl);
92
- const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
93
- const manifest = await getManifest(adminUrl, timeoutMs);
94
-
95
- const allTags = manifest["tags"] as Array<Record<string, unknown>> | undefined;
96
- if (!allTags || allTags.length === 0) {
97
- engineLog.warn("No tags found.");
98
- return;
99
- }
100
-
101
- // Filter to tags that have data for the requested protocol
102
- const tags = allTags.filter((s) => {
103
- const protocols = s["protocols"] as Record<string, unknown> | undefined;
104
- return protocols?.[adapterName] !== undefined;
105
- });
106
-
107
- if (tags.length === 0) {
108
- engineLog.warn(`No tags found for ${adapterName} adapter.`);
109
- return;
110
- }
111
-
112
- if (options.json) {
113
- writeOutput(JSON.stringify(tags, null, 2));
114
- return;
115
- }
116
-
117
- // Protocol-specific table rendering
118
- if (adapterName === "modbus") {
119
- renderModbusTable(tags);
120
- } else if (adapterName === "opcua") {
121
- renderOpcuaTable(tags);
122
- } else {
123
- renderMqttTable(tags);
124
- }
125
- } catch (error) {
126
- handleCommandError(error);
127
- }
128
- };
129
-
130
- adapter
131
- .command("inspect [name]")
132
- .description("Inspect a running adapter's tag map (default: modbus)")
133
- .option("--json", "Machine-readable JSON output")
134
- .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
135
- .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
136
- .addHelpText(
137
- "after",
138
- `
139
- Examples:
140
- $ miravo adapter inspect modbus Show Modbus register map
141
- $ miravo adapter inspect opcua Show OPC UA node map
142
- $ miravo adapter inspect mqtt Show MQTT topic map
143
- $ miravo adapter inspect --json JSON output for scripting`,
144
- )
145
- .action(inspectAction);
146
- }
147
-
148
- function renderModbusTable(tags: Array<Record<string, unknown>>): void {
149
- const instanceCount = new Set(tags.map((s) => s["instance"])).size;
150
- modbusLog.info(
151
- `Register Map — ${colors.bold(String(tags.length))} entries, ${colors.bold(String(instanceCount))} instances`,
152
- );
153
- writeOutput();
154
-
155
- const rows = tags.map((s) => {
156
- const modbus = (s["protocols"] as Record<string, Record<string, unknown>>)?.["modbus"] ?? {};
157
- return {
158
- address: String(modbus["address"] ?? ""),
159
- count: String(modbus["count"] ?? ""),
160
- instance: String(s["instance"] ?? ""),
161
- member: String(s["member"] ?? ""),
162
- type: String(modbus["registerType"] ?? ""),
163
- access: String(s["access"] ?? ""),
164
- };
165
- });
166
-
167
- writeOutput(
168
- formatTable(
169
- {
170
- address: colors.dim("Address"),
171
- count: colors.dim("Regs"),
172
- instance: colors.dim("Instance"),
173
- member: colors.dim("Member"),
174
- type: colors.dim("Type"),
175
- access: colors.dim("Access"),
176
- },
177
- rows,
178
- ),
179
- );
180
- writeOutput();
181
- }
182
-
183
- function renderOpcuaTable(tags: Array<Record<string, unknown>>): void {
184
- const instanceCount = new Set(tags.map((s) => s["instance"])).size;
185
- engineLog.info(
186
- `OPC UA — ${colors.bold(String(tags.length))} tags, ${colors.bold(String(instanceCount))} instances`,
187
- );
188
- writeOutput();
189
-
190
- const rows = tags.map((s) => {
191
- const opcua = (s["protocols"] as Record<string, Record<string, unknown>>)?.["opcua"] ?? {};
192
- return {
193
- nodeId: String(opcua["nodeId"] ?? ""),
194
- instance: String(s["instance"] ?? ""),
195
- member: String(s["member"] ?? ""),
196
- dataType: String(s["dataType"] ?? ""),
197
- access: String(s["access"] ?? ""),
198
- };
199
- });
200
-
201
- writeOutput(
202
- formatTable(
203
- {
204
- nodeId: colors.dim("NodeId"),
205
- instance: colors.dim("Instance"),
206
- member: colors.dim("Member"),
207
- dataType: colors.dim("Type"),
208
- access: colors.dim("Access"),
209
- },
210
- rows,
211
- ),
212
- );
213
- writeOutput();
214
- }
215
-
216
- function renderMqttTable(tags: Array<Record<string, unknown>>): void {
217
- const instanceCount = new Set(tags.map((s) => s["instance"])).size;
218
- engineLog.info(
219
- `MQTT — ${colors.bold(String(tags.length))} tags, ${colors.bold(String(instanceCount))} instances`,
220
- );
221
- writeOutput();
222
-
223
- const rows = tags.map((s) => {
224
- const mqtt = (s["protocols"] as Record<string, Record<string, unknown>>)?.["mqtt"] ?? {};
225
- return {
226
- topic: String(mqtt["topic"] ?? ""),
227
- instance: String(s["instance"] ?? ""),
228
- member: String(s["member"] ?? ""),
229
- dataType: String(s["dataType"] ?? ""),
230
- access: String(s["access"] ?? ""),
231
- unit: String(s["unit"] ?? ""),
232
- };
233
- });
234
-
235
- writeOutput(
236
- formatTable(
237
- {
238
- topic: colors.dim("Topic"),
239
- instance: colors.dim("Instance"),
240
- member: colors.dim("Member"),
241
- dataType: colors.dim("Type"),
242
- access: colors.dim("Access"),
243
- unit: colors.dim("Unit"),
244
- },
245
- rows,
246
- ),
247
- );
248
- writeOutput();
249
- }
@@ -1,62 +0,0 @@
1
- import { afterEach, beforeEach, expect, test } from "bun:test";
2
- import { resetExitCode, runCliCommand } from "./cli-test-setup.ts";
3
-
4
- beforeEach(resetExitCode);
5
- afterEach(resetExitCode);
6
-
7
- test("apply le rejects invalid --scope values", async () => {
8
- await runCliCommand([
9
- "apply",
10
- "le",
11
- "--template",
12
- "smart-factory",
13
- "--source-hostname",
14
- "10.0.0.1",
15
- "--le-hostname",
16
- "le.example.local",
17
- "--le-api-token",
18
- "token",
19
- "--scope",
20
- "foo,bar",
21
- ]);
22
-
23
- expect(process.exitCode).toBe(1);
24
- });
25
-
26
- test("apply le rejects invalid --if-unmanaged values", async () => {
27
- await runCliCommand([
28
- "apply",
29
- "le",
30
- "--template",
31
- "smart-factory",
32
- "--source-hostname",
33
- "10.0.0.1",
34
- "--le-hostname",
35
- "le.example.local",
36
- "--le-api-token",
37
- "token",
38
- "--if-unmanaged",
39
- "ignore",
40
- ]);
41
-
42
- expect(process.exitCode).toBe(1);
43
- });
44
-
45
- test("apply le rejects invalid --protocol values", async () => {
46
- await runCliCommand([
47
- "apply",
48
- "le",
49
- "--template",
50
- "smart-factory",
51
- "--source-hostname",
52
- "10.0.0.1",
53
- "--le-hostname",
54
- "le.example.local",
55
- "--le-api-token",
56
- "token",
57
- "--protocol",
58
- "http",
59
- ]);
60
-
61
- expect(process.exitCode).toBe(1);
62
- });