miravo 0.1.4 → 0.2.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/README.md +38 -32
- package/package.json +11 -5
- package/src/__tests__/integration/executable-methods-runner.ts +253 -0
- package/src/__tests__/integration/executable-methods.integration.ts +1688 -0
- package/src/commands/fork.ts +205 -0
- package/src/commands/inject.test.ts +6 -9
- package/src/commands/inject.ts +48 -158
- package/src/commands/models.test.ts +32 -8
- package/src/commands/models.ts +71 -45
- package/src/commands/start.test.ts +32 -63
- package/src/commands/start.ts +333 -193
- package/src/commands/status.test.ts +3 -22
- package/src/commands/status.ts +114 -279
- package/src/commands/templates.test.ts +4 -50
- package/src/commands/templates.ts +12 -191
- package/src/index.ts +42 -8
- package/src/lib/admin-client.ts +214 -0
- package/src/lib/admin-server.test.ts +276 -0
- package/src/lib/admin-server.ts +307 -0
- package/src/lib/shutdown.test.ts +221 -93
- package/src/lib/shutdown.ts +478 -98
- package/src/lib/template-category.test.ts +14 -7
- package/src/lib/template-category.ts +6 -1
- package/src/lib/template-locator.ts +5 -8
- package/src/lib/template-utils.ts +14 -0
- package/src/logger.test.ts +2 -2
- package/src/logger.ts +68 -15
- package/src/output.ts +41 -1
- package/src/lib/health-server.test.ts +0 -161
- package/src/lib/health-server.ts +0 -154
package/src/lib/shutdown.test.ts
CHANGED
|
@@ -11,9 +11,52 @@ import {
|
|
|
11
11
|
lockLogger,
|
|
12
12
|
logger,
|
|
13
13
|
mqttLog,
|
|
14
|
+
opcuaLog,
|
|
14
15
|
unlockLogger,
|
|
15
16
|
} from "../logger.ts";
|
|
16
|
-
import {
|
|
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
|
+
unlockLogger();
|
|
25
|
+
configureLogger({
|
|
26
|
+
plain: options.plain ?? false,
|
|
27
|
+
logFormat: options.logFormat ?? "pretty",
|
|
28
|
+
});
|
|
29
|
+
for (const l of [
|
|
30
|
+
logger,
|
|
31
|
+
engineLog,
|
|
32
|
+
mqttLog,
|
|
33
|
+
opcuaLog,
|
|
34
|
+
configLog,
|
|
35
|
+
lifecycleLog,
|
|
36
|
+
faultLog,
|
|
37
|
+
controlLog,
|
|
38
|
+
]) {
|
|
39
|
+
l.level = -999;
|
|
40
|
+
l.setReporters([noopReporter]);
|
|
41
|
+
}
|
|
42
|
+
lockLogger();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setStdoutIsTTY(value: boolean): () => void {
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
47
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
48
|
+
configurable: true,
|
|
49
|
+
value,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
if (descriptor) {
|
|
54
|
+
Object.defineProperty(process.stdout, "isTTY", descriptor);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
delete (process.stdout as { isTTY?: boolean }).isTTY;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
17
60
|
|
|
18
61
|
function createMockEngine(): Engine & { stopCalled: boolean; stopReason: string | undefined } {
|
|
19
62
|
const metrics: EngineMetricsSnapshot = {
|
|
@@ -23,7 +66,7 @@ function createMockEngine(): Engine & { stopCalled: boolean; stopReason: string
|
|
|
23
66
|
tickCount: 100,
|
|
24
67
|
simulationTimeSeconds: 100,
|
|
25
68
|
instanceCount: 5,
|
|
26
|
-
|
|
69
|
+
memberValueCount: 25,
|
|
27
70
|
quarantinedInstanceCount: 0,
|
|
28
71
|
engineErrorsCount: 0,
|
|
29
72
|
tickOverrunsCount: 0,
|
|
@@ -53,31 +96,33 @@ function createMockEngine(): Engine & { stopCalled: boolean; stopReason: string
|
|
|
53
96
|
return mock;
|
|
54
97
|
}
|
|
55
98
|
|
|
56
|
-
describe("
|
|
57
|
-
|
|
99
|
+
describe("createShutdownController", () => {
|
|
100
|
+
const teardowns: Array<() => void> = [];
|
|
58
101
|
|
|
59
102
|
afterEach(() => {
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
while (teardowns.length > 0) {
|
|
104
|
+
teardowns.pop()?.();
|
|
105
|
+
}
|
|
106
|
+
muteAllLoggers();
|
|
62
107
|
});
|
|
63
108
|
|
|
64
109
|
test("first SIGINT calls engine.stop and exit(0)", async () => {
|
|
65
110
|
const engine = createMockEngine();
|
|
66
111
|
let exitCode: number | undefined;
|
|
67
112
|
|
|
68
|
-
|
|
69
|
-
engine,
|
|
113
|
+
const controller = createShutdownController({
|
|
70
114
|
seed: 42,
|
|
71
115
|
seedExplicit: true,
|
|
72
116
|
exit: (code) => {
|
|
73
117
|
exitCode = code;
|
|
74
118
|
},
|
|
75
119
|
});
|
|
120
|
+
teardowns.push(controller.teardown);
|
|
121
|
+
controller.setEngine(engine);
|
|
76
122
|
|
|
77
123
|
process.emit("SIGINT");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
124
|
+
await controller.waitForShutdownRequest();
|
|
125
|
+
await controller.runShutdown();
|
|
81
126
|
|
|
82
127
|
expect(engine.stopCalled).toBe(true);
|
|
83
128
|
expect(engine.stopReason).toBe("user interrupt");
|
|
@@ -88,153 +133,236 @@ describe("installShutdownHandlers", () => {
|
|
|
88
133
|
const engine = createMockEngine();
|
|
89
134
|
const exitCodes: number[] = [];
|
|
90
135
|
|
|
91
|
-
// Make stop() slow so we can fire a second signal during shutdown
|
|
92
136
|
(engine as unknown as Record<string, unknown>)["stop"] = () =>
|
|
93
137
|
new Promise((resolve) => setTimeout(resolve, 200));
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
engine,
|
|
139
|
+
const controller = createShutdownController({
|
|
97
140
|
seed: 42,
|
|
98
|
-
seedExplicit:
|
|
141
|
+
seedExplicit: true,
|
|
99
142
|
exit: (code) => {
|
|
100
143
|
exitCodes.push(code);
|
|
101
144
|
},
|
|
102
145
|
});
|
|
146
|
+
teardowns.push(controller.teardown);
|
|
147
|
+
controller.setEngine(engine);
|
|
103
148
|
|
|
104
|
-
// First signal starts shutdown
|
|
105
149
|
process.emit("SIGINT");
|
|
106
|
-
|
|
150
|
+
await controller.waitForShutdownRequest();
|
|
151
|
+
const shutdownPromise = controller.runShutdown();
|
|
107
152
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
108
|
-
// Second signal should force exit
|
|
109
153
|
process.emit("SIGINT");
|
|
110
|
-
|
|
111
|
-
await
|
|
154
|
+
await controller.completion;
|
|
155
|
+
await shutdownPromise;
|
|
112
156
|
|
|
113
157
|
expect(exitCodes).toContain(1);
|
|
114
158
|
});
|
|
115
159
|
|
|
116
|
-
test("
|
|
160
|
+
test("shutdown request is inert until the owner runs shutdown", async () => {
|
|
117
161
|
const engine = createMockEngine();
|
|
118
|
-
let
|
|
162
|
+
let stopCallCount = 0;
|
|
119
163
|
|
|
120
|
-
// Make stop() throw
|
|
121
164
|
(engine as unknown as Record<string, unknown>)["stop"] = async () => {
|
|
122
|
-
|
|
165
|
+
stopCallCount++;
|
|
123
166
|
};
|
|
124
167
|
|
|
125
|
-
|
|
126
|
-
engine,
|
|
168
|
+
const controller = createShutdownController({
|
|
127
169
|
seed: 1,
|
|
128
170
|
seedExplicit: true,
|
|
129
|
-
exit: (
|
|
130
|
-
exitCode = code;
|
|
131
|
-
},
|
|
171
|
+
exit: () => {},
|
|
132
172
|
});
|
|
173
|
+
teardowns.push(controller.teardown);
|
|
174
|
+
controller.setEngine(engine);
|
|
133
175
|
|
|
134
176
|
process.emit("SIGINT");
|
|
135
|
-
await
|
|
177
|
+
await controller.waitForShutdownRequest();
|
|
136
178
|
|
|
137
|
-
expect(
|
|
179
|
+
expect(stopCallCount).toBe(0);
|
|
180
|
+
|
|
181
|
+
await controller.runShutdown();
|
|
182
|
+
|
|
183
|
+
expect(stopCallCount).toBe(1);
|
|
138
184
|
});
|
|
139
185
|
|
|
140
|
-
test("
|
|
186
|
+
test("teardown removes signal handlers", async () => {
|
|
141
187
|
const engine = createMockEngine();
|
|
142
|
-
let stopCallCount = 0;
|
|
143
|
-
|
|
144
|
-
(engine as unknown as Record<string, unknown>)["stop"] = async () => {
|
|
145
|
-
stopCallCount++;
|
|
146
|
-
};
|
|
147
188
|
|
|
148
|
-
|
|
149
|
-
engine,
|
|
189
|
+
const controller = createShutdownController({
|
|
150
190
|
seed: 1,
|
|
151
191
|
seedExplicit: true,
|
|
152
192
|
exit: () => {},
|
|
153
193
|
});
|
|
194
|
+
controller.setEngine(engine);
|
|
195
|
+
controller.teardown();
|
|
154
196
|
|
|
155
|
-
// Fire both signals nearly simultaneously
|
|
156
197
|
process.emit("SIGINT");
|
|
157
|
-
process.emit("SIGTERM");
|
|
158
198
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
159
199
|
|
|
160
|
-
|
|
161
|
-
expect(stopCallCount).toBe(1);
|
|
200
|
+
expect(engine.stopCalled).toBe(false);
|
|
162
201
|
});
|
|
163
202
|
|
|
164
|
-
test("
|
|
203
|
+
test("runs cleanup before summary and exit", async () => {
|
|
165
204
|
const engine = createMockEngine();
|
|
205
|
+
const steps: string[] = [];
|
|
206
|
+
configureSummaryLogger({ plain: false, logFormat: "pretty" });
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
seed: 1,
|
|
208
|
+
const controller = createShutdownController({
|
|
209
|
+
seed: 42,
|
|
170
210
|
seedExplicit: true,
|
|
171
|
-
|
|
211
|
+
writeFinalOutput: async () => {
|
|
212
|
+
steps.push("summary");
|
|
213
|
+
},
|
|
214
|
+
exit: () => {
|
|
215
|
+
steps.push("exit");
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
teardowns.push(controller.teardown);
|
|
219
|
+
controller.setEngine(engine);
|
|
220
|
+
controller.addTask("cleanup", "cleanup", async () => {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
222
|
+
steps.push("cleanup");
|
|
223
|
+
});
|
|
224
|
+
controller.addTask("finalize", "finalize", () => {
|
|
225
|
+
steps.push("finalize");
|
|
172
226
|
});
|
|
173
227
|
|
|
174
|
-
// Remove handlers
|
|
175
|
-
teardown();
|
|
176
|
-
teardown = undefined;
|
|
177
|
-
|
|
178
|
-
// Fire SIGINT after teardown — our handler should NOT run.
|
|
179
|
-
// If handlers weren't removed, the mock engine would get stopped.
|
|
180
228
|
process.emit("SIGINT");
|
|
181
|
-
await
|
|
182
|
-
|
|
229
|
+
await controller.waitForShutdownRequest();
|
|
230
|
+
await controller.runShutdown();
|
|
231
|
+
|
|
232
|
+
expect(steps).toEqual(["cleanup", "summary", "finalize", "exit"]);
|
|
183
233
|
});
|
|
184
234
|
|
|
185
|
-
test("
|
|
235
|
+
test("cleanup failure does not suppress the shutdown summary", async () => {
|
|
186
236
|
const engine = createMockEngine();
|
|
187
237
|
let exitCode: number | undefined;
|
|
188
|
-
const
|
|
189
|
-
const calls: string[] = [];
|
|
190
|
-
console.log = (...args: unknown[]) => {
|
|
191
|
-
calls.push(args.map((arg) => String(arg)).join(" "));
|
|
192
|
-
};
|
|
238
|
+
const renders: string[] = [];
|
|
193
239
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
240
|
+
configureSummaryLogger({ plain: false, logFormat: "pretty" });
|
|
241
|
+
|
|
242
|
+
const controller = createShutdownController({
|
|
243
|
+
seed: 42,
|
|
244
|
+
seedExplicit: true,
|
|
245
|
+
writeFinalOutput: async (text) => {
|
|
246
|
+
renders.push(text);
|
|
247
|
+
},
|
|
248
|
+
exit: (code) => {
|
|
249
|
+
exitCode = code;
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
teardowns.push(controller.teardown);
|
|
253
|
+
controller.setEngine(engine);
|
|
254
|
+
controller.addTask("cleanup", "cleanup", () => {
|
|
255
|
+
throw new Error("cleanup failed");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
process.emit("SIGINT");
|
|
259
|
+
await controller.waitForShutdownRequest();
|
|
260
|
+
await controller.runShutdown();
|
|
261
|
+
|
|
262
|
+
expect(exitCode).toBe(0);
|
|
263
|
+
expect(renders).toHaveLength(1);
|
|
264
|
+
expect(renders[0]?.includes("Shutdown Summary")).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("renders the rounded shutdown box in pretty mode even when stdout is not a TTY", async () => {
|
|
268
|
+
const engine = createMockEngine();
|
|
269
|
+
let exitCode: number | undefined;
|
|
270
|
+
const renders: string[] = [];
|
|
271
|
+
const restoreTTY = setStdoutIsTTY(false);
|
|
272
|
+
configureSummaryLogger({ plain: false, logFormat: "pretty" });
|
|
205
273
|
|
|
206
274
|
try {
|
|
207
|
-
|
|
208
|
-
engine,
|
|
275
|
+
const controller = createShutdownController({
|
|
209
276
|
seed: 42,
|
|
210
277
|
seedExplicit: true,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
["mqtt:backpressure", 5],
|
|
215
|
-
["mqtt:reconnect", 3],
|
|
216
|
-
['circuit-breaker instance="pump-001"', 2],
|
|
217
|
-
["mqtt:client", 1],
|
|
218
|
-
]),
|
|
278
|
+
writeFinalOutput: async (text) => {
|
|
279
|
+
renders.push(text);
|
|
280
|
+
},
|
|
219
281
|
exit: (code) => {
|
|
220
282
|
exitCode = code;
|
|
221
283
|
},
|
|
222
284
|
});
|
|
285
|
+
teardowns.push(controller.teardown);
|
|
286
|
+
controller.setEngine(engine);
|
|
223
287
|
|
|
224
288
|
process.emit("SIGINT");
|
|
225
|
-
await
|
|
289
|
+
await controller.waitForShutdownRequest();
|
|
290
|
+
await controller.runShutdown();
|
|
226
291
|
} finally {
|
|
227
|
-
|
|
228
|
-
muteAllLoggers();
|
|
292
|
+
restoreTTY();
|
|
229
293
|
}
|
|
230
294
|
|
|
231
295
|
expect(exitCode).toBe(0);
|
|
232
|
-
expect(
|
|
233
|
-
expect(
|
|
234
|
-
expect(
|
|
235
|
-
expect(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
296
|
+
expect(renders).toHaveLength(1);
|
|
297
|
+
expect(renders[0]?.includes("Shutdown Summary")).toBe(true);
|
|
298
|
+
expect(renders[0]?.includes("╭")).toBe(true);
|
|
299
|
+
expect(renders[0]?.includes("╰")).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("keeps shutdown output plain when --plain is enabled", async () => {
|
|
303
|
+
const engine = createMockEngine();
|
|
304
|
+
const renders: string[] = [];
|
|
305
|
+
configureSummaryLogger({ plain: true, logFormat: "pretty" });
|
|
306
|
+
|
|
307
|
+
const controller = createShutdownController({
|
|
308
|
+
seed: 42,
|
|
309
|
+
seedExplicit: true,
|
|
310
|
+
writeFinalOutput: async (text) => {
|
|
311
|
+
renders.push(text);
|
|
312
|
+
},
|
|
313
|
+
exit: () => {},
|
|
314
|
+
});
|
|
315
|
+
teardowns.push(controller.teardown);
|
|
316
|
+
controller.setEngine(engine);
|
|
317
|
+
|
|
318
|
+
process.emit("SIGINT");
|
|
319
|
+
await controller.waitForShutdownRequest();
|
|
320
|
+
await controller.runShutdown();
|
|
321
|
+
|
|
322
|
+
expect(renders).toHaveLength(1);
|
|
323
|
+
expect(renders[0]?.startsWith("Shutdown Summary\n")).toBe(true);
|
|
324
|
+
expect(renders[0]?.includes("╭")).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("prints top 5 error contexts in shutdown summary", async () => {
|
|
328
|
+
const engine = createMockEngine();
|
|
329
|
+
let exitCode: number | undefined;
|
|
330
|
+
const renders: string[] = [];
|
|
331
|
+
configureSummaryLogger({ plain: true, logFormat: "pretty" });
|
|
332
|
+
|
|
333
|
+
const controller = createShutdownController({
|
|
334
|
+
seed: 42,
|
|
335
|
+
seedExplicit: true,
|
|
336
|
+
errorContextCounts: new Map([
|
|
337
|
+
["mqtt:publish", 9],
|
|
338
|
+
["tick-overrun", 7],
|
|
339
|
+
["mqtt:backpressure", 5],
|
|
340
|
+
["mqtt:reconnect", 3],
|
|
341
|
+
['circuit-breaker instance="pump-001"', 2],
|
|
342
|
+
["mqtt:client", 1],
|
|
343
|
+
]),
|
|
344
|
+
writeFinalOutput: async (text) => {
|
|
345
|
+
renders.push(text);
|
|
346
|
+
},
|
|
347
|
+
exit: (code) => {
|
|
348
|
+
exitCode = code;
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
teardowns.push(controller.teardown);
|
|
352
|
+
controller.setEngine(engine);
|
|
353
|
+
|
|
354
|
+
process.emit("SIGINT");
|
|
355
|
+
await controller.waitForShutdownRequest();
|
|
356
|
+
await controller.runShutdown();
|
|
357
|
+
|
|
358
|
+
const output = renders[0] ?? "";
|
|
359
|
+
expect(exitCode).toBe(0);
|
|
360
|
+
expect(output.includes("Top errors")).toBe(true);
|
|
361
|
+
expect(output.includes("mqtt:publish")).toBe(true);
|
|
362
|
+
expect(output.includes("tick-overrun")).toBe(true);
|
|
363
|
+
expect(output.includes("mqtt:backpressure")).toBe(true);
|
|
364
|
+
expect(output.includes("mqtt:reconnect")).toBe(true);
|
|
365
|
+
expect(output.includes('circuit-breaker instance="pump-001"')).toBe(true);
|
|
366
|
+
expect(output.includes("mqtt:client")).toBe(false);
|
|
239
367
|
});
|
|
240
368
|
});
|