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.
Files changed (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,87 +0,0 @@
1
- /**
2
- * Credential storage — persists admin password at ~/.miravo/auth.json.
3
- *
4
- * Password priority: MIRAVO_ADMIN_PASSWORD env > ~/.miravo/auth.json > default "admin".
5
- */
6
-
7
- import { mkdir } from "node:fs/promises";
8
- import { resolveMiravoHome } from "@miravo/core";
9
- import type { PersistedCredential } from "@miravo/shared";
10
- import { PersistedCredentialSchema } from "@miravo/shared";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Paths
14
- // ---------------------------------------------------------------------------
15
-
16
- function authFilePath(): string {
17
- return `${resolveMiravoHome()}/auth.json`;
18
- }
19
-
20
- // ---------------------------------------------------------------------------
21
- // Read / Write
22
- // ---------------------------------------------------------------------------
23
-
24
- /** Load persisted credential from ~/.miravo/auth.json. Returns null if missing or invalid. */
25
- export async function loadCredential(): Promise<PersistedCredential | null> {
26
- const file = Bun.file(authFilePath());
27
- if (!(await file.exists())) return null;
28
- try {
29
- const raw: unknown = await file.json();
30
- const result = PersistedCredentialSchema.safeParse(raw);
31
- return result.success ? result.data : null;
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- /** Persist credential to ~/.miravo/auth.json with restricted permissions. */
38
- export async function saveCredential(username: string, passwordHash: string): Promise<void> {
39
- const filePath = authFilePath();
40
- const dir = resolveMiravoHome();
41
- await mkdir(dir, { recursive: true });
42
- const data: PersistedCredential = { username, passwordHash };
43
- await Bun.write(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Password resolution
48
- // ---------------------------------------------------------------------------
49
-
50
- interface ResolvedPassword {
51
- /** Argon2id hash of the effective admin password. */
52
- readonly passwordHash: string;
53
- /** True when the effective password is the literal default "admin". */
54
- readonly isDefault: boolean;
55
- }
56
-
57
- /**
58
- * Resolve the admin password with priority:
59
- * 1. MIRAVO_ADMIN_PASSWORD env var (plaintext → hashed)
60
- * 2. ~/.miravo/auth.json (already hashed)
61
- * 3. Default "admin" (plaintext → hashed)
62
- */
63
- export async function resolveAdminPassword(): Promise<ResolvedPassword> {
64
- // 1. Environment override
65
- const envPassword = Bun.env["MIRAVO_ADMIN_PASSWORD"];
66
- if (envPassword && envPassword.trim().length > 0) {
67
- return {
68
- passwordHash: await Bun.password.hash(envPassword),
69
- isDefault: envPassword === "admin",
70
- };
71
- }
72
-
73
- // 2. Persisted credential file
74
- const credential = await loadCredential();
75
- if (credential) {
76
- return {
77
- passwordHash: credential.passwordHash,
78
- isDefault: false, // File exists → user changed password at some point
79
- };
80
- }
81
-
82
- // 3. Default
83
- return {
84
- passwordHash: await Bun.password.hash("admin"),
85
- isDefault: true,
86
- };
87
- }
@@ -1,429 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { DaemonRegistryEntry } from "./daemon-registry.ts";
6
- import {
7
- acquireNameLock,
8
- checkNameConflict,
9
- checkPortConflicts,
10
- displayHost,
11
- isPidAlive,
12
- listEntries,
13
- probeEntryIdentity,
14
- readEntry,
15
- removeEntry,
16
- sweepStaleEntries,
17
- writeEntry,
18
- } from "./daemon-registry.ts";
19
-
20
- /** Override MIRAVO_HOME to isolate tests from real daemon state. */
21
- let testHome: string;
22
- const serverHandles: Array<{ stop: () => void }> = [];
23
-
24
- async function setupTestHome(): Promise<void> {
25
- testHome = await mkdtemp(join(tmpdir(), "miravo-daemon-test-"));
26
- Bun.env["MIRAVO_HOME"] = testHome;
27
- }
28
-
29
- afterEach(async () => {
30
- for (const handle of serverHandles) {
31
- handle.stop();
32
- }
33
- serverHandles.length = 0;
34
- delete Bun.env["MIRAVO_HOME"];
35
- if (testHome) {
36
- await rm(testHome, { recursive: true, force: true });
37
- }
38
- });
39
-
40
- function makeEntry(overrides: Partial<DaemonRegistryEntry> = {}): DaemonRegistryEntry {
41
- return {
42
- name: "test-sim",
43
- pid: 99999,
44
- adminPort: 8080,
45
- adminHost: "127.0.0.1",
46
- adapters: {
47
- mqtt: { port: 1883, host: "0.0.0.0" },
48
- },
49
- templatePath: "/tmp/test.miravo.yaml",
50
- startedAt: "2026-03-17T10:00:00Z",
51
- seed: 42,
52
- runId: "test-run-id",
53
- ...overrides,
54
- };
55
- }
56
-
57
- describe("daemon-registry", () => {
58
- describe("writeEntry + readEntry", () => {
59
- test("round-trip writes and reads an entry", async () => {
60
- await setupTestHome();
61
- const entry = makeEntry();
62
- await writeEntry(entry);
63
- const read = await readEntry("test-sim");
64
- expect(read).toEqual(entry);
65
- });
66
-
67
- test("overwrites existing entry", async () => {
68
- await setupTestHome();
69
- const entry1 = makeEntry({ pid: 111 });
70
- const entry2 = makeEntry({ pid: 222 });
71
- await writeEntry(entry1);
72
- await writeEntry(entry2);
73
- const read = await readEntry("test-sim");
74
- expect(read?.pid).toBe(222);
75
- });
76
- });
77
-
78
- describe("readEntry", () => {
79
- test("returns null for missing file", async () => {
80
- await setupTestHome();
81
- const result = await readEntry("nonexistent");
82
- expect(result).toBeNull();
83
- });
84
-
85
- test("returns null for invalid JSON", async () => {
86
- await setupTestHome();
87
- const dir = join(testHome, "daemon");
88
- const { mkdir } = await import("node:fs/promises");
89
- await mkdir(dir, { recursive: true });
90
- await Bun.write(join(dir, "bad.json"), "not json");
91
- const result = await readEntry("bad");
92
- expect(result).toBeNull();
93
- });
94
-
95
- test("returns null for valid JSON that fails schema", async () => {
96
- await setupTestHome();
97
- const dir = join(testHome, "daemon");
98
- const { mkdir } = await import("node:fs/promises");
99
- await mkdir(dir, { recursive: true });
100
- await Bun.write(join(dir, "partial.json"), JSON.stringify({ name: "partial" }));
101
- const result = await readEntry("partial");
102
- expect(result).toBeNull();
103
- });
104
- });
105
-
106
- describe("removeEntry", () => {
107
- test("removes an existing entry", async () => {
108
- await setupTestHome();
109
- await writeEntry(makeEntry());
110
- await removeEntry("test-sim");
111
- const result = await readEntry("test-sim");
112
- expect(result).toBeNull();
113
- });
114
-
115
- test("does not throw for missing entry", async () => {
116
- await setupTestHome();
117
- await expect(removeEntry("nonexistent")).resolves.toBeUndefined();
118
- });
119
- });
120
-
121
- describe("listEntries", () => {
122
- test("returns empty array when daemon dir does not exist", async () => {
123
- await setupTestHome();
124
- const entries = await listEntries();
125
- expect(entries).toEqual([]);
126
- });
127
-
128
- test("returns all valid entries", async () => {
129
- await setupTestHome();
130
- await writeEntry(makeEntry({ name: "sim-a" }));
131
- await writeEntry(makeEntry({ name: "sim-b" }));
132
- const entries = await listEntries();
133
- expect(entries).toHaveLength(2);
134
- const names = entries.map((e) => e.name).sort();
135
- expect(names).toEqual(["sim-a", "sim-b"]);
136
- });
137
-
138
- test("skips invalid files", async () => {
139
- await setupTestHome();
140
- await writeEntry(makeEntry({ name: "valid" }));
141
- const dir = join(testHome, "daemon");
142
- await Bun.write(join(dir, "invalid.json"), "broken");
143
- const entries = await listEntries();
144
- expect(entries).toHaveLength(1);
145
- expect(entries[0]?.name).toBe("valid");
146
- });
147
-
148
- test("skips .tmp files", async () => {
149
- await setupTestHome();
150
- await writeEntry(makeEntry({ name: "valid" }));
151
- const dir = join(testHome, "daemon");
152
- await Bun.write(join(dir, "leftover.json.tmp"), JSON.stringify(makeEntry({ name: "tmp" })));
153
- const entries = await listEntries();
154
- expect(entries).toHaveLength(1);
155
- });
156
- });
157
-
158
- describe("isPidAlive", () => {
159
- test("returns true for own PID", () => {
160
- expect(isPidAlive(process.pid)).toBe(true);
161
- });
162
-
163
- test("returns false for impossible PID", () => {
164
- // PID 2^30 is extremely unlikely to exist
165
- expect(isPidAlive(1_073_741_824)).toBe(false);
166
- });
167
- });
168
-
169
- describe("sweepStaleEntries", () => {
170
- test("removes entries with dead PIDs", async () => {
171
- await setupTestHome();
172
- // Use impossible PIDs so they're guaranteed dead
173
- await writeEntry(makeEntry({ name: "dead-sim-a", pid: 1_073_741_824 }));
174
- await writeEntry(makeEntry({ name: "dead-sim-b", pid: 1_073_741_825 }));
175
-
176
- await sweepStaleEntries();
177
-
178
- const entries = await listEntries();
179
- expect(entries).toHaveLength(0);
180
- });
181
-
182
- test("keeps entries whose /readyz reports the same runId with 503", async () => {
183
- await setupTestHome();
184
- const server = Bun.serve({
185
- hostname: "127.0.0.1",
186
- port: 0,
187
- fetch() {
188
- return Response.json(
189
- { status: "degraded", reason: "mqtt disconnected", runId: "test-run-id" },
190
- { status: 503 },
191
- );
192
- },
193
- });
194
- serverHandles.push(server);
195
- const port = server.port;
196
- if (port === undefined) {
197
- throw new Error("Failed to allocate test port.");
198
- }
199
-
200
- await writeEntry(
201
- makeEntry({
202
- name: "paused-sim",
203
- pid: process.pid,
204
- adminPort: port,
205
- runId: "test-run-id",
206
- }),
207
- );
208
-
209
- await sweepStaleEntries();
210
-
211
- expect(await readEntry("paused-sim")).not.toBeNull();
212
- });
213
- });
214
-
215
- describe("probeEntryIdentity", () => {
216
- test("treats non-ready responses with a matching runId as verified", async () => {
217
- const server = Bun.serve({
218
- hostname: "127.0.0.1",
219
- port: 0,
220
- fetch() {
221
- return Response.json({ status: "paused", runId: "test-run-id" }, { status: 503 });
222
- },
223
- });
224
- serverHandles.push(server);
225
- const port = server.port;
226
- if (port === undefined) {
227
- throw new Error("Failed to allocate test port.");
228
- }
229
-
230
- const result = await probeEntryIdentity(
231
- makeEntry({
232
- pid: process.pid,
233
- adminPort: port,
234
- runId: "test-run-id",
235
- }),
236
- );
237
-
238
- expect(result).toEqual({ status: "verified", ready: false });
239
- });
240
-
241
- test("fails closed on malformed /readyz bodies", async () => {
242
- const server = Bun.serve({
243
- hostname: "127.0.0.1",
244
- port: 0,
245
- fetch() {
246
- return new Response("<html>not json</html>", {
247
- status: 200,
248
- headers: { "content-type": "text/html" },
249
- });
250
- },
251
- });
252
- serverHandles.push(server);
253
- const port = server.port;
254
- if (port === undefined) {
255
- throw new Error("Failed to allocate test port.");
256
- }
257
-
258
- const result = await probeEntryIdentity(
259
- makeEntry({
260
- pid: process.pid,
261
- adminPort: port,
262
- runId: "test-run-id",
263
- }),
264
- );
265
-
266
- expect(result).toEqual({ status: "mismatch", ready: false });
267
- });
268
- });
269
-
270
- describe("checkPortConflicts", () => {
271
- test("detects admin port conflict", async () => {
272
- await setupTestHome();
273
- await writeEntry(makeEntry({ name: "existing", pid: process.pid, adminPort: 8080 }));
274
-
275
- const ports = new Map([["admin", 8080]]);
276
- const conflicts = await checkPortConflicts(ports);
277
- expect(conflicts).toHaveLength(1);
278
- expect(conflicts[0]).toContain("8080");
279
- expect(conflicts[0]).toContain("existing");
280
- });
281
-
282
- test("detects adapter port conflict", async () => {
283
- await setupTestHome();
284
- await writeEntry(
285
- makeEntry({
286
- name: "existing",
287
- pid: process.pid,
288
- adapters: { mqtt: { port: 1883, host: "0.0.0.0" } },
289
- }),
290
- );
291
-
292
- const ports = new Map([["mqtt", 1883]]);
293
- const conflicts = await checkPortConflicts(ports);
294
- expect(conflicts.length).toBeGreaterThan(0);
295
- expect(conflicts[0]).toContain("1883");
296
- });
297
-
298
- test("excludes named entry from conflict check", async () => {
299
- await setupTestHome();
300
- await writeEntry(makeEntry({ name: "self", pid: process.pid, adminPort: 8080 }));
301
-
302
- const ports = new Map([["admin", 8080]]);
303
- const conflicts = await checkPortConflicts(ports, "self");
304
- expect(conflicts).toHaveLength(0);
305
- });
306
-
307
- test("ignores entries with dead PIDs", async () => {
308
- await setupTestHome();
309
- await writeEntry(makeEntry({ name: "dead", pid: 1_073_741_824, adminPort: 8080 }));
310
-
311
- const ports = new Map([["admin", 8080]]);
312
- const conflicts = await checkPortConflicts(ports);
313
- expect(conflicts).toHaveLength(0);
314
- });
315
-
316
- test("returns empty array when no conflicts", async () => {
317
- await setupTestHome();
318
- await writeEntry(makeEntry({ name: "existing", pid: process.pid, adminPort: 9090 }));
319
-
320
- const ports = new Map([["admin", 8080]]);
321
- const conflicts = await checkPortConflicts(ports);
322
- expect(conflicts).toHaveLength(0);
323
- });
324
- });
325
-
326
- describe("checkNameConflict", () => {
327
- test("returns null when no entry exists", async () => {
328
- await setupTestHome();
329
- const result = await checkNameConflict("nonexistent");
330
- expect(result).toBeNull();
331
- });
332
-
333
- test("removes entry when PID is dead", async () => {
334
- await setupTestHome();
335
- await writeEntry(makeEntry({ name: "stale", pid: 1_073_741_824 }));
336
- const result = await checkNameConflict("stale");
337
- expect(result).toBeNull();
338
- // Verify file was cleaned up
339
- const entry = await readEntry("stale");
340
- expect(entry).toBeNull();
341
- });
342
-
343
- test("keeps the entry when the daemon is alive but not ready", async () => {
344
- await setupTestHome();
345
- const server = Bun.serve({
346
- hostname: "127.0.0.1",
347
- port: 0,
348
- fetch() {
349
- return Response.json({ status: "paused", runId: "test-run-id" }, { status: 503 });
350
- },
351
- });
352
- serverHandles.push(server);
353
- const port = server.port;
354
- if (port === undefined) {
355
- throw new Error("Failed to allocate test port.");
356
- }
357
-
358
- await writeEntry(
359
- makeEntry({
360
- name: "paused-sim",
361
- pid: process.pid,
362
- adminPort: port,
363
- runId: "test-run-id",
364
- }),
365
- );
366
-
367
- const result = await checkNameConflict("paused-sim");
368
-
369
- expect(result?.name).toBe("paused-sim");
370
- expect(await readEntry("paused-sim")).not.toBeNull();
371
- });
372
- });
373
-
374
- describe("acquireNameLock", () => {
375
- test("acquires lock when none exists", async () => {
376
- await setupTestHome();
377
- const release = await acquireNameLock("test-sim");
378
- await release();
379
- });
380
-
381
- test("throws when lock is held by a live process", async () => {
382
- await setupTestHome();
383
- const dir = join(testHome, "daemon");
384
- await mkdir(dir, { recursive: true });
385
- await Bun.write(join(dir, "test-sim.lock"), String(process.pid));
386
-
387
- await expect(acquireNameLock("test-sim")).rejects.toThrow("already starting");
388
-
389
- // Clean up the manually created lock so afterEach can remove the dir
390
- await Bun.file(join(dir, "test-sim.lock"))
391
- .exists()
392
- .then(async (exists) => {
393
- if (exists) {
394
- const { unlink } = await import("node:fs/promises");
395
- await unlink(join(dir, "test-sim.lock")).catch(() => {});
396
- }
397
- });
398
- });
399
-
400
- test("clears stale lock from a dead process and succeeds", async () => {
401
- await setupTestHome();
402
- const dir = join(testHome, "daemon");
403
- await mkdir(dir, { recursive: true });
404
- // Write a lock with an impossible PID — guaranteed to be dead
405
- await Bun.write(join(dir, "test-sim.lock"), "1073741824");
406
-
407
- const release = await acquireNameLock("test-sim");
408
- await release();
409
- });
410
- });
411
-
412
- describe("displayHost", () => {
413
- test("maps 0.0.0.0 to 127.0.0.1", () => {
414
- expect(displayHost("0.0.0.0")).toBe("127.0.0.1");
415
- });
416
-
417
- test("maps :: to 127.0.0.1", () => {
418
- expect(displayHost("::")).toBe("127.0.0.1");
419
- });
420
-
421
- test("passes through 127.0.0.1", () => {
422
- expect(displayHost("127.0.0.1")).toBe("127.0.0.1");
423
- });
424
-
425
- test("passes through custom host", () => {
426
- expect(displayHost("192.168.1.100")).toBe("192.168.1.100");
427
- });
428
- });
429
- });