miravo 0.2.3 → 0.3.0
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 +116 -30
- 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/index-BXtW73cF.css +2 -0
- package/console-dist/assets/index-ChSocZFW.js +119 -0
- 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/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/favicon.svg +20 -0
- package/console-dist/index.html +14 -0
- package/console-dist/miravo-logo.png +0 -0
- package/package.json +10 -7
- package/src/__tests__/integration/executable-methods-runner.ts +52 -45
- package/src/__tests__/integration/executable-methods.integration.ts +609 -47
- package/src/commands/adapter.ts +76 -0
- package/src/commands/cli-test-setup.ts +1 -1
- package/src/commands/fork.ts +14 -0
- package/src/commands/inject.test.ts +1 -1
- package/src/commands/inject.ts +16 -8
- package/src/commands/load.ts +167 -0
- package/src/commands/logs.ts +175 -0
- package/src/commands/models.test.ts +22 -1
- package/src/commands/models.ts +14 -0
- package/src/commands/pause.ts +67 -0
- package/src/commands/ps.ts +116 -0
- package/src/commands/remove.ts +38 -0
- package/src/commands/reset.ts +44 -0
- package/src/commands/rm.ts +97 -0
- package/src/commands/spawn.ts +45 -0
- package/src/commands/speed.ts +40 -0
- package/src/commands/start.test.ts +93 -0
- package/src/commands/start.ts +974 -192
- package/src/commands/status.test.ts +3 -1
- package/src/commands/status.ts +12 -6
- package/src/commands/stop.test.ts +84 -0
- package/src/commands/stop.ts +158 -0
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +13 -0
- package/src/index.ts +49 -7
- package/src/lib/admin-client.test.ts +35 -0
- package/src/lib/admin-client.ts +13 -1
- package/src/lib/admin-server.test.ts +339 -53
- package/src/lib/admin-server.ts +413 -62
- package/src/lib/daemon-registry.test.ts +429 -0
- package/src/lib/daemon-registry.ts +345 -0
- package/src/lib/mqtt-client.ts +1 -1
- package/src/lib/parse.ts +18 -0
- package/src/lib/resolve-simulation.ts +1 -1
- package/src/lib/shutdown.test.ts +16 -4
- package/src/lib/shutdown.ts +3 -3
- package/src/lib/template-category.test.ts +1 -0
- package/src/lib/template-utils.ts +33 -2
- package/src/lib/ws-hub.ts +255 -0
- package/src/logger.test.ts +19 -19
- package/src/logger.ts +34 -6
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { EngineMetricsSnapshot } from "@miravo/shared";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { isPidAlive, listEntries, sweepStaleEntries } from "../lib/daemon-registry.ts";
|
|
4
|
+
import { handleCommandError, writeOutput } from "../logger.ts";
|
|
5
|
+
import { formatTable } from "../output.ts";
|
|
6
|
+
|
|
7
|
+
interface PsOptions {
|
|
8
|
+
readonly json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerPsCommand(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command("ps")
|
|
14
|
+
.description("List background simulations with live metrics")
|
|
15
|
+
.option("--json", "Machine-readable JSON output")
|
|
16
|
+
.addHelpText(
|
|
17
|
+
"after",
|
|
18
|
+
`
|
|
19
|
+
Examples:
|
|
20
|
+
$ miravo ps List background simulations
|
|
21
|
+
$ miravo ps --json Machine-readable JSON output`,
|
|
22
|
+
)
|
|
23
|
+
.action(async (options: PsOptions) => {
|
|
24
|
+
try {
|
|
25
|
+
await psAction(options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
handleCommandError(error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type PsRow = Record<string, string> & {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly status: string;
|
|
35
|
+
readonly uptime: string;
|
|
36
|
+
readonly ticks: string;
|
|
37
|
+
readonly instances: string;
|
|
38
|
+
readonly admin: string;
|
|
39
|
+
readonly mqtt: string;
|
|
40
|
+
readonly opcua: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function psAction(options: PsOptions): Promise<void> {
|
|
44
|
+
await sweepStaleEntries();
|
|
45
|
+
const entries = await listEntries();
|
|
46
|
+
|
|
47
|
+
if (entries.length === 0) {
|
|
48
|
+
writeOutput("No running simulations.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rows: PsRow[] = [];
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const alive = isPidAlive(entry.pid);
|
|
56
|
+
let status = alive ? "running" : "dead";
|
|
57
|
+
let uptime = "-";
|
|
58
|
+
let ticks = "-";
|
|
59
|
+
let instances = "-";
|
|
60
|
+
|
|
61
|
+
if (alive) {
|
|
62
|
+
try {
|
|
63
|
+
const url = `http://${entry.adminHost}:${entry.adminPort}/metrics`;
|
|
64
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
const metrics = (await response.json()) as EngineMetricsSnapshot;
|
|
67
|
+
status = metrics.core.state === "running" ? "running" : metrics.core.state;
|
|
68
|
+
uptime = formatUptime(metrics.core.uptimeMs);
|
|
69
|
+
ticks = String(metrics.core.tickCount);
|
|
70
|
+
instances = String(metrics.core.instanceCount);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
status = "starting";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mqttAdapter = entry.adapters["mqtt"];
|
|
78
|
+
const opcuaAdapter = entry.adapters["opcua"];
|
|
79
|
+
|
|
80
|
+
rows.push({
|
|
81
|
+
name: entry.name,
|
|
82
|
+
status,
|
|
83
|
+
uptime,
|
|
84
|
+
ticks,
|
|
85
|
+
instances,
|
|
86
|
+
admin: `:${entry.adminPort}`,
|
|
87
|
+
mqtt: mqttAdapter ? `:${mqttAdapter.port}` : "-",
|
|
88
|
+
opcua: opcuaAdapter ? `:${opcuaAdapter.port}` : "-",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (options.json) {
|
|
93
|
+
writeOutput(JSON.stringify(rows, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
name: "NAME",
|
|
99
|
+
status: "STATUS",
|
|
100
|
+
uptime: "UPTIME",
|
|
101
|
+
ticks: "TICKS",
|
|
102
|
+
instances: "INSTANCES",
|
|
103
|
+
admin: "ADMIN",
|
|
104
|
+
mqtt: "MQTT",
|
|
105
|
+
opcua: "OPC UA",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
writeOutput(formatTable(headers, rows));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatUptime(ms: number): string {
|
|
112
|
+
const totalSeconds = Math.floor(ms / 1_000);
|
|
113
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
114
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
115
|
+
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
+
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
+
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
+
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
+
|
|
8
|
+
interface RemoveOptions {
|
|
9
|
+
readonly json?: boolean;
|
|
10
|
+
readonly adminUrl?: string;
|
|
11
|
+
readonly timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerRemoveCommand(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command("remove <instanceId>")
|
|
17
|
+
.description("Remove an instance from the running simulation")
|
|
18
|
+
.option("--json", "Machine-readable JSON output")
|
|
19
|
+
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
20
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
21
|
+
.addHelpText(
|
|
22
|
+
"after",
|
|
23
|
+
`
|
|
24
|
+
Examples:
|
|
25
|
+
$ miravo remove pump-001 Remove instance by ID`,
|
|
26
|
+
)
|
|
27
|
+
.action(async (instanceId: string, options: RemoveOptions) => {
|
|
28
|
+
try {
|
|
29
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
30
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
31
|
+
const command: EngineCommand = { type: "removeInstance", instanceId };
|
|
32
|
+
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
33
|
+
handleCommandResult(result, options.json);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
handleCommandError(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
+
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
+
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
+
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
+
|
|
8
|
+
interface ResetOptions {
|
|
9
|
+
readonly clearPersistence?: boolean;
|
|
10
|
+
readonly json?: boolean;
|
|
11
|
+
readonly adminUrl?: string;
|
|
12
|
+
readonly timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerResetCommand(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command("reset")
|
|
18
|
+
.description("Reset the simulation: remove all instances and template runs")
|
|
19
|
+
.option("--clear-persistence", "Also clear persisted state (start fresh on next boot)")
|
|
20
|
+
.option("--json", "Machine-readable JSON output")
|
|
21
|
+
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
22
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
23
|
+
.addHelpText(
|
|
24
|
+
"after",
|
|
25
|
+
`
|
|
26
|
+
Examples:
|
|
27
|
+
$ miravo reset Reset simulation state
|
|
28
|
+
$ miravo reset --clear-persistence Reset and clear saved state`,
|
|
29
|
+
)
|
|
30
|
+
.action(async (options: ResetOptions) => {
|
|
31
|
+
try {
|
|
32
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
33
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
34
|
+
const command: EngineCommand = {
|
|
35
|
+
type: "resetSimulation",
|
|
36
|
+
clearPersistence: options.clearPersistence ?? false,
|
|
37
|
+
};
|
|
38
|
+
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
39
|
+
handleCommandResult(result, options.json);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
handleCommandError(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { probeEntryIdentity, readEntry, removeEntry } from "../lib/daemon-registry.ts";
|
|
3
|
+
import { handleCommandError, writeOutput } from "../logger.ts";
|
|
4
|
+
|
|
5
|
+
interface RmOptions {
|
|
6
|
+
readonly force?: boolean;
|
|
7
|
+
readonly json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function registerRmCommand(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("rm <name>")
|
|
13
|
+
.description("Remove a stale daemon registry entry")
|
|
14
|
+
.option("-f, --force", "Kill the process if still alive before removing")
|
|
15
|
+
.option("--json", "Machine-readable JSON output")
|
|
16
|
+
.addHelpText(
|
|
17
|
+
"after",
|
|
18
|
+
`
|
|
19
|
+
Examples:
|
|
20
|
+
$ miravo rm my-sim Remove stale registry entry
|
|
21
|
+
$ miravo rm my-sim --force Kill process and remove entry`,
|
|
22
|
+
)
|
|
23
|
+
.action(async (name: string, options: RmOptions) => {
|
|
24
|
+
try {
|
|
25
|
+
await rmAction(name, options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
handleCommandError(error);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function jsonOut(obj: Record<string, unknown>): void {
|
|
33
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function rmAction(name: string, options: RmOptions): Promise<void> {
|
|
37
|
+
const entry = await readEntry(name);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
const msg = `Simulation "${name}" not found in daemon registry.`;
|
|
40
|
+
if (options.json) {
|
|
41
|
+
jsonOut({ success: false, name, message: msg });
|
|
42
|
+
} else {
|
|
43
|
+
writeOutput(msg);
|
|
44
|
+
}
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const identity = await probeEntryIdentity(entry);
|
|
50
|
+
|
|
51
|
+
if (identity.status === "unreachable") {
|
|
52
|
+
const msg = `Simulation "${name}" could not be safely verified because its admin server is unreachable while PID ${entry.pid} is still alive.`;
|
|
53
|
+
if (options.json) {
|
|
54
|
+
jsonOut({
|
|
55
|
+
success: false,
|
|
56
|
+
name,
|
|
57
|
+
message: msg,
|
|
58
|
+
error: "Retry later, or inspect the process manually before removing the registry entry.",
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
writeOutput(
|
|
62
|
+
`${msg} Retry later, or inspect the process manually before removing the registry entry.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (identity.status === "verified") {
|
|
70
|
+
if (!options.force) {
|
|
71
|
+
const msg = `Simulation "${name}" is still running (PID ${entry.pid}). Use --force to kill it, or 'miravo stop ${name}' for graceful shutdown.`;
|
|
72
|
+
if (options.json) {
|
|
73
|
+
jsonOut({ success: false, name, message: msg });
|
|
74
|
+
} else {
|
|
75
|
+
writeOutput(msg);
|
|
76
|
+
}
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Force kill
|
|
82
|
+
try {
|
|
83
|
+
process.kill(entry.pid, "SIGKILL");
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore — may have exited between check and kill
|
|
86
|
+
}
|
|
87
|
+
await Bun.sleep(200);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await removeEntry(name);
|
|
91
|
+
const msg = `Removed daemon registry entry for "${name}".`;
|
|
92
|
+
if (options.json) {
|
|
93
|
+
jsonOut({ success: true, name, message: msg });
|
|
94
|
+
} else {
|
|
95
|
+
writeOutput(msg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
+
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
+
import { parsePositiveInt, parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
+
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
+
|
|
8
|
+
interface SpawnOptions {
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
readonly json?: boolean;
|
|
11
|
+
readonly adminUrl?: string;
|
|
12
|
+
readonly timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerSpawnCommand(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command("spawn <model>")
|
|
18
|
+
.description("Add new instances of a model to the running simulation")
|
|
19
|
+
.option("-c, --count <n>", "Number of instances to spawn (default: 1)", parsePositiveInt)
|
|
20
|
+
.option("--json", "Machine-readable JSON output")
|
|
21
|
+
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
22
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
23
|
+
.addHelpText(
|
|
24
|
+
"after",
|
|
25
|
+
`
|
|
26
|
+
Examples:
|
|
27
|
+
$ miravo spawn centrifugal-pump Spawn one instance
|
|
28
|
+
$ miravo spawn centrifugal-pump -c 5 Spawn five instances`,
|
|
29
|
+
)
|
|
30
|
+
.action(async (model: string, options: SpawnOptions) => {
|
|
31
|
+
try {
|
|
32
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
33
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
34
|
+
const command: EngineCommand = {
|
|
35
|
+
type: "spawn",
|
|
36
|
+
model,
|
|
37
|
+
count: options.count ?? 1,
|
|
38
|
+
};
|
|
39
|
+
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
40
|
+
handleCommandResult(result, options.json);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
handleCommandError(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
+
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
+
import { parsePositiveFloat, parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
+
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
+
|
|
8
|
+
interface SpeedOptions {
|
|
9
|
+
readonly json?: boolean;
|
|
10
|
+
readonly adminUrl?: string;
|
|
11
|
+
readonly timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerSpeedCommand(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command("speed <multiplier>")
|
|
17
|
+
.description("Set simulation time multiplier (1-1000)")
|
|
18
|
+
.option("--json", "Machine-readable JSON output")
|
|
19
|
+
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
20
|
+
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
21
|
+
.addHelpText(
|
|
22
|
+
"after",
|
|
23
|
+
`
|
|
24
|
+
Examples:
|
|
25
|
+
$ miravo speed 10 Run at 10x real time
|
|
26
|
+
$ miravo speed 0.5 Run at half speed`,
|
|
27
|
+
)
|
|
28
|
+
.action(async (multiplierStr: string, options: SpeedOptions) => {
|
|
29
|
+
try {
|
|
30
|
+
const multiplier = parsePositiveFloat(multiplierStr);
|
|
31
|
+
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
32
|
+
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
33
|
+
const command: EngineCommand = { type: "setSpeed", multiplier };
|
|
34
|
+
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
35
|
+
handleCommandResult(result, options.json);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
handleCommandError(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -73,6 +73,7 @@ function discoveredTemplate(id: string, name: string, description?: string): Dis
|
|
|
73
73
|
twinAssignmentCount: 1,
|
|
74
74
|
inlineModelCount: 0,
|
|
75
75
|
uniqueModelCount: 2,
|
|
76
|
+
modelNames: ["model-a", "model-b"],
|
|
76
77
|
totalTwinCount: 5,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
@@ -207,6 +208,98 @@ describe("resolveSimulationName", () => {
|
|
|
207
208
|
});
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
describe("resolveProtocolBootPlans", () => {
|
|
212
|
+
test("preserves restored adapter state when no explicit protocol overrides are provided", async () => {
|
|
213
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
resolveProtocolBootPlans({
|
|
217
|
+
options: {},
|
|
218
|
+
optionSources: {},
|
|
219
|
+
restoredFromPersistence: true,
|
|
220
|
+
}),
|
|
221
|
+
).toEqual([
|
|
222
|
+
{ name: "mqtt", shouldEnable: true, overrideRestore: false },
|
|
223
|
+
{ name: "opcua", shouldEnable: false, overrideRestore: false },
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("applies boot defaults when no persisted snapshot was restored", async () => {
|
|
228
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
resolveProtocolBootPlans({
|
|
232
|
+
options: {},
|
|
233
|
+
optionSources: {},
|
|
234
|
+
restoredFromPersistence: false,
|
|
235
|
+
}),
|
|
236
|
+
).toEqual([
|
|
237
|
+
{ name: "mqtt", shouldEnable: true, overrideRestore: true },
|
|
238
|
+
{ name: "opcua", shouldEnable: false, overrideRestore: true },
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("treats explicit MQTT disable as authoritative over persisted state", async () => {
|
|
243
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
244
|
+
|
|
245
|
+
expect(
|
|
246
|
+
resolveProtocolBootPlans({
|
|
247
|
+
options: { mqtt: false },
|
|
248
|
+
optionSources: { mqtt: "cli" },
|
|
249
|
+
restoredFromPersistence: true,
|
|
250
|
+
}),
|
|
251
|
+
).toContainEqual({ name: "mqtt", shouldEnable: false, overrideRestore: true });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("treats explicit CLI MQTT config overrides as authoritative over persisted config", async () => {
|
|
255
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
256
|
+
|
|
257
|
+
expect(
|
|
258
|
+
resolveProtocolBootPlans({
|
|
259
|
+
options: { mqttPort: 1884 },
|
|
260
|
+
optionSources: { mqttPort: "cli" },
|
|
261
|
+
restoredFromPersistence: true,
|
|
262
|
+
}),
|
|
263
|
+
).toContainEqual({ name: "mqtt", shouldEnable: true, overrideRestore: true });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("does not treat env fallback config as authoritative over persisted MQTT state", async () => {
|
|
267
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
268
|
+
|
|
269
|
+
expect(
|
|
270
|
+
resolveProtocolBootPlans({
|
|
271
|
+
options: {},
|
|
272
|
+
optionSources: {},
|
|
273
|
+
restoredFromPersistence: true,
|
|
274
|
+
}),
|
|
275
|
+
).toContainEqual({ name: "mqtt", shouldEnable: true, overrideRestore: false });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("treats explicit OPC UA enable as authoritative over persisted state", async () => {
|
|
279
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
280
|
+
|
|
281
|
+
expect(
|
|
282
|
+
resolveProtocolBootPlans({
|
|
283
|
+
options: { opcua: true },
|
|
284
|
+
optionSources: { opcua: "cli" },
|
|
285
|
+
restoredFromPersistence: true,
|
|
286
|
+
}),
|
|
287
|
+
).toContainEqual({ name: "opcua", shouldEnable: true, overrideRestore: true });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("does not treat ignored OPC UA config flags as persisted-state overrides", async () => {
|
|
291
|
+
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
resolveProtocolBootPlans({
|
|
295
|
+
options: { opcuaPort: 4841 },
|
|
296
|
+
optionSources: { opcuaPort: "cli" },
|
|
297
|
+
restoredFromPersistence: true,
|
|
298
|
+
}),
|
|
299
|
+
).toContainEqual({ name: "opcua", shouldEnable: false, overrideRestore: false });
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
210
303
|
describe("error context aggregation", () => {
|
|
211
304
|
test("normalizeErrorContext redacts high-cardinality fragments", async () => {
|
|
212
305
|
const { normalizeErrorContext } = await import("./start.ts");
|