miravo 0.6.0 → 0.6.1

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 +7 -70
  3. package/bin/miravo.js +84 -0
  4. package/package.json +20 -36
  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,781 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import type { EventEmitter } from "node:events";
3
- import { createServer } from "node:net";
4
- import type { EngineCoreMetrics, EngineStateSnapshot } from "@miravo/core";
5
- import {
6
- type CommandResult,
7
- ConsoleBootstrapResponseSchema,
8
- type EngineCommand,
9
- } from "@miravo/shared";
10
- import { startAdminServer } from "./admin-server.ts";
11
- import { createSessionStore } from "./session-store.ts";
12
-
13
- /** Default test auth config: password "admin", not default. */
14
- function testAuth() {
15
- return {
16
- verifyPassword: async (pw: string) => pw === "testpass",
17
- isDefaultPassword: false,
18
- sessionStore: createSessionStore(),
19
- changePassword: async (_old: string, _new: string) => {},
20
- };
21
- }
22
-
23
- /** Login to the test server and return the session cookie string for use in headers. */
24
- async function loginAndGetCookie(baseUrl: string, password = "testpass"): Promise<string> {
25
- const res = await fetch(`${baseUrl}/auth/login`, {
26
- method: "POST",
27
- headers: { "content-type": "application/json" },
28
- body: JSON.stringify({ username: "admin", password }),
29
- });
30
- if (!res.ok) throw new Error(`Login failed: ${res.status}`);
31
- const setCookie = res.headers.get("set-cookie") ?? "";
32
- return setCookie.split(";")[0] ?? "";
33
- }
34
-
35
- async function canBindLocalTcp(): Promise<boolean> {
36
- const probe = createServer();
37
- return new Promise((resolve) => {
38
- const cleanup = () => {
39
- try {
40
- probe.close();
41
- } catch {
42
- // Ignore close races in test cleanup.
43
- }
44
- };
45
-
46
- (probe as unknown as EventEmitter).once("error", () => {
47
- cleanup();
48
- resolve(false);
49
- });
50
- (probe as unknown as EventEmitter).once("listening", () => {
51
- cleanup();
52
- resolve(true);
53
- });
54
- probe.listen(0, "127.0.0.1");
55
- });
56
- }
57
-
58
- async function reservePort(): Promise<number> {
59
- const probe = createServer();
60
- return new Promise((resolve, reject) => {
61
- const cleanup = () => {
62
- try {
63
- probe.close();
64
- } catch {
65
- // Ignore close races in test cleanup.
66
- }
67
- };
68
-
69
- (probe as unknown as EventEmitter).once("error", (error: Error) => {
70
- cleanup();
71
- reject(error);
72
- });
73
- (probe as unknown as EventEmitter).once("listening", () => {
74
- const address = probe.address();
75
- if (!address || typeof address === "string") {
76
- cleanup();
77
- reject(new Error("Failed to reserve a local TCP port for the admin server test."));
78
- return;
79
- }
80
-
81
- const { port } = address;
82
- probe.close((error) => {
83
- if (error) {
84
- reject(error);
85
- return;
86
- }
87
- resolve(port);
88
- });
89
- });
90
- probe.listen(0, "127.0.0.1");
91
- });
92
- }
93
-
94
- const TCP_AVAILABLE = await canBindLocalTcp();
95
- const describeIfTcp = TCP_AVAILABLE ? describe : describe.skip;
96
-
97
- function createMockAdminEngine(
98
- initialState: EngineCoreMetrics["state"],
99
- tickIntervalMs: number,
100
- connectedAdapters: string[] = [],
101
- enabledAdapters: string[] = [],
102
- ) {
103
- const runtime = {
104
- state: initialState as EngineCoreMetrics["state"],
105
- tickCount: 0,
106
- lastTickProgressAt: performance.now(),
107
- executedCommands: [] as EngineCommand[],
108
- connectedAdapters: [...connectedAdapters],
109
- enabledAdapters: [...enabledAdapters],
110
- };
111
-
112
- const snapshot: EngineStateSnapshot = {
113
- simulationName: "test-sim",
114
- models: [],
115
- instances: [],
116
- config: {
117
- seed: 1,
118
- tickIntervalMs,
119
- timeMultiplier: 1,
120
- adapters: [],
121
- },
122
- };
123
-
124
- return {
125
- engine: {
126
- getMetrics: () => ({
127
- simulationName: snapshot.simulationName,
128
- core: {
129
- state: runtime.state,
130
- tickCount: runtime.tickCount,
131
- simulationTimeSeconds: 0,
132
- instanceCount: 1,
133
- memberValueCount: 1,
134
- quarantinedInstanceCount: 0,
135
- engineErrorsCount: 0,
136
- tickOverrunsCount: 0,
137
- circuitBreakerTripsCount: 0,
138
- uptimeMs: 0,
139
- actualTicksPerSecond: 1,
140
- },
141
- fleet: {
142
- modelBreakdown: [{ model: "test-sensor", count: 1 }],
143
- lifecycleBreakdown: { running: 1 },
144
- activeFaultCount: 0,
145
- faultBreakdown: [],
146
- },
147
- adapters: runtime.connectedAdapters.map((name) => ({
148
- name,
149
- metrics: { connected: true },
150
- })),
151
- }),
152
- getState: () => ({
153
- ...snapshot,
154
- availableAdapters: runtime.enabledAdapters.map((name) => ({
155
- name,
156
- enabled: true,
157
- pending: false,
158
- config: {},
159
- })),
160
- }),
161
- getAvailableAdapters: () =>
162
- runtime.enabledAdapters.map((name) => ({
163
- name,
164
- enabled: true,
165
- })),
166
- getInstanceDetail: (_id: string) => undefined,
167
- getModelSchema: (_name: string) => undefined,
168
- getEventLog: () => ({ events: [], nextCursor: null, hasMore: false }),
169
- getRunningTemplates: () => [],
170
- getManifest: () => ({
171
- generatedAt: new Date().toISOString(),
172
- protocols: {},
173
- tags: [],
174
- summary: { totalTags: 0, totalInstances: 0, enabledProtocols: [] },
175
- }),
176
- execute: async (command: EngineCommand): Promise<CommandResult> => {
177
- runtime.executedCommands.push(command);
178
- return {
179
- success: true,
180
- command: command.type,
181
- correlationId: command.correlationId,
182
- message: `Executed ${command.type}`,
183
- };
184
- },
185
- },
186
- setState(state: EngineCoreMetrics["state"]) {
187
- runtime.state = state;
188
- },
189
- setTickCount(tickCount: number) {
190
- runtime.tickCount = tickCount;
191
- },
192
- setLastTickProgressAt(timestamp: number) {
193
- runtime.lastTickProgressAt = timestamp;
194
- },
195
- getLastTickProgressAt() {
196
- return runtime.lastTickProgressAt;
197
- },
198
- getExecutedCommands() {
199
- return [...runtime.executedCommands];
200
- },
201
- setConnectedAdapters(adapterNames: string[]) {
202
- runtime.connectedAdapters = [...adapterNames];
203
- },
204
- setEnabledAdapters(adapterNames: string[]) {
205
- runtime.enabledAdapters = [...adapterNames];
206
- },
207
- };
208
- }
209
-
210
- function serverBaseUrl(endpoint: string): string {
211
- const url = new URL(endpoint);
212
- return `http://127.0.0.1:${url.port}`;
213
- }
214
-
215
- interface StartServerOptions {
216
- readonly state: EngineCoreMetrics["state"];
217
- readonly tickIntervalMs?: number;
218
- readonly host?: string;
219
- /** Adapter names that should report as connected in mock metrics. */
220
- readonly connectedAdapters?: string[];
221
- /** Adapter names that should report as enabled in the runtime state. */
222
- readonly enabledAdapters?: string[];
223
- readonly getLeExport?:
224
- | ((
225
- id: string,
226
- opts: { host: string; protocol: import("@miravo/shared").ProtocolName | undefined },
227
- ) => Promise<{ templateJson: string; tagsCSV: string } | null>)
228
- | undefined;
229
- }
230
-
231
- async function startTestServer(options: StartServerOptions, handles: Array<{ stop: () => void }>) {
232
- const port = await reservePort();
233
- const mock = createMockAdminEngine(
234
- options.state,
235
- options.tickIntervalMs ?? 1000,
236
- options.connectedAdapters,
237
- options.enabledAdapters,
238
- );
239
- const handle = startAdminServer({
240
- engine: mock.engine,
241
- network: {
242
- host: options.host ?? "127.0.0.1",
243
- port,
244
- tickIntervalMs: options.tickIntervalMs ?? 1000,
245
- },
246
- protocols: {
247
- getLastTickProgressAt: () => mock.getLastTickProgressAt(),
248
- },
249
- auth: testAuth(),
250
- ...(options.getLeExport ? { getLeExport: options.getLeExport } : {}),
251
- });
252
- handles.push(handle);
253
- const baseUrl = serverBaseUrl(handle.endpoint);
254
- return { mock, baseUrl };
255
- }
256
-
257
- async function startShutdownTestServer(handles: Array<{ stop: () => void }>): Promise<{
258
- baseUrl: string;
259
- getShutdownReason: () => string;
260
- }> {
261
- const port = await reservePort();
262
- const mock = createMockAdminEngine("running", 1000);
263
- let shutdownReason = "";
264
- const handle = startAdminServer({
265
- engine: mock.engine,
266
- network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
267
- protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
268
- auth: testAuth(),
269
- requestShutdown: (reason) => {
270
- shutdownReason = reason;
271
- },
272
- });
273
- handles.push(handle);
274
- return { baseUrl: serverBaseUrl(handle.endpoint), getShutdownReason: () => shutdownReason };
275
- }
276
-
277
- describeIfTcp("admin server", () => {
278
- const handles: Array<{ stop: () => void }> = [];
279
-
280
- afterEach(() => {
281
- for (const handle of handles) {
282
- handle.stop();
283
- }
284
- handles.length = 0;
285
- });
286
-
287
- // --- Health endpoints (unauthenticated) ---
288
-
289
- test.each([
290
- {
291
- label: "200 when running and mqtt connected",
292
- state: "running" as const,
293
- healthStatus: 200,
294
- readyStatus: 200,
295
- connectedAdapters: ["mqtt"] as string[],
296
- },
297
- { label: "503 when paused", state: "paused" as const, healthStatus: 200, readyStatus: 503 },
298
- ])("/readyz returns $label", async ({ state, healthStatus, readyStatus, connectedAdapters }) => {
299
- const { baseUrl } = await startTestServer(
300
- {
301
- state,
302
- enabledAdapters: ["mqtt"],
303
- connectedAdapters: connectedAdapters ?? [],
304
- },
305
- handles,
306
- );
307
-
308
- const health = await fetch(`${baseUrl}/healthz`);
309
- const ready = await fetch(`${baseUrl}/readyz`);
310
-
311
- expect(health.status).toBe(healthStatus);
312
- expect(ready.status).toBe(readyStatus);
313
- });
314
-
315
- test("/readyz returns 503 when mqtt is enabled but disconnected", async () => {
316
- const { baseUrl } = await startTestServer(
317
- { state: "running", enabledAdapters: ["mqtt"] },
318
- handles,
319
- );
320
-
321
- const ready = await fetch(`${baseUrl}/readyz`);
322
- expect(ready.status).toBe(503);
323
- });
324
-
325
- test("/readyz returns 503 when opcua is enabled but disconnected", async () => {
326
- const { baseUrl } = await startTestServer(
327
- {
328
- state: "running",
329
- enabledAdapters: ["opcua"],
330
- },
331
- handles,
332
- );
333
-
334
- const ready = await fetch(`${baseUrl}/readyz`);
335
- expect(ready.status).toBe(503);
336
- const body = (await ready.json()) as { reason?: string };
337
- expect(body.reason).toBe("opcua disconnected");
338
- });
339
-
340
- test("/readyz follows live adapter enablement instead of boot-time flags", async () => {
341
- const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
342
-
343
- const initiallyReady = await fetch(`${baseUrl}/readyz`);
344
- expect(initiallyReady.status).toBe(200);
345
-
346
- mock.setEnabledAdapters(["mqtt"]);
347
- const disconnected = await fetch(`${baseUrl}/readyz`);
348
- expect(disconnected.status).toBe(503);
349
-
350
- mock.setConnectedAdapters(["mqtt"]);
351
- const connected = await fetch(`${baseUrl}/readyz`);
352
- expect(connected.status).toBe(200);
353
-
354
- mock.setEnabledAdapters([]);
355
- mock.setConnectedAdapters([]);
356
- const disabledAgain = await fetch(`${baseUrl}/readyz`);
357
- expect(disabledAgain.status).toBe(200);
358
- });
359
-
360
- test("/healthz returns 503 on the first probe after running tick loop stalls", async () => {
361
- const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
362
- mock.setLastTickProgressAt(performance.now() - 3_500);
363
-
364
- const stalled = await fetch(`${baseUrl}/healthz`);
365
- expect(stalled.status).toBe(503);
366
-
367
- mock.setLastTickProgressAt(performance.now());
368
- mock.setTickCount(1);
369
- const recovered = await fetch(`${baseUrl}/healthz`);
370
- expect(recovered.status).toBe(200);
371
- });
372
-
373
- // --- Protected endpoints (require session cookie) ---
374
-
375
- test("GET /metrics returns the engine metrics snapshot", async () => {
376
- const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
377
- mock.setTickCount(12);
378
- const cookie = await loginAndGetCookie(baseUrl);
379
-
380
- const response = await fetch(`${baseUrl}/metrics`, {
381
- headers: { cookie },
382
- });
383
- const body = (await response.json()) as {
384
- core: { tickCount: number; state: EngineCoreMetrics["state"] };
385
- simulationName: string;
386
- };
387
-
388
- expect(response.status).toBe(200);
389
- expect(body.simulationName).toBe("test-sim");
390
- expect(body.core.tickCount).toBe(12);
391
- expect(body.core.state).toBe("running");
392
- });
393
-
394
- test("GET /state returns the engine state snapshot", async () => {
395
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
396
- const cookie = await loginAndGetCookie(baseUrl);
397
-
398
- const response = await fetch(`${baseUrl}/state`, {
399
- headers: { cookie },
400
- });
401
- const body = (await response.json()) as EngineStateSnapshot;
402
-
403
- expect(response.status).toBe(200);
404
- expect(body.simulationName).toBe("test-sim");
405
- expect(body.config.tickIntervalMs).toBe(1000);
406
- });
407
-
408
- test("POST /commands validates and dispatches engine commands", async () => {
409
- const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
410
- const cookie = await loginAndGetCookie(baseUrl);
411
-
412
- const response = await fetch(`${baseUrl}/commands`, {
413
- method: "POST",
414
- headers: { "content-type": "application/json", cookie },
415
- body: JSON.stringify({
416
- type: "pause",
417
- correlationId: "req-1",
418
- }),
419
- });
420
- const body = (await response.json()) as CommandResult;
421
-
422
- expect(response.status).toBe(200);
423
- expect(body.success).toBe(true);
424
- expect(body.command).toBe("pause");
425
- expect(mock.getExecutedCommands()).toEqual([{ type: "pause", correlationId: "req-1" }]);
426
- });
427
-
428
- test("POST /commands requires session auth", async () => {
429
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
430
- // Without session cookie
431
- const response = await fetch(`${baseUrl}/commands`, {
432
- method: "POST",
433
- headers: { "content-type": "application/json" },
434
- body: JSON.stringify({ type: "pause", correlationId: "req-1" }),
435
- });
436
- expect(response.status).toBe(401);
437
- });
438
-
439
- test("POST /commands returns 400 for invalid JSON", async () => {
440
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
441
- const cookie = await loginAndGetCookie(baseUrl);
442
-
443
- const response = await fetch(`${baseUrl}/commands`, {
444
- method: "POST",
445
- headers: { "content-type": "application/json", cookie },
446
- body: "{bad json",
447
- });
448
- const body = (await response.json()) as { message: string };
449
-
450
- expect(response.status).toBe(400);
451
- expect(body.message).toBe("Invalid JSON in command request body.");
452
- });
453
-
454
- test("POST /commands returns 400 for invalid command shape", async () => {
455
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
456
- const cookie = await loginAndGetCookie(baseUrl);
457
-
458
- const response = await fetch(`${baseUrl}/commands`, {
459
- method: "POST",
460
- headers: { "content-type": "application/json", cookie },
461
- body: JSON.stringify({ type: "setSpeed" }),
462
- });
463
- const body = (await response.json()) as CommandResult;
464
-
465
- expect(response.status).toBe(400);
466
- expect(body.success).toBe(false);
467
- expect(body.command).toBe("setSpeed");
468
- });
469
-
470
- test.each([
471
- { label: "missing host", suffix: "" },
472
- { label: "blank host", suffix: "?host=%20%20%20" },
473
- ])("GET /catalog/templates/:id/le-template.json returns 400 for $label", async ({ suffix }) => {
474
- const { baseUrl } = await startTestServer(
475
- {
476
- state: "running",
477
- getLeExport: async () => ({
478
- templateJson: JSON.stringify({ ok: true }),
479
- tagsCSV: "name,value\n",
480
- }),
481
- },
482
- handles,
483
- );
484
- const cookie = await loginAndGetCookie(baseUrl);
485
-
486
- const response = await fetch(
487
- `${baseUrl}/catalog/templates/smart-factory/le-template.json${suffix}`,
488
- {
489
- headers: { cookie },
490
- },
491
- );
492
- const body = (await response.json()) as { message: string; error: string };
493
-
494
- expect(response.status).toBe(400);
495
- expect(body.message).toBe("Missing host query parameter.");
496
- expect(body.error).toContain("?host=<hostname-or-ip>");
497
- });
498
-
499
- test("GET /catalog/templates/:id/le-template.json accepts a non-empty host", async () => {
500
- const { baseUrl } = await startTestServer(
501
- {
502
- state: "running",
503
- getLeExport: async (_id, { host }) => ({
504
- templateJson: JSON.stringify({ host }),
505
- tagsCSV: "name,value\n",
506
- }),
507
- },
508
- handles,
509
- );
510
- const cookie = await loginAndGetCookie(baseUrl);
511
-
512
- const response = await fetch(
513
- `${baseUrl}/catalog/templates/smart-factory/le-template.json?host=172.17.0.1`,
514
- {
515
- headers: { cookie },
516
- },
517
- );
518
- const body = (await response.json()) as { host: string };
519
-
520
- expect(response.status).toBe(200);
521
- expect(body.host).toBe("172.17.0.1");
522
- });
523
-
524
- // --- Daemon mode endpoints ---
525
-
526
- test("/readyz includes simulationName and runId when provided", async () => {
527
- const port = await reservePort();
528
- const mock = createMockAdminEngine("running", 1000);
529
- const handle = startAdminServer({
530
- engine: mock.engine,
531
- network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
532
- protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
533
- auth: testAuth(),
534
- simulationName: "test-daemon",
535
- runId: "run-abc-123",
536
- });
537
- handles.push(handle);
538
- const baseUrl = serverBaseUrl(handle.endpoint);
539
-
540
- const response = await fetch(`${baseUrl}/readyz`);
541
- const body = (await response.json()) as Record<string, unknown>;
542
-
543
- expect(response.status).toBe(200);
544
- expect(body["simulationName"]).toBe("test-daemon");
545
- expect(body["runId"]).toBe("run-abc-123");
546
- });
547
-
548
- test("/readyz omits simulationName and runId when not provided", async () => {
549
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
550
-
551
- const response = await fetch(`${baseUrl}/readyz`);
552
- const body = (await response.json()) as Record<string, unknown>;
553
-
554
- expect(response.status).toBe(200);
555
- expect(body["simulationName"]).toBeUndefined();
556
- expect(body["runId"]).toBeUndefined();
557
- });
558
-
559
- test("POST /shutdown calls requestShutdown callback and returns 200", async () => {
560
- const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
561
- const cookie = await loginAndGetCookie(baseUrl);
562
-
563
- const response = await fetch(`${baseUrl}/shutdown`, {
564
- method: "POST",
565
- headers: { "content-type": "application/json", cookie },
566
- body: JSON.stringify({ reason: "miravo stop" }),
567
- });
568
- const body = (await response.json()) as Record<string, unknown>;
569
-
570
- expect(response.status).toBe(200);
571
- expect(body["status"]).toBe("shutting_down");
572
- expect(getShutdownReason()).toBe("miravo stop");
573
- });
574
-
575
- test("POST /shutdown uses default reason when body is empty", async () => {
576
- const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
577
- const cookie = await loginAndGetCookie(baseUrl);
578
-
579
- const response = await fetch(`${baseUrl}/shutdown`, {
580
- method: "POST",
581
- headers: { "content-type": "application/json", cookie },
582
- });
583
- expect(response.status).toBe(200);
584
- expect(getShutdownReason()).toBe("remote shutdown request");
585
- });
586
-
587
- test("GET /shutdown returns 405", async () => {
588
- const port = await reservePort();
589
- const mock = createMockAdminEngine("running", 1000);
590
- const handle = startAdminServer({
591
- engine: mock.engine,
592
- network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
593
- protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
594
- auth: testAuth(),
595
- requestShutdown: () => {},
596
- });
597
- handles.push(handle);
598
- const baseUrl = serverBaseUrl(handle.endpoint);
599
-
600
- const response = await fetch(`${baseUrl}/shutdown`);
601
- expect(response.status).toBe(405);
602
- });
603
-
604
- test("POST /shutdown returns 501 when requestShutdown not provided", async () => {
605
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
606
- const cookie = await loginAndGetCookie(baseUrl);
607
-
608
- const response = await fetch(`${baseUrl}/shutdown`, {
609
- method: "POST",
610
- headers: { cookie },
611
- });
612
- expect(response.status).toBe(501);
613
- });
614
-
615
- test("POST /shutdown requires session auth", async () => {
616
- const { baseUrl } = await startShutdownTestServer(handles);
617
- // Without session cookie
618
- const noAuth = await fetch(`${baseUrl}/shutdown`, {
619
- method: "POST",
620
- headers: { "content-type": "application/json" },
621
- });
622
- expect(noAuth.status).toBe(401);
623
- // With session cookie
624
- const cookie = await loginAndGetCookie(baseUrl);
625
- const withAuth = await fetch(`${baseUrl}/shutdown`, {
626
- method: "POST",
627
- headers: { "content-type": "application/json", cookie },
628
- });
629
- expect(withAuth.status).toBe(200);
630
- });
631
-
632
- test("POST /commands accepts session auth", async () => {
633
- const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
634
- const cookie = await loginAndGetCookie(baseUrl);
635
- const response = await fetch(`${baseUrl}/commands`, {
636
- method: "POST",
637
- headers: { "content-type": "application/json", cookie },
638
- body: JSON.stringify({ type: "pause", correlationId: "req-2" }),
639
- });
640
- const body = (await response.json()) as CommandResult;
641
- expect(response.status).toBe(200);
642
- expect(body.success).toBe(true);
643
- expect(mock.getExecutedCommands()).toEqual([{ type: "pause", correlationId: "req-2" }]);
644
- });
645
-
646
- // --- Auth endpoint tests ---
647
-
648
- test("POST /auth/login returns 200 with valid credentials", async () => {
649
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
650
- const res = await fetch(`${baseUrl}/auth/login`, {
651
- method: "POST",
652
- headers: { "content-type": "application/json" },
653
- body: JSON.stringify({ username: "admin", password: "testpass" }),
654
- });
655
- expect(res.status).toBe(200);
656
- const body = (await res.json()) as { ok: boolean; changePassword: boolean };
657
- expect(body.ok).toBe(true);
658
- expect(body.changePassword).toBe(false);
659
- expect(res.headers.get("set-cookie")).toContain("miravo_session=");
660
- });
661
-
662
- test("POST /auth/login returns 401 with invalid credentials", async () => {
663
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
664
- const res = await fetch(`${baseUrl}/auth/login`, {
665
- method: "POST",
666
- headers: { "content-type": "application/json" },
667
- body: JSON.stringify({ username: "admin", password: "wrong" }),
668
- });
669
- expect(res.status).toBe(401);
670
- });
671
-
672
- test("GET /auth/status returns authenticated with valid session", async () => {
673
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
674
- const cookie = await loginAndGetCookie(baseUrl);
675
- const res = await fetch(`${baseUrl}/auth/status`, { headers: { cookie } });
676
- const body = (await res.json()) as { authenticated: boolean; username?: string };
677
- expect(body.authenticated).toBe(true);
678
- expect(body.username).toBe("admin");
679
- });
680
-
681
- test("GET /auth/status returns unauthenticated without session", async () => {
682
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
683
- const res = await fetch(`${baseUrl}/auth/status`);
684
- const body = (await res.json()) as { authenticated: boolean };
685
- expect(body.authenticated).toBe(false);
686
- });
687
-
688
- test("GET /bootstrap returns unauthenticated without session", async () => {
689
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
690
- const res = await fetch(`${baseUrl}/bootstrap`);
691
- expect(res.status).toBe(200);
692
- const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
693
- expect(parsed.success).toBe(true);
694
- expect(parsed.data).toEqual({ authenticated: false });
695
- });
696
-
697
- test("GET /bootstrap returns auth plus canonical runtime snapshot with valid session", async () => {
698
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
699
- const cookie = await loginAndGetCookie(baseUrl);
700
- const res = await fetch(`${baseUrl}/bootstrap`, { headers: { cookie } });
701
- expect(res.status).toBe(200);
702
- const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
703
- expect(parsed.success).toBe(true);
704
- if (!parsed.success || !parsed.data.authenticated) {
705
- throw new Error("Expected authenticated bootstrap payload.");
706
- }
707
- expect(parsed.data.username).toBe("admin");
708
- expect(parsed.data.runtime.state.instances.length).toBeGreaterThanOrEqual(0);
709
- expect(parsed.data.runtime.metrics.core.state).toBe("running");
710
- expect(Array.isArray(parsed.data.runtime.runningTemplates)).toBe(true);
711
- expect(typeof parsed.data.runtime.revision).toBe("number");
712
- });
713
-
714
- test("GET /state requires session auth", async () => {
715
- const { baseUrl } = await startTestServer({ state: "running" }, handles);
716
- const res = await fetch(`${baseUrl}/state`);
717
- expect(res.status).toBe(401);
718
- });
719
- });
720
-
721
- describeIfTcp("anonymous mode (--no-auth)", () => {
722
- const handles: Array<{ stop: () => void }> = [];
723
- afterEach(() => {
724
- for (const h of handles) h.stop();
725
- handles.length = 0;
726
- });
727
-
728
- async function startAnonServer() {
729
- const port = await reservePort();
730
- const mock = createMockAdminEngine("running", 1000);
731
- const handle = startAdminServer({
732
- engine: mock.engine,
733
- network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
734
- protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
735
- auth: { ...testAuth(), anonymousMode: true },
736
- });
737
- handles.push(handle);
738
- return { baseUrl: serverBaseUrl(handle.endpoint) };
739
- }
740
-
741
- test("GET /bootstrap returns authenticated:true without a session cookie", async () => {
742
- const { baseUrl } = await startAnonServer();
743
- const res = await fetch(`${baseUrl}/bootstrap`);
744
- expect(res.status).toBe(200);
745
- const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
746
- expect(parsed.success).toBe(true);
747
- if (!parsed.success || !parsed.data.authenticated) {
748
- throw new Error("Expected authenticated bootstrap payload.");
749
- }
750
- expect(parsed.data.username).toBe("anonymous");
751
- expect(parsed.data.runtime.metrics.core.state).toBe("running");
752
- });
753
-
754
- test("GET /state succeeds without a session cookie", async () => {
755
- const { baseUrl } = await startAnonServer();
756
- const res = await fetch(`${baseUrl}/state`);
757
- expect(res.status).toBe(200);
758
- });
759
-
760
- test("POST /auth/login succeeds with any credentials", async () => {
761
- const { baseUrl } = await startAnonServer();
762
- const res = await fetch(`${baseUrl}/auth/login`, {
763
- method: "POST",
764
- headers: { "content-type": "application/json" },
765
- body: JSON.stringify({ username: "whoever", password: "anything" }),
766
- });
767
- expect(res.status).toBe(200);
768
- const body = (await res.json()) as { ok: boolean; changePassword: boolean };
769
- expect(body.ok).toBe(true);
770
- expect(body.changePassword).toBe(false);
771
- });
772
-
773
- test("GET /auth/status returns authenticated:true without a session cookie", async () => {
774
- const { baseUrl } = await startAnonServer();
775
- const res = await fetch(`${baseUrl}/auth/status`);
776
- expect(res.status).toBe(200);
777
- const body = (await res.json()) as { authenticated: boolean; username?: string };
778
- expect(body.authenticated).toBe(true);
779
- expect(body.username).toBe("anonymous");
780
- });
781
- });