mindsim 0.1.6 → 0.1.8

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
+ });