miravo 0.3.0 → 0.4.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 +14 -236
- package/console-dist/assets/index-C3XMYq26.css +2 -0
- package/console-dist/assets/index-hUJSpKtB.js +119 -0
- package/console-dist/favicon.svg +31 -14
- package/console-dist/index.html +2 -2
- package/console-dist/miravo-logo.png +0 -0
- package/package.json +7 -7
- package/src/__tests__/integration/executable-methods-runner.ts +1 -0
- package/src/__tests__/integration/executable-methods.integration.ts +45 -39
- package/src/commands/inject.test.ts +0 -24
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +6 -0
- package/src/commands/start.ts +69 -1
- package/src/commands/status.test.ts +0 -26
- package/src/commands/templates.test.ts +0 -13
- package/console-dist/assets/index-BXtW73cF.css +0 -2
- package/console-dist/assets/index-ChSocZFW.js +0 -119
package/console-dist/favicon.svg
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
|
1
|
+
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
|
2
2
|
<defs>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<!-- Subtle mirror gradient -->
|
|
4
|
+
<linearGradient id="mirror" x1="0" y1="0" x2="0" y2="1">
|
|
5
|
+
<stop offset="0%" stop-color="#f6d28a"/>
|
|
6
|
+
<stop offset="50%" stop-color="#e5a218"/>
|
|
7
|
+
<stop offset="100%" stop-color="#c58514"/>
|
|
8
|
+
</linearGradient>
|
|
6
9
|
</defs>
|
|
7
|
-
<g clip-path="url(#rounded)">
|
|
8
|
-
<!-- background -->
|
|
9
|
-
<rect width="64" height="64" fill="#0a0a0f"/>
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
<!-- Left M shape -->
|
|
12
|
+
<path d="M10 12 L30 12 L21 28 L10 28 Z"
|
|
13
|
+
fill="#e5a218"
|
|
14
|
+
stroke="#e5a218"
|
|
15
|
+
stroke-width="1.2"
|
|
16
|
+
stroke-linejoin="round"/>
|
|
17
|
+
<path d="M10 28 L21 28 L21 52 L10 48 Z"
|
|
18
|
+
fill="#e5a218"
|
|
19
|
+
stroke="#e5a218"
|
|
20
|
+
stroke-width="1.2"
|
|
21
|
+
stroke-linejoin="round"/>
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
<!-- Right mirrored M -->
|
|
24
|
+
<path d="M54 12 L34 12 L43 28 L54 28 Z"
|
|
25
|
+
fill="#e5a218"
|
|
26
|
+
stroke="#e5a218"
|
|
27
|
+
stroke-width="1.2"
|
|
28
|
+
stroke-linejoin="round"/>
|
|
29
|
+
<path d="M54 28 L43 28 L43 52 L54 48 Z"
|
|
30
|
+
fill="#e5a218"
|
|
31
|
+
stroke="#e5a218"
|
|
32
|
+
stroke-width="1.2"
|
|
33
|
+
stroke-linejoin="round"/>
|
|
16
34
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</g>
|
|
35
|
+
<!-- Mirror axis (thin + subtle reflective feel) -->
|
|
36
|
+
<rect x="31.5" y="10" width="1" height="44" rx="0.5" fill="url(#mirror)" opacity="0.9"/>
|
|
20
37
|
</svg>
|
package/console-dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Miravo</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-hUJSpKtB.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C3XMYq26.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miravo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Industrial environment platform with canonical asset models and MQTT + OPC UA protocol adapters",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,19 +32,19 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.1.0",
|
|
35
|
-
"@miravo/core": "0.
|
|
36
|
-
"@miravo/mqtt": "0.
|
|
37
|
-
"@miravo/opcua": "0.
|
|
38
|
-
"@miravo/shared": "0.
|
|
35
|
+
"@miravo/core": "0.4.0",
|
|
36
|
+
"@miravo/mqtt": "0.4.0",
|
|
37
|
+
"@miravo/opcua": "0.4.0",
|
|
38
|
+
"@miravo/shared": "0.4.0",
|
|
39
39
|
"commander": "^13.0.0",
|
|
40
40
|
"consola": "^3.4.0",
|
|
41
41
|
"mqtt": "^5.15.0",
|
|
42
|
+
"yaml": "^2.8.1",
|
|
42
43
|
"zod": "^4.3.6"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@miravo/console": "0.0.0",
|
|
46
47
|
"node-opcua-client": "^2.163.1",
|
|
47
|
-
"node-opcua-variant": "^2.163.0"
|
|
48
|
-
"yaml": "^2.8.1"
|
|
48
|
+
"node-opcua-variant": "^2.163.0"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -139,6 +139,7 @@ async function main(): Promise<void> {
|
|
|
139
139
|
variation: twin.variation,
|
|
140
140
|
unsBasePath: buildUNSBasePath(template, area.name, line.name),
|
|
141
141
|
...(twin.parameters !== undefined ? { parameterOverrides: twin.parameters } : {}),
|
|
142
|
+
...(twin.starting_hours !== undefined ? { startingHours: twin.starting_hours } : {}),
|
|
142
143
|
});
|
|
143
144
|
});
|
|
144
145
|
|
|
@@ -387,12 +387,12 @@ function createLifecycleTestModel(): TwinModel {
|
|
|
387
387
|
stages: [
|
|
388
388
|
{
|
|
389
389
|
name: "healthy",
|
|
390
|
-
duration_hours: [0.
|
|
390
|
+
duration_hours: [0.1, 0.1],
|
|
391
391
|
effects: {},
|
|
392
392
|
},
|
|
393
393
|
{
|
|
394
394
|
name: "degraded",
|
|
395
|
-
duration_hours: [0.
|
|
395
|
+
duration_hours: [0.05, 0.05],
|
|
396
396
|
effects: {
|
|
397
397
|
pressure: { multiplier: 0.8, offset: -5 },
|
|
398
398
|
},
|
|
@@ -744,15 +744,20 @@ async function waitForMQTTTopicValue(
|
|
|
744
744
|
recorder: ReturnType<typeof createMQTTRecorder>,
|
|
745
745
|
topic: string,
|
|
746
746
|
expected?: number | string | boolean,
|
|
747
|
+
timeoutMs?: number,
|
|
747
748
|
): Promise<number | string | boolean> {
|
|
748
|
-
return waitFor(
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
749
|
+
return waitFor(
|
|
750
|
+
`MQTT topic ${topic}`,
|
|
751
|
+
() => {
|
|
752
|
+
const payload = recorder.latestByTopic.get(topic)?.parsed;
|
|
753
|
+
const value = readPayloadValue(payload);
|
|
754
|
+
if (value === undefined) {
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
return expected === undefined || value === expected ? value : undefined;
|
|
758
|
+
},
|
|
759
|
+
timeoutMs,
|
|
760
|
+
);
|
|
756
761
|
}
|
|
757
762
|
|
|
758
763
|
async function waitForMQTTTopicQuality(
|
|
@@ -1977,13 +1982,16 @@ describeIfTcp("executable methods integration", () => {
|
|
|
1977
1982
|
}, 30000);
|
|
1978
1983
|
|
|
1979
1984
|
test("lifecycle effects apply multiplier-then-offset and fleet metrics track stage transitions", async () => {
|
|
1980
|
-
// time_multiplier=
|
|
1981
|
-
// healthy:
|
|
1982
|
-
//
|
|
1985
|
+
// time_multiplier=60: each 100ms tick = 0.00167 simulated hours.
|
|
1986
|
+
// healthy: 2h (~1200 ticks / 120s), degraded: 1h (~600 ticks / 60s), critical: terminal.
|
|
1987
|
+
// Must be slow enough for the test to connect MQTT and observe healthy before
|
|
1988
|
+
// it transitions, but fast enough to reach critical within the 30s test timeout.
|
|
1989
|
+
// With 60× multiplier, healthy lasts ~6s (enough to observe) and the full
|
|
1990
|
+
// healthy→degraded→critical path completes in ~12s including ramp blends.
|
|
1983
1991
|
const runtime = await createSmokeRuntime({
|
|
1984
1992
|
model: createLifecycleTestModel(),
|
|
1985
1993
|
namespacePrefix: "lifecycle-test",
|
|
1986
|
-
templateSettings: { tick_interval_ms: 100, time_multiplier:
|
|
1994
|
+
templateSettings: { tick_interval_ms: 100, time_multiplier: 60, seed: 42 },
|
|
1987
1995
|
});
|
|
1988
1996
|
let client: MqttClient | undefined;
|
|
1989
1997
|
let stage = "connect MQTT observer";
|
|
@@ -1997,30 +2005,28 @@ describeIfTcp("executable methods integration", () => {
|
|
|
1997
2005
|
|
|
1998
2006
|
const recorder = createMQTTRecorder(client);
|
|
1999
2007
|
try {
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
stage = "wait for degraded stage (pressure = 100 * 0.8 - 5 = 75)";
|
|
2008
|
-
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/pressure`, 75)).toBe(75);
|
|
2009
|
-
|
|
2010
|
-
const degradedMetrics = await waitForAsync("fleet lifecycle degraded", async () => {
|
|
2011
|
-
const m = await readAdminJson(runtime, "/metrics", EngineMetricsSnapshotSchema);
|
|
2012
|
-
return m.fleet.lifecycleBreakdown["degraded"] === 1 ? m : undefined;
|
|
2013
|
-
});
|
|
2014
|
-
expect(degradedMetrics.fleet.lifecycleBreakdown["degraded"]).toBe(1);
|
|
2015
|
-
|
|
2008
|
+
// The scheduler has been ticking since engine.start(). With fast
|
|
2009
|
+
// time_multiplier the lifecycle may have already advanced past healthy
|
|
2010
|
+
// by the time we connect. Instead of racing to catch each stage,
|
|
2011
|
+
// verify that lifecycle reaches critical and that the pressure value
|
|
2012
|
+
// matches the critical-stage effect (multiplier 0.5, offset -20):
|
|
2013
|
+
// pressure = 100 * 0.5 - 20 = 30.
|
|
2016
2014
|
stage = "wait for critical stage (pressure = 100 * 0.5 - 20 = 30)";
|
|
2017
|
-
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/pressure`, 30)).toBe(
|
|
2015
|
+
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/pressure`, 30, 15000)).toBe(
|
|
2016
|
+
30,
|
|
2017
|
+
);
|
|
2018
2018
|
|
|
2019
|
-
const criticalMetrics = await waitForAsync(
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2019
|
+
const criticalMetrics = await waitForAsync(
|
|
2020
|
+
"fleet lifecycle critical",
|
|
2021
|
+
async () => {
|
|
2022
|
+
const m = await readAdminJson(runtime, "/metrics", EngineMetricsSnapshotSchema);
|
|
2023
|
+
return m.fleet.lifecycleBreakdown["critical"] === 1 ? m : undefined;
|
|
2024
|
+
},
|
|
2025
|
+
15000,
|
|
2026
|
+
);
|
|
2023
2027
|
expect(criticalMetrics.fleet.lifecycleBreakdown["critical"]).toBe(1);
|
|
2028
|
+
// Verify lifecycle effects applied correctly: multiplier then offset
|
|
2029
|
+
expect(criticalMetrics.fleet.activeFaultCount).toBe(0);
|
|
2024
2030
|
} finally {
|
|
2025
2031
|
recorder.dispose();
|
|
2026
2032
|
}
|
|
@@ -2077,10 +2083,10 @@ describeIfTcp("executable methods integration", () => {
|
|
|
2077
2083
|
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/fault_count`, 1)).toBe(1);
|
|
2078
2084
|
expect(await waitForMQTTTopicValue(recorder, `${topicPrefix}/pressure`, 50)).toBe(50);
|
|
2079
2085
|
|
|
2080
|
-
stage = "verify quality
|
|
2081
|
-
expect(
|
|
2082
|
-
|
|
2083
|
-
)
|
|
2086
|
+
stage = "verify quality stays good during active fault (faults change plant, not sensor)";
|
|
2087
|
+
expect(await waitForMQTTTopicQuality(recorder, `${topicPrefix}/pressure`, "good")).toBe(
|
|
2088
|
+
"good",
|
|
2089
|
+
);
|
|
2084
2090
|
|
|
2085
2091
|
// Verify fleet metrics show active fault
|
|
2086
2092
|
const faultMetrics = await readAdminJson(runtime, "/metrics", EngineMetricsSnapshotSchema);
|
|
@@ -5,30 +5,6 @@ beforeEach(resetExitCode);
|
|
|
5
5
|
afterEach(resetExitCode);
|
|
6
6
|
|
|
7
7
|
describe("miravo inject", () => {
|
|
8
|
-
test("command is registered with correct name and options", async () => {
|
|
9
|
-
const { cmd: injectCmd } = await findCliCommand("inject");
|
|
10
|
-
expect(injectCmd).toBeDefined();
|
|
11
|
-
expect(injectCmd?.description()).toBe("Trigger or clear a fault on a running instance");
|
|
12
|
-
|
|
13
|
-
const optionNames = injectCmd?.options.map((o) => o.long);
|
|
14
|
-
expect(optionNames).toContain("--clear");
|
|
15
|
-
expect(optionNames).toContain("--name");
|
|
16
|
-
expect(optionNames).toContain("--admin-url");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("accepts instanceId and fault as positional arguments", async () => {
|
|
20
|
-
const { cmd: injectCmd } = await findCliCommand("inject");
|
|
21
|
-
expect(injectCmd).toBeDefined();
|
|
22
|
-
|
|
23
|
-
const args = injectCmd?.registeredArguments;
|
|
24
|
-
expect(args).toHaveLength(2);
|
|
25
|
-
expect(args?.[0]?.name()).toBe("instanceId");
|
|
26
|
-
expect(args?.[1]?.name()).toBe("fault");
|
|
27
|
-
// Both are optional
|
|
28
|
-
expect(args?.[0]?.required).toBe(false);
|
|
29
|
-
expect(args?.[1]?.required).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
8
|
test("sets exit code 1 with invalid admin URL", async () => {
|
|
33
9
|
const { program } = await findCliCommand("inject");
|
|
34
10
|
program.exitOverride();
|
|
@@ -88,7 +88,7 @@ describe("formatModelInspect", () => {
|
|
|
88
88
|
expect(output).toContain("centrifugal-pump");
|
|
89
89
|
expect(output).toContain("Parameters:");
|
|
90
90
|
expect(output).toContain("rated_rpm");
|
|
91
|
-
expect(output).toContain("Members (
|
|
91
|
+
expect(output).toContain("Members (30):");
|
|
92
92
|
expect(output).toContain("Methods (5):");
|
|
93
93
|
expect(output).toContain("availability_state");
|
|
94
94
|
expect(output).toContain("Lifecycle: healthy");
|
package/src/commands/models.ts
CHANGED
|
@@ -209,6 +209,12 @@ export function formatGeneratorSummary(generator: GeneratorConfig): string {
|
|
|
209
209
|
return `weighted-sum (${generator.terms.length} terms)`;
|
|
210
210
|
case "gate":
|
|
211
211
|
return `gate (enabled: ${generator.enabled_member})`;
|
|
212
|
+
case "waveform":
|
|
213
|
+
return `waveform (shape: ${generator.shape}, period: ${generator.period}s)`;
|
|
214
|
+
case "chirp":
|
|
215
|
+
return `chirp (${generator.start_freq_hz}–${generator.end_freq_hz}Hz, ${generator.sweep_duration}s)`;
|
|
216
|
+
case "impulse":
|
|
217
|
+
return `impulse (amplitude: ${generator.amplitude}${generator.probability !== undefined ? `, p=${generator.probability}` : ``})`;
|
|
212
218
|
default: {
|
|
213
219
|
const _exhaustive: never = generator;
|
|
214
220
|
return String(_exhaustive);
|
package/src/commands/start.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
discoverTemplates,
|
|
10
10
|
loadTemplate,
|
|
11
11
|
loadTwinModel,
|
|
12
|
+
resolveMiravoHome,
|
|
12
13
|
resolveModelPath,
|
|
13
14
|
validateFullTemplate,
|
|
14
15
|
} from "@miravo/core";
|
|
@@ -31,10 +32,12 @@ import {
|
|
|
31
32
|
MIRAVO_VERSION,
|
|
32
33
|
MiravoError,
|
|
33
34
|
MiravoTemplateSchema,
|
|
35
|
+
PersistedStateSchema,
|
|
34
36
|
SimulationNameSchema,
|
|
35
37
|
TWIN_FILE_EXTENSION,
|
|
36
38
|
TwinModelSchema,
|
|
37
39
|
} from "@miravo/shared";
|
|
40
|
+
import { Glob } from "bun";
|
|
38
41
|
import type { Command } from "commander";
|
|
39
42
|
import { box, colors, stripAnsi } from "consola/utils";
|
|
40
43
|
import { parse as parseYAML } from "yaml";
|
|
@@ -403,7 +406,29 @@ async function startAction(options: StartOptions): Promise<void> {
|
|
|
403
406
|
adminPort ??= 8080;
|
|
404
407
|
|
|
405
408
|
// Step 1: Resolve template path(s)
|
|
406
|
-
|
|
409
|
+
// If the simulation name is explicitly known and no template was given,
|
|
410
|
+
// peek at persisted state to recover the template path — skipping the
|
|
411
|
+
// interactive picker on resume.
|
|
412
|
+
let effectiveTemplateOption = templateOption;
|
|
413
|
+
if (!effectiveTemplateOption && !options.resetState) {
|
|
414
|
+
const knownName = options.name ?? simulationNameEnv;
|
|
415
|
+
if (knownName) {
|
|
416
|
+
// Named: peek by explicit simulation name
|
|
417
|
+
const peeked = await peekStateTemplatePath(knownName);
|
|
418
|
+
if (peeked) {
|
|
419
|
+
effectiveTemplateOption = peeked;
|
|
420
|
+
configLog.debug(`Resuming from persisted state: template=${peeked}`);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// Unnamed: scan state dir — auto-resume if exactly one simulation exists
|
|
424
|
+
const peeked = await peekSingleStateTemplatePath();
|
|
425
|
+
if (peeked) {
|
|
426
|
+
effectiveTemplateOption = peeked;
|
|
427
|
+
configLog.debug(`Resuming from persisted state: template=${peeked}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const templatePath = await resolveTemplatePath(effectiveTemplateOption, catalog);
|
|
407
432
|
configLog.debug(`Resolved template: ${templatePath}`);
|
|
408
433
|
// Resolve any additional comma-separated templates upfront so errors surface before engine start
|
|
409
434
|
const extraTemplatePaths = await Promise.all(
|
|
@@ -946,6 +971,47 @@ function printDetachSummary(entry: DaemonRegistryEntry): void {
|
|
|
946
971
|
// Step 1: Template resolution
|
|
947
972
|
// ---------------------------------------------------------------------------
|
|
948
973
|
|
|
974
|
+
async function peekStateTemplatePath(simulationName: string): Promise<string | undefined> {
|
|
975
|
+
const stateDir = `${resolveMiravoHome()}/state`;
|
|
976
|
+
const file = Bun.file(`${stateDir}/${simulationName}.json`);
|
|
977
|
+
if (!(await file.exists())) return undefined;
|
|
978
|
+
try {
|
|
979
|
+
const parsed = JSON.parse(await file.text()) as unknown;
|
|
980
|
+
const result = PersistedStateSchema.safeParse(parsed);
|
|
981
|
+
if (!result.success) return undefined;
|
|
982
|
+
return result.data.templateRuns.find((r) => r.templatePath.trim().length > 0)?.templatePath;
|
|
983
|
+
} catch {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function peekSingleStateTemplatePath(): Promise<string | undefined> {
|
|
989
|
+
const stateDir = `${resolveMiravoHome()}/state`;
|
|
990
|
+
let bestTemplatePath: string | undefined;
|
|
991
|
+
let bestSavedAt = "";
|
|
992
|
+
try {
|
|
993
|
+
const glob = new Glob("*.json");
|
|
994
|
+
for await (const file of glob.scan({ cwd: stateDir, onlyFiles: true })) {
|
|
995
|
+
try {
|
|
996
|
+
const parsed = JSON.parse(await Bun.file(`${stateDir}/${file}`).text()) as unknown;
|
|
997
|
+
const result = PersistedStateSchema.safeParse(parsed);
|
|
998
|
+
if (!result.success) continue;
|
|
999
|
+
const templatePath = result.data.templateRuns.find(
|
|
1000
|
+
(r) => r.templatePath.trim().length > 0,
|
|
1001
|
+
)?.templatePath;
|
|
1002
|
+
if (!templatePath) continue;
|
|
1003
|
+
if (result.data.savedAt > bestSavedAt) {
|
|
1004
|
+
bestSavedAt = result.data.savedAt;
|
|
1005
|
+
bestTemplatePath = templatePath;
|
|
1006
|
+
}
|
|
1007
|
+
} catch {}
|
|
1008
|
+
}
|
|
1009
|
+
} catch {
|
|
1010
|
+
return undefined; // state dir doesn't exist yet (first-ever run)
|
|
1011
|
+
}
|
|
1012
|
+
return bestTemplatePath;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
949
1015
|
async function resolveTemplatePath(
|
|
950
1016
|
templateOption: string | undefined,
|
|
951
1017
|
catalog: ContentCatalog,
|
|
@@ -1251,6 +1317,7 @@ function createAndPopulateEngine(
|
|
|
1251
1317
|
variation: twin.variation,
|
|
1252
1318
|
unsBasePath,
|
|
1253
1319
|
...(twin.parameters !== undefined ? { parameterOverrides: twin.parameters } : {}),
|
|
1320
|
+
...(twin.starting_hours !== undefined ? { startingHours: twin.starting_hours } : {}),
|
|
1254
1321
|
templateRunId: bootRunId,
|
|
1255
1322
|
});
|
|
1256
1323
|
for (const node of spawned) {
|
|
@@ -1357,6 +1424,7 @@ function createRuntimeTemplateLoader(
|
|
|
1357
1424
|
variation: twin.variation,
|
|
1358
1425
|
unsBasePath,
|
|
1359
1426
|
...(twin.parameters !== undefined ? { parameterOverrides: twin.parameters } : {}),
|
|
1427
|
+
...(twin.starting_hours !== undefined ? { startingHours: twin.starting_hours } : {}),
|
|
1360
1428
|
templateRunId: runId,
|
|
1361
1429
|
});
|
|
1362
1430
|
for (const node of spawned) {
|
|
@@ -5,32 +5,6 @@ beforeEach(resetExitCode);
|
|
|
5
5
|
afterEach(resetExitCode);
|
|
6
6
|
|
|
7
7
|
describe("miravo status", () => {
|
|
8
|
-
test("command is registered with correct name and options", async () => {
|
|
9
|
-
const { cmd: statusCmd } = await findCliCommand("status");
|
|
10
|
-
expect(statusCmd).toBeDefined();
|
|
11
|
-
expect(statusCmd?.description()).toBe(
|
|
12
|
-
"Show live status, metrics, and instances of a running simulation",
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
const optionNames = statusCmd?.options.map((o) => o.long);
|
|
16
|
-
expect(optionNames).toContain("--name");
|
|
17
|
-
expect(optionNames).toContain("--discover");
|
|
18
|
-
expect(optionNames).toContain("--watch");
|
|
19
|
-
expect(optionNames).toContain("--instance");
|
|
20
|
-
expect(optionNames).toContain("--json");
|
|
21
|
-
expect(optionNames).toContain("--admin-url");
|
|
22
|
-
expect(optionNames).toContain("--template");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("--watch has short alias -w", async () => {
|
|
26
|
-
const { cmd: statusCmd } = await findCliCommand("status");
|
|
27
|
-
expect(statusCmd).toBeDefined();
|
|
28
|
-
|
|
29
|
-
const watchOpt = statusCmd?.options.find((o) => o.long === "--watch");
|
|
30
|
-
expect(watchOpt).toBeDefined();
|
|
31
|
-
expect(watchOpt?.short).toBe("-w");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
8
|
test("sets exit code 1 with invalid admin URL", async () => {
|
|
35
9
|
const { program } = await findCliCommand("status");
|
|
36
10
|
program.exitOverride();
|
|
@@ -41,19 +41,6 @@ describe("formatTemplateInspect", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
describe("miravo templates", () => {
|
|
44
|
-
test("command is registered with list and inspect subcommands", async () => {
|
|
45
|
-
const { createProgram } = await import("../index.ts");
|
|
46
|
-
const program = createProgram();
|
|
47
|
-
|
|
48
|
-
const templatesCmd = program.commands.find((c) => c.name() === "templates");
|
|
49
|
-
expect(templatesCmd).toBeDefined();
|
|
50
|
-
expect(templatesCmd?.description()).toBe("List and inspect Miravo templates");
|
|
51
|
-
|
|
52
|
-
const subcommandNames = templatesCmd?.commands.map((c) => c.name()) ?? [];
|
|
53
|
-
expect(subcommandNames).toContain("list");
|
|
54
|
-
expect(subcommandNames).toContain("inspect");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
44
|
test("templates inspect nonexistent template sets exit code 1", async () => {
|
|
58
45
|
await runCliCommand(["templates", "inspect", "nonexistent-template"]);
|
|
59
46
|
expect(process.exitCode).toBe(1);
|