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,562 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import type { Engine, EngineMetricsSnapshot } from "@miravo/core";
3
- import { muteAllLoggers } from "../commands/cli-test-setup.ts";
4
- import {
5
- configLog,
6
- configureLogger,
7
- controlLog,
8
- engineLog,
9
- faultLog,
10
- lifecycleLog,
11
- lockLogger,
12
- logger,
13
- mqttLog,
14
- opcuaLog,
15
- unlockLogger,
16
- } from "../logger.ts";
17
- import { createShutdownController } from "./shutdown.ts";
18
-
19
- const noopReporter = { log() {} };
20
-
21
- function configureSummaryLogger(
22
- options: { plain?: boolean; logFormat?: "pretty" | "json" } = {},
23
- ): void {
24
- const previousNoColor = Bun.env["NO_COLOR"];
25
- if (!(options.plain ?? false)) {
26
- delete Bun.env["NO_COLOR"];
27
- }
28
- unlockLogger();
29
- try {
30
- configureLogger({
31
- plain: options.plain ?? false,
32
- logFormat: options.logFormat ?? "pretty",
33
- });
34
- } finally {
35
- if (previousNoColor === undefined) {
36
- delete Bun.env["NO_COLOR"];
37
- } else {
38
- Bun.env["NO_COLOR"] = previousNoColor;
39
- }
40
- }
41
- for (const l of [
42
- logger,
43
- engineLog,
44
- mqttLog,
45
- opcuaLog,
46
- configLog,
47
- lifecycleLog,
48
- faultLog,
49
- controlLog,
50
- ]) {
51
- l.level = -999;
52
- l.setReporters([noopReporter]);
53
- }
54
- lockLogger();
55
- }
56
-
57
- function setStdoutIsTTY(value: boolean): () => void {
58
- const descriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
59
- Object.defineProperty(process.stdout, "isTTY", {
60
- configurable: true,
61
- value,
62
- });
63
-
64
- return () => {
65
- if (descriptor) {
66
- Object.defineProperty(process.stdout, "isTTY", descriptor);
67
- return;
68
- }
69
- delete (process.stdout as { isTTY?: boolean }).isTTY;
70
- };
71
- }
72
-
73
- function createMockEngine(): Engine & { stopCalled: boolean; stopReason: string | undefined } {
74
- const metrics: EngineMetricsSnapshot = {
75
- simulationName: "test-sim",
76
- core: {
77
- state: "running",
78
- tickCount: 100,
79
- simulationTimeSeconds: 100,
80
- instanceCount: 5,
81
- memberValueCount: 25,
82
- quarantinedInstanceCount: 0,
83
- engineErrorsCount: 0,
84
- tickOverrunsCount: 0,
85
- circuitBreakerTripsCount: 0,
86
- uptimeMs: 60_000,
87
- actualTicksPerSecond: 1.0,
88
- },
89
- fleet: {
90
- modelBreakdown: [{ model: "pump", count: 5 }],
91
- lifecycleBreakdown: { normal: 5 },
92
- activeFaultCount: 0,
93
- faultBreakdown: [],
94
- },
95
- adapters: [],
96
- };
97
-
98
- const mock = {
99
- stopCalled: false,
100
- stopReason: undefined as string | undefined,
101
- getMetrics: () => metrics,
102
- stop: async (reason?: string) => {
103
- mock.stopCalled = true;
104
- mock.stopReason = reason;
105
- },
106
- } as unknown as Engine & { stopCalled: boolean; stopReason: string | undefined };
107
-
108
- return mock;
109
- }
110
-
111
- describe("createShutdownController", () => {
112
- const teardowns: Array<() => void> = [];
113
-
114
- afterEach(() => {
115
- while (teardowns.length > 0) {
116
- teardowns.pop()?.();
117
- }
118
- muteAllLoggers();
119
- });
120
-
121
- test("first SIGINT calls engine.stop and exit(0)", async () => {
122
- const engine = createMockEngine();
123
- let exitCode: number | undefined;
124
-
125
- const controller = createShutdownController({
126
- seed: 42,
127
- seedExplicit: true,
128
- exit: (code) => {
129
- exitCode = code;
130
- },
131
- });
132
- teardowns.push(controller.teardown);
133
- controller.setEngine(engine);
134
-
135
- process.emit("SIGINT");
136
- await controller.waitForShutdownRequest();
137
- await controller.runShutdown();
138
-
139
- expect(engine.stopCalled).toBe(true);
140
- expect(engine.stopReason).toBe("user interrupt");
141
- expect(exitCode).toBe(0);
142
- });
143
-
144
- test("SIGTERM during graceful shutdown does not force-exit (parent runner cleanup)", async () => {
145
- const engine = createMockEngine();
146
- let exitCode: number | undefined;
147
- let engineStopped = false;
148
-
149
- (engine as unknown as Record<string, unknown>)["stop"] = () =>
150
- new Promise<void>((resolve) =>
151
- setTimeout(() => {
152
- engineStopped = true;
153
- resolve();
154
- }, 50),
155
- );
156
-
157
- const controller = createShutdownController({
158
- seed: 42,
159
- seedExplicit: true,
160
- exit: (code) => {
161
- exitCode = code;
162
- },
163
- });
164
- teardowns.push(controller.teardown);
165
- controller.setEngine(engine);
166
-
167
- process.emit("SIGINT");
168
- await controller.waitForShutdownRequest();
169
- const shutdownPromise = controller.runShutdown();
170
- await new Promise((resolve) => setTimeout(resolve, 10));
171
- // Simulate bun run / bunx forwarding SIGTERM to the child as the parent exits
172
- process.emit("SIGTERM");
173
- await controller.completion;
174
- await shutdownPromise;
175
-
176
- // Graceful shutdown must complete: engine stopped, exit(0), not force-stopped
177
- expect(engineStopped).toBe(true);
178
- expect(exitCode).toBe(0);
179
- });
180
-
181
- test("second signal during shutdown calls exit(1)", async () => {
182
- const engine = createMockEngine();
183
- const exitCodes: number[] = [];
184
-
185
- (engine as unknown as Record<string, unknown>)["stop"] = () =>
186
- new Promise((resolve) => setTimeout(resolve, 200));
187
-
188
- const controller = createShutdownController({
189
- seed: 42,
190
- seedExplicit: true,
191
- sigintCoalesceWindowMs: 20,
192
- exit: (code) => {
193
- exitCodes.push(code);
194
- },
195
- });
196
- teardowns.push(controller.teardown);
197
- controller.setEngine(engine);
198
-
199
- process.emit("SIGINT");
200
- await controller.waitForShutdownRequest();
201
- const shutdownPromise = controller.runShutdown();
202
- await new Promise((resolve) => setTimeout(resolve, 40));
203
- process.emit("SIGINT");
204
- await controller.completion;
205
- await shutdownPromise;
206
-
207
- expect(exitCodes).toEqual([1]);
208
- });
209
-
210
- test("duplicate SIGINT from bun run does not force-exit on a single Ctrl+C", async () => {
211
- const engine = createMockEngine();
212
- let exitCode: number | undefined;
213
- let engineStopped = false;
214
-
215
- (engine as unknown as Record<string, unknown>)["stop"] = () =>
216
- new Promise<void>((resolve) =>
217
- setTimeout(() => {
218
- engineStopped = true;
219
- resolve();
220
- }, 50),
221
- );
222
-
223
- const controller = createShutdownController({
224
- seed: 42,
225
- seedExplicit: true,
226
- sigintCoalesceWindowMs: 20,
227
- exit: (code) => {
228
- exitCode = code;
229
- },
230
- });
231
- teardowns.push(controller.teardown);
232
- controller.setEngine(engine);
233
-
234
- process.emit("SIGINT");
235
- await controller.waitForShutdownRequest();
236
- const shutdownPromise = controller.runShutdown();
237
- // Regression: `bun run <script>` forwards a second SIGINT to its child
238
- // immediately after the terminal already delivered SIGINT to the group.
239
- process.emit("SIGINT");
240
- await controller.completion;
241
- await shutdownPromise;
242
-
243
- expect(engineStopped).toBe(true);
244
- expect(exitCode).toBe(0);
245
- });
246
-
247
- test("runShutdown() without prior requestShutdown() throws synchronously", () => {
248
- const controller = createShutdownController({
249
- seed: 1,
250
- seedExplicit: true,
251
- exit: () => {},
252
- });
253
- teardowns.push(controller.teardown);
254
-
255
- expect(() => controller.runShutdown()).toThrow(
256
- "Shutdown was not requested before runShutdown()",
257
- );
258
- });
259
-
260
- test("duplicate requestShutdown before runShutdown is deduplicated (no force-exit)", async () => {
261
- const engine = createMockEngine();
262
- const exitCodes: number[] = [];
263
-
264
- const controller = createShutdownController({
265
- seed: 1,
266
- seedExplicit: true,
267
- exit: (code) => {
268
- exitCodes.push(code);
269
- },
270
- });
271
- teardowns.push(controller.teardown);
272
- controller.setEngine(engine);
273
-
274
- // First request via signal, second via direct API (simulating admin /shutdown race with SIGINT)
275
- process.emit("SIGINT");
276
- await controller.waitForShutdownRequest();
277
- controller.requestShutdown("admin api"); // second call before runShutdown starts
278
- await controller.runShutdown();
279
-
280
- expect(exitCodes).toEqual([0]);
281
- expect(engine.stopCalled).toBe(true);
282
- });
283
-
284
- test("shutdown request is inert until the owner runs shutdown", async () => {
285
- const engine = createMockEngine();
286
- let stopCallCount = 0;
287
-
288
- (engine as unknown as Record<string, unknown>)["stop"] = async () => {
289
- stopCallCount++;
290
- };
291
-
292
- const controller = createShutdownController({
293
- seed: 1,
294
- seedExplicit: true,
295
- exit: () => {},
296
- });
297
- teardowns.push(controller.teardown);
298
- controller.setEngine(engine);
299
-
300
- process.emit("SIGINT");
301
- await controller.waitForShutdownRequest();
302
-
303
- expect(stopCallCount).toBe(0);
304
-
305
- await controller.runShutdown();
306
-
307
- expect(stopCallCount).toBe(1);
308
- });
309
-
310
- test("task that hangs past timeout: warns and shutdown continues", async () => {
311
- const engine = createMockEngine();
312
- const steps: string[] = [];
313
- let exitCode: number | undefined;
314
-
315
- const controller = createShutdownController({
316
- seed: 1,
317
- seedExplicit: true,
318
- taskTimeoutMs: 30,
319
- exit: (code) => {
320
- exitCode = code;
321
- },
322
- });
323
- teardowns.push(controller.teardown);
324
- controller.setEngine(engine);
325
-
326
- controller.addTask("cleanup", "hanging task", () => new Promise<void>(() => {}));
327
- controller.addTask("finalize", "post-timeout step", () => {
328
- steps.push("finalize ran");
329
- });
330
-
331
- process.emit("SIGINT");
332
- await controller.waitForShutdownRequest();
333
- await controller.runShutdown();
334
-
335
- expect(steps).toEqual(["finalize ran"]);
336
- expect(exitCode).toBe(0);
337
- });
338
-
339
- test("task that rejects after timeout does not cause unhandled rejection", async () => {
340
- const engine = createMockEngine();
341
- const unhandledReasons: unknown[] = [];
342
- const onUnhandled = (reason: unknown) => {
343
- unhandledReasons.push(reason);
344
- };
345
- process.on("unhandledRejection", onUnhandled);
346
-
347
- try {
348
- const controller = createShutdownController({
349
- seed: 1,
350
- seedExplicit: true,
351
- taskTimeoutMs: 30,
352
- exit: () => {},
353
- });
354
- teardowns.push(controller.teardown);
355
- controller.setEngine(engine);
356
-
357
- let rejectLate!: (err: Error) => void;
358
- controller.addTask(
359
- "cleanup",
360
- "slow-then-reject",
361
- () =>
362
- new Promise<void>((_, reject) => {
363
- rejectLate = reject;
364
- }),
365
- );
366
-
367
- process.emit("SIGINT");
368
- await controller.waitForShutdownRequest();
369
- await controller.runShutdown(); // task times out after 30ms
370
-
371
- rejectLate(new Error("late rejection after timeout"));
372
- await new Promise((resolve) => setTimeout(resolve, 50));
373
- } finally {
374
- process.removeListener("unhandledRejection", onUnhandled);
375
- }
376
-
377
- expect(unhandledReasons).toHaveLength(0);
378
- });
379
-
380
- test("teardown removes signal handlers", async () => {
381
- const engine = createMockEngine();
382
-
383
- const controller = createShutdownController({
384
- seed: 1,
385
- seedExplicit: true,
386
- exit: () => {},
387
- });
388
- controller.setEngine(engine);
389
- controller.teardown();
390
-
391
- process.emit("SIGINT");
392
- await new Promise((resolve) => setTimeout(resolve, 50));
393
-
394
- expect(engine.stopCalled).toBe(false);
395
- });
396
-
397
- test("runs cleanup before summary and exit", async () => {
398
- const engine = createMockEngine();
399
- const steps: string[] = [];
400
- configureSummaryLogger({ plain: false, logFormat: "pretty" });
401
-
402
- const controller = createShutdownController({
403
- seed: 42,
404
- seedExplicit: true,
405
- writeFinalOutput: async () => {
406
- steps.push("summary");
407
- },
408
- exit: () => {
409
- steps.push("exit");
410
- },
411
- });
412
- teardowns.push(controller.teardown);
413
- controller.setEngine(engine);
414
- controller.addTask("cleanup", "cleanup", async () => {
415
- await new Promise((resolve) => setTimeout(resolve, 25));
416
- steps.push("cleanup");
417
- });
418
- controller.addTask("finalize", "finalize", () => {
419
- steps.push("finalize");
420
- });
421
-
422
- process.emit("SIGINT");
423
- await controller.waitForShutdownRequest();
424
- await controller.runShutdown();
425
-
426
- expect(steps).toEqual(["cleanup", "summary", "finalize", "exit"]);
427
- });
428
-
429
- test("cleanup failure does not suppress the shutdown summary", async () => {
430
- const engine = createMockEngine();
431
- let exitCode: number | undefined;
432
- const renders: string[] = [];
433
-
434
- configureSummaryLogger({ plain: false, logFormat: "pretty" });
435
-
436
- const controller = createShutdownController({
437
- seed: 42,
438
- seedExplicit: true,
439
- writeFinalOutput: async (text) => {
440
- renders.push(text);
441
- },
442
- exit: (code) => {
443
- exitCode = code;
444
- },
445
- });
446
- teardowns.push(controller.teardown);
447
- controller.setEngine(engine);
448
- controller.addTask("cleanup", "cleanup", () => {
449
- throw new Error("cleanup failed");
450
- });
451
-
452
- process.emit("SIGINT");
453
- await controller.waitForShutdownRequest();
454
- await controller.runShutdown();
455
-
456
- expect(exitCode).toBe(0);
457
- expect(renders).toHaveLength(1);
458
- expect(renders[0]?.includes("Shutdown Summary")).toBe(true);
459
- });
460
-
461
- test("renders the rounded shutdown box in pretty mode even when stdout is not a TTY", async () => {
462
- const engine = createMockEngine();
463
- let exitCode: number | undefined;
464
- const renders: string[] = [];
465
- const restoreTTY = setStdoutIsTTY(false);
466
- configureSummaryLogger({ plain: false, logFormat: "pretty" });
467
-
468
- try {
469
- const controller = createShutdownController({
470
- seed: 42,
471
- seedExplicit: true,
472
- writeFinalOutput: async (text) => {
473
- renders.push(text);
474
- },
475
- exit: (code) => {
476
- exitCode = code;
477
- },
478
- });
479
- teardowns.push(controller.teardown);
480
- controller.setEngine(engine);
481
-
482
- process.emit("SIGINT");
483
- await controller.waitForShutdownRequest();
484
- await controller.runShutdown();
485
- } finally {
486
- restoreTTY();
487
- }
488
-
489
- expect(exitCode).toBe(0);
490
- expect(renders).toHaveLength(1);
491
- expect(renders[0]?.includes("Shutdown Summary")).toBe(true);
492
- expect(renders[0]?.includes("╭")).toBe(true);
493
- expect(renders[0]?.includes("╰")).toBe(true);
494
- });
495
-
496
- test("keeps shutdown output plain when --plain is enabled", async () => {
497
- const engine = createMockEngine();
498
- const renders: string[] = [];
499
- configureSummaryLogger({ plain: true, logFormat: "pretty" });
500
-
501
- const controller = createShutdownController({
502
- seed: 42,
503
- seedExplicit: true,
504
- writeFinalOutput: async (text) => {
505
- renders.push(text);
506
- },
507
- exit: () => {},
508
- });
509
- teardowns.push(controller.teardown);
510
- controller.setEngine(engine);
511
-
512
- process.emit("SIGINT");
513
- await controller.waitForShutdownRequest();
514
- await controller.runShutdown();
515
-
516
- expect(renders).toHaveLength(1);
517
- expect(renders[0]?.startsWith("Shutdown Summary\n")).toBe(true);
518
- expect(renders[0]?.includes("╭")).toBe(false);
519
- });
520
-
521
- test("prints top 5 error contexts in shutdown summary", async () => {
522
- const engine = createMockEngine();
523
- let exitCode: number | undefined;
524
- const renders: string[] = [];
525
- configureSummaryLogger({ plain: true, logFormat: "pretty" });
526
-
527
- const controller = createShutdownController({
528
- seed: 42,
529
- seedExplicit: true,
530
- errorContextCounts: new Map([
531
- ["mqtt:publish", 9],
532
- ["tick-overrun", 7],
533
- ["mqtt:backpressure", 5],
534
- ["mqtt:reconnect", 3],
535
- ['circuit-breaker instance="pump-001"', 2],
536
- ["mqtt:client", 1],
537
- ]),
538
- writeFinalOutput: async (text) => {
539
- renders.push(text);
540
- },
541
- exit: (code) => {
542
- exitCode = code;
543
- },
544
- });
545
- teardowns.push(controller.teardown);
546
- controller.setEngine(engine);
547
-
548
- process.emit("SIGINT");
549
- await controller.waitForShutdownRequest();
550
- await controller.runShutdown();
551
-
552
- const output = renders[0] ?? "";
553
- expect(exitCode).toBe(0);
554
- expect(output.includes("Top errors")).toBe(true);
555
- expect(output.includes("mqtt:publish")).toBe(true);
556
- expect(output.includes("tick-overrun")).toBe(true);
557
- expect(output.includes("mqtt:backpressure")).toBe(true);
558
- expect(output.includes("mqtt:reconnect")).toBe(true);
559
- expect(output.includes('circuit-breaker instance="pump-001"')).toBe(true);
560
- expect(output.includes("mqtt:client")).toBe(false);
561
- });
562
- });