mindsim 0.1.5 → 0.1.7

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.
@@ -0,0 +1,567 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import * as configModule from "../src/config";
3
+ import { isBrowserEnvironment, isNodeEnvironment, MindSim } from "../src/index";
4
+ import type { MindsimServiceJson } from "../src/types";
5
+
6
+ // =============================================================================
7
+ // Test Data
8
+ // =============================================================================
9
+
10
+ const VALID_SERVICE_JSON: MindsimServiceJson = {
11
+ type: "mindsim_service_account",
12
+ version: "1",
13
+ project_id: "test-app",
14
+ project_name: "Test App",
15
+ environment: "production",
16
+ client_email: "test-app@mindsim.reasoner.com",
17
+ client_id: "550e8400-e29b-41d4-a716-446655440000",
18
+ api_key: "ms_prod_testkey123456789",
19
+ api_base_url: "https://api.reasoner.com/api/mindsim",
20
+ scopes: ["minds:read", "simulate:run"],
21
+ app_metadata: {
22
+ app_id: "550e8400-e29b-41d4-a716-446655440001",
23
+ workspace_id: "550e8400-e29b-41d4-a716-446655440002",
24
+ mindsim_org_id: "550e8400-e29b-41d4-a716-446655440003",
25
+ use_case: "internal-workflow",
26
+ },
27
+ created_at: "2025-12-21T00:00:00.000Z",
28
+ };
29
+
30
+ // =============================================================================
31
+ // Mock Setup
32
+ // =============================================================================
33
+
34
+ // Mock the config module to control loadApiKey and loadServiceJson
35
+ vi.mock("../src/config", async (importOriginal) => {
36
+ const actual = await importOriginal<typeof configModule>();
37
+ return {
38
+ ...actual,
39
+ loadApiKey: vi.fn(),
40
+ loadServiceJson: vi.fn(),
41
+ };
42
+ });
43
+
44
+ // Mock version check to prevent network calls
45
+ vi.mock("../src/version", () => ({
46
+ checkForUpdates: vi.fn().mockResolvedValue(undefined),
47
+ getPackageVersion: vi.fn().mockReturnValue("1.0.0-test"),
48
+ }));
49
+
50
+ // =============================================================================
51
+ // Environment Detection Tests
52
+ // =============================================================================
53
+
54
+ describe("Environment Detection", () => {
55
+ describe("isNodeEnvironment()", () => {
56
+ it("should return true in Node.js environment", () => {
57
+ // In the test environment (Node.js), this should be true
58
+ expect(isNodeEnvironment()).toBe(true);
59
+ });
60
+
61
+ it("should check for process.versions.node", () => {
62
+ // Verify the function checks the right property
63
+ expect(typeof process.versions.node).toBe("string");
64
+ expect(isNodeEnvironment()).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe("isBrowserEnvironment()", () => {
69
+ it("should return false in Node.js environment", () => {
70
+ // In Node.js test environment, there's no window/document
71
+ expect(isBrowserEnvironment()).toBe(false);
72
+ });
73
+ });
74
+ });
75
+
76
+ // =============================================================================
77
+ // MindSim Constructor with Service JSON Tests
78
+ // =============================================================================
79
+
80
+ describe("MindSim Constructor with Service JSON", () => {
81
+ beforeEach(() => {
82
+ vi.resetModules();
83
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
84
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
85
+ });
86
+
87
+ afterEach(() => {
88
+ vi.unstubAllEnvs();
89
+ });
90
+
91
+ describe("serviceJson via constructor options", () => {
92
+ it("should accept valid serviceJson passed via options", () => {
93
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
94
+ expect(mindsim).toBeInstanceOf(MindSim);
95
+ expect(mindsim.getServiceJson()).toEqual(VALID_SERVICE_JSON);
96
+ });
97
+
98
+ it("should use api_key from serviceJson", () => {
99
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
100
+ expect(mindsim).toBeInstanceOf(MindSim);
101
+ // The constructor should have used the api_key from service JSON
102
+ expect(mindsim.getAuthMethod()).toBe("service_json");
103
+ });
104
+
105
+ it("should set authMethod to service_json", () => {
106
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
107
+ expect(mindsim.getAuthMethod()).toBe("service_json");
108
+ });
109
+
110
+ it("should throw on invalid serviceJson", () => {
111
+ const invalidServiceJson = {
112
+ ...VALID_SERVICE_JSON,
113
+ type: "invalid_type" as const, // Invalid type
114
+ };
115
+
116
+ expect(() => {
117
+ new MindSim(undefined, { serviceJson: invalidServiceJson as MindsimServiceJson });
118
+ }).toThrow("Invalid serviceJson");
119
+ });
120
+
121
+ it("should throw with detailed validation errors", () => {
122
+ const invalidServiceJson = {
123
+ ...VALID_SERVICE_JSON,
124
+ client_id: "not-a-uuid", // Invalid UUID
125
+ };
126
+
127
+ expect(() => {
128
+ new MindSim(undefined, { serviceJson: invalidServiceJson as MindsimServiceJson });
129
+ }).toThrow("client_id");
130
+ });
131
+
132
+ it("should validate email format in serviceJson", () => {
133
+ const invalidServiceJson = {
134
+ ...VALID_SERVICE_JSON,
135
+ client_email: "invalid-email", // Missing @ and domain
136
+ };
137
+
138
+ expect(() => {
139
+ new MindSim(undefined, { serviceJson: invalidServiceJson as MindsimServiceJson });
140
+ }).toThrow("client_email");
141
+ });
142
+
143
+ it("should validate api_base_url as HTTPS", () => {
144
+ const invalidServiceJson = {
145
+ ...VALID_SERVICE_JSON,
146
+ api_base_url: "http://insecure.com", // HTTP instead of HTTPS
147
+ };
148
+
149
+ expect(() => {
150
+ new MindSim(undefined, { serviceJson: invalidServiceJson as MindsimServiceJson });
151
+ }).toThrow("api_base_url");
152
+ });
153
+
154
+ it("should validate scopes array", () => {
155
+ const invalidServiceJson = {
156
+ ...VALID_SERVICE_JSON,
157
+ scopes: ["minds:read", "invalid:scope"] as any[], // Invalid scope
158
+ };
159
+
160
+ expect(() => {
161
+ new MindSim(undefined, { serviceJson: invalidServiceJson as MindsimServiceJson });
162
+ }).toThrow("scopes");
163
+ });
164
+
165
+ it("should use api_base_url from serviceJson", () => {
166
+ const customServiceJson = {
167
+ ...VALID_SERVICE_JSON,
168
+ api_base_url: "https://custom.api.example.com/v1",
169
+ };
170
+
171
+ const mindsim = new MindSim(undefined, { serviceJson: customServiceJson });
172
+ expect(mindsim).toBeInstanceOf(MindSim);
173
+ // The client should be configured with the custom base URL
174
+ });
175
+ });
176
+
177
+ describe("serviceJson via environment (loadServiceJson)", () => {
178
+ it("should load serviceJson from environment when no constructor args", () => {
179
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(VALID_SERVICE_JSON);
180
+
181
+ const mindsim = new MindSim();
182
+ expect(mindsim).toBeInstanceOf(MindSim);
183
+ expect(mindsim.getServiceJson()).toEqual(VALID_SERVICE_JSON);
184
+ expect(mindsim.getAuthMethod()).toBe("service_json");
185
+ });
186
+
187
+ it("should use api_key from loaded serviceJson", () => {
188
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(VALID_SERVICE_JSON);
189
+
190
+ const mindsim = new MindSim();
191
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe("credential priority", () => {
196
+ it("should prioritize constructor apiKey over serviceJson option", () => {
197
+ const mindsim = new MindSim("direct-api-key", { serviceJson: VALID_SERVICE_JSON });
198
+
199
+ expect(mindsim.getAuthMethod()).toBe("api_key_constructor");
200
+ expect(mindsim.getServiceJson()).toBeNull();
201
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(false);
202
+ });
203
+
204
+ it("should prioritize constructor apiKey over environment serviceJson", () => {
205
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(VALID_SERVICE_JSON);
206
+
207
+ const mindsim = new MindSim("direct-api-key");
208
+
209
+ expect(mindsim.getAuthMethod()).toBe("api_key_constructor");
210
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(false);
211
+ });
212
+
213
+ it("should prioritize constructor serviceJson over environment serviceJson", () => {
214
+ const envServiceJson = {
215
+ ...VALID_SERVICE_JSON,
216
+ project_id: "env-app",
217
+ api_key: "ms_prod_env_key",
218
+ };
219
+
220
+ const constructorServiceJson = {
221
+ ...VALID_SERVICE_JSON,
222
+ project_id: "constructor-app",
223
+ api_key: "ms_prod_constructor_key",
224
+ };
225
+
226
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(envServiceJson);
227
+
228
+ const mindsim = new MindSim(undefined, { serviceJson: constructorServiceJson });
229
+
230
+ expect(mindsim.getServiceJson()?.project_id).toBe("constructor-app");
231
+ });
232
+
233
+ it("should fallback to loadApiKey if no serviceJson", () => {
234
+ vi.mocked(configModule.loadApiKey).mockReturnValue("env-api-key");
235
+
236
+ const mindsim = new MindSim();
237
+
238
+ expect(mindsim).toBeInstanceOf(MindSim);
239
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(false);
240
+ });
241
+ });
242
+
243
+ describe("custom apiBaseUrl option", () => {
244
+ it("should override serviceJson api_base_url when explicitly provided", () => {
245
+ const mindsim = new MindSim(undefined, {
246
+ serviceJson: VALID_SERVICE_JSON,
247
+ apiBaseUrl: "https://override.api.com/v2",
248
+ });
249
+
250
+ expect(mindsim).toBeInstanceOf(MindSim);
251
+ // The apiBaseUrl option should take precedence over serviceJson.api_base_url
252
+ });
253
+
254
+ it("should use serviceJson api_base_url when no explicit override", () => {
255
+ const mindsim = new MindSim(undefined, {
256
+ serviceJson: VALID_SERVICE_JSON,
257
+ });
258
+
259
+ expect(mindsim).toBeInstanceOf(MindSim);
260
+ // Should use VALID_SERVICE_JSON.api_base_url
261
+ });
262
+ });
263
+ });
264
+
265
+ // =============================================================================
266
+ // Auth Method Detection Tests
267
+ // =============================================================================
268
+
269
+ describe("Auth Method Detection", () => {
270
+ beforeEach(() => {
271
+ vi.resetModules();
272
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
273
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
274
+ });
275
+
276
+ afterEach(() => {
277
+ vi.unstubAllEnvs();
278
+ });
279
+
280
+ describe("getAuthMethod()", () => {
281
+ it("should return api_key_constructor when apiKey passed to constructor", () => {
282
+ const mindsim = new MindSim("test-key");
283
+ expect(mindsim.getAuthMethod()).toBe("api_key_constructor");
284
+ });
285
+
286
+ it("should return service_json when using serviceJson", () => {
287
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
288
+ expect(mindsim.getAuthMethod()).toBe("service_json");
289
+ });
290
+
291
+ it("should return api_key_env when MINDSIM_API_KEY is set", () => {
292
+ vi.stubEnv("MINDSIM_API_KEY", "env-key");
293
+ vi.mocked(configModule.loadApiKey).mockReturnValue("env-key");
294
+
295
+ const mindsim = new MindSim();
296
+ expect(mindsim.getAuthMethod()).toBe("api_key_env");
297
+ });
298
+
299
+ it("should return config_file when using config file", () => {
300
+ vi.mocked(configModule.loadApiKey).mockReturnValue("config-file-key");
301
+
302
+ const mindsim = new MindSim();
303
+ expect(mindsim.getAuthMethod()).toBe("config_file");
304
+ });
305
+ });
306
+
307
+ describe("isUsingServiceJsonAuth()", () => {
308
+ it("should return true when using serviceJson", () => {
309
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
310
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(true);
311
+ });
312
+
313
+ it("should return false when using direct apiKey", () => {
314
+ const mindsim = new MindSim("test-key");
315
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(false);
316
+ });
317
+
318
+ it("should return false when using env apiKey", () => {
319
+ vi.mocked(configModule.loadApiKey).mockReturnValue("env-key");
320
+ const mindsim = new MindSim();
321
+ expect(mindsim.isUsingServiceJsonAuth()).toBe(false);
322
+ });
323
+ });
324
+
325
+ describe("getAuthInfo()", () => {
326
+ it("should return complete auth info for serviceJson auth", () => {
327
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
328
+ const authInfo = mindsim.getAuthInfo();
329
+
330
+ expect(authInfo).toEqual({
331
+ method: "service_json",
332
+ isServiceJson: true,
333
+ projectId: VALID_SERVICE_JSON.project_id,
334
+ projectName: VALID_SERVICE_JSON.project_name,
335
+ environment: VALID_SERVICE_JSON.environment,
336
+ });
337
+ });
338
+
339
+ it("should return null project info for non-serviceJson auth", () => {
340
+ const mindsim = new MindSim("test-key");
341
+ const authInfo = mindsim.getAuthInfo();
342
+
343
+ expect(authInfo).toEqual({
344
+ method: "api_key_constructor",
345
+ isServiceJson: false,
346
+ projectId: null,
347
+ projectName: null,
348
+ environment: null,
349
+ });
350
+ });
351
+
352
+ it("should not expose the API key", () => {
353
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
354
+ const authInfo = mindsim.getAuthInfo();
355
+
356
+ // Ensure no api_key in the returned object
357
+ expect(authInfo).not.toHaveProperty("apiKey");
358
+ expect(authInfo).not.toHaveProperty("api_key");
359
+ expect(JSON.stringify(authInfo)).not.toContain(VALID_SERVICE_JSON.api_key);
360
+ });
361
+ });
362
+ });
363
+
364
+ // =============================================================================
365
+ // Service JSON Getter Tests
366
+ // =============================================================================
367
+
368
+ describe("getServiceJson()", () => {
369
+ beforeEach(() => {
370
+ vi.resetModules();
371
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
372
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
373
+ });
374
+
375
+ afterEach(() => {
376
+ vi.unstubAllEnvs();
377
+ });
378
+
379
+ it("should return the loaded serviceJson", () => {
380
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
381
+ expect(mindsim.getServiceJson()).toEqual(VALID_SERVICE_JSON);
382
+ });
383
+
384
+ it("should return null when not using serviceJson", () => {
385
+ const mindsim = new MindSim("test-key");
386
+ expect(mindsim.getServiceJson()).toBeNull();
387
+ });
388
+
389
+ it("should return the full serviceJson object with all fields", () => {
390
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
391
+ const serviceJson = mindsim.getServiceJson();
392
+
393
+ expect(serviceJson).toHaveProperty("type", "mindsim_service_account");
394
+ expect(serviceJson).toHaveProperty("version");
395
+ expect(serviceJson).toHaveProperty("project_id");
396
+ expect(serviceJson).toHaveProperty("project_name");
397
+ expect(serviceJson).toHaveProperty("environment", "production");
398
+ expect(serviceJson).toHaveProperty("client_email");
399
+ expect(serviceJson).toHaveProperty("client_id");
400
+ expect(serviceJson).toHaveProperty("api_key");
401
+ expect(serviceJson).toHaveProperty("api_base_url");
402
+ expect(serviceJson).toHaveProperty("scopes");
403
+ expect(serviceJson).toHaveProperty("app_metadata");
404
+ expect(serviceJson).toHaveProperty("created_at");
405
+ });
406
+
407
+ it("should return serviceJson with app_metadata containing all required fields", () => {
408
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
409
+ const serviceJson = mindsim.getServiceJson();
410
+
411
+ expect(serviceJson?.app_metadata).toHaveProperty("app_id");
412
+ expect(serviceJson?.app_metadata).toHaveProperty("workspace_id");
413
+ expect(serviceJson?.app_metadata).toHaveProperty("mindsim_org_id");
414
+ expect(serviceJson?.app_metadata).toHaveProperty("use_case");
415
+ });
416
+ });
417
+
418
+ // =============================================================================
419
+ // Error Handling Tests
420
+ // =============================================================================
421
+
422
+ describe("Error Handling", () => {
423
+ beforeEach(() => {
424
+ vi.resetModules();
425
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
426
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
427
+ });
428
+
429
+ afterEach(() => {
430
+ vi.unstubAllEnvs();
431
+ });
432
+
433
+ it("should throw helpful error when no credentials found", () => {
434
+ expect(() => {
435
+ new MindSim();
436
+ }).toThrow("API Key not found");
437
+ });
438
+
439
+ it("should mention serviceJson in error message", () => {
440
+ expect(() => {
441
+ new MindSim();
442
+ }).toThrow(/MINDSIM_SERVICE_JSON/);
443
+ });
444
+
445
+ it("should throw for serviceJson with missing required fields", () => {
446
+ const incompleteServiceJson = {
447
+ type: "mindsim_service_account" as const,
448
+ // Missing most required fields
449
+ };
450
+
451
+ expect(() => {
452
+ new MindSim(undefined, { serviceJson: incompleteServiceJson as MindsimServiceJson });
453
+ }).toThrow("Invalid serviceJson");
454
+ });
455
+
456
+ it("should throw for serviceJson with wrong type", () => {
457
+ const wrongTypeServiceJson = {
458
+ ...VALID_SERVICE_JSON,
459
+ type: "google_service_account" as const, // Wrong type
460
+ };
461
+
462
+ expect(() => {
463
+ new MindSim(undefined, { serviceJson: wrongTypeServiceJson as MindsimServiceJson });
464
+ }).toThrow();
465
+ });
466
+ });
467
+
468
+ // =============================================================================
469
+ // Optional Fields Tests
470
+ // =============================================================================
471
+
472
+ describe("Optional Service JSON Fields", () => {
473
+ beforeEach(() => {
474
+ vi.resetModules();
475
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
476
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
477
+ });
478
+
479
+ afterEach(() => {
480
+ vi.unstubAllEnvs();
481
+ });
482
+
483
+ it("should accept serviceJson with expires_at", () => {
484
+ const serviceJsonWithExpiry = {
485
+ ...VALID_SERVICE_JSON,
486
+ expires_at: "2026-12-21T00:00:00.000Z",
487
+ };
488
+
489
+ const mindsim = new MindSim(undefined, { serviceJson: serviceJsonWithExpiry });
490
+ expect(mindsim.getServiceJson()?.expires_at).toBe("2026-12-21T00:00:00.000Z");
491
+ });
492
+
493
+ it("should accept serviceJson with issued_at", () => {
494
+ const serviceJsonWithIssuedAt = {
495
+ ...VALID_SERVICE_JSON,
496
+ issued_at: "2025-12-21T00:00:00.000Z",
497
+ };
498
+
499
+ const mindsim = new MindSim(undefined, { serviceJson: serviceJsonWithIssuedAt });
500
+ expect(mindsim.getServiceJson()?.issued_at).toBe("2025-12-21T00:00:00.000Z");
501
+ });
502
+
503
+ it("should accept serviceJson without optional fields", () => {
504
+ // VALID_SERVICE_JSON doesn't have expires_at or issued_at
505
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
506
+ expect(mindsim.getServiceJson()?.expires_at).toBeUndefined();
507
+ expect(mindsim.getServiceJson()?.issued_at).toBeUndefined();
508
+ });
509
+ });
510
+
511
+ // =============================================================================
512
+ // Type Safety Tests
513
+ // =============================================================================
514
+
515
+ describe("Type Safety", () => {
516
+ beforeEach(() => {
517
+ vi.resetModules();
518
+ vi.mocked(configModule.loadApiKey).mockReturnValue(null);
519
+ vi.mocked(configModule.loadServiceJson).mockReturnValue(null);
520
+ });
521
+
522
+ afterEach(() => {
523
+ vi.unstubAllEnvs();
524
+ });
525
+
526
+ it("should accept all valid scopes", () => {
527
+ const allScopes: MindsimServiceJson["scopes"] = [
528
+ "minds:read",
529
+ "minds:write",
530
+ "simulate:run",
531
+ "simulate:read",
532
+ "users:read",
533
+ "users:write",
534
+ "org:admin",
535
+ ];
536
+
537
+ const serviceJsonWithAllScopes = {
538
+ ...VALID_SERVICE_JSON,
539
+ scopes: allScopes,
540
+ };
541
+
542
+ const mindsim = new MindSim(undefined, { serviceJson: serviceJsonWithAllScopes });
543
+ expect(mindsim.getServiceJson()?.scopes).toEqual(allScopes);
544
+ });
545
+
546
+ it("should accept all valid use_case values", () => {
547
+ const useCases = ["internal-workflow", "customer-facing", "agentic-ai"] as const;
548
+
549
+ for (const useCase of useCases) {
550
+ const serviceJson = {
551
+ ...VALID_SERVICE_JSON,
552
+ app_metadata: {
553
+ ...VALID_SERVICE_JSON.app_metadata,
554
+ use_case: useCase,
555
+ },
556
+ };
557
+
558
+ const mindsim = new MindSim(undefined, { serviceJson });
559
+ expect(mindsim.getServiceJson()?.app_metadata.use_case).toBe(useCase);
560
+ }
561
+ });
562
+
563
+ it("should enforce environment as production", () => {
564
+ const mindsim = new MindSim(undefined, { serviceJson: VALID_SERVICE_JSON });
565
+ expect(mindsim.getServiceJson()?.environment).toBe("production");
566
+ });
567
+ });
@@ -165,17 +165,27 @@ describe("Version Module", () => {
165
165
  });
166
166
 
167
167
  describe("updateSdk", () => {
168
+ const MOCK_CWD = "/mock/project";
169
+
168
170
  beforeEach(() => {
169
171
  // Default local version
170
172
  mockedFs.existsSync.mockReturnValue(true);
171
173
  mockedFs.readFileSync.mockReturnValue(JSON.stringify({ version: "1.0.0" }));
174
+ // Mock process.cwd for package manager detection
175
+ vi.spyOn(process, "cwd").mockReturnValue(MOCK_CWD);
176
+ // Mock path.join to return predictable paths for lockfile checks
177
+ mockedPath.join.mockImplementation((...args) => args.join("/"));
172
178
  });
173
179
 
174
- it("should run npm install if update available", async () => {
180
+ it("should use npm install when no lockfile is found", async () => {
181
+ // No lockfiles found - defaults to npm
182
+ mockedFs.existsSync.mockImplementation((p) => {
183
+ if (p === MOCK_PACKAGE_PATH) return true; // package.json exists
184
+ return false; // no lockfiles
185
+ });
175
186
  mockedAxios.get.mockResolvedValue({
176
187
  data: { "dist-tags": { latest: "2.0.0" } },
177
188
  });
178
- // Mock the execAsync resolution
179
189
  mockExecAsync.mockResolvedValue({ stdout: "ok", stderr: "" });
180
190
 
181
191
  const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -186,7 +196,49 @@ describe("Version Module", () => {
186
196
  expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Update complete"));
187
197
  });
188
198
 
199
+ it("should use pnpm add when pnpm-lock.yaml is found", async () => {
200
+ mockedFs.existsSync.mockImplementation((p) => {
201
+ if (p === MOCK_PACKAGE_PATH) return true;
202
+ if (p === `${MOCK_CWD}/pnpm-lock.yaml`) return true;
203
+ return false;
204
+ });
205
+ mockedAxios.get.mockResolvedValue({
206
+ data: { "dist-tags": { latest: "2.0.0" } },
207
+ });
208
+ mockExecAsync.mockResolvedValue({ stdout: "ok", stderr: "" });
209
+
210
+ const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
211
+
212
+ await updateSdk();
213
+
214
+ expect(mockExecAsync).toHaveBeenCalledWith("pnpm add mindsim@latest");
215
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Update complete"));
216
+ });
217
+
218
+ it("should use yarn add when yarn.lock is found", async () => {
219
+ mockedFs.existsSync.mockImplementation((p) => {
220
+ if (p === MOCK_PACKAGE_PATH) return true;
221
+ if (p === `${MOCK_CWD}/yarn.lock`) return true;
222
+ return false;
223
+ });
224
+ mockedAxios.get.mockResolvedValue({
225
+ data: { "dist-tags": { latest: "2.0.0" } },
226
+ });
227
+ mockExecAsync.mockResolvedValue({ stdout: "ok", stderr: "" });
228
+
229
+ const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
230
+
231
+ await updateSdk();
232
+
233
+ expect(mockExecAsync).toHaveBeenCalledWith("yarn add mindsim@latest");
234
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Update complete"));
235
+ });
236
+
189
237
  it("should do nothing if already on latest", async () => {
238
+ mockedFs.existsSync.mockImplementation((p) => {
239
+ if (p === MOCK_PACKAGE_PATH) return true;
240
+ return false;
241
+ });
190
242
  mockedAxios.get.mockResolvedValue({
191
243
  data: { "dist-tags": { latest: "1.0.0" } },
192
244
  });
@@ -201,11 +253,15 @@ describe("Version Module", () => {
201
253
  );
202
254
  });
203
255
 
204
- it("should handle exec errors", async () => {
256
+ it("should handle exec errors and show correct manual command", async () => {
257
+ mockedFs.existsSync.mockImplementation((p) => {
258
+ if (p === MOCK_PACKAGE_PATH) return true;
259
+ if (p === `${MOCK_CWD}/pnpm-lock.yaml`) return true;
260
+ return false;
261
+ });
205
262
  mockedAxios.get.mockResolvedValue({
206
263
  data: { "dist-tags": { latest: "2.0.0" } },
207
264
  });
208
- // Mock execAsync rejection
209
265
  mockExecAsync.mockRejectedValue(new Error("Permission denied"));
210
266
 
211
267
  const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
@@ -214,6 +270,9 @@ describe("Version Module", () => {
214
270
 
215
271
  expect(consoleErrorSpy).toHaveBeenCalledWith("❌ Failed to update MindSim SDK.");
216
272
  expect(consoleErrorSpy).toHaveBeenCalledWith("Permission denied");
273
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
274
+ "Please try running manually: pnpm add mindsim@latest",
275
+ );
217
276
  });
218
277
  });
219
278
  });