@revenium/claude-code-metering 0.1.4 → 0.1.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 (75) hide show
  1. package/.env.example +15 -0
  2. package/.eslintrc.js +24 -0
  3. package/.github/workflows/branch-bypass-alert.yml +68 -0
  4. package/CODE_OF_CONDUCT.md +57 -0
  5. package/CONTRIBUTING.md +73 -0
  6. package/README.md +57 -3
  7. package/SECURITY.md +46 -0
  8. package/dist/cli/commands/setup.js +3 -1
  9. package/dist/cli/commands/setup.js.map +1 -1
  10. package/dist/core/api/client.d.ts.map +1 -1
  11. package/dist/core/api/client.js +4 -1
  12. package/dist/core/api/client.js.map +1 -1
  13. package/dist/core/tool-context.d.ts +6 -0
  14. package/dist/core/tool-context.d.ts.map +1 -0
  15. package/dist/core/tool-context.js +21 -0
  16. package/dist/core/tool-context.js.map +1 -0
  17. package/dist/core/tool-tracker.d.ts +4 -0
  18. package/dist/core/tool-tracker.d.ts.map +1 -0
  19. package/dist/core/tool-tracker.js +156 -0
  20. package/dist/core/tool-tracker.js.map +1 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/index.js +15 -0
  28. package/dist/types/index.js.map +1 -1
  29. package/dist/types/tool-metering.d.ts +36 -0
  30. package/dist/types/tool-metering.d.ts.map +1 -0
  31. package/dist/types/tool-metering.js +3 -0
  32. package/dist/types/tool-metering.js.map +1 -0
  33. package/docs/research/settings-json-telemetry-findings.md +171 -0
  34. package/examples/README.md +114 -0
  35. package/examples/validation/validate-installation.sh +212 -0
  36. package/package.json +4 -10
  37. package/public-allowlist-node.txt +7 -0
  38. package/src/cli/commands/backfill.ts +865 -0
  39. package/src/cli/commands/setup.ts +254 -0
  40. package/src/cli/commands/status.ts +108 -0
  41. package/src/cli/commands/test.ts +91 -0
  42. package/src/cli/index.ts +103 -0
  43. package/src/core/api/client.ts +194 -0
  44. package/src/core/config/loader.ts +217 -0
  45. package/src/core/config/validator.ts +142 -0
  46. package/src/core/config/writer.ts +212 -0
  47. package/src/core/shell/detector.ts +92 -0
  48. package/src/core/shell/profile-updater.ts +131 -0
  49. package/src/core/tool-context.ts +23 -0
  50. package/src/core/tool-tracker.ts +204 -0
  51. package/src/index.ts +12 -0
  52. package/src/types/index.ts +110 -0
  53. package/src/types/tool-metering.ts +38 -0
  54. package/src/utils/constants.ts +80 -0
  55. package/src/utils/hashing.ts +35 -0
  56. package/src/utils/masking.ts +32 -0
  57. package/tests/integration/cli-commands.test.ts +158 -0
  58. package/tests/unit/backfill-command.test.ts +366 -0
  59. package/tests/unit/backfill-helpers.test.ts +397 -0
  60. package/tests/unit/backfill-parse.test.ts +276 -0
  61. package/tests/unit/backfill-stream.test.ts +147 -0
  62. package/tests/unit/backfill.test.ts +344 -0
  63. package/tests/unit/cli-index.test.ts +193 -0
  64. package/tests/unit/client.test.ts +195 -0
  65. package/tests/unit/detector.test.ts +247 -0
  66. package/tests/unit/hashing.test.ts +121 -0
  67. package/tests/unit/loader.test.ts +272 -0
  68. package/tests/unit/masking.test.ts +46 -0
  69. package/tests/unit/profile-updater.test.ts +146 -0
  70. package/tests/unit/setup.test.ts +557 -0
  71. package/tests/unit/status.test.ts +149 -0
  72. package/tests/unit/test.test.ts +165 -0
  73. package/tests/unit/validator.test.ts +211 -0
  74. package/tests/unit/writer.test.ts +176 -0
  75. package/tsconfig.json +20 -0
@@ -0,0 +1,557 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ vi.mock("inquirer");
4
+ vi.mock("../../src/core/config/writer.js");
5
+ vi.mock("../../src/core/api/client.js");
6
+ vi.mock("../../src/core/shell/profile-updater.js");
7
+ vi.mock("../../src/core/shell/detector.js");
8
+ vi.mock("ora", () => ({
9
+ default: vi.fn(() => ({
10
+ start: vi.fn().mockReturnThis(),
11
+ succeed: vi.fn().mockReturnThis(),
12
+ fail: vi.fn().mockReturnThis(),
13
+ warn: vi.fn().mockReturnThis(),
14
+ text: "",
15
+ })),
16
+ }));
17
+
18
+ import { setupCommand } from "../../src/cli/commands/setup.js";
19
+ import inquirer from "inquirer";
20
+ import * as writer from "../../src/core/config/writer.js";
21
+ import * as client from "../../src/core/api/client.js";
22
+ import * as profileUpdater from "../../src/core/shell/profile-updater.js";
23
+ import * as detector from "../../src/core/shell/detector.js";
24
+
25
+ describe("setupCommand", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ vi.spyOn(console, "log").mockImplementation(() => {});
29
+ vi.spyOn(console, "error").mockImplementation(() => {});
30
+ vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ describe("Input Validation", () => {
38
+ it("should accept valid API key format", async () => {
39
+ vi.mocked(inquirer.prompt).mockResolvedValue({
40
+ apiKey: "hak_test_valid_key_123",
41
+ email: "test@example.com",
42
+ tier: "pro",
43
+ endpoint: "https://api.revenium.ai",
44
+ });
45
+
46
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
47
+ healthy: true,
48
+ statusCode: 200,
49
+ message: "Endpoint healthy",
50
+ latencyMs: 100,
51
+ });
52
+
53
+ vi.mocked(writer.writeConfig).mockResolvedValue(
54
+ "/home/user/.claude/revenium.env",
55
+ );
56
+
57
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
58
+ success: true,
59
+ shellType: "bash",
60
+ profilePath: "/home/user/.bashrc",
61
+ message: "Profile updated",
62
+ });
63
+
64
+ await setupCommand({});
65
+
66
+ expect(inquirer.prompt).toHaveBeenCalled();
67
+ expect(process.exit).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it("should accept valid email format", async () => {
71
+ vi.mocked(inquirer.prompt).mockResolvedValue({
72
+ apiKey: "hak_test_key",
73
+ email: "user@domain.com",
74
+ tier: "pro",
75
+ endpoint: "https://api.revenium.ai",
76
+ });
77
+
78
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
79
+ healthy: true,
80
+ statusCode: 200,
81
+ message: "OK",
82
+ latencyMs: 50,
83
+ });
84
+
85
+ vi.mocked(writer.writeConfig).mockResolvedValue(
86
+ "/home/user/.claude/revenium.env",
87
+ );
88
+
89
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
90
+ success: true,
91
+ shellType: "zsh",
92
+ message: "Updated",
93
+ });
94
+
95
+ await setupCommand({});
96
+
97
+ expect(process.exit).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("should accept valid subscription tier", async () => {
101
+ vi.mocked(inquirer.prompt).mockResolvedValue({
102
+ apiKey: "hak_test",
103
+ tier: "enterprise",
104
+ endpoint: "https://api.revenium.ai",
105
+ });
106
+
107
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
108
+ healthy: true,
109
+ statusCode: 200,
110
+ message: "OK",
111
+ latencyMs: 75,
112
+ });
113
+
114
+ vi.mocked(writer.writeConfig).mockResolvedValue(
115
+ "/home/user/.claude/revenium.env",
116
+ );
117
+
118
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
119
+ success: true,
120
+ shellType: "fish",
121
+ message: "Updated",
122
+ });
123
+
124
+ await setupCommand({});
125
+
126
+ expect(process.exit).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it("should accept HTTPS endpoint", async () => {
130
+ vi.mocked(inquirer.prompt).mockResolvedValue({
131
+ apiKey: "hak_test",
132
+ endpoint: "https://custom.revenium.ai",
133
+ });
134
+
135
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
136
+ healthy: true,
137
+ statusCode: 200,
138
+ message: "OK",
139
+ latencyMs: 60,
140
+ });
141
+
142
+ vi.mocked(writer.writeConfig).mockResolvedValue(
143
+ "/home/user/.claude/revenium.env",
144
+ );
145
+
146
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
147
+ success: true,
148
+ shellType: "bash",
149
+ message: "Updated",
150
+ });
151
+
152
+ await setupCommand({});
153
+
154
+ expect(process.exit).not.toHaveBeenCalled();
155
+ });
156
+ });
157
+
158
+ describe("Health Check", () => {
159
+ it("should exit on unhealthy endpoint", async () => {
160
+ vi.mocked(inquirer.prompt).mockResolvedValue({
161
+ apiKey: "hak_test",
162
+ endpoint: "https://api.revenium.ai",
163
+ });
164
+
165
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
166
+ healthy: false,
167
+ statusCode: 401,
168
+ message: "Unauthorized",
169
+ latencyMs: 50,
170
+ });
171
+
172
+ await setupCommand({});
173
+
174
+ expect(process.exit).toHaveBeenCalledWith(1);
175
+ });
176
+
177
+ it("should exit on network error during health check", async () => {
178
+ vi.mocked(inquirer.prompt).mockResolvedValue({
179
+ apiKey: "hak_test",
180
+ endpoint: "https://api.revenium.ai",
181
+ });
182
+
183
+ vi.mocked(client.checkEndpointHealth).mockRejectedValue(
184
+ new Error("Network error"),
185
+ );
186
+
187
+ await setupCommand({});
188
+
189
+ expect(process.exit).toHaveBeenCalledWith(1);
190
+ });
191
+
192
+ it("should succeed on healthy endpoint with low latency", async () => {
193
+ vi.mocked(inquirer.prompt).mockResolvedValue({
194
+ apiKey: "hak_test",
195
+ endpoint: "https://api.revenium.ai",
196
+ });
197
+
198
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
199
+ healthy: true,
200
+ statusCode: 200,
201
+ message: "OK",
202
+ latencyMs: 25,
203
+ });
204
+
205
+ vi.mocked(writer.writeConfig).mockResolvedValue(
206
+ "/home/user/.claude/revenium.env",
207
+ );
208
+
209
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
210
+ success: true,
211
+ shellType: "bash",
212
+ message: "Updated",
213
+ });
214
+
215
+ await setupCommand({});
216
+
217
+ expect(process.exit).not.toHaveBeenCalled();
218
+ });
219
+ });
220
+
221
+ describe("Configuration Writing", () => {
222
+ it("should write config file with correct content", async () => {
223
+ vi.mocked(writer.writeConfig).mockResolvedValue(
224
+ "/home/user/.claude/revenium.env",
225
+ );
226
+
227
+ vi.mocked(inquirer.prompt).mockResolvedValue({
228
+ apiKey: "hak_test_key",
229
+ email: "user@example.com",
230
+ tier: "pro",
231
+ endpoint: "https://api.revenium.ai",
232
+ });
233
+
234
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
235
+ healthy: true,
236
+ statusCode: 200,
237
+ message: "OK",
238
+ latencyMs: 50,
239
+ });
240
+
241
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
242
+ success: true,
243
+ shellType: "bash",
244
+ message: "Updated",
245
+ });
246
+
247
+ await setupCommand({});
248
+
249
+ expect(writer.writeConfig).toHaveBeenCalledWith({
250
+ apiKey: "hak_test_key",
251
+ email: "user@example.com",
252
+ subscriptionTier: "pro",
253
+ endpoint: "https://api.revenium.ai",
254
+ });
255
+ });
256
+
257
+ it("should exit on config write failure", async () => {
258
+ vi.mocked(inquirer.prompt).mockResolvedValue({
259
+ apiKey: "hak_test",
260
+ endpoint: "https://api.revenium.ai",
261
+ });
262
+
263
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
264
+ healthy: true,
265
+ statusCode: 200,
266
+ message: "OK",
267
+ latencyMs: 50,
268
+ });
269
+
270
+ vi.mocked(writer.writeConfig).mockRejectedValue(
271
+ new Error("Permission denied"),
272
+ );
273
+
274
+ await setupCommand({});
275
+
276
+ expect(process.exit).toHaveBeenCalledWith(1);
277
+ });
278
+ });
279
+
280
+ describe("Shell Profile Update", () => {
281
+ it("should update shell profile on success", async () => {
282
+ const updateShellSpy = vi
283
+ .spyOn(profileUpdater, "updateShellProfile")
284
+ .mockResolvedValue({
285
+ success: true,
286
+ shellType: "bash",
287
+ profilePath: "/home/user/.bashrc",
288
+ message: "Added configuration to /home/user/.bashrc",
289
+ });
290
+
291
+ vi.mocked(inquirer.prompt).mockResolvedValue({
292
+ apiKey: "hak_test",
293
+ endpoint: "https://api.revenium.ai",
294
+ });
295
+
296
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
297
+ healthy: true,
298
+ statusCode: 200,
299
+ message: "OK",
300
+ latencyMs: 50,
301
+ });
302
+
303
+ vi.mocked(writer.writeConfig).mockResolvedValue(
304
+ "/home/user/.claude/revenium.env",
305
+ );
306
+
307
+ await setupCommand({});
308
+
309
+ expect(updateShellSpy).toHaveBeenCalled();
310
+ });
311
+
312
+ it("should show manual instructions on shell update failure", async () => {
313
+ vi.mocked(inquirer.prompt).mockResolvedValue({
314
+ apiKey: "hak_test",
315
+ endpoint: "https://api.revenium.ai",
316
+ });
317
+
318
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
319
+ healthy: true,
320
+ statusCode: 200,
321
+ message: "OK",
322
+ latencyMs: 50,
323
+ });
324
+
325
+ vi.mocked(writer.writeConfig).mockResolvedValue(
326
+ "/home/user/.claude/revenium.env",
327
+ );
328
+
329
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
330
+ success: false,
331
+ shellType: "unknown",
332
+ message: "Could not detect shell",
333
+ });
334
+
335
+ vi.mocked(detector.detectShell).mockReturnValue("unknown");
336
+
337
+ const getManualSpy = vi
338
+ .spyOn(profileUpdater, "getManualInstructions")
339
+ .mockReturnValue("Manual instructions here");
340
+
341
+ await setupCommand({});
342
+
343
+ expect(getManualSpy).toHaveBeenCalledWith("unknown");
344
+ });
345
+
346
+ it("should skip shell update when skipShellUpdate is true", async () => {
347
+ vi.mocked(inquirer.prompt).mockResolvedValue({
348
+ apiKey: "hak_test",
349
+ endpoint: "https://api.revenium.ai",
350
+ });
351
+
352
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
353
+ healthy: true,
354
+ statusCode: 200,
355
+ message: "OK",
356
+ latencyMs: 50,
357
+ });
358
+
359
+ vi.mocked(writer.writeConfig).mockResolvedValue(
360
+ "/home/user/.claude/revenium.env",
361
+ );
362
+
363
+ await setupCommand({ skipShellUpdate: true });
364
+
365
+ expect(profileUpdater.updateShellProfile).not.toHaveBeenCalled();
366
+ });
367
+ });
368
+
369
+ describe("CLI Options", () => {
370
+ it("should use provided API key from options", async () => {
371
+ vi.mocked(inquirer.prompt).mockResolvedValue({
372
+ endpoint: "https://api.revenium.ai",
373
+ });
374
+
375
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
376
+ healthy: true,
377
+ statusCode: 200,
378
+ message: "OK",
379
+ latencyMs: 50,
380
+ });
381
+
382
+ vi.mocked(writer.writeConfig).mockResolvedValue(
383
+ "/home/user/.claude/revenium.env",
384
+ );
385
+
386
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
387
+ success: true,
388
+ shellType: "bash",
389
+ message: "Updated",
390
+ });
391
+
392
+ await setupCommand({ apiKey: "hak_from_cli" });
393
+
394
+ expect(writer.writeConfig).toHaveBeenCalledWith(
395
+ expect.objectContaining({
396
+ apiKey: "hak_from_cli",
397
+ }),
398
+ );
399
+ });
400
+
401
+ it("should use provided email from options", async () => {
402
+ vi.mocked(inquirer.prompt).mockResolvedValue({
403
+ apiKey: "hak_test",
404
+ endpoint: "https://api.revenium.ai",
405
+ });
406
+
407
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
408
+ healthy: true,
409
+ statusCode: 200,
410
+ message: "OK",
411
+ latencyMs: 50,
412
+ });
413
+
414
+ vi.mocked(writer.writeConfig).mockResolvedValue(
415
+ "/home/user/.claude/revenium.env",
416
+ );
417
+
418
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
419
+ success: true,
420
+ shellType: "bash",
421
+ message: "Updated",
422
+ });
423
+
424
+ await setupCommand({ email: "cli@example.com" });
425
+
426
+ expect(writer.writeConfig).toHaveBeenCalledWith(
427
+ expect.objectContaining({
428
+ email: "cli@example.com",
429
+ }),
430
+ );
431
+ });
432
+
433
+ it("should use provided tier from options", async () => {
434
+ vi.mocked(inquirer.prompt).mockResolvedValue({
435
+ apiKey: "hak_test",
436
+ endpoint: "https://api.revenium.ai",
437
+ });
438
+
439
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
440
+ healthy: true,
441
+ statusCode: 200,
442
+ message: "OK",
443
+ latencyMs: 50,
444
+ });
445
+
446
+ vi.mocked(writer.writeConfig).mockResolvedValue(
447
+ "/home/user/.claude/revenium.env",
448
+ );
449
+
450
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
451
+ success: true,
452
+ shellType: "bash",
453
+ message: "Updated",
454
+ });
455
+
456
+ await setupCommand({ tier: "enterprise" });
457
+
458
+ expect(writer.writeConfig).toHaveBeenCalledWith(
459
+ expect.objectContaining({
460
+ subscriptionTier: "enterprise",
461
+ }),
462
+ );
463
+ });
464
+
465
+ it("should use provided endpoint from options", async () => {
466
+ vi.mocked(inquirer.prompt).mockResolvedValue({
467
+ apiKey: "hak_test",
468
+ });
469
+
470
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
471
+ healthy: true,
472
+ statusCode: 200,
473
+ message: "OK",
474
+ latencyMs: 50,
475
+ });
476
+
477
+ vi.mocked(writer.writeConfig).mockResolvedValue(
478
+ "/home/user/.claude/revenium.env",
479
+ );
480
+
481
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
482
+ success: true,
483
+ shellType: "bash",
484
+ message: "Updated",
485
+ });
486
+
487
+ await setupCommand({ endpoint: "https://custom.revenium.ai" });
488
+
489
+ expect(writer.writeConfig).toHaveBeenCalledWith(
490
+ expect.objectContaining({
491
+ endpoint: "https://custom.revenium.ai",
492
+ }),
493
+ );
494
+ });
495
+
496
+ it("should handle shell profile update failure", async () => {
497
+ vi.mocked(inquirer.prompt).mockResolvedValue({
498
+ apiKey: "hak_test123",
499
+ email: "test@example.com",
500
+ tier: "pro",
501
+ endpoint: "https://api.revenium.ai",
502
+ });
503
+
504
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
505
+ healthy: true,
506
+ statusCode: 200,
507
+ message: "Endpoint healthy",
508
+ latencyMs: 100,
509
+ });
510
+
511
+ vi.mocked(writer.writeConfig).mockResolvedValue();
512
+
513
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
514
+ success: false,
515
+ message: "Failed to update shell profile",
516
+ });
517
+
518
+ vi.mocked(detector.detectShell).mockReturnValue("bash");
519
+
520
+ await setupCommand({});
521
+
522
+ expect(profileUpdater.updateShellProfile).toHaveBeenCalled();
523
+ expect(detector.detectShell).toHaveBeenCalled();
524
+ });
525
+
526
+ it("should handle endpoint with /meter path", async () => {
527
+ vi.mocked(inquirer.prompt).mockResolvedValue({
528
+ apiKey: "hak_test123",
529
+ email: "test@example.com",
530
+ tier: "pro",
531
+ endpoint: "https://api.revenium.ai/meter/v1",
532
+ });
533
+
534
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
535
+ healthy: true,
536
+ statusCode: 200,
537
+ message: "Endpoint healthy",
538
+ latencyMs: 100,
539
+ });
540
+
541
+ vi.mocked(writer.writeConfig).mockResolvedValue();
542
+
543
+ vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
544
+ success: true,
545
+ message: "Shell profile updated",
546
+ });
547
+
548
+ await setupCommand({});
549
+
550
+ expect(writer.writeConfig).toHaveBeenCalledWith(
551
+ expect.objectContaining({
552
+ endpoint: "https://api.revenium.ai",
553
+ }),
554
+ );
555
+ });
556
+ });
557
+ });
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { statusCommand } from "../../src/cli/commands/status.js";
3
+ import * as loader from "../../src/core/config/loader.js";
4
+ import * as client from "../../src/core/api/client.js";
5
+ import * as detector from "../../src/core/shell/detector.js";
6
+
7
+ vi.mock("../../src/core/config/loader.js");
8
+ vi.mock("../../src/core/api/client.js");
9
+ vi.mock("../../src/core/shell/detector.js");
10
+ vi.mock("chalk", () => ({
11
+ default: {
12
+ bold: (str: string) => str,
13
+ green: (str: string) => str,
14
+ red: (str: string) => str,
15
+ yellow: (str: string) => str,
16
+ dim: (str: string) => str,
17
+ },
18
+ }));
19
+ vi.mock("ora", () => ({
20
+ default: () => ({
21
+ start: () => ({
22
+ succeed: vi.fn(),
23
+ fail: vi.fn(),
24
+ }),
25
+ }),
26
+ }));
27
+
28
+ describe("statusCommand", () => {
29
+ const mockExit = vi
30
+ .spyOn(process, "exit")
31
+ .mockImplementation((code?: number) => {
32
+ throw new Error(`process.exit unexpectedly called with "${code}"`);
33
+ });
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ it("should exit with error when config file does not exist", async () => {
44
+ vi.mocked(loader.configExists).mockReturnValue(false);
45
+ vi.mocked(loader.getConfigPath).mockReturnValue(
46
+ "/home/user/.claude/revenium.env",
47
+ );
48
+
49
+ await expect(statusCommand()).rejects.toThrow(
50
+ 'process.exit unexpectedly called with "1"',
51
+ );
52
+ });
53
+
54
+ it("should exit with error when config cannot be parsed", async () => {
55
+ vi.mocked(loader.configExists).mockReturnValue(true);
56
+ vi.mocked(loader.getConfigPath).mockReturnValue(
57
+ "/home/user/.claude/revenium.env",
58
+ );
59
+ vi.mocked(loader.loadConfig).mockResolvedValue(null);
60
+
61
+ await expect(statusCommand()).rejects.toThrow(
62
+ 'process.exit unexpectedly called with "1"',
63
+ );
64
+ });
65
+
66
+ it("should display config when file exists and is valid", async () => {
67
+ vi.mocked(loader.configExists).mockReturnValue(true);
68
+ vi.mocked(loader.getConfigPath).mockReturnValue(
69
+ "/home/user/.claude/revenium.env",
70
+ );
71
+ vi.mocked(loader.loadConfig).mockResolvedValue({
72
+ apiKey: "hak_test123",
73
+ endpoint: "https://api.revenium.ai",
74
+ email: "test@example.com",
75
+ subscriptionTier: "pro",
76
+ });
77
+ vi.mocked(loader.isEnvLoaded).mockReturnValue(true);
78
+ vi.mocked(detector.detectShell).mockReturnValue("bash");
79
+ vi.mocked(detector.getProfilePath).mockReturnValue("/home/user/.bashrc");
80
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
81
+ healthy: true,
82
+ statusCode: 200,
83
+ message: "OK",
84
+ latencyMs: 100,
85
+ });
86
+
87
+ await statusCommand();
88
+
89
+ expect(loader.loadConfig).toHaveBeenCalled();
90
+ expect(client.checkEndpointHealth).toHaveBeenCalled();
91
+ });
92
+
93
+ it("should call checkEndpointHealth with correct parameters", async () => {
94
+ vi.mocked(loader.configExists).mockReturnValue(true);
95
+ vi.mocked(loader.getConfigPath).mockReturnValue(
96
+ "/home/user/.claude/revenium.env",
97
+ );
98
+ vi.mocked(loader.loadConfig).mockResolvedValue({
99
+ apiKey: "hak_test123",
100
+ endpoint: "https://api.revenium.ai",
101
+ organizationId: "org-123",
102
+ productId: "prod-456",
103
+ });
104
+ vi.mocked(loader.isEnvLoaded).mockReturnValue(true);
105
+ vi.mocked(detector.detectShell).mockReturnValue("bash");
106
+ vi.mocked(detector.getProfilePath).mockReturnValue("/home/user/.bashrc");
107
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
108
+ healthy: true,
109
+ statusCode: 200,
110
+ message: "OK",
111
+ latencyMs: 100,
112
+ });
113
+
114
+ await statusCommand();
115
+
116
+ expect(client.checkEndpointHealth).toHaveBeenCalledWith(
117
+ "https://api.revenium.ai",
118
+ "hak_test123",
119
+ expect.objectContaining({
120
+ organizationName: "org-123",
121
+ productName: "prod-456",
122
+ }),
123
+ );
124
+ });
125
+
126
+ it("should handle unhealthy endpoint", async () => {
127
+ vi.mocked(loader.configExists).mockReturnValue(true);
128
+ vi.mocked(loader.getConfigPath).mockReturnValue(
129
+ "/home/user/.claude/revenium.env",
130
+ );
131
+ vi.mocked(loader.loadConfig).mockResolvedValue({
132
+ apiKey: "hak_test123",
133
+ endpoint: "https://api.revenium.ai",
134
+ });
135
+ vi.mocked(loader.isEnvLoaded).mockReturnValue(false);
136
+ vi.mocked(detector.detectShell).mockReturnValue("zsh");
137
+ vi.mocked(detector.getProfilePath).mockReturnValue("/home/user/.zshrc");
138
+ vi.mocked(client.checkEndpointHealth).mockResolvedValue({
139
+ healthy: false,
140
+ statusCode: 500,
141
+ message: "Internal Server Error",
142
+ latencyMs: 200,
143
+ });
144
+
145
+ await statusCommand();
146
+
147
+ expect(client.checkEndpointHealth).toHaveBeenCalled();
148
+ });
149
+ });