miravo 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- 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.ts
DELETED
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
import type { Engine, EngineMetricsSnapshot } from "@miravo/core";
|
|
2
|
-
import type { AdapterHealthSnapshot } from "@miravo/shared";
|
|
3
|
-
import type { TreeItem } from "consola/utils";
|
|
4
|
-
import { box, colors, formatTree, stripAnsi } from "consola/utils";
|
|
5
|
-
import {
|
|
6
|
-
adapterLogger,
|
|
7
|
-
engineLog,
|
|
8
|
-
isJsonLogFormat,
|
|
9
|
-
isPlain,
|
|
10
|
-
isQuiet,
|
|
11
|
-
writeOutput,
|
|
12
|
-
} from "../logger.ts";
|
|
13
|
-
import { computeAverageThroughput, formatCount, formatDuration, sep } from "../output.ts";
|
|
14
|
-
|
|
15
|
-
const SHUTDOWN_TASK_TIMEOUT_MS = 6_000;
|
|
16
|
-
const FINAL_OUTPUT_GRACE_MS = 50;
|
|
17
|
-
const FORWARDED_SIGINT_COALESCE_WINDOW_MS = 100;
|
|
18
|
-
|
|
19
|
-
type ShutdownTaskStage = "cleanup" | "finalize";
|
|
20
|
-
type ShutdownPhase = "booting" | "running" | "shutting_down" | "forced_exit" | "exited";
|
|
21
|
-
type ShutdownRequestSource = "api" | "sigint" | "sigterm";
|
|
22
|
-
|
|
23
|
-
interface ShutdownTask {
|
|
24
|
-
readonly name: string;
|
|
25
|
-
readonly run: () => void | Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Parameters for the shutdown controller. Supports dependency injection for tests. */
|
|
29
|
-
interface ShutdownControllerOptions {
|
|
30
|
-
/** The RNG seed used for this run (printed in summary for reproducibility). */
|
|
31
|
-
readonly seed: number;
|
|
32
|
-
/** Whether the seed was explicitly configured (vs. randomly generated). */
|
|
33
|
-
readonly seedExplicit: boolean;
|
|
34
|
-
/** Optional aggregated engine-error counts by context. */
|
|
35
|
-
readonly errorContextCounts?: ReadonlyMap<string, number>;
|
|
36
|
-
/** Process exit function. Defaults to process.exit. Inject for testing. */
|
|
37
|
-
readonly exit?: (code: number) => void;
|
|
38
|
-
/** Optional writer override for the final shutdown summary render. */
|
|
39
|
-
readonly writeFinalOutput?: (text: string) => Promise<void>;
|
|
40
|
-
/** Per-task timeout in ms. Defaults to SHUTDOWN_TASK_TIMEOUT_MS. Inject a small value in tests. */
|
|
41
|
-
readonly taskTimeoutMs?: number;
|
|
42
|
-
/** Duplicate SIGINT coalescing window in ms. Inject a small value in tests. */
|
|
43
|
-
readonly sigintCoalesceWindowMs?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface ShutdownController {
|
|
47
|
-
readonly completion: Promise<void>;
|
|
48
|
-
addTask(stage: ShutdownTaskStage, name: string, run: () => void | Promise<void>): void;
|
|
49
|
-
isShuttingDown(): boolean;
|
|
50
|
-
markRunning(): void;
|
|
51
|
-
requestShutdown(reason: string): void;
|
|
52
|
-
runShutdown(): Promise<void>;
|
|
53
|
-
setEngine(engine: Engine): void;
|
|
54
|
-
setErrorContextCounts(counts: ReadonlyMap<string, number>): void;
|
|
55
|
-
teardown(): void;
|
|
56
|
-
waitForShutdownRequest(): Promise<string>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Create a single-owner shutdown controller for `miravo start`.
|
|
61
|
-
*
|
|
62
|
-
* Signals are installed immediately. The controller owns the full shutdown
|
|
63
|
-
* lifecycle: pre-stop cleanup, engine stop, post-stop cleanup, final summary,
|
|
64
|
-
* and process exit. Callers should await `completion` to keep the command alive
|
|
65
|
-
* until shutdown fully finishes.
|
|
66
|
-
*/
|
|
67
|
-
export function createShutdownController(options: ShutdownControllerOptions): ShutdownController {
|
|
68
|
-
const exit = options.exit ?? ((code: number) => process.exit(code));
|
|
69
|
-
const writeFinalOutput = options.writeFinalOutput ?? writeShutdownOutput;
|
|
70
|
-
const taskTimeoutMs = options.taskTimeoutMs ?? SHUTDOWN_TASK_TIMEOUT_MS;
|
|
71
|
-
const sigintCoalesceWindowMs =
|
|
72
|
-
options.sigintCoalesceWindowMs ?? FORWARDED_SIGINT_COALESCE_WINDOW_MS;
|
|
73
|
-
|
|
74
|
-
let phase: ShutdownPhase = "booting";
|
|
75
|
-
let forcedExitRequested = false;
|
|
76
|
-
let engine: Engine | null = null;
|
|
77
|
-
let errorContextCounts = options.errorContextCounts;
|
|
78
|
-
let requestedReason: string | null = null;
|
|
79
|
-
let requestSource: ShutdownRequestSource | null = null;
|
|
80
|
-
let shutdownPromise: Promise<void> | null = null;
|
|
81
|
-
let requestedAtMs = 0;
|
|
82
|
-
let forwardedSigintIgnored = false;
|
|
83
|
-
|
|
84
|
-
const tasks: Record<ShutdownTaskStage, ShutdownTask[]> = {
|
|
85
|
-
cleanup: [],
|
|
86
|
-
finalize: [],
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
let resolveCompletion = () => {};
|
|
90
|
-
const completion = new Promise<void>((resolve) => {
|
|
91
|
-
resolveCompletion = resolve;
|
|
92
|
-
});
|
|
93
|
-
let resolveRequest = (_reason: string) => {};
|
|
94
|
-
const request = new Promise<string>((resolve) => {
|
|
95
|
-
resolveRequest = resolve;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const keepAlive = setInterval(() => {}, 60_000);
|
|
99
|
-
const shouldIgnoreForwardedSigint = (): boolean => {
|
|
100
|
-
if (requestSource !== "sigint" || forwardedSigintIgnored) {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
const elapsedMs = performance.now() - requestedAtMs;
|
|
104
|
-
if (elapsedMs > sigintCoalesceWindowMs) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
forwardedSigintIgnored = true;
|
|
108
|
-
engineLog.debug(`Ignoring duplicate SIGINT ${elapsedMs.toFixed(1)}ms after shutdown request.`);
|
|
109
|
-
return true;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const forceSignalHandler = (): void => {
|
|
113
|
-
if (shouldIgnoreForwardedSigint()) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
forceExit();
|
|
117
|
-
};
|
|
118
|
-
// Absorbs SIGTERM during graceful shutdown without terminating.
|
|
119
|
-
// bun run / bunx forwards SIGTERM to the child process when the parent
|
|
120
|
-
// runner receives SIGINT and exits — leaving SIGTERM unhandled would restore
|
|
121
|
-
// the OS default (abrupt termination). This no-op keeps the process alive
|
|
122
|
-
// long enough for cleanup tasks and engine.stop() to complete.
|
|
123
|
-
const absorbSigterm = (): void => {};
|
|
124
|
-
|
|
125
|
-
const forceExit = (): void => {
|
|
126
|
-
if (phase === "forced_exit" || phase === "exited") return;
|
|
127
|
-
forcedExitRequested = true;
|
|
128
|
-
phase = "forced_exit";
|
|
129
|
-
clearInterval(keepAlive);
|
|
130
|
-
engineLog.warn("Force stopping. Goodbye.");
|
|
131
|
-
resolveCompletion();
|
|
132
|
-
exit(1);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const runWithTimeout = async (name: string, run: () => void | Promise<void>): Promise<void> => {
|
|
136
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
137
|
-
// Declared outside try so finally can suppress late rejections on timeout.
|
|
138
|
-
// Remains null if run() throws synchronously — no dangling promise in that case.
|
|
139
|
-
let runPromise: Promise<void> | null = null;
|
|
140
|
-
try {
|
|
141
|
-
runPromise = Promise.resolve(run()); // sync throws are caught by this try block
|
|
142
|
-
await Promise.race([
|
|
143
|
-
runPromise,
|
|
144
|
-
new Promise<void>((resolve) => {
|
|
145
|
-
timer = setTimeout(() => {
|
|
146
|
-
engineLog.warn(`${name} timed out after ${taskTimeoutMs / 1000}s.`);
|
|
147
|
-
resolve();
|
|
148
|
-
}, taskTimeoutMs);
|
|
149
|
-
}),
|
|
150
|
-
]);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
engineLog.error(`Error during ${name}:`, error);
|
|
153
|
-
} finally {
|
|
154
|
-
if (timer !== null) {
|
|
155
|
-
clearTimeout(timer);
|
|
156
|
-
}
|
|
157
|
-
// If the timeout fired first, runPromise is still pending. Attach a no-op
|
|
158
|
-
// catch so a future rejection from the dangling task does not become an
|
|
159
|
-
// unhandled-rejection crash.
|
|
160
|
-
runPromise?.catch(() => {});
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const runStage = async (stage: ShutdownTaskStage): Promise<void> => {
|
|
165
|
-
const stageTasks = tasks[stage];
|
|
166
|
-
let index = 0;
|
|
167
|
-
while (index < stageTasks.length) {
|
|
168
|
-
const task = stageTasks[index];
|
|
169
|
-
if (task !== undefined) {
|
|
170
|
-
await runWithTimeout(task.name, task.run);
|
|
171
|
-
}
|
|
172
|
-
index++;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const shutdown = async (reason: string): Promise<void> => {
|
|
177
|
-
if (phase === "forced_exit" || phase === "exited") return;
|
|
178
|
-
phase = "shutting_down";
|
|
179
|
-
engineLog.info("Stopping simulation...");
|
|
180
|
-
|
|
181
|
-
const finalMetrics = engine?.getMetrics() ?? null;
|
|
182
|
-
|
|
183
|
-
await runStage("cleanup");
|
|
184
|
-
|
|
185
|
-
const engineToStop = engine;
|
|
186
|
-
if (engineToStop) {
|
|
187
|
-
await runWithTimeout("engine stop", () => engineToStop.stop(reason));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!forcedExitRequested && finalMetrics !== null) {
|
|
191
|
-
await printShutdownSummary(
|
|
192
|
-
finalMetrics,
|
|
193
|
-
options.seed,
|
|
194
|
-
options.seedExplicit,
|
|
195
|
-
errorContextCounts,
|
|
196
|
-
writeFinalOutput,
|
|
197
|
-
).catch((error) => {
|
|
198
|
-
engineLog.error("Error rendering shutdown summary:", error);
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
await runStage("finalize");
|
|
203
|
-
|
|
204
|
-
const forced = forcedExitRequested;
|
|
205
|
-
clearInterval(keepAlive);
|
|
206
|
-
phase = "exited";
|
|
207
|
-
resolveCompletion();
|
|
208
|
-
|
|
209
|
-
if (!forced) {
|
|
210
|
-
exit(0);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const armForceExitHandlers = (): void => {
|
|
215
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
216
|
-
process.removeListener("SIGTERM", sigtermHandler);
|
|
217
|
-
// Use process.on (not once): under `bun run <script>` a single Ctrl+C sends
|
|
218
|
-
// SIGINT to the entire foreground process group, then Bun forwards the same
|
|
219
|
-
// SIGINT to its child process as well. The first duplicate is coalesced
|
|
220
|
-
// inside forceSignalHandler(); later SIGINTs still force-exit.
|
|
221
|
-
process.on("SIGINT", forceSignalHandler);
|
|
222
|
-
// SIGTERM is absorbed (no-op) rather than re-armed as a force handler.
|
|
223
|
-
// bun run / bunx forwards SIGTERM to the child process when the parent
|
|
224
|
-
// runner exits from its own SIGINT — leaving SIGTERM unhandled would
|
|
225
|
-
// restore the OS default (abrupt kill), so we absorb it instead and let
|
|
226
|
-
// cleanup tasks finish. A second Ctrl+C (SIGINT) still force-stops.
|
|
227
|
-
// Use process.on (not once): after the first SIGTERM is absorbed the listener
|
|
228
|
-
// must remain registered. If it were once, a second OS-level SIGTERM would
|
|
229
|
-
// find no handler and revert to the OS default (abrupt termination).
|
|
230
|
-
// teardown() removes it when shutdown completes.
|
|
231
|
-
process.on("SIGTERM", absorbSigterm);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const requestShutdownInternal = (reason: string, source: ShutdownRequestSource): void => {
|
|
235
|
-
if (phase === "forced_exit" || phase === "exited") return;
|
|
236
|
-
if (requestedReason !== null) {
|
|
237
|
-
// Only force-exit if shutdown is actively running. A duplicate request
|
|
238
|
-
// that arrives before runShutdown() starts (e.g. admin /shutdown racing
|
|
239
|
-
// with SIGINT) is silently deduplicated — first caller wins.
|
|
240
|
-
if (phase === "shutting_down") {
|
|
241
|
-
forceExit();
|
|
242
|
-
}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
requestedReason = reason;
|
|
246
|
-
requestSource = source;
|
|
247
|
-
requestedAtMs = performance.now();
|
|
248
|
-
forwardedSigintIgnored = false;
|
|
249
|
-
resolveRequest(reason);
|
|
250
|
-
armForceExitHandlers();
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const requestShutdown = (reason: string): void => {
|
|
254
|
-
requestShutdownInternal(reason, "api");
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const sigintHandler = (): void => {
|
|
258
|
-
requestShutdownInternal("user interrupt", "sigint");
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const sigtermHandler = (): void => {
|
|
262
|
-
requestShutdownInternal("SIGTERM", "sigterm");
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
process.once("SIGINT", sigintHandler);
|
|
266
|
-
process.once("SIGTERM", sigtermHandler);
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
completion,
|
|
270
|
-
addTask(stage, name, run) {
|
|
271
|
-
tasks[stage].push({ name, run });
|
|
272
|
-
},
|
|
273
|
-
isShuttingDown() {
|
|
274
|
-
return requestedReason !== null || phase === "shutting_down" || phase === "forced_exit";
|
|
275
|
-
},
|
|
276
|
-
markRunning() {
|
|
277
|
-
if (phase === "booting") {
|
|
278
|
-
phase = "running";
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
requestShutdown,
|
|
282
|
-
runShutdown() {
|
|
283
|
-
if (shutdownPromise) {
|
|
284
|
-
return shutdownPromise;
|
|
285
|
-
}
|
|
286
|
-
if (requestedReason === null) {
|
|
287
|
-
throw new Error("Shutdown was not requested before runShutdown().");
|
|
288
|
-
}
|
|
289
|
-
shutdownPromise = shutdown(requestedReason);
|
|
290
|
-
return shutdownPromise;
|
|
291
|
-
},
|
|
292
|
-
setEngine(nextEngine) {
|
|
293
|
-
engine = nextEngine;
|
|
294
|
-
},
|
|
295
|
-
setErrorContextCounts(counts) {
|
|
296
|
-
errorContextCounts = counts;
|
|
297
|
-
},
|
|
298
|
-
teardown() {
|
|
299
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
300
|
-
process.removeListener("SIGTERM", sigtermHandler);
|
|
301
|
-
process.removeListener("SIGINT", forceSignalHandler);
|
|
302
|
-
process.removeListener("SIGTERM", absorbSigterm);
|
|
303
|
-
clearInterval(keepAlive);
|
|
304
|
-
},
|
|
305
|
-
waitForShutdownRequest() {
|
|
306
|
-
return request;
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ---------------------------------------------------------------------------
|
|
312
|
-
// Shared helpers
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
|
|
315
|
-
interface SummaryInput {
|
|
316
|
-
readonly metrics: EngineMetricsSnapshot;
|
|
317
|
-
readonly seed?: number | undefined;
|
|
318
|
-
readonly seedExplicit?: boolean | undefined;
|
|
319
|
-
readonly errorContextCounts?: ReadonlyMap<string, number> | undefined;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function healthStateLabel(state: AdapterHealthSnapshot["state"]): string {
|
|
323
|
-
switch (state) {
|
|
324
|
-
case "healthy":
|
|
325
|
-
return colors.green(state);
|
|
326
|
-
case "busy":
|
|
327
|
-
return colors.yellow(state);
|
|
328
|
-
case "saturated":
|
|
329
|
-
return colors.red(state);
|
|
330
|
-
default: {
|
|
331
|
-
const _exhaustive: never = state;
|
|
332
|
-
return _exhaustive;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function adapterLabel(name: string): string {
|
|
338
|
-
if (name === "opcua") return "OPC UA";
|
|
339
|
-
if (name === "modbus") return "Modbus";
|
|
340
|
-
return name.toUpperCase();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function formatAdapterLine(
|
|
344
|
-
name: string,
|
|
345
|
-
m: EngineMetricsSnapshot["adapters"][number]["metrics"],
|
|
346
|
-
uptimeMs: number,
|
|
347
|
-
): string {
|
|
348
|
-
const connStr = m.connected ? colors.green("✓") : colors.red("✗");
|
|
349
|
-
const verb = name === "opcua" || name === "modbus" ? "updates" : "published";
|
|
350
|
-
const avgThroughput = computeAverageThroughput(m.messagesPublished, uptimeMs);
|
|
351
|
-
const throughputStr = avgThroughput !== null ? `${sep}${formatCount(avgThroughput)} msg/s` : "";
|
|
352
|
-
const errStr =
|
|
353
|
-
m.errorsCount > 0 ? `${sep}${colors.yellow(formatCount(m.errorsCount))} errors` : "";
|
|
354
|
-
const health = m.health ? `${sep}${healthStateLabel(m.health.state)}` : "";
|
|
355
|
-
return `${connStr} ${formatCount(m.messagesPublished)} ${verb}${throughputStr}${errStr}${health}`;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// Periodic summary (single-line per component)
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Print a compact periodic summary: one line for core stats, one per adapter.
|
|
364
|
-
*/
|
|
365
|
-
export function printSummaryLine(metrics: EngineMetricsSnapshot): void {
|
|
366
|
-
if (isQuiet()) return;
|
|
367
|
-
|
|
368
|
-
if (isJsonLogFormat()) {
|
|
369
|
-
engineLog.info("Periodic summary", buildJsonSummary({ metrics }));
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Blank line before each summary block to visually separate it from event lines
|
|
374
|
-
writeOutput();
|
|
375
|
-
|
|
376
|
-
const { core, fleet, adapters } = metrics;
|
|
377
|
-
|
|
378
|
-
// Core stats — omit tick count (redundant with rate) and instance count (static, already in banner)
|
|
379
|
-
const parts = [
|
|
380
|
-
formatDuration(core.uptimeMs),
|
|
381
|
-
`${core.actualTicksPerSecond.toFixed(1)} ticks/s`,
|
|
382
|
-
`${formatCount(core.memberValueCount)} data points`,
|
|
383
|
-
];
|
|
384
|
-
if (fleet.activeFaultCount > 0) {
|
|
385
|
-
parts.push(`${colors.yellow(String(fleet.activeFaultCount))} faults`);
|
|
386
|
-
}
|
|
387
|
-
engineLog.info(parts.join(sep));
|
|
388
|
-
|
|
389
|
-
// Per-adapter line
|
|
390
|
-
for (const adapter of adapters) {
|
|
391
|
-
adapterLogger(adapter.name).info(
|
|
392
|
-
formatAdapterLine(adapter.name, adapter.metrics, core.uptimeMs),
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ---------------------------------------------------------------------------
|
|
398
|
-
// Shutdown summary (box)
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
|
|
401
|
-
function buildSummaryLines(input: SummaryInput): string[] {
|
|
402
|
-
const { core, fleet, adapters } = input.metrics;
|
|
403
|
-
const lines: string[] = [];
|
|
404
|
-
|
|
405
|
-
// Core stats
|
|
406
|
-
lines.push(
|
|
407
|
-
`${colors.bold(formatDuration(core.uptimeMs))}${sep}${colors.bold(formatCount(core.tickCount))} ticks${sep}${colors.bold(String(core.actualTicksPerSecond.toFixed(1)))} ticks/s`,
|
|
408
|
-
);
|
|
409
|
-
lines.push(
|
|
410
|
-
`${colors.bold(formatCount(core.instanceCount))} instances${sep}${colors.bold(formatCount(core.memberValueCount))} data points${sep}sim: ${colors.bold(core.simulationTimeSeconds.toFixed(1))}s`,
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
// Adapters
|
|
414
|
-
for (const adapter of adapters) {
|
|
415
|
-
const m = adapter.metrics;
|
|
416
|
-
const label = adapterLabel(adapter.name);
|
|
417
|
-
const connStr = m.connected ? colors.green("connected") : colors.red("disconnected");
|
|
418
|
-
const errStr =
|
|
419
|
-
m.errorsCount === 0
|
|
420
|
-
? colors.green(formatCount(m.errorsCount))
|
|
421
|
-
: colors.yellow(formatCount(m.errorsCount));
|
|
422
|
-
const avgThroughput = computeAverageThroughput(m.messagesPublished, core.uptimeMs);
|
|
423
|
-
const throughputStr = avgThroughput !== null ? `${sep}${formatCount(avgThroughput)} msg/s` : "";
|
|
424
|
-
const verb = adapter.name === "opcua" || adapter.name === "modbus" ? "updates" : "published";
|
|
425
|
-
lines.push(
|
|
426
|
-
`${label}: ${connStr}${sep}${colors.bold(formatCount(m.messagesPublished))} ${verb}${sep}${errStr} errors${throughputStr}`,
|
|
427
|
-
);
|
|
428
|
-
if (m.health) {
|
|
429
|
-
const pendingPrefix =
|
|
430
|
-
m.health.pendingWorkCount > 0
|
|
431
|
-
? `${formatCount(m.health.pendingWorkCount)} pending${sep}`
|
|
432
|
-
: "";
|
|
433
|
-
lines.push(` ${pendingPrefix}${healthStateLabel(m.health.state)}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Fleet health warnings
|
|
438
|
-
const healthParts: string[] = [];
|
|
439
|
-
if (fleet.activeFaultCount > 0) {
|
|
440
|
-
healthParts.push(`${colors.yellow(String(fleet.activeFaultCount))} active faults`);
|
|
441
|
-
}
|
|
442
|
-
if (core.quarantinedInstanceCount > 0) {
|
|
443
|
-
healthParts.push(`${colors.yellow(String(core.quarantinedInstanceCount))} quarantined`);
|
|
444
|
-
}
|
|
445
|
-
if (core.engineErrorsCount > 0) {
|
|
446
|
-
healthParts.push(`${colors.yellow(formatCount(core.engineErrorsCount))} engine errors`);
|
|
447
|
-
}
|
|
448
|
-
if (core.tickOverrunsCount > 0) {
|
|
449
|
-
healthParts.push(`${colors.yellow(formatCount(core.tickOverrunsCount))} overruns`);
|
|
450
|
-
}
|
|
451
|
-
if (core.circuitBreakerTripsCount > 0) {
|
|
452
|
-
healthParts.push(`${colors.yellow(formatCount(core.circuitBreakerTripsCount))} breaker trips`);
|
|
453
|
-
}
|
|
454
|
-
if (healthParts.length > 0) {
|
|
455
|
-
lines.push(healthParts.join(sep));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Fault breakdown
|
|
459
|
-
if (fleet.faultBreakdown.length > 0) {
|
|
460
|
-
const faultItems: TreeItem[] = fleet.faultBreakdown.map(
|
|
461
|
-
(f) => `${colors.yellow(String(f.count))} ${f.fault}`,
|
|
462
|
-
);
|
|
463
|
-
lines.push(formatTree(faultItems, { color: "yellow", prefix: " " }).trimEnd());
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Models & lifecycle
|
|
467
|
-
const treeItems: TreeItem[] = [];
|
|
468
|
-
if (fleet.modelBreakdown.length > 0) {
|
|
469
|
-
treeItems.push({
|
|
470
|
-
text: `${colors.cyan("Models")} ${colors.dim(`(${fleet.modelBreakdown.length})`)}`,
|
|
471
|
-
children: fleet.modelBreakdown.map((m) => `${colors.bold(String(m.count))} ${m.model}`),
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
const lifecycleEntries = Object.entries(fleet.lifecycleBreakdown).filter(
|
|
475
|
-
([, count]) => count > 0,
|
|
476
|
-
);
|
|
477
|
-
if (lifecycleEntries.length > 0) {
|
|
478
|
-
treeItems.push({
|
|
479
|
-
text: colors.cyan("Lifecycle"),
|
|
480
|
-
children: lifecycleEntries.map(([stage, count]) => `${colors.bold(String(count))} ${stage}`),
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
if (treeItems.length > 0) {
|
|
484
|
-
lines.push(formatTree(treeItems, { color: "gray", prefix: " " }).trimEnd());
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Seed
|
|
488
|
-
if (input.seed !== undefined) {
|
|
489
|
-
const seedLine = input.seedExplicit
|
|
490
|
-
? `Seed: ${colors.bold(String(input.seed))}`
|
|
491
|
-
: `Seed: ${colors.bold(String(input.seed))} ${colors.gray("(add seed to config to reproduce)")}`;
|
|
492
|
-
lines.push(seedLine);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Top error contexts
|
|
496
|
-
if (input.errorContextCounts && input.errorContextCounts.size > 0) {
|
|
497
|
-
const topContexts = [...input.errorContextCounts.entries()]
|
|
498
|
-
.sort((a, b) => b[1] - a[1])
|
|
499
|
-
.slice(0, 5);
|
|
500
|
-
const errorItems: TreeItem[] = topContexts.map(
|
|
501
|
-
([context, count]) => `${context} ${colors.dim("\u00B7")} ${colors.bold(formatCount(count))}`,
|
|
502
|
-
);
|
|
503
|
-
lines.push(colors.cyan("Top errors"));
|
|
504
|
-
lines.push(formatTree(errorItems, { color: "red", prefix: " " }).trimEnd());
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return lines;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function buildJsonSummary(input: SummaryInput): Record<string, unknown> {
|
|
511
|
-
const { core, fleet, adapters } = input.metrics;
|
|
512
|
-
|
|
513
|
-
const adapterSummaries: Record<string, unknown>[] = [];
|
|
514
|
-
for (const adapter of adapters) {
|
|
515
|
-
const m = adapter.metrics;
|
|
516
|
-
const avgThroughput = computeAverageThroughput(m.messagesPublished, core.uptimeMs);
|
|
517
|
-
adapterSummaries.push({
|
|
518
|
-
name: adapter.name,
|
|
519
|
-
messagesPublished: m.messagesPublished,
|
|
520
|
-
errorsCount: m.errorsCount,
|
|
521
|
-
connected: m.connected,
|
|
522
|
-
...(avgThroughput !== null ? { avgMsgPerSecond: avgThroughput } : {}),
|
|
523
|
-
...(m.health
|
|
524
|
-
? { health: { state: m.health.state, pendingWorkCount: m.health.pendingWorkCount } }
|
|
525
|
-
: {}),
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const summary: Record<string, unknown> = {
|
|
530
|
-
uptimeMs: core.uptimeMs,
|
|
531
|
-
tickCount: core.tickCount,
|
|
532
|
-
ticksPerSecond: Number(core.actualTicksPerSecond.toFixed(1)),
|
|
533
|
-
simulationTimeSeconds: Number(core.simulationTimeSeconds.toFixed(1)),
|
|
534
|
-
instanceCount: core.instanceCount,
|
|
535
|
-
memberValueCount: core.memberValueCount,
|
|
536
|
-
engineErrorsCount: core.engineErrorsCount,
|
|
537
|
-
activeFaultCount: fleet.activeFaultCount,
|
|
538
|
-
quarantinedInstanceCount: core.quarantinedInstanceCount,
|
|
539
|
-
tickOverrunsCount: core.tickOverrunsCount,
|
|
540
|
-
circuitBreakerTripsCount: core.circuitBreakerTripsCount,
|
|
541
|
-
lifecycleBreakdown: fleet.lifecycleBreakdown,
|
|
542
|
-
adapters: adapterSummaries,
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
if (input.seed !== undefined) {
|
|
546
|
-
summary["seed"] = input.seed;
|
|
547
|
-
summary["seedExplicit"] = input.seedExplicit ?? false;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (input.errorContextCounts && input.errorContextCounts.size > 0) {
|
|
551
|
-
const topContexts = [...input.errorContextCounts.entries()]
|
|
552
|
-
.sort((a, b) => b[1] - a[1])
|
|
553
|
-
.slice(0, 5);
|
|
554
|
-
summary["topErrorContexts"] = Object.fromEntries(topContexts);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return summary;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function renderSummaryText(title: string, input: SummaryInput): string {
|
|
561
|
-
const lines = buildSummaryLines(input);
|
|
562
|
-
|
|
563
|
-
if (isPlain()) {
|
|
564
|
-
return [title, ...lines.map((line) => ` ${stripAnsi(line)}`), ""].join("\n");
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return box(lines.join("\n"), {
|
|
568
|
-
title: ` ${title} `,
|
|
569
|
-
style: { borderColor: "gray", borderStyle: "rounded" },
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
async function printShutdownSummary(
|
|
574
|
-
metrics: EngineMetricsSnapshot,
|
|
575
|
-
seed: number,
|
|
576
|
-
seedExplicit: boolean,
|
|
577
|
-
errorContextCounts?: ReadonlyMap<string, number>,
|
|
578
|
-
writeFinal: (text: string) => Promise<void> = writeShutdownOutput,
|
|
579
|
-
): Promise<void> {
|
|
580
|
-
if (isQuiet()) return;
|
|
581
|
-
|
|
582
|
-
if (isJsonLogFormat()) {
|
|
583
|
-
engineLog.info(
|
|
584
|
-
"Shutdown summary",
|
|
585
|
-
buildJsonSummary({ metrics, seed, seedExplicit, errorContextCounts }),
|
|
586
|
-
);
|
|
587
|
-
await flushOutputStreams();
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
await writeFinal(
|
|
592
|
-
renderSummaryText("Shutdown Summary", {
|
|
593
|
-
metrics,
|
|
594
|
-
seed,
|
|
595
|
-
seedExplicit,
|
|
596
|
-
errorContextCounts,
|
|
597
|
-
}),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function writeShutdownOutput(text: string): Promise<void> {
|
|
602
|
-
await writeToStream(process.stdout, text.endsWith("\n") ? text : `${text}\n`);
|
|
603
|
-
await flushOutputStreams();
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async function writeToStream(stream: NodeJS.WriteStream, text: string): Promise<void> {
|
|
607
|
-
await new Promise<void>((resolve, reject) => {
|
|
608
|
-
stream.write(text, (error?: Error | null) => {
|
|
609
|
-
if (error) {
|
|
610
|
-
reject(error);
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
resolve();
|
|
614
|
-
});
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
async function flushOutputStreams(): Promise<void> {
|
|
619
|
-
await Promise.all([writeToStream(process.stdout, ""), writeToStream(process.stderr, "")]);
|
|
620
|
-
await Bun.sleep(FINAL_OUTPUT_GRACE_MS);
|
|
621
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { DiscoveredTemplate } from "@miravo/shared";
|
|
3
|
-
import {
|
|
4
|
-
categorizeTemplate,
|
|
5
|
-
groupTemplatesByCategory,
|
|
6
|
-
sortTemplatesByCategoryAndName,
|
|
7
|
-
} from "./template-category.ts";
|
|
8
|
-
|
|
9
|
-
function discoveredTemplate(id: string, name: string): DiscoveredTemplate {
|
|
10
|
-
return {
|
|
11
|
-
path: `/tmp/${id}.miravo.yaml`,
|
|
12
|
-
id,
|
|
13
|
-
name,
|
|
14
|
-
areaCount: 1,
|
|
15
|
-
twinAssignmentCount: 1,
|
|
16
|
-
inlineModelCount: 0,
|
|
17
|
-
uniqueModelCount: 1,
|
|
18
|
-
modelNames: ["model-a"],
|
|
19
|
-
totalTwinCount: 1,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("template category helpers", () => {
|
|
24
|
-
test("categorizeTemplate maps stress templates to Stress & Benchmark", () => {
|
|
25
|
-
expect(categorizeTemplate(discoveredTemplate("stress-5k", "Stress 5K"))).toBe(
|
|
26
|
-
"Stress & Benchmark",
|
|
27
|
-
);
|
|
28
|
-
expect(categorizeTemplate(discoveredTemplate("stress-10k", "Stress 10K"))).toBe(
|
|
29
|
-
"Stress & Benchmark",
|
|
30
|
-
);
|
|
31
|
-
expect(categorizeTemplate(discoveredTemplate("smart-factory", "Smart Factory"))).toBe(
|
|
32
|
-
"Industrial Scenarios",
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("sortTemplatesByCategoryAndName sorts by category then name", () => {
|
|
37
|
-
const templates = [
|
|
38
|
-
discoveredTemplate("stress-100k", "Stress 100K"),
|
|
39
|
-
discoveredTemplate("water-treatment", "Water Treatment"),
|
|
40
|
-
discoveredTemplate("smart-factory", "Smart Factory"),
|
|
41
|
-
discoveredTemplate("stress-10k", "Stress 10K"),
|
|
42
|
-
discoveredTemplate("stress-5k", "Stress 5K"),
|
|
43
|
-
discoveredTemplate("stress-50k", "Stress 50K"),
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const sortedIds = sortTemplatesByCategoryAndName(templates).map((template) => template.id);
|
|
47
|
-
expect(sortedIds).toEqual([
|
|
48
|
-
"smart-factory",
|
|
49
|
-
"water-treatment",
|
|
50
|
-
"stress-100k",
|
|
51
|
-
"stress-10k",
|
|
52
|
-
"stress-50k",
|
|
53
|
-
"stress-5k",
|
|
54
|
-
]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("groupTemplatesByCategory returns non-empty groups in category order", () => {
|
|
58
|
-
const templates = sortTemplatesByCategoryAndName([
|
|
59
|
-
discoveredTemplate("stress-50k", "Stress 50K"),
|
|
60
|
-
discoveredTemplate("water-treatment", "Water Treatment"),
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
const grouped = groupTemplatesByCategory(templates);
|
|
64
|
-
expect(grouped.map((group) => group.category)).toEqual([
|
|
65
|
-
"Industrial Scenarios",
|
|
66
|
-
"Stress & Benchmark",
|
|
67
|
-
]);
|
|
68
|
-
expect(grouped[0]?.templates.map((template) => template.id)).toEqual(["water-treatment"]);
|
|
69
|
-
expect(grouped[1]?.templates.map((template) => template.id)).toEqual(["stress-50k"]);
|
|
70
|
-
});
|
|
71
|
-
});
|