@phystack/device-simulator 6.3.4 → 6.3.6

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 (101) hide show
  1. package/dist/command.d.ts +6 -0
  2. package/dist/command.d.ts.map +1 -0
  3. package/dist/command.js +44 -0
  4. package/dist/command.js.map +1 -0
  5. package/dist/commands/list.d.ts +2 -0
  6. package/dist/commands/list.d.ts.map +1 -0
  7. package/dist/commands/list.js +20 -0
  8. package/dist/commands/list.js.map +1 -0
  9. package/dist/commands/remove.d.ts +2 -0
  10. package/dist/commands/remove.d.ts.map +1 -0
  11. package/dist/commands/remove.js +19 -0
  12. package/dist/commands/remove.js.map +1 -0
  13. package/dist/commands/run.d.ts +13 -0
  14. package/dist/commands/run.d.ts.map +1 -0
  15. package/dist/commands/run.js +353 -0
  16. package/dist/commands/run.js.map +1 -0
  17. package/dist/commands/start.d.ts +6 -0
  18. package/dist/commands/start.d.ts.map +1 -0
  19. package/dist/commands/start.js +216 -0
  20. package/dist/commands/start.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/services/dev-token.d.ts +2 -0
  26. package/dist/services/dev-token.d.ts.map +1 -0
  27. package/dist/services/dev-token.js +36 -0
  28. package/dist/services/dev-token.js.map +1 -0
  29. package/dist/services/env.d.ts +2 -0
  30. package/dist/services/env.d.ts.map +1 -0
  31. package/dist/services/env.js +14 -0
  32. package/dist/services/env.js.map +1 -0
  33. package/dist/simulator/index.d.ts +19 -0
  34. package/dist/simulator/index.d.ts.map +1 -0
  35. package/dist/simulator/index.js +153 -0
  36. package/dist/simulator/index.js.map +1 -0
  37. package/dist/simulator/local-server.d.ts +24 -0
  38. package/dist/simulator/local-server.d.ts.map +1 -0
  39. package/dist/simulator/local-server.js +125 -0
  40. package/dist/simulator/local-server.js.map +1 -0
  41. package/dist/simulator/logger.d.ts +11 -0
  42. package/dist/simulator/logger.d.ts.map +1 -0
  43. package/dist/simulator/logger.js +42 -0
  44. package/dist/simulator/logger.js.map +1 -0
  45. package/dist/simulator/message-router.d.ts +34 -0
  46. package/dist/simulator/message-router.d.ts.map +1 -0
  47. package/dist/simulator/message-router.js +388 -0
  48. package/dist/simulator/message-router.js.map +1 -0
  49. package/dist/simulator/twin-cache.d.ts +16 -0
  50. package/dist/simulator/twin-cache.d.ts.map +1 -0
  51. package/dist/simulator/twin-cache.js +54 -0
  52. package/dist/simulator/twin-cache.js.map +1 -0
  53. package/dist/simulator/types.d.ts +48 -0
  54. package/dist/simulator/types.d.ts.map +1 -0
  55. package/dist/simulator/types.js +16 -0
  56. package/dist/simulator/types.js.map +1 -0
  57. package/dist/utils/config-paths.d.ts +12 -0
  58. package/dist/utils/config-paths.d.ts.map +1 -0
  59. package/dist/utils/config-paths.js +33 -0
  60. package/dist/utils/config-paths.js.map +1 -0
  61. package/dist/utils/index.d.ts +3 -0
  62. package/dist/utils/index.d.ts.map +1 -0
  63. package/dist/utils/index.js +37 -0
  64. package/dist/utils/index.js.map +1 -0
  65. package/dist/utils/simulator-config.d.ts +25 -0
  66. package/dist/utils/simulator-config.d.ts.map +1 -0
  67. package/dist/utils/simulator-config.js +140 -0
  68. package/dist/utils/simulator-config.js.map +1 -0
  69. package/dist/utils/tenant-storage.d.ts +14 -0
  70. package/dist/utils/tenant-storage.d.ts.map +1 -0
  71. package/dist/utils/tenant-storage.js +65 -0
  72. package/dist/utils/tenant-storage.js.map +1 -0
  73. package/package.json +5 -2
  74. package/CHANGELOG.md +0 -42
  75. package/build-binary.sh +0 -37
  76. package/src/__tests__/e2e/binary.e2e.test.ts +0 -394
  77. package/src/__tests__/preload.ts +0 -44
  78. package/src/command.ts +0 -66
  79. package/src/commands/__tests__/run-helpers.test.ts +0 -181
  80. package/src/commands/list.ts +0 -25
  81. package/src/commands/remove.ts +0 -16
  82. package/src/commands/run.ts +0 -518
  83. package/src/commands/start.ts +0 -309
  84. package/src/index.ts +0 -45
  85. package/src/services/__tests__/dev-token.test.ts +0 -156
  86. package/src/services/dev-token.ts +0 -52
  87. package/src/services/env.ts +0 -10
  88. package/src/simulator/__tests__/message-router.test.ts +0 -782
  89. package/src/simulator/__tests__/twin-cache.test.ts +0 -129
  90. package/src/simulator/index.ts +0 -200
  91. package/src/simulator/local-server.ts +0 -184
  92. package/src/simulator/logger.ts +0 -44
  93. package/src/simulator/message-router.ts +0 -525
  94. package/src/simulator/twin-cache.ts +0 -61
  95. package/src/simulator/types.ts +0 -53
  96. package/src/utils/__tests__/simulator-config.test.ts +0 -230
  97. package/src/utils/config-paths.ts +0 -38
  98. package/src/utils/index.ts +0 -41
  99. package/src/utils/simulator-config.ts +0 -185
  100. package/src/utils/tenant-storage.ts +0 -106
  101. package/tsconfig.json +0 -12
@@ -1,394 +0,0 @@
1
- import { afterAll, describe, expect, test } from "bun:test";
2
- import { spawn, spawnSync, type ChildProcess } from "child_process";
3
- import fs from "fs";
4
- import net from "net";
5
- import os from "os";
6
- import path from "path";
7
- import { fileURLToPath } from "url";
8
- import { io as socketIOClient, type Socket } from "socket.io-client";
9
-
10
- // --- Locate package root + the compiled binary ----------------------------
11
- const PACKAGE_ROOT = path.resolve(
12
- path.dirname(fileURLToPath(import.meta.url)),
13
- "../../..",
14
- );
15
- const BINARY = path.join(PACKAGE_ROOT, "device-simulator-test");
16
-
17
- // Build the host binary once, here, if it isn't already present. This is the
18
- // crux of the e2e suite: every assertion below runs against the COMPILED Bun
19
- // binary (a child process), never against the TypeScript source.
20
- function ensureBinaryBuilt(): void {
21
- if (fs.existsSync(BINARY)) return;
22
- const result = spawnSync(
23
- "bash",
24
- ["build-binary.sh", "", "./device-simulator-test"],
25
- {
26
- cwd: PACKAGE_ROOT,
27
- stdio: "pipe",
28
- encoding: "utf8",
29
- },
30
- );
31
- if (result.status !== 0) {
32
- throw new Error(
33
- `Failed to build the device-simulator test binary: ${result.stderr || result.stdout}`,
34
- );
35
- }
36
- }
37
- ensureBinaryBuilt();
38
-
39
- // Each e2e gets its own throwaway HOME so the child binary's config-paths
40
- // (CONFIG_DIR = $HOME/.config/phystack-cli) resolve under a temp dir — never the
41
- // real home. The binary reads HOME from its spawn env (process env, not the OS
42
- // user db, for the child). Tracked for afterAll cleanup.
43
- const tempHomes: string[] = [];
44
- function makeTempHome(): string {
45
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sim-e2e-home-"));
46
- tempHomes.push(dir);
47
- return dir;
48
- }
49
-
50
- // Track spawned simulator processes so we always kill them, even on assertion
51
- // failure (cleanup is the sanctioned use of afterAll/finally per the test rules).
52
- const runningProcesses: ChildProcess[] = [];
53
- function trackProcess(child: ChildProcess): ChildProcess {
54
- runningProcesses.push(child);
55
- return child;
56
- }
57
-
58
- afterAll(() => {
59
- for (const child of runningProcesses) {
60
- if (!child.killed) {
61
- try {
62
- child.kill("SIGKILL");
63
- } catch (error) {
64
- console.error(
65
- `Failed to kill simulator process ${child.pid}: ${
66
- error instanceof Error ? error.message : String(error)
67
- }`,
68
- );
69
- }
70
- }
71
- }
72
- for (const dir of tempHomes) {
73
- fs.rmSync(dir, { recursive: true, force: true });
74
- }
75
- });
76
-
77
- // --- Helpers ---------------------------------------------------------------
78
-
79
- function runBinary(
80
- args: string[],
81
- home: string,
82
- ): { status: number | null; stdout: string; stderr: string } {
83
- const result = spawnSync(BINARY, args, {
84
- env: { ...process.env, HOME: home, USERPROFILE: home },
85
- encoding: "utf8",
86
- timeout: 15000,
87
- });
88
- return {
89
- status: result.status,
90
- stdout: result.stdout ?? "",
91
- stderr: result.stderr ?? "",
92
- };
93
- }
94
-
95
- function getFreePort(): Promise<number> {
96
- return new Promise((resolve, reject) => {
97
- const server = net.createServer();
98
- server.unref();
99
- server.on("error", reject);
100
- server.listen(0, "127.0.0.1", () => {
101
- const address = server.address();
102
- if (address && typeof address === "object") {
103
- const { port } = address;
104
- server.close(() => resolve(port));
105
- } else {
106
- server.close(() =>
107
- reject(new Error("Could not determine a free port")),
108
- );
109
- }
110
- });
111
- });
112
- }
113
-
114
- function waitForListening(port: number, timeoutMs = 10000): Promise<void> {
115
- const deadline = Date.now() + timeoutMs;
116
- return new Promise((resolve, reject) => {
117
- const attempt = () => {
118
- const socket = net.connect(port, "127.0.0.1");
119
- socket.on("connect", () => {
120
- socket.end();
121
- resolve();
122
- });
123
- socket.on("error", () => {
124
- socket.destroy();
125
- if (Date.now() > deadline) {
126
- reject(
127
- new Error(
128
- `Simulator did not start listening on port ${port} in time`,
129
- ),
130
- );
131
- } else {
132
- setTimeout(attempt, 100);
133
- }
134
- });
135
- };
136
- attempt();
137
- });
138
- }
139
-
140
- function startSimulator(port: number, home: string): ChildProcess {
141
- const child = spawn(BINARY, ["start", "--port", String(port)], {
142
- env: { ...process.env, HOME: home, USERPROFILE: home },
143
- stdio: "pipe",
144
- });
145
- return trackProcess(child);
146
- }
147
-
148
- function connectClient(port: number): Promise<Socket> {
149
- return new Promise((resolve, reject) => {
150
- const socket = socketIOClient(`http://localhost:${port}`, {
151
- reconnectionAttempts: 1,
152
- timeout: 5000,
153
- });
154
- socket.on("connect", () => resolve(socket));
155
- socket.on("connect_error", (error: Error) =>
156
- reject(new Error(`Cannot connect to simulator: ${error.message}`)),
157
- );
158
- setTimeout(
159
- () => reject(new Error("Timed out connecting to simulator")),
160
- 8000,
161
- );
162
- });
163
- }
164
-
165
- function emitSimulator(
166
- socket: Socket,
167
- payload: Record<string, unknown>,
168
- ): Promise<any> {
169
- return new Promise((resolve, reject) => {
170
- const timer = setTimeout(
171
- () => reject(new Error("Timed out waiting for simulator ack")),
172
- 8000,
173
- );
174
- socket.emit("simulator", payload, (response: any) => {
175
- clearTimeout(timer);
176
- resolve(response);
177
- });
178
- });
179
- }
180
-
181
- // --- 1. CLI surface parity -------------------------------------------------
182
-
183
- describe("e2e: CLI surface (compiled binary)", () => {
184
- test("--help lists every command", () => {
185
- const home = makeTempHome();
186
- const { stdout, status } = runBinary(["--help"], home);
187
- expect(status).toBe(0);
188
- expect(stdout).toContain("start");
189
- expect(stdout).toContain("run");
190
- expect(stdout).toContain("list");
191
- expect(stdout).toContain("remove");
192
- expect(stdout).toContain("Local device simulator for app development");
193
- });
194
-
195
- test("start --help shows the exact option strings and defaults", () => {
196
- const home = makeTempHome();
197
- const { stdout } = runBinary(["start", "--help"], home);
198
- expect(stdout).toContain("-p, --port <port>");
199
- expect(stdout).toContain('(default: "55000")');
200
- expect(stdout).toContain("--connect <name>");
201
- });
202
-
203
- test("run --help shows the type / dev-command / settings-dir options", () => {
204
- const home = makeTempHome();
205
- const { stdout } = runBinary(["run", "--help"], home);
206
- expect(stdout).toContain("--type <type>");
207
- expect(stdout).toContain("--dev-command <command>");
208
- expect(stdout).toContain("--settings-dir <path>");
209
- expect(stdout).toMatch(/run \[options\] <path>/);
210
- });
211
-
212
- test("--version prints a semver", () => {
213
- const home = makeTempHome();
214
- const { stdout, status } = runBinary(["--version"], home);
215
- expect(status).toBe(0);
216
- expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
217
- });
218
- });
219
-
220
- // --- 2. list / remove against a temp HOME ---------------------------------
221
-
222
- describe("e2e: list / remove persistence contract (compiled binary)", () => {
223
- test("seeded simulator shows in list, then remove deletes it", () => {
224
- const home = makeTempHome();
225
- const simulatorsDir = path.join(
226
- home,
227
- ".config",
228
- "phystack-cli",
229
- "simulators",
230
- );
231
- fs.mkdirSync(simulatorsDir, { recursive: true });
232
-
233
- const seeded = {
234
- name: "foo",
235
- deviceId: "device-foo-123",
236
- accessKey: "key-foo",
237
- serialNumber: "SIM-FOO",
238
- environment: "dev",
239
- provisionedAt: "2026-06-16T12:00:00.000Z",
240
- tenantId: "tenant-foo",
241
- };
242
- fs.writeFileSync(
243
- path.join(simulatorsDir, "foo.json"),
244
- JSON.stringify(seeded, null, 2),
245
- );
246
-
247
- const listed = runBinary(["list"], home);
248
- expect(listed.status).toBe(0);
249
- expect(listed.stdout).toContain("foo");
250
- expect(listed.stdout).toContain("device-foo-123");
251
-
252
- const removed = runBinary(["remove", "foo"], home);
253
- expect(removed.status).toBe(0);
254
- expect(removed.stdout).toContain('Removed simulator "foo".');
255
- expect(fs.existsSync(path.join(simulatorsDir, "foo.json"))).toBe(false);
256
-
257
- const emptyList = runBinary(["list"], home);
258
- expect(emptyList.stdout).toContain("No provisioned simulators");
259
- });
260
-
261
- test("remove of a non-existent simulator exits non-zero", () => {
262
- const home = makeTempHome();
263
- const { status, stderr } = runBinary(["remove", "ghost"], home);
264
- expect(status).not.toBe(0);
265
- expect(stderr).toContain('Simulator "ghost" not found.');
266
- });
267
-
268
- test("list with no config prints the empty-state hint", () => {
269
- const home = makeTempHome();
270
- const { stdout, status } = runBinary(["list"], home);
271
- expect(status).toBe(0);
272
- expect(stdout).toContain("No provisioned simulators");
273
- });
274
- });
275
-
276
- // --- 3. start server + socket.io handshake (the heart) --------------------
277
-
278
- describe("e2e: start server + socket.io protocol (compiled binary)", () => {
279
- test("full twin handshake: status, create, get, report patch echo", async () => {
280
- const home = makeTempHome();
281
- const port = await getFreePort();
282
- const child = startSimulator(port, home);
283
- let client: Socket | undefined;
284
-
285
- try {
286
- await waitForListening(port);
287
- client = await connectClient(port);
288
-
289
- // ping → pong
290
- expect(await emitSimulator(client, { method: "ping" })).toEqual({
291
- status: "success",
292
- message: "pong",
293
- });
294
-
295
- // getDeviceStatus reports the synthetic local Device twin. tenantId is the
296
- // deterministic mockObjectId('local') the simulator derives at startup.
297
- const status = await emitSimulator(client, { method: "getDeviceStatus" });
298
- expect(status.status).toBe("success");
299
- expect(status.socketConnected).toBe(true);
300
- expect(status.tenantId).toBe("25bf8e1a2393f1108d37029b");
301
- expect(status.displayName).toBe("Local Simulator");
302
- expect(status.twins.Device).toHaveLength(1);
303
-
304
- // createInstanceTwin registers a Screen twin and echoes it back.
305
- const created = await emitSimulator(client, {
306
- method: "createInstanceTwin",
307
- data: { type: "Screen", desiredProperties: { appName: "e2e-app" } },
308
- });
309
- expect(created.status).toBe("success");
310
- const twinId = created.twin.id as string;
311
- expect(twinId).toBeTruthy();
312
- expect(created.twin.type).toBe("Screen");
313
- expect(created.twin.properties.desired).toEqual({ appName: "e2e-app" });
314
-
315
- // getTwinById round-trips the freshly created twin.
316
- const fetched = await emitSimulator(client, {
317
- method: "getTwinById",
318
- data: { twinId },
319
- });
320
- expect(fetched.status).toBe("success");
321
- expect(fetched.twin.id).toBe(twinId);
322
-
323
- // Subscribe to the twin so the reported-property patch is echoed back as a
324
- // twinMessage/twinUpdated to this socket.
325
- const twinUpdate = new Promise<any>((resolve, reject) => {
326
- const timer = setTimeout(
327
- () => reject(new Error("No twinUpdated received")),
328
- 8000,
329
- );
330
- client!.on("twinMessage", (message: any) => {
331
- if (message?.method === "twinUpdated" && message?.twinId === twinId) {
332
- clearTimeout(timer);
333
- resolve(message);
334
- }
335
- });
336
- });
337
- await emitSimulator(client, { method: "twinSubscribe", twinId });
338
-
339
- // reportScreenTwinProperties merges reported props and emits twinUpdated.
340
- const reported = await emitSimulator(client, {
341
- method: "reportScreenTwinProperties",
342
- twinId,
343
- data: { online: true },
344
- });
345
- expect(reported.status).toBe("success");
346
- expect(reported.twin.properties.reported).toEqual({ online: true });
347
-
348
- // The simulator pushed the update back over the socket (twinMessage).
349
- const echoed = await twinUpdate;
350
- expect(echoed.data.id).toBe(twinId);
351
- expect(echoed.data.properties.reported).toEqual({ online: true });
352
-
353
- // Unknown method → structured error.
354
- expect(await emitSimulator(client, { method: "bogusMethod" })).toEqual({
355
- status: "error",
356
- message: "Unknown method: bogusMethod",
357
- });
358
- } finally {
359
- if (client) client.disconnect();
360
- child.kill("SIGTERM");
361
- }
362
- });
363
-
364
- test("start persists the device identity under the temp HOME (not the real home)", async () => {
365
- const home = makeTempHome();
366
- const port = await getFreePort();
367
- const child = startSimulator(port, home);
368
-
369
- try {
370
- await waitForListening(port);
371
- const deviceConfigPath = path.join(
372
- home,
373
- ".config",
374
- "phystack-cli",
375
- "simulator",
376
- "tenants",
377
- "local",
378
- "device.json",
379
- );
380
- // Give the start sequence a beat to flush the device config to disk.
381
- const deadline = Date.now() + 5000;
382
- while (!fs.existsSync(deviceConfigPath) && Date.now() < deadline) {
383
- await new Promise((resolve) => setTimeout(resolve, 100));
384
- }
385
- expect(fs.existsSync(deviceConfigPath)).toBe(true);
386
- const config = JSON.parse(fs.readFileSync(deviceConfigPath, "utf8"));
387
- expect(config.tenantId).toBe("25bf8e1a2393f1108d37029b");
388
- expect(typeof config.deviceId).toBe("string");
389
- expect(typeof config.deviceTwinId).toBe("string");
390
- } finally {
391
- child.kill("SIGTERM");
392
- }
393
- });
394
- });
@@ -1,44 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
-
5
- // Bun runs every test file in a SINGLE process with a SHARED module registry.
6
- // config-paths.ts computes CONFIG_DIR from os.homedir() at import time, and
7
- // os.homedir() ignores process.env.HOME on macOS/Linux (it reads the OS user
8
- // database). If any test file imports config-paths (directly or transitively,
9
- // e.g. via commands/run -> utils/simulator-config) before an override is in
10
- // place, CONFIG_DIR locks onto the REAL ~/.config/phystack-cli and every later
11
- // override is too late — tests then read/write the user's real config.
12
- //
13
- // This preload runs BEFORE any test module is evaluated (wired via
14
- // bunfig.toml [test].preload), so it is the single, authoritative place to
15
- // redirect os.homedir() to a throwaway temp dir for the whole suite. Persistence
16
- // tests share this one config root; they use unique device/tenant/simulator
17
- // names so they don't collide.
18
- //
19
- // Exported so individual test files can reference the same root and assert
20
- // isolation.
21
- export const SUITE_TEST_HOME: string = fs.mkdtempSync(
22
- path.join(os.tmpdir(), "ps-sim-suite-home-"),
23
- );
24
-
25
- const realHomedir = os.homedir;
26
- (os as { homedir: () => string }).homedir = () => SUITE_TEST_HOME;
27
- process.env.HOME = SUITE_TEST_HOME;
28
- process.env.USERPROFILE = SUITE_TEST_HOME;
29
-
30
- // Best-effort cleanup of the suite temp dir when the process exits. Restoring
31
- // the original homedir keeps any post-test tooling honest.
32
- process.on("exit", () => {
33
- (os as { homedir: () => string }).homedir = realHomedir;
34
- try {
35
- fs.rmSync(SUITE_TEST_HOME, { recursive: true, force: true });
36
- } catch (error) {
37
- // Surface but don't fail the run — the temp dir is under os.tmpdir().
38
- console.error(
39
- `Failed to remove suite test home ${SUITE_TEST_HOME}: ${
40
- error instanceof Error ? error.message : String(error)
41
- }`,
42
- );
43
- }
44
- });
package/src/command.ts DELETED
@@ -1,66 +0,0 @@
1
- import { Command } from "commander";
2
- import { handleError } from "./utils";
3
- import start from "./commands/start";
4
- import run from "./commands/run";
5
- import simulatorList from "./commands/list";
6
- import simulatorRemove from "./commands/remove";
7
-
8
- // Re-export the simulator domain types so consumers (e.g. the legacy CLI's
9
- // utils/simulator-config) can resolve them from the package rather than the
10
- // now-removed tools/cli/src/simulator/ tree.
11
- export type {
12
- SimulatorConfig,
13
- DeviceConfig,
14
- AppConfig,
15
- TwinResponse,
16
- EventPayload,
17
- } from "./simulator/types";
18
- export { AppTypeEnum, TwinTypeEnum } from "./simulator/types";
19
- export type { SimulatorConnectConfig } from "./utils/simulator-config";
20
-
21
- /**
22
- * Build the `simulator` command tree as a standalone commander Command so the
23
- * legacy CLI (tools/cli) can `program.addCommand(buildSimulatorCommand())` and
24
- * preserve the `phy simulator …` surface unchanged.
25
- */
26
- export function buildSimulatorCommand() {
27
- const simulator = new Command("simulator");
28
- simulator.description("Local device simulator for app development");
29
-
30
- simulator
31
- .command("start")
32
- .description("Start the local simulator server")
33
- .option("-p, --port <port>", "Port to listen on", "55000")
34
- .option("--connect <name>", "Connect to phyhub as a provisioned device")
35
- .action(handleError(start));
36
-
37
- simulator
38
- .command("run <path>")
39
- .description(
40
- "Run an app connected to the simulator (reconciles twin, spawns dev server)",
41
- )
42
- .option("--type <type>", "Override app type detection (screen or edge)")
43
- .option(
44
- "--dev-command <command>",
45
- "Override dev command (default: npm run dev)",
46
- )
47
- .option(
48
- "--settings-dir <path>",
49
- "Settings directory path (default: src/settings)",
50
- )
51
- .action(handleError(run));
52
-
53
- simulator
54
- .command("list")
55
- .description("List all provisioned simulator devices")
56
- .action(simulatorList);
57
-
58
- simulator
59
- .command("remove <name>")
60
- .description("Remove a saved simulator device")
61
- .action(simulatorRemove);
62
-
63
- return simulator;
64
- }
65
-
66
- export default buildSimulatorCommand;
@@ -1,181 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- buildDockerRunArgs,
4
- resolveAppType,
5
- resolveStartCommand,
6
- } from "../run";
7
- import { AppTypeEnum } from "../../simulator/types";
8
-
9
- describe("resolveAppType", () => {
10
- test("option type takes precedence over package type", () => {
11
- expect(resolveAppType("screen", "edge")).toBe(AppTypeEnum.Screen);
12
- });
13
-
14
- test("falls back to the package application-type when no option", () => {
15
- expect(resolveAppType(undefined, "edge")).toBe(AppTypeEnum.Edge);
16
- });
17
-
18
- test("lowercases the raw value before matching", () => {
19
- expect(resolveAppType("SCREEN")).toBe(AppTypeEnum.Screen);
20
- });
21
-
22
- test("throws when neither option nor package type is provided", () => {
23
- expect(() => resolveAppType()).toThrow(
24
- 'Cannot detect app type. Set "application-type" in package.json or pass --type.',
25
- );
26
- });
27
-
28
- test("throws on an unsupported type", () => {
29
- expect(() => resolveAppType("cloud")).toThrow(
30
- /Unsupported app type "cloud"/,
31
- );
32
- });
33
- });
34
-
35
- describe("resolveStartCommand", () => {
36
- test("explicit --dev-command wins", () => {
37
- expect(
38
- resolveStartCommand(
39
- { devCommand: "bun run dev" },
40
- {},
41
- AppTypeEnum.Screen,
42
- ),
43
- ).toBe("bun run dev");
44
- });
45
-
46
- test("uses `npm start` when package.json has a start script", () => {
47
- expect(
48
- resolveStartCommand(
49
- {},
50
- { scripts: { start: "node index.js" } },
51
- AppTypeEnum.Screen,
52
- ),
53
- ).toBe("npm start");
54
- });
55
-
56
- test("edge app without a start script returns undefined (Docker path)", () => {
57
- expect(resolveStartCommand({}, {}, AppTypeEnum.Edge)).toBeUndefined();
58
- });
59
-
60
- test("screen app without a start script throws", () => {
61
- expect(() => resolveStartCommand({}, {}, AppTypeEnum.Screen)).toThrow(
62
- "No start script found in package.json. Pass --dev-command to specify one.",
63
- );
64
- });
65
- });
66
-
67
- describe("buildDockerRunArgs", () => {
68
- const TWIN_ID = "twin-xyz";
69
- const IMAGE = "my-image:1.0.0";
70
-
71
- test("null config: base args, TWIN_ID + bridge-mode host mapping, image last", () => {
72
- const args = buildDockerRunArgs(null, IMAGE, TWIN_ID);
73
-
74
- expect(args[0]).toBe("run");
75
- expect(args).toContain("--rm");
76
- expect(args[args.length - 1]).toBe(IMAGE);
77
-
78
- const twinEnvIndex = args.indexOf("TWIN_ID=twin-xyz");
79
- expect(twinEnvIndex).toBeGreaterThan(-1);
80
- expect(args[twinEnvIndex - 1]).toBe("-e");
81
-
82
- // Bridge mode (no host network): host.docker.internal + phyos→host-gateway
83
- expect(args).toContain("--add-host=host.docker.internal:host-gateway");
84
- const phyosIndex = args.indexOf("phyos:host-gateway");
85
- expect(phyosIndex).toBeGreaterThan(-1);
86
- expect(args[phyosIndex - 1]).toBe("--add-host");
87
-
88
- expect(args).toContain("-e");
89
- expect(args).toContain(
90
- "PHYSTACK_SIMULATOR_URL=http://host.docker.internal:55000",
91
- );
92
- });
93
-
94
- test("host network mode switches the host mappings", () => {
95
- const args = buildDockerRunArgs(
96
- { HostConfig: { NetworkMode: "host" } },
97
- IMAGE,
98
- TWIN_ID,
99
- );
100
- expect(args).toContain("--network");
101
- expect(args).toContain("host");
102
- expect(args).toContain("phyos:127.0.0.1");
103
- expect(args).not.toContain("--add-host=host.docker.internal:host-gateway");
104
- });
105
-
106
- test("custom Env, Entrypoint, port bindings, binds and Cmd are translated", () => {
107
- const config = {
108
- Env: ["FOO=bar", "BAZ=qux"],
109
- Entrypoint: ["/entry.sh"],
110
- Cmd: ["--flag", "value"],
111
- HostConfig: {
112
- PortBindings: { "8080/tcp": [{ HostPort: "9090" }] },
113
- Binds: ["/host/path:/container/path"],
114
- },
115
- };
116
- const args = buildDockerRunArgs(config, IMAGE, TWIN_ID);
117
-
118
- expect(args).toContain("FOO=bar");
119
- expect(args).toContain("BAZ=qux");
120
-
121
- const entrypointIndex = args.indexOf("--entrypoint");
122
- expect(entrypointIndex).toBeGreaterThan(-1);
123
- expect(args[entrypointIndex + 1]).toBe("/entry.sh");
124
-
125
- const portIndex = args.indexOf("-p");
126
- expect(portIndex).toBeGreaterThan(-1);
127
- expect(args).toContain("9090:8080");
128
-
129
- const volIndex = args.indexOf("-v");
130
- expect(args[volIndex + 1]).toBe("/host/path:/container/path");
131
-
132
- // Cmd is appended AFTER the image.
133
- const imageIndex = args.indexOf(IMAGE);
134
- expect(args.slice(imageIndex + 1)).toEqual(["--flag", "value"]);
135
- });
136
-
137
- test("resource limits, privileged, readonly, ulimits, gpu device requests", () => {
138
- const config = {
139
- HostConfig: {
140
- Privileged: true,
141
- ReadonlyRootfs: true,
142
- Memory: 536870912,
143
- CpuShares: 512,
144
- Ulimits: [{ Name: "nofile", Soft: 1024, Hard: 2048 }],
145
- Devices: [{ PathOnHost: "/dev/ttyUSB0" }],
146
- DeviceRequests: [{ Capabilities: [["gpu"]] }],
147
- },
148
- };
149
- const args = buildDockerRunArgs(config, IMAGE, TWIN_ID);
150
-
151
- expect(args).toContain("--privileged");
152
- expect(args).toContain("--read-only");
153
-
154
- const memIndex = args.indexOf("--memory");
155
- expect(args[memIndex + 1]).toBe("536870912");
156
-
157
- const cpuIndex = args.indexOf("--cpu-shares");
158
- expect(args[cpuIndex + 1]).toBe("512");
159
-
160
- const ulimitIndex = args.indexOf("--ulimit");
161
- expect(args[ulimitIndex + 1]).toBe("nofile=1024:2048");
162
-
163
- const deviceIndex = args.indexOf("--device");
164
- expect(args[deviceIndex + 1]).toBe("/dev/ttyUSB0");
165
-
166
- expect(args).toContain("--gpus");
167
- expect(args).toContain("all");
168
- });
169
-
170
- test("TZ env is forwarded when set in the environment", () => {
171
- const originalTz = process.env.TZ;
172
- process.env.TZ = "Europe/Stockholm";
173
- try {
174
- const args = buildDockerRunArgs(null, IMAGE, TWIN_ID);
175
- expect(args).toContain("TZ=Europe/Stockholm");
176
- } finally {
177
- if (originalTz === undefined) delete process.env.TZ;
178
- else process.env.TZ = originalTz;
179
- }
180
- });
181
- });