miravo 0.1.4 → 0.2.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.
@@ -16,6 +16,7 @@ describe("miravo status", () => {
16
16
  expect(optionNames).toContain("--watch");
17
17
  expect(optionNames).toContain("--instance");
18
18
  expect(optionNames).toContain("--json");
19
+ expect(optionNames).toContain("--admin-url");
19
20
  expect(optionNames).toContain("--template");
20
21
  });
21
22
 
@@ -28,32 +29,12 @@ describe("miravo status", () => {
28
29
  expect(watchOpt?.short).toBe("-w");
29
30
  });
30
31
 
31
- test("sets exit code 1 when no simulation is running", async () => {
32
+ test("sets exit code 1 with invalid admin URL", async () => {
32
33
  const { program } = await findCliCommand("status");
33
34
  program.exitOverride();
34
35
 
35
36
  try {
36
- await program.parseAsync(["node", "miravo", "status"]);
37
- } catch {
38
- // Expected: connection error
39
- }
40
-
41
- // Should fail because no broker is running on default port
42
- expect(process.exitCode).toBe(1);
43
- });
44
-
45
- test("sets exit code 1 with bad template path", async () => {
46
- const { program } = await findCliCommand("status");
47
- program.exitOverride();
48
-
49
- try {
50
- await program.parseAsync([
51
- "node",
52
- "miravo",
53
- "status",
54
- "--template",
55
- "/nonexistent.miravo.yaml",
56
- ]);
37
+ await program.parseAsync(["node", "miravo", "status", "--admin-url", "not-a-url"]);
57
38
  } catch {
58
39
  // Expected
59
40
  }
@@ -1,22 +1,25 @@
1
1
  import {
2
2
  CONTROL_STATUS_TIMEOUT_MS,
3
3
  type EngineMetricsSnapshot,
4
- EngineMetricsSnapshotSchema,
5
4
  type EngineStateSnapshot,
6
- EngineStateSnapshotSchema,
5
+ MiravoError,
7
6
  } from "@miravo/shared";
8
7
  import type { Command } from "commander";
9
8
  import { colors } from "consola/utils";
10
- import type { IPublishPacket, MqttClient } from "mqtt";
9
+ import { getAdminSnapshot, resolveAdminUrl } from "../lib/admin-client.ts";
11
10
  import { connectToBroker, disconnectClient } from "../lib/mqtt-client.ts";
12
11
  import { parseDurationMs } from "../lib/parse.ts";
13
- import {
14
- discoverRunningSimulations,
15
- resolveTargetSimulation,
16
- type SimulationTopics,
17
- } from "../lib/resolve-simulation.ts";
12
+ import { discoverRunningSimulations } from "../lib/resolve-simulation.ts";
18
13
  import { engineLog, handleCommandError, isPlain, writeOutput } from "../logger.ts";
19
- import { formatCount, formatDuration, formatKeyValue, formatTable } from "../output.ts";
14
+ import {
15
+ formatCount,
16
+ formatDuration,
17
+ formatHealthSuffix,
18
+ formatKeyValue,
19
+ formatTable,
20
+ } from "../output.ts";
21
+
22
+ const STATUS_WATCH_INTERVAL_MS = 1000;
20
23
 
21
24
  interface StatusOptions {
22
25
  readonly name?: string;
@@ -24,13 +27,14 @@ interface StatusOptions {
24
27
  readonly watch?: boolean;
25
28
  readonly instance?: string;
26
29
  readonly json?: boolean;
30
+ readonly adminUrl?: string;
27
31
  readonly template?: string;
28
32
  readonly timeout?: number;
29
33
  }
30
34
 
31
35
  /**
32
36
  * Register the `miravo status` command with the root Commander program.
33
- * Shows status of a running simulation via the MQTT control plane.
37
+ * Shows status of a running simulation via the admin HTTP server.
34
38
  */
35
39
  export function registerStatusCommand(program: Command): void {
36
40
  program
@@ -38,16 +42,17 @@ export function registerStatusCommand(program: Command): void {
38
42
  .description("Show status of a running simulation")
39
43
  .option("--name <name>", "Target simulation name")
40
44
  .option("--discover", "Discover running simulations and exit")
41
- .option("-w, --watch", "Stay subscribed and re-render on updates")
45
+ .option("-w, --watch", "Poll the admin server and re-render on updates")
42
46
  .option("--instance <id>", "Show detailed status for one instance")
43
47
  .option("--json", "Machine-readable JSON output")
48
+ .option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
44
49
  .option(
45
50
  "--template <path>",
46
- "Path to .miravo.yaml for broker connection info (env: MIRAVO_TEMPLATE)",
51
+ "Path to .miravo.yaml for MQTT discovery connection info (env: MIRAVO_TEMPLATE)",
47
52
  )
48
53
  .option(
49
54
  "--timeout <ms>",
50
- "Connection/data timeout in milliseconds (default: 5000)",
55
+ "Admin request timeout in milliseconds (default: 5000)",
51
56
  parseDurationMs,
52
57
  )
53
58
  .action(async (options: StatusOptions) => {
@@ -55,93 +60,70 @@ export function registerStatusCommand(program: Command): void {
55
60
  });
56
61
  }
57
62
 
58
- // ---------------------------------------------------------------------------
59
- // Types for parsed control plane messages
60
- // ---------------------------------------------------------------------------
61
-
62
63
  type MetricsSnapshot = EngineMetricsSnapshot;
63
64
  type StateSnapshot = EngineStateSnapshot;
64
65
 
65
- function parseMetricsSnapshot(payload: unknown): MetricsSnapshot | null {
66
- const parsed = EngineMetricsSnapshotSchema.safeParse(payload);
67
- return parsed.success ? (parsed.data as MetricsSnapshot) : null;
68
- }
69
-
70
- function parseStateSnapshot(payload: unknown): StateSnapshot | null {
71
- const parsed = EngineStateSnapshotSchema.safeParse(payload);
72
- return parsed.success ? (parsed.data as StateSnapshot) : null;
73
- }
74
-
75
66
  // ---------------------------------------------------------------------------
76
67
  // Action
77
68
  // ---------------------------------------------------------------------------
78
69
 
79
70
  async function statusAction(options: StatusOptions): Promise<void> {
80
71
  try {
81
- const templateOption = options.template ?? Bun.env["MIRAVO_TEMPLATE"];
82
72
  const timeoutMs = options.timeout ?? CONTROL_STATUS_TIMEOUT_MS;
83
- const { client, brokerInfo } = await connectToBroker(templateOption, timeoutMs);
84
-
85
- try {
86
- if (options.discover) {
87
- const simulations = await discoverRunningSimulations(client, {
88
- timeoutMs,
89
- brokerUrl: brokerInfo.url,
90
- });
91
- if (simulations.length === 0) {
92
- engineLog.error(
93
- `No running simulations found on broker ${brokerInfo.url}. Start one with 'miravo start'.`,
94
- );
95
- process.exitCode = 1;
96
- return;
97
- }
98
- writeOutput(`Found ${simulations.length} simulation(s):`);
99
- for (const simulationName of simulations) {
100
- writeOutput(` ${simulationName}`);
101
- }
102
- return;
103
- }
104
-
105
- const target = await resolveTargetSimulation(client, {
106
- timeoutMs,
107
- brokerUrl: brokerInfo.url,
108
- ...(options.name !== undefined ? { name: options.name } : {}),
109
- });
73
+ if (options.discover) {
74
+ await discoverMode(options, timeoutMs);
75
+ return;
76
+ }
110
77
 
111
- if (options.watch) {
112
- await watchMode(client, options, timeoutMs, target);
113
- } else {
114
- await snapshotMode(client, brokerInfo.url, options, timeoutMs, target);
115
- }
116
- } finally {
117
- if (!options.watch) {
118
- disconnectClient(client);
119
- }
78
+ const adminUrl = resolveAdminUrl(options.adminUrl);
79
+ if (options.watch) {
80
+ await watchMode(adminUrl, options, timeoutMs);
81
+ } else {
82
+ await snapshotMode(adminUrl, options, timeoutMs);
120
83
  }
121
84
  } catch (error) {
122
85
  handleCommandError(error, engineLog);
123
86
  }
124
87
  }
125
88
 
89
+ // ---------------------------------------------------------------------------
90
+ // Discover mode (MQTT-only)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ async function discoverMode(options: StatusOptions, timeoutMs: number): Promise<void> {
94
+ const templateOption = options.template ?? Bun.env["MIRAVO_TEMPLATE"];
95
+ const { client, brokerInfo } = await connectToBroker(templateOption, timeoutMs);
96
+ try {
97
+ const simulations = await discoverRunningSimulations(client, {
98
+ timeoutMs,
99
+ brokerUrl: brokerInfo.url,
100
+ });
101
+ if (simulations.length === 0) {
102
+ engineLog.error(
103
+ `No running simulations found on broker ${brokerInfo.url}. Start one with 'miravo start'.`,
104
+ );
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ writeOutput(`Found ${simulations.length} simulation(s):`);
109
+ for (const simulationName of simulations) {
110
+ writeOutput(` ${simulationName}`);
111
+ }
112
+ } finally {
113
+ disconnectClient(client);
114
+ }
115
+ }
116
+
126
117
  // ---------------------------------------------------------------------------
127
118
  // Snapshot mode (default): get one reading and exit
128
119
  // ---------------------------------------------------------------------------
129
120
 
130
121
  async function snapshotMode(
131
- client: MqttClient,
132
- brokerUrl: string,
122
+ adminUrl: string,
133
123
  options: StatusOptions,
134
124
  timeoutMs: number,
135
- target: { readonly simulationName: string; readonly topics: SimulationTopics },
136
125
  ): Promise<void> {
137
- const data = await waitForData(
138
- client,
139
- brokerUrl,
140
- timeoutMs,
141
- target.topics,
142
- target.simulationName,
143
- );
144
- if (!data) return;
126
+ const data = await getStatusSnapshot(adminUrl, timeoutMs, options.name);
145
127
 
146
128
  if (options.json) {
147
129
  const filtered = filterForJson(data.metrics, data.state, options.instance);
@@ -164,79 +146,33 @@ async function snapshotMode(
164
146
  // ---------------------------------------------------------------------------
165
147
 
166
148
  async function watchMode(
167
- client: MqttClient,
149
+ adminUrl: string,
168
150
  options: StatusOptions,
169
151
  timeoutMs: number,
170
- target: { readonly simulationName: string; readonly topics: SimulationTopics },
171
152
  ): Promise<void> {
172
- let latestMetrics: MetricsSnapshot | null = null;
173
- let latestState: StateSnapshot | null = null;
174
153
  let firstRender = true;
154
+ let stopped = false;
155
+ const stop = (): void => {
156
+ stopped = true;
157
+ };
175
158
 
176
- // Subscribe to both topics
177
- client.subscribe(target.topics.metrics, { qos: 0 });
178
- client.subscribe(target.topics.state, { qos: 0 });
179
-
180
- // Wait for initial data
181
- const initial = await waitForDataFromSubscribed(client, timeoutMs, target.topics);
182
- if (!initial) {
183
- disconnectClient(client);
184
- return;
185
- }
159
+ process.once("SIGINT", stop);
160
+ process.once("SIGTERM", stop);
186
161
 
187
- latestMetrics = initial.metrics;
188
- latestState = initial.state;
189
-
190
- // Render initial
191
- renderFrame(latestMetrics, latestState, options, firstRender);
192
- firstRender = false;
193
-
194
- const onMessage = (topic: string, message: Buffer): void => {
195
- try {
196
- const parsed = JSON.parse(message.toString()) as unknown;
197
- if (topic === target.topics.metrics) {
198
- const metrics = parseMetricsSnapshot(parsed);
199
- if (!metrics) return;
200
- latestMetrics = metrics;
201
- } else if (topic === target.topics.state) {
202
- const state = parseStateSnapshot(parsed);
203
- if (!state) return;
204
- latestState = state;
205
- } else {
206
- return;
207
- }
162
+ try {
163
+ while (!stopped) {
164
+ const snapshot = await getStatusSnapshot(adminUrl, timeoutMs, options.name);
165
+ renderFrame(snapshot.metrics, snapshot.state, options, firstRender);
166
+ firstRender = false;
208
167
 
209
- if (latestMetrics && latestState) {
210
- renderFrame(latestMetrics, latestState, options, firstRender);
211
- firstRender = false;
168
+ if (!stopped) {
169
+ await Bun.sleep(STATUS_WATCH_INTERVAL_MS);
212
170
  }
213
- } catch {
214
- // Ignore parse errors in watch mode
215
171
  }
216
- };
217
-
218
- // Set up clean shutdown
219
- let cleaned = false;
220
- const cleanup = (): void => {
221
- if (cleaned) return;
222
- cleaned = true;
223
- client.off("message", onMessage);
224
- client.off("close", cleanup);
225
- client.unsubscribe(target.topics.metrics);
226
- client.unsubscribe(target.topics.state);
227
- process.off("SIGINT", cleanup);
228
- process.off("SIGTERM", cleanup);
229
- disconnectClient(client);
230
- const code =
231
- typeof process.exitCode === "number" && process.exitCode !== 0 ? process.exitCode : 0;
232
- process.exit(code);
233
- };
234
- process.once("SIGINT", cleanup);
235
- process.once("SIGTERM", cleanup);
236
- client.on("close", cleanup);
237
-
238
- // Continue rendering on each new message
239
- client.on("message", onMessage);
172
+ } finally {
173
+ process.off("SIGINT", stop);
174
+ process.off("SIGTERM", stop);
175
+ }
240
176
  }
241
177
 
242
178
  function renderFrame(
@@ -305,143 +241,19 @@ function filterForJson(
305
241
  };
306
242
  }
307
243
 
308
- // ---------------------------------------------------------------------------
309
- // Wait for initial data from control plane
310
- // ---------------------------------------------------------------------------
311
-
312
- interface ControlPlaneData {
313
- readonly metrics: MetricsSnapshot;
314
- readonly state: StateSnapshot;
315
- }
316
-
317
- interface WaitForDataOptions {
318
- readonly client: MqttClient;
319
- readonly timeoutMs: number;
320
- readonly topics: SimulationTopics;
321
- /** Error message shown when timeout expires. */
322
- readonly timeoutMessage: string;
323
- /** When true, subscribe/unsubscribe to control topics. When false, caller manages subscriptions. */
324
- readonly manageSubscriptions: boolean;
325
- }
326
-
327
- function waitForData(
328
- client: MqttClient,
329
- brokerUrl: string,
244
+ async function getStatusSnapshot(
245
+ adminUrl: string,
330
246
  timeoutMs: number,
331
- topics: SimulationTopics,
332
- simulationName: string,
333
- ): Promise<ControlPlaneData | null> {
334
- return waitForControlPlaneData({
335
- client,
336
- timeoutMs,
337
- topics,
338
- timeoutMessage: `No control-plane data received for simulation "${simulationName}" on ${brokerUrl}. Verify the simulation is running and publish permissions are enabled.`,
339
- manageSubscriptions: true,
340
- });
341
- }
342
-
343
- function waitForDataFromSubscribed(
344
- client: MqttClient,
345
- timeoutMs: number,
346
- topics: SimulationTopics,
347
- ): Promise<ControlPlaneData | null> {
348
- return waitForControlPlaneData({
349
- client,
350
- timeoutMs,
351
- topics,
352
- timeoutMessage: "No data received from simulation. Is the control plane active?",
353
- manageSubscriptions: false,
354
- });
355
- }
356
-
357
- function waitForControlPlaneData(opts: WaitForDataOptions): Promise<ControlPlaneData | null> {
358
- const { client, timeoutMs, timeoutMessage, manageSubscriptions, topics } = opts;
359
-
360
- return new Promise((resolve) => {
361
- let metrics: MetricsSnapshot | null = null;
362
- let metricsFromRetained = false;
363
- let state: StateSnapshot | null = null;
364
- let stateFromRetained = false;
365
- let retainedFallbackTimer: ReturnType<typeof setTimeout> | null = null;
366
- let settled = false;
367
-
368
- const retainedFallbackMs = Math.max(100, Math.min(750, Math.floor(timeoutMs / 4)));
369
-
370
- const cleanup = (): void => {
371
- clearTimeout(timer);
372
- if (retainedFallbackTimer) {
373
- clearTimeout(retainedFallbackTimer);
374
- retainedFallbackTimer = null;
375
- }
376
- client.off("message", handler);
377
- if (manageSubscriptions) {
378
- client.unsubscribe(topics.metrics);
379
- client.unsubscribe(topics.state);
380
- }
381
- };
382
-
383
- const settle = (value: ControlPlaneData | null): void => {
384
- if (settled) return;
385
- settled = true;
386
- cleanup();
387
- resolve(value);
388
- };
389
-
390
- const maybeResolve = (): void => {
391
- if (!metrics || !state) return;
392
-
393
- if (!metricsFromRetained && !stateFromRetained) {
394
- settle({ metrics, state });
395
- return;
396
- }
397
-
398
- if (retainedFallbackTimer) return;
399
-
400
- // Prefer fresh non-retained payloads when available, but don't block
401
- // indefinitely if only retained snapshots are present.
402
- retainedFallbackTimer = setTimeout(() => {
403
- if (metrics && state) {
404
- settle({ metrics, state });
405
- }
406
- }, retainedFallbackMs);
407
- };
408
-
409
- const timer = setTimeout(() => {
410
- if (settled) return;
411
- settled = true;
412
- cleanup();
413
- engineLog.error(timeoutMessage);
414
- process.exitCode = 1;
415
- resolve(null);
416
- }, timeoutMs);
417
-
418
- const handler = (topic: string, message: Buffer, packet: IPublishPacket): void => {
419
- try {
420
- const parsed = JSON.parse(message.toString()) as unknown;
421
- if (topic === topics.metrics && (!metrics || metricsFromRetained)) {
422
- const validMetrics = parseMetricsSnapshot(parsed);
423
- if (!validMetrics) return;
424
- metrics = validMetrics;
425
- metricsFromRetained = packet.retain;
426
- } else if (topic === topics.state && (!state || stateFromRetained)) {
427
- const validState = parseStateSnapshot(parsed);
428
- if (!validState) return;
429
- state = validState;
430
- stateFromRetained = packet.retain;
431
- }
432
-
433
- maybeResolve();
434
- } catch {
435
- // Ignore parse errors, wait for valid data
436
- }
437
- };
438
-
439
- if (manageSubscriptions) {
440
- client.subscribe(topics.metrics, { qos: 0 });
441
- client.subscribe(topics.state, { qos: 0 });
442
- }
443
- client.on("message", handler);
444
- });
247
+ expectedName?: string,
248
+ ): Promise<{ readonly metrics: MetricsSnapshot; readonly state: StateSnapshot }> {
249
+ const snapshot = await getAdminSnapshot(adminUrl, timeoutMs);
250
+ if (expectedName && snapshot.state.simulationName !== expectedName) {
251
+ throw new MiravoError(
252
+ `Connected to simulation "${snapshot.state.simulationName}" instead of "${expectedName}".`,
253
+ "Pass --admin-url for the correct Miravo process, or omit --name to inspect the connected simulation.",
254
+ );
255
+ }
256
+ return snapshot;
445
257
  }
446
258
 
447
259
  // ---------------------------------------------------------------------------
@@ -462,7 +274,8 @@ function renderOverview(metrics: MetricsSnapshot, state: StateSnapshot): void {
462
274
  // Engine line
463
275
  const stateColor =
464
276
  core.state === "running" ? colors.green : core.state === "paused" ? colors.yellow : colors.red;
465
- const tps = core.actualTicksPerSecond > 0 ? `${core.actualTicksPerSecond.toFixed(1)} tps` : "";
277
+ const tps =
278
+ core.actualTicksPerSecond > 0 ? `${core.actualTicksPerSecond.toFixed(1)} ticks/s` : "";
466
279
 
467
280
  writeOutput(
468
281
  formatKeyValue([
@@ -472,7 +285,7 @@ function renderOverview(metrics: MetricsSnapshot, state: StateSnapshot): void {
472
285
  ],
473
286
  [
474
287
  colors.dim("Fleet"),
475
- `${colors.bold(String(core.instanceCount))} instances${sep}${colors.bold(String(core.unsEntryCount))} UNS topics`,
288
+ `${colors.bold(String(core.instanceCount))} instances${sep}${colors.bold(String(core.memberValueCount))} data points`,
476
289
  ],
477
290
  ]),
478
291
  );
@@ -513,11 +326,33 @@ function renderOverview(metrics: MetricsSnapshot, state: StateSnapshot): void {
513
326
  mqttAdapter.metrics.errorsCount > 0
514
327
  ? colors.yellow(String(mqttAdapter.metrics.errorsCount))
515
328
  : colors.green("0");
329
+ const healthStr = formatHealthSuffix(mqttAdapter.metrics, sep);
516
330
  writeOutput(
517
331
  formatKeyValue([
518
332
  [
519
333
  colors.dim("MQTT"),
520
- `${connStr}${sep}${formatCount(mqttAdapter.metrics.messagesPublished)} published${sep}${errStr} errors`,
334
+ `${connStr}${sep}${formatCount(mqttAdapter.metrics.messagesPublished)} published${sep}${errStr} errors${healthStr}`,
335
+ ],
336
+ ]),
337
+ );
338
+ }
339
+
340
+ // OPC UA
341
+ const opcuaAdapter = metrics.adapters.find((a) => a.name === "opcua");
342
+ if (opcuaAdapter) {
343
+ const connStr = opcuaAdapter.metrics.connected
344
+ ? colors.green("connected")
345
+ : colors.red("disconnected");
346
+ const errStr =
347
+ opcuaAdapter.metrics.errorsCount > 0
348
+ ? colors.yellow(String(opcuaAdapter.metrics.errorsCount))
349
+ : colors.green("0");
350
+ const healthStr = formatHealthSuffix(opcuaAdapter.metrics, sep);
351
+ writeOutput(
352
+ formatKeyValue([
353
+ [
354
+ colors.dim("OPC UA"),
355
+ `${connStr}${sep}${formatCount(opcuaAdapter.metrics.messagesPublished)} updates${sep}${errStr} errors${healthStr}`,
521
356
  ],
522
357
  ]),
523
358
  );
@@ -601,7 +436,7 @@ function renderInstanceDetail(
601
436
 
602
437
  writeOutput(
603
438
  formatKeyValue([
604
- [colors.dim("UNS Path"), instance.unsPath],
439
+ [colors.dim("UNS Path"), instance.browsePath.join("/")],
605
440
  [
606
441
  colors.dim("Lifecycle"),
607
442
  `${instance.lifecycleStage} (${formatCount(Math.floor(instance.operatingHours))} operating hours)`,
@@ -1,20 +1,16 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir } from "node:fs/promises";
3
2
  import { resolve } from "node:path";
4
3
  import { loadTemplate } from "@miravo/core";
5
4
  import { createTmpFileUtils } from "@miravo/core/test-tmp-utils";
6
- import { MIRAVO_CONTENT_DIR_ENV } from "@miravo/shared";
7
5
  import { stripAnsi } from "consola/utils";
8
6
  import { configureLogger } from "../logger.ts";
9
- import { runCliCommand, withEnvVar } from "./cli-test-setup.ts";
7
+ import { runCliCommand } from "./cli-test-setup.ts";
10
8
  import { formatTemplateInspect } from "./templates.ts";
11
9
 
12
10
  configureLogger({ silent: true });
13
11
 
14
- const templatesDir = resolve(import.meta.dir, "..", "..", "..", "..", "templates");
15
- const { tmpDir, cleanupTmpDir } = createTmpFileUtils(
16
- resolve(import.meta.dir, ".tmp-test-templates"),
17
- );
12
+ const templatesDir = resolve(import.meta.dir, "..", "..", "..", "content", "templates");
13
+ const { cleanupTmpDir } = createTmpFileUtils(resolve(import.meta.dir, ".tmp-test-templates"));
18
14
 
19
15
  beforeEach(() => {
20
16
  process.exitCode = 0;
@@ -34,7 +30,7 @@ describe("formatTemplateInspect", () => {
34
30
  const output = stripAnsi(raw);
35
31
 
36
32
  expect(output).toContain("Smart Factory Production Line");
37
- expect(output).toContain("built-in (templates/smart-factory.miravo.yaml)");
33
+ expect(output).toContain("built-in");
38
34
  expect(output).toContain("Template ID:");
39
35
  expect(output).toContain("smart-factory");
40
36
  expect(output).toContain("Namespace:");
@@ -68,48 +64,6 @@ describe("miravo templates", () => {
68
64
  expect(process.exitCode).toBe(0);
69
65
  });
70
66
 
71
- test("templates inspect --copy copies template and referenced built-in models", async () => {
72
- await withEnvVar(MIRAVO_CONTENT_DIR_ENV, tmpDir, async () => {
73
- await runCliCommand(["templates", "inspect", "smart-factory", "--copy"]);
74
-
75
- expect(process.exitCode).toBe(0);
76
-
77
- const copiedTemplatePath = resolve(tmpDir, "templates", "smart-factory.miravo.yaml");
78
- expect(await Bun.file(copiedTemplatePath).exists()).toBe(true);
79
-
80
- const copiedTemplate = await loadTemplate(copiedTemplatePath);
81
- const inlineModels = new Set((copiedTemplate.models ?? []).map((model) => model.name));
82
- const referencedModels = new Set<string>();
83
- for (const area of copiedTemplate.areas) {
84
- for (const line of area.lines) {
85
- for (const twin of line.twins) {
86
- referencedModels.add(twin.model);
87
- }
88
- }
89
- }
90
-
91
- for (const modelName of referencedModels) {
92
- if (inlineModels.has(modelName)) continue;
93
- const copiedModelPath = resolve(tmpDir, "models", `${modelName}.twin.yaml`);
94
- expect(await Bun.file(copiedModelPath).exists()).toBe(true);
95
- }
96
- });
97
- });
98
-
99
- test("templates inspect --copy skips existing files with a message", async () => {
100
- const targetPath = resolve(tmpDir, "templates", "smart-factory.miravo.yaml");
101
- await mkdir(resolve(tmpDir, "templates"), { recursive: true });
102
- const dummyContent = `kind: MiravoTemplate\nversion: "1"\nname: keep-existing\nnamespace:\n prefix: test\nareas:\n - name: a\n lines:\n - name: l\n twins:\n - model: m\n count: 1\n`;
103
- await Bun.write(targetPath, dummyContent);
104
-
105
- await withEnvVar(MIRAVO_CONTENT_DIR_ENV, tmpDir, async () => {
106
- await runCliCommand(["templates", "inspect", "smart-factory", "--copy"]);
107
-
108
- expect(process.exitCode).toBe(0);
109
- expect(await Bun.file(targetPath).text()).toContain("name: keep-existing");
110
- });
111
- });
112
-
113
67
  test("templates list with bad directory sets exit code 1", async () => {
114
68
  await runCliCommand(["templates", "list", "--dir", "/nonexistent/templates-dir"]);
115
69
  expect(process.exitCode).toBe(1);