miravo 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +199 -0
- package/README.md +7 -70
- package/bin/miravo.js +84 -0
- package/package.json +20 -36
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
package/src/lib/auth-store.ts
DELETED
|
@@ -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
|
-
});
|