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.
- package/LICENSE +199 -0
- package/README.md +7 -70
- package/bin/miravo.js +84 -0
- package/package.json +20 -36
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
package/src/lib/shutdown.test.ts
DELETED
|
@@ -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
|
-
});
|