@dotta/xc 0.1.0

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 (143) hide show
  1. package/README.md +348 -0
  2. package/dist/__tests__/bookmarks.test.d.ts +4 -0
  3. package/dist/__tests__/bookmarks.test.js +104 -0
  4. package/dist/__tests__/bookmarks.test.js.map +1 -0
  5. package/dist/__tests__/budget.test.d.ts +6 -0
  6. package/dist/__tests__/budget.test.js +105 -0
  7. package/dist/__tests__/budget.test.js.map +1 -0
  8. package/dist/__tests__/dm.test.d.ts +4 -0
  9. package/dist/__tests__/dm.test.js +115 -0
  10. package/dist/__tests__/dm.test.js.map +1 -0
  11. package/dist/__tests__/followers.test.d.ts +4 -0
  12. package/dist/__tests__/followers.test.js +129 -0
  13. package/dist/__tests__/followers.test.js.map +1 -0
  14. package/dist/__tests__/lib/api.test.d.ts +5 -0
  15. package/dist/__tests__/lib/api.test.js +202 -0
  16. package/dist/__tests__/lib/api.test.js.map +1 -0
  17. package/dist/__tests__/lib/budget.test.d.ts +5 -0
  18. package/dist/__tests__/lib/budget.test.js +194 -0
  19. package/dist/__tests__/lib/budget.test.js.map +1 -0
  20. package/dist/__tests__/lib/config.test.d.ts +6 -0
  21. package/dist/__tests__/lib/config.test.js +228 -0
  22. package/dist/__tests__/lib/config.test.js.map +1 -0
  23. package/dist/__tests__/lib/cost.test.d.ts +6 -0
  24. package/dist/__tests__/lib/cost.test.js +177 -0
  25. package/dist/__tests__/lib/cost.test.js.map +1 -0
  26. package/dist/__tests__/lib/format.test.d.ts +4 -0
  27. package/dist/__tests__/lib/format.test.js +139 -0
  28. package/dist/__tests__/lib/format.test.js.map +1 -0
  29. package/dist/__tests__/lib/oauth.test.d.ts +5 -0
  30. package/dist/__tests__/lib/oauth.test.js +123 -0
  31. package/dist/__tests__/lib/oauth.test.js.map +1 -0
  32. package/dist/__tests__/lib/resolve.test.d.ts +4 -0
  33. package/dist/__tests__/lib/resolve.test.js +154 -0
  34. package/dist/__tests__/lib/resolve.test.js.map +1 -0
  35. package/dist/__tests__/lists.test.d.ts +4 -0
  36. package/dist/__tests__/lists.test.js +96 -0
  37. package/dist/__tests__/lists.test.js.map +1 -0
  38. package/dist/__tests__/media.test.d.ts +4 -0
  39. package/dist/__tests__/media.test.js +132 -0
  40. package/dist/__tests__/media.test.js.map +1 -0
  41. package/dist/cli.d.ts +2 -0
  42. package/dist/cli.js +93 -0
  43. package/dist/cli.js.map +1 -0
  44. package/dist/commands/auth.d.ts +2 -0
  45. package/dist/commands/auth.js +191 -0
  46. package/dist/commands/auth.js.map +1 -0
  47. package/dist/commands/block.d.ts +15 -0
  48. package/dist/commands/block.js +117 -0
  49. package/dist/commands/block.js.map +1 -0
  50. package/dist/commands/bookmarks.d.ts +12 -0
  51. package/dist/commands/bookmarks.js +100 -0
  52. package/dist/commands/bookmarks.js.map +1 -0
  53. package/dist/commands/budget.d.ts +9 -0
  54. package/dist/commands/budget.js +124 -0
  55. package/dist/commands/budget.js.map +1 -0
  56. package/dist/commands/cost.d.ts +5 -0
  57. package/dist/commands/cost.js +75 -0
  58. package/dist/commands/cost.js.map +1 -0
  59. package/dist/commands/delete.d.ts +8 -0
  60. package/dist/commands/delete.js +31 -0
  61. package/dist/commands/delete.js.map +1 -0
  62. package/dist/commands/dm.d.ts +10 -0
  63. package/dist/commands/dm.js +179 -0
  64. package/dist/commands/dm.js.map +1 -0
  65. package/dist/commands/engagement.d.ts +14 -0
  66. package/dist/commands/engagement.js +167 -0
  67. package/dist/commands/engagement.js.map +1 -0
  68. package/dist/commands/followers.d.ts +14 -0
  69. package/dist/commands/followers.js +138 -0
  70. package/dist/commands/followers.js.map +1 -0
  71. package/dist/commands/get.d.ts +2 -0
  72. package/dist/commands/get.js +63 -0
  73. package/dist/commands/get.js.map +1 -0
  74. package/dist/commands/hide.d.ts +10 -0
  75. package/dist/commands/hide.js +58 -0
  76. package/dist/commands/hide.js.map +1 -0
  77. package/dist/commands/like.d.ts +3 -0
  78. package/dist/commands/like.js +52 -0
  79. package/dist/commands/like.js.map +1 -0
  80. package/dist/commands/lists.d.ts +20 -0
  81. package/dist/commands/lists.js +384 -0
  82. package/dist/commands/lists.js.map +1 -0
  83. package/dist/commands/media.d.ts +19 -0
  84. package/dist/commands/media.js +205 -0
  85. package/dist/commands/media.js.map +1 -0
  86. package/dist/commands/mentions.d.ts +8 -0
  87. package/dist/commands/mentions.js +59 -0
  88. package/dist/commands/mentions.js.map +1 -0
  89. package/dist/commands/mute.d.ts +12 -0
  90. package/dist/commands/mute.js +99 -0
  91. package/dist/commands/mute.js.map +1 -0
  92. package/dist/commands/post.d.ts +11 -0
  93. package/dist/commands/post.js +87 -0
  94. package/dist/commands/post.js.map +1 -0
  95. package/dist/commands/repost.d.ts +10 -0
  96. package/dist/commands/repost.js +59 -0
  97. package/dist/commands/repost.js.map +1 -0
  98. package/dist/commands/search.d.ts +2 -0
  99. package/dist/commands/search.js +49 -0
  100. package/dist/commands/search.js.map +1 -0
  101. package/dist/commands/stream.d.ts +13 -0
  102. package/dist/commands/stream.js +251 -0
  103. package/dist/commands/stream.js.map +1 -0
  104. package/dist/commands/timeline.d.ts +2 -0
  105. package/dist/commands/timeline.js +61 -0
  106. package/dist/commands/timeline.js.map +1 -0
  107. package/dist/commands/trends.d.ts +10 -0
  108. package/dist/commands/trends.js +59 -0
  109. package/dist/commands/trends.js.map +1 -0
  110. package/dist/commands/usage.d.ts +2 -0
  111. package/dist/commands/usage.js +52 -0
  112. package/dist/commands/usage.js.map +1 -0
  113. package/dist/commands/user.d.ts +2 -0
  114. package/dist/commands/user.js +43 -0
  115. package/dist/commands/user.js.map +1 -0
  116. package/dist/commands/usersearch.d.ts +8 -0
  117. package/dist/commands/usersearch.js +48 -0
  118. package/dist/commands/usersearch.js.map +1 -0
  119. package/dist/commands/whoami.d.ts +2 -0
  120. package/dist/commands/whoami.js +54 -0
  121. package/dist/commands/whoami.js.map +1 -0
  122. package/dist/lib/api.d.ts +12 -0
  123. package/dist/lib/api.js +91 -0
  124. package/dist/lib/api.js.map +1 -0
  125. package/dist/lib/budget.d.ts +44 -0
  126. package/dist/lib/budget.js +119 -0
  127. package/dist/lib/budget.js.map +1 -0
  128. package/dist/lib/config.d.ts +39 -0
  129. package/dist/lib/config.js +63 -0
  130. package/dist/lib/config.js.map +1 -0
  131. package/dist/lib/cost.d.ts +43 -0
  132. package/dist/lib/cost.js +224 -0
  133. package/dist/lib/cost.js.map +1 -0
  134. package/dist/lib/format.d.ts +24 -0
  135. package/dist/lib/format.js +72 -0
  136. package/dist/lib/format.js.map +1 -0
  137. package/dist/lib/oauth.d.ts +32 -0
  138. package/dist/lib/oauth.js +132 -0
  139. package/dist/lib/oauth.js.map +1 -0
  140. package/dist/lib/resolve.d.ts +12 -0
  141. package/dist/lib/resolve.js +48 -0
  142. package/dist/lib/resolve.js.map +1 -0
  143. package/package.json +46 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tests for budget enforcement: load/save, password locking, and checkBudget.
3
+ * Uses vi.resetModules() + dynamic import for filesystem isolation.
4
+ */
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import os from "node:os";
9
+ let tmpDir;
10
+ let origConfigDir;
11
+ let budget;
12
+ beforeEach(async () => {
13
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xc-budget-lib-test-"));
14
+ origConfigDir = process.env.XC_CONFIG_DIR;
15
+ process.env.XC_CONFIG_DIR = tmpDir;
16
+ vi.resetModules();
17
+ budget = await import("../../lib/budget.js");
18
+ });
19
+ afterEach(() => {
20
+ if (origConfigDir !== undefined) {
21
+ process.env.XC_CONFIG_DIR = origConfigDir;
22
+ }
23
+ else {
24
+ delete process.env.XC_CONFIG_DIR;
25
+ }
26
+ fs.rmSync(tmpDir, { recursive: true, force: true });
27
+ });
28
+ /** Write a budget.json directly into the temp config dir. */
29
+ function writeBudgetFile(config) {
30
+ fs.writeFileSync(path.join(tmpDir, "budget.json"), JSON.stringify(config, null, 2) + "\n");
31
+ }
32
+ /** Write usage entries into usage.jsonl in the temp config dir. */
33
+ function writeUsageLog(entries) {
34
+ const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
35
+ fs.writeFileSync(path.join(tmpDir, "usage.jsonl"), lines);
36
+ }
37
+ describe("budget basics", () => {
38
+ it("returns defaults when no budget file exists", () => {
39
+ const b = budget.loadBudget();
40
+ expect(b.action).toBe("warn");
41
+ expect(b.daily).toBeUndefined();
42
+ });
43
+ it("saves and loads budget config", () => {
44
+ budget.saveBudget({ daily: 5.0, action: "block" });
45
+ const b = budget.loadBudget();
46
+ expect(b.daily).toBe(5.0);
47
+ expect(b.action).toBe("block");
48
+ });
49
+ it("resets budget by removing file", () => {
50
+ budget.saveBudget({ daily: 5.0, action: "warn" });
51
+ expect(fs.existsSync(budget.getBudgetPath())).toBe(true);
52
+ budget.resetBudget();
53
+ expect(fs.existsSync(budget.getBudgetPath())).toBe(false);
54
+ });
55
+ it("reset is safe when no file exists", () => {
56
+ expect(() => budget.resetBudget()).not.toThrow();
57
+ });
58
+ it("getBudgetPath returns path inside config dir", () => {
59
+ expect(budget.getBudgetPath()).toBe(path.join(tmpDir, "budget.json"));
60
+ });
61
+ });
62
+ describe("password hashing", () => {
63
+ it("produces consistent hashes with same salt", () => {
64
+ const salt = "abcdef1234567890";
65
+ const h1 = budget.hashPassword("mypassword", salt);
66
+ const h2 = budget.hashPassword("mypassword", salt);
67
+ expect(h1).toBe(h2);
68
+ });
69
+ it("produces different hashes with different salts", () => {
70
+ const h1 = budget.hashPassword("mypassword", "salt1");
71
+ const h2 = budget.hashPassword("mypassword", "salt2");
72
+ expect(h1).not.toBe(h2);
73
+ });
74
+ it("produces different hashes for different passwords", () => {
75
+ const salt = "samesalt";
76
+ const h1 = budget.hashPassword("password1", salt);
77
+ const h2 = budget.hashPassword("password2", salt);
78
+ expect(h1).not.toBe(h2);
79
+ });
80
+ it("returns a 128-char hex string (64-byte scrypt key)", () => {
81
+ const hash = budget.hashPassword("test", "salt");
82
+ expect(hash).toMatch(/^[0-9a-f]+$/);
83
+ expect(hash).toHaveLength(128);
84
+ });
85
+ });
86
+ describe("budget locking", () => {
87
+ it("reports unlocked when no password set", () => {
88
+ budget.saveBudget({ daily: 5.0, action: "warn" });
89
+ expect(budget.isLocked()).toBe(false);
90
+ });
91
+ it("locks budget with password", () => {
92
+ budget.saveBudget({ daily: 5.0, action: "warn" });
93
+ budget.lockBudget("secret123");
94
+ expect(budget.isLocked()).toBe(true);
95
+ });
96
+ it("verifies correct password", () => {
97
+ budget.saveBudget({ daily: 5.0, action: "warn" });
98
+ budget.lockBudget("secret123");
99
+ expect(budget.verifyPassword("secret123")).toBe(true);
100
+ });
101
+ it("rejects incorrect password", () => {
102
+ budget.saveBudget({ daily: 5.0, action: "warn" });
103
+ budget.lockBudget("secret123");
104
+ expect(budget.verifyPassword("wrong")).toBe(false);
105
+ });
106
+ it("unlocks budget and removes password fields", () => {
107
+ budget.saveBudget({ daily: 5.0, action: "warn" });
108
+ budget.lockBudget("secret123");
109
+ expect(budget.isLocked()).toBe(true);
110
+ budget.unlockBudget();
111
+ expect(budget.isLocked()).toBe(false);
112
+ expect(budget.verifyPassword("anything")).toBe(true);
113
+ });
114
+ it("preserves budget config when locking", () => {
115
+ budget.saveBudget({ daily: 10.0, action: "block" });
116
+ budget.lockBudget("mypass");
117
+ const b = budget.loadBudget();
118
+ expect(b.daily).toBe(10.0);
119
+ expect(b.action).toBe("block");
120
+ expect(b.passwordHash).toBeDefined();
121
+ expect(b.passwordSalt).toBeDefined();
122
+ });
123
+ it("verifyPassword returns true when no lock exists", () => {
124
+ budget.saveBudget({ daily: 5.0, action: "warn" });
125
+ expect(budget.verifyPassword("anything")).toBe(true);
126
+ });
127
+ it("isLocked returns false when budget file does not exist", () => {
128
+ expect(budget.isLocked()).toBe(false);
129
+ });
130
+ });
131
+ describe("checkBudget", () => {
132
+ it("passes when no daily limit is set", async () => {
133
+ writeBudgetFile({ action: "warn" });
134
+ await expect(budget.checkBudget("posts.create")).resolves.toBeUndefined();
135
+ });
136
+ it("passes when spend is under the limit", async () => {
137
+ writeBudgetFile({ daily: 1.0, action: "block" });
138
+ // No usage entries — well under budget
139
+ await expect(budget.checkBudget("users.getMe")).resolves.toBeUndefined();
140
+ });
141
+ it("throws when action is 'block' and over budget", async () => {
142
+ writeBudgetFile({ daily: 0.001, action: "block" });
143
+ writeUsageLog([
144
+ {
145
+ timestamp: new Date().toISOString(),
146
+ endpoint: "posts.create",
147
+ method: "POST",
148
+ estimatedCost: 0.01,
149
+ },
150
+ ]);
151
+ await expect(budget.checkBudget("posts.create")).rejects.toThrow(/budget.*exceeded/i);
152
+ });
153
+ it("warns to stderr when action is 'warn' and over budget", async () => {
154
+ writeBudgetFile({ daily: 0.001, action: "warn" });
155
+ writeUsageLog([
156
+ {
157
+ timestamp: new Date().toISOString(),
158
+ endpoint: "posts.create",
159
+ method: "POST",
160
+ estimatedCost: 0.01,
161
+ },
162
+ ]);
163
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
164
+ await budget.checkBudget("posts.create");
165
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Warning:"));
166
+ errorSpy.mockRestore();
167
+ });
168
+ it("includes spend details in block error message", async () => {
169
+ writeBudgetFile({ daily: 0.005, action: "block" });
170
+ writeUsageLog([
171
+ {
172
+ timestamp: new Date().toISOString(),
173
+ endpoint: "users.getMe",
174
+ method: "GET",
175
+ estimatedCost: 0.01,
176
+ },
177
+ ]);
178
+ await expect(budget.checkBudget("users.getMe")).rejects.toThrow(/\$0\.01/);
179
+ });
180
+ it("passes when spend exactly equals the limit", async () => {
181
+ // daily = 0.015, today spend = 0.01, call cost = 0.005 → total = 0.015 ≤ 0.015
182
+ writeBudgetFile({ daily: 0.015, action: "block" });
183
+ writeUsageLog([
184
+ {
185
+ timestamp: new Date().toISOString(),
186
+ endpoint: "posts.create",
187
+ method: "POST",
188
+ estimatedCost: 0.01,
189
+ },
190
+ ]);
191
+ await expect(budget.checkBudget("users.getMe")).resolves.toBeUndefined();
192
+ });
193
+ });
194
+ //# sourceMappingURL=budget.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.test.js","sourceRoot":"","sources":["../../../src/__tests__/lib/budget.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tests for config management: load/save, accounts, migration.
3
+ * Uses vi.resetModules() + dynamic import so the module-level
4
+ * CONFIG_DIR constant picks up process.env.XC_CONFIG_DIR from each test.
5
+ */
6
+ export {};
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Tests for config management: load/save, accounts, migration.
3
+ * Uses vi.resetModules() + dynamic import so the module-level
4
+ * CONFIG_DIR constant picks up process.env.XC_CONFIG_DIR from each test.
5
+ */
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ let tmpDir;
11
+ let legacyDir;
12
+ let origConfigDir;
13
+ let origXdgHome;
14
+ let config;
15
+ beforeEach(async () => {
16
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xc-config-test-"));
17
+ legacyDir = fs.mkdtempSync(path.join(os.tmpdir(), "xc-legacy-test-"));
18
+ origConfigDir = process.env.XC_CONFIG_DIR;
19
+ origXdgHome = process.env.XDG_CONFIG_HOME;
20
+ process.env.XC_CONFIG_DIR = tmpDir;
21
+ process.env.XDG_CONFIG_HOME = legacyDir;
22
+ // Re-import so module-level constants re-evaluate with new env vars
23
+ vi.resetModules();
24
+ config = await import("../../lib/config.js");
25
+ });
26
+ afterEach(() => {
27
+ if (origConfigDir !== undefined) {
28
+ process.env.XC_CONFIG_DIR = origConfigDir;
29
+ }
30
+ else {
31
+ delete process.env.XC_CONFIG_DIR;
32
+ }
33
+ if (origXdgHome !== undefined) {
34
+ process.env.XDG_CONFIG_HOME = origXdgHome;
35
+ }
36
+ else {
37
+ delete process.env.XDG_CONFIG_HOME;
38
+ }
39
+ fs.rmSync(tmpDir, { recursive: true, force: true });
40
+ fs.rmSync(legacyDir, { recursive: true, force: true });
41
+ });
42
+ describe("getConfigDir / getConfigPath", () => {
43
+ it("returns the XC_CONFIG_DIR path", () => {
44
+ expect(config.getConfigDir()).toBe(tmpDir);
45
+ });
46
+ it("config path is config.json inside config dir", () => {
47
+ expect(config.getConfigPath()).toBe(path.join(tmpDir, "config.json"));
48
+ });
49
+ });
50
+ describe("ensureConfigDir", () => {
51
+ it("creates the config directory", () => {
52
+ // tmpDir already exists from mkdtempSync, remove it to test creation
53
+ fs.rmSync(tmpDir, { recursive: true, force: true });
54
+ expect(fs.existsSync(tmpDir)).toBe(false);
55
+ config.ensureConfigDir();
56
+ expect(fs.existsSync(tmpDir)).toBe(true);
57
+ });
58
+ it("is idempotent when directory already exists", () => {
59
+ config.ensureConfigDir();
60
+ config.ensureConfigDir();
61
+ expect(fs.existsSync(config.getConfigDir())).toBe(true);
62
+ });
63
+ });
64
+ describe("loadConfig / saveConfig", () => {
65
+ it("returns defaults when no config file exists", () => {
66
+ const cfg = config.loadConfig();
67
+ expect(cfg.defaultAccount).toBe("default");
68
+ expect(cfg.accounts).toEqual({});
69
+ });
70
+ it("saves and loads config roundtrip", () => {
71
+ const testConfig = {
72
+ defaultAccount: "myaccount",
73
+ accounts: {
74
+ myaccount: {
75
+ name: "myaccount",
76
+ auth: { type: "bearer", bearerToken: "tok123" },
77
+ },
78
+ },
79
+ };
80
+ config.saveConfig(testConfig);
81
+ const loaded = config.loadConfig();
82
+ expect(loaded.defaultAccount).toBe("myaccount");
83
+ expect(loaded.accounts.myaccount.auth.bearerToken).toBe("tok123");
84
+ });
85
+ it("writes valid JSON with trailing newline", () => {
86
+ config.saveConfig({ defaultAccount: "test", accounts: {} });
87
+ const raw = fs.readFileSync(config.getConfigPath(), "utf-8");
88
+ expect(raw.endsWith("\n")).toBe(true);
89
+ expect(() => JSON.parse(raw)).not.toThrow();
90
+ });
91
+ it("creates config dir when saving if it doesn't exist", () => {
92
+ fs.rmSync(tmpDir, { recursive: true, force: true });
93
+ config.saveConfig({ defaultAccount: "test", accounts: {} });
94
+ expect(fs.existsSync(config.getConfigPath())).toBe(true);
95
+ });
96
+ });
97
+ describe("getAccount", () => {
98
+ it("returns undefined when no accounts exist", () => {
99
+ expect(config.getAccount("nonexistent")).toBeUndefined();
100
+ });
101
+ it("returns account by name", () => {
102
+ config.saveConfig({
103
+ defaultAccount: "default",
104
+ accounts: {
105
+ myaccount: {
106
+ name: "myaccount",
107
+ auth: { type: "bearer", bearerToken: "tok" },
108
+ },
109
+ },
110
+ });
111
+ const account = config.getAccount("myaccount");
112
+ expect(account).toBeDefined();
113
+ expect(account.name).toBe("myaccount");
114
+ });
115
+ it("returns default account when no name specified", () => {
116
+ config.saveConfig({
117
+ defaultAccount: "primary",
118
+ accounts: {
119
+ primary: {
120
+ name: "primary",
121
+ auth: { type: "bearer", bearerToken: "tok1" },
122
+ },
123
+ secondary: {
124
+ name: "secondary",
125
+ auth: { type: "bearer", bearerToken: "tok2" },
126
+ },
127
+ },
128
+ });
129
+ const account = config.getAccount();
130
+ expect(account?.name).toBe("primary");
131
+ });
132
+ it("returns undefined for default account if not in accounts map", () => {
133
+ config.saveConfig({
134
+ defaultAccount: "missing",
135
+ accounts: {},
136
+ });
137
+ expect(config.getAccount()).toBeUndefined();
138
+ });
139
+ });
140
+ describe("setAccount", () => {
141
+ it("adds a new account to config", () => {
142
+ config.setAccount("new", {
143
+ name: "new",
144
+ auth: { type: "bearer", bearerToken: "newtok" },
145
+ });
146
+ const account = config.getAccount("new");
147
+ expect(account).toBeDefined();
148
+ expect(account.auth.bearerToken).toBe("newtok");
149
+ });
150
+ it("overwrites an existing account", () => {
151
+ config.setAccount("test", {
152
+ name: "test",
153
+ auth: { type: "bearer", bearerToken: "old" },
154
+ });
155
+ config.setAccount("test", {
156
+ name: "test",
157
+ auth: { type: "bearer", bearerToken: "new" },
158
+ });
159
+ const account = config.getAccount("test");
160
+ expect(account.auth.bearerToken).toBe("new");
161
+ });
162
+ it("preserves other accounts when adding", () => {
163
+ config.setAccount("a", {
164
+ name: "a",
165
+ auth: { type: "bearer", bearerToken: "tokA" },
166
+ });
167
+ config.setAccount("b", {
168
+ name: "b",
169
+ auth: { type: "bearer", bearerToken: "tokB" },
170
+ });
171
+ expect(config.getAccount("a")?.auth.bearerToken).toBe("tokA");
172
+ expect(config.getAccount("b")?.auth.bearerToken).toBe("tokB");
173
+ });
174
+ });
175
+ describe("setDefaultAccount", () => {
176
+ it("changes the default account name", () => {
177
+ config.saveConfig({ defaultAccount: "old", accounts: {} });
178
+ config.setDefaultAccount("new");
179
+ const cfg = config.loadConfig();
180
+ expect(cfg.defaultAccount).toBe("new");
181
+ });
182
+ });
183
+ describe("config migration", () => {
184
+ it("migrates from legacy config dir when new config absent", async () => {
185
+ // Set up legacy config at $XDG_CONFIG_HOME/xc/config.json
186
+ const legacyXcDir = path.join(legacyDir, "xc");
187
+ fs.mkdirSync(legacyXcDir, { recursive: true });
188
+ const legacyConfig = {
189
+ defaultAccount: "migrated",
190
+ accounts: {
191
+ migrated: {
192
+ name: "migrated",
193
+ auth: { type: "bearer", bearerToken: "legacytoken" },
194
+ },
195
+ },
196
+ };
197
+ fs.writeFileSync(path.join(legacyXcDir, "config.json"), JSON.stringify(legacyConfig));
198
+ // Remove new config file so migration triggers
199
+ const newConfigPath = config.getConfigPath();
200
+ if (fs.existsSync(newConfigPath)) {
201
+ fs.unlinkSync(newConfigPath);
202
+ }
203
+ // Re-import to get a fresh module that will trigger migration on loadConfig
204
+ vi.resetModules();
205
+ const freshConfig = await import("../../lib/config.js");
206
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
207
+ const loaded = freshConfig.loadConfig();
208
+ errSpy.mockRestore();
209
+ expect(loaded.defaultAccount).toBe("migrated");
210
+ expect(loaded.accounts.migrated.auth.bearerToken).toBe("legacytoken");
211
+ // New config file should now exist
212
+ expect(fs.existsSync(freshConfig.getConfigPath())).toBe(true);
213
+ });
214
+ it("does not migrate when new config already exists", async () => {
215
+ // Create legacy config
216
+ const legacyXcDir = path.join(legacyDir, "xc");
217
+ fs.mkdirSync(legacyXcDir, { recursive: true });
218
+ fs.writeFileSync(path.join(legacyXcDir, "config.json"), JSON.stringify({ defaultAccount: "legacy", accounts: {} }));
219
+ // Create new config that should NOT be overwritten
220
+ config.saveConfig({ defaultAccount: "new", accounts: {} });
221
+ // Re-import — migration should not trigger
222
+ vi.resetModules();
223
+ const freshConfig = await import("../../lib/config.js");
224
+ const loaded = freshConfig.loadConfig();
225
+ expect(loaded.defaultAccount).toBe("new");
226
+ });
227
+ });
228
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../../src/__tests__/lib/config.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAKzB,IAAI,MAAc,CAAC;AACnB,IAAI,SAAiB,CAAC;AACtB,IAAI,aAAiC,CAAC;AACtC,IAAI,WAA+B,CAAC;AACpC,IAAI,MAAoB,CAAC;AAEzB,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACnE,SAAS,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAEtE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC1C,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAE1C,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,SAAS,CAAC;IAExC,oEAAoE;IACpE,EAAE,CAAC,YAAY,EAAE,CAAC;IAClB,MAAM,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,aAAa,CAAC;IAC5C,CAAC;SAAM,CAAC;QACN,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IACnC,CAAC;IACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,WAAW,CAAC;IAC5C,CAAC;SAAM,CAAC;QACN,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IACrC,CAAC;IAED,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,qEAAqE;QACrE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,CAAC,eAAe,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,eAAe,EAAE,CAAC;QACzB,MAAM,CAAC,eAAe,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,UAAU,GAAG;YACjB,cAAc,EAAE,WAAW;YAC3B,QAAQ,EAAE;gBACR,SAAS,EAAE;oBACT,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE,WAAW,EAAE,QAAQ,EAAE;iBACzD;aACF;SACF,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,UAAU,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5D,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,OAAO,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,CAAC,UAAU,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5D,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,UAAU,CAAC;YAChB,cAAc,EAAE,SAAS;YACzB,QAAQ,EAAE;gBACR,SAAS,EAAE;oBACT,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE;iBAC7C;aACF;SACF,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,MAAM,CAAC,OAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,UAAU,CAAC;YAChB,cAAc,EAAE,SAAS;YACzB,QAAQ,EAAE;gBACR,OAAO,EAAE;oBACP,IAAI,EAAE,SAAS;oBACf,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE;iBAC9C;gBACD,SAAS,EAAE;oBACT,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE;iBAC9C;aACF;SACF,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QACpC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,CAAC,UAAU,CAAC;YAChB,cAAc,EAAE,SAAS;YACzB,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE;YACvB,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE;SAChD,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,MAAM,CAAC,OAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE;YACxB,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE;SAC7C,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE;YACxB,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE;SAC7C,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACrB,IAAI,EAAE,GAAG;YACT,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE;SAC9C,CAAC,CAAC;QACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACrB,IAAI,EAAE,GAAG;YACT,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE;SAC9C,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9D,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,UAAU,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,0DAA0D;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC/C,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG;YACnB,cAAc,EAAE,UAAU;YAC1B,QAAQ,EAAE;gBACR,QAAQ,EAAE;oBACR,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE;iBACrD;aACF;SACF,CAAC;QACF,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EACrC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAC7B,CAAC;QAEF,+CAA+C;QAC/C,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;QAC7C,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YACjC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAC/B,CAAC;QAED,4EAA4E;QAC5E,EAAE,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,WAAW,GAAiB,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvE,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,EAAE,CAAC;QACxC,MAAM,CAAC,WAAW,EAAE,CAAC;QAErB,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACtE,mCAAmC;QACnC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,uBAAuB;QACvB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC/C,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EACrC,IAAI,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAC3D,CAAC;QAEF,mDAAmD;QACnD,MAAM,CAAC,UAAU,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QAE3D,2CAA2C;QAC3C,EAAE,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,WAAW,GAAiB,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,EAAE,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tests for API cost tracking: estimateCost, inferMethod,
3
+ * logApiCall/loadUsageLog, computeSpend, formatCostFooter.
4
+ * Uses vi.resetModules() + dynamic import for filesystem isolation.
5
+ */
6
+ export {};
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tests for API cost tracking: estimateCost, inferMethod,
3
+ * logApiCall/loadUsageLog, computeSpend, formatCostFooter.
4
+ * Uses vi.resetModules() + dynamic import for filesystem isolation.
5
+ */
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ let tmpDir;
11
+ let origConfigDir;
12
+ let cost;
13
+ beforeEach(async () => {
14
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xc-cost-test-"));
15
+ origConfigDir = process.env.XC_CONFIG_DIR;
16
+ process.env.XC_CONFIG_DIR = tmpDir;
17
+ vi.resetModules();
18
+ cost = await import("../../lib/cost.js");
19
+ });
20
+ afterEach(() => {
21
+ if (origConfigDir !== undefined) {
22
+ process.env.XC_CONFIG_DIR = origConfigDir;
23
+ }
24
+ else {
25
+ delete process.env.XC_CONFIG_DIR;
26
+ }
27
+ fs.rmSync(tmpDir, { recursive: true, force: true });
28
+ });
29
+ describe("estimateCost", () => {
30
+ it("returns known cost for mapped endpoints", () => {
31
+ expect(cost.estimateCost("posts.create")).toBe(0.01);
32
+ expect(cost.estimateCost("posts.searchAll")).toBe(0.02);
33
+ expect(cost.estimateCost("users.getMe")).toBe(0.005);
34
+ });
35
+ it("returns default cost for unknown endpoints", () => {
36
+ expect(cost.estimateCost("unknown.endpoint")).toBe(0.005);
37
+ });
38
+ it("returns zero cost for free endpoints", () => {
39
+ expect(cost.estimateCost("media.appendUpload")).toBe(0.0);
40
+ expect(cost.estimateCost("media.getUploadStatus")).toBe(0.0);
41
+ expect(cost.estimateCost("usage.get")).toBe(0.0);
42
+ });
43
+ });
44
+ describe("inferMethod", () => {
45
+ it("returns POST for write endpoints", () => {
46
+ expect(cost.inferMethod("posts.create")).toBe("POST");
47
+ expect(cost.inferMethod("users.likePost")).toBe("POST");
48
+ expect(cost.inferMethod("media.upload")).toBe("POST");
49
+ });
50
+ it("returns DELETE for removal endpoints", () => {
51
+ expect(cost.inferMethod("users.unlikePost")).toBe("DELETE");
52
+ expect(cost.inferMethod("users.deleteBookmark")).toBe("DELETE");
53
+ expect(cost.inferMethod("users.unfollowUser")).toBe("DELETE");
54
+ });
55
+ it("defaults to GET for unmapped endpoints", () => {
56
+ expect(cost.inferMethod("users.getMe")).toBe("GET");
57
+ expect(cost.inferMethod("posts.searchRecent")).toBe("GET");
58
+ expect(cost.inferMethod("unknown.endpoint")).toBe("GET");
59
+ });
60
+ });
61
+ describe("logApiCall / loadUsageLog", () => {
62
+ it("returns empty array when no log file exists", () => {
63
+ expect(cost.loadUsageLog()).toEqual([]);
64
+ });
65
+ it("appends a single entry to JSONL file", () => {
66
+ cost.logApiCall("posts.create");
67
+ const entries = cost.loadUsageLog();
68
+ expect(entries).toHaveLength(1);
69
+ expect(entries[0].endpoint).toBe("posts.create");
70
+ expect(entries[0].method).toBe("POST");
71
+ expect(entries[0].estimatedCost).toBe(0.01);
72
+ expect(entries[0].timestamp).toBeDefined();
73
+ });
74
+ it("appends multiple entries in order", () => {
75
+ cost.logApiCall("users.getMe");
76
+ cost.logApiCall("posts.create");
77
+ cost.logApiCall("users.likePost");
78
+ const entries = cost.loadUsageLog();
79
+ expect(entries).toHaveLength(3);
80
+ expect(entries[0].endpoint).toBe("users.getMe");
81
+ expect(entries[1].endpoint).toBe("posts.create");
82
+ expect(entries[2].endpoint).toBe("users.likePost");
83
+ });
84
+ it("skips malformed JSON lines", () => {
85
+ const logPath = cost.getUsageLogPath();
86
+ fs.writeFileSync(logPath, '{"timestamp":"2025-01-01T00:00:00Z","endpoint":"a","method":"GET","estimatedCost":0.01}\n' +
87
+ "not-valid-json\n" +
88
+ '{"timestamp":"2025-01-01T00:00:01Z","endpoint":"b","method":"GET","estimatedCost":0.02}\n');
89
+ const entries = cost.loadUsageLog();
90
+ expect(entries).toHaveLength(2);
91
+ expect(entries[0].endpoint).toBe("a");
92
+ expect(entries[1].endpoint).toBe("b");
93
+ });
94
+ it("returns empty array for empty file", () => {
95
+ const logPath = cost.getUsageLogPath();
96
+ fs.writeFileSync(logPath, "");
97
+ expect(cost.loadUsageLog()).toEqual([]);
98
+ });
99
+ });
100
+ describe("computeSpend", () => {
101
+ it("sums entries within the time window", () => {
102
+ const now = Date.now();
103
+ const entries = [
104
+ { timestamp: new Date(now - 1_000).toISOString(), endpoint: "a", method: "GET", estimatedCost: 0.01 },
105
+ { timestamp: new Date(now - 2_000).toISOString(), endpoint: "b", method: "GET", estimatedCost: 0.02 },
106
+ { timestamp: new Date(now - 100_000).toISOString(), endpoint: "c", method: "GET", estimatedCost: 0.05 },
107
+ ];
108
+ // 10-second window should include only the first two entries
109
+ const spend = cost.computeSpend(entries, 10_000);
110
+ expect(spend).toBeCloseTo(0.03);
111
+ });
112
+ it("returns 0 for empty entries", () => {
113
+ expect(cost.computeSpend([], cost.HOUR)).toBe(0);
114
+ });
115
+ it("excludes all entries when outside the window", () => {
116
+ const entries = [
117
+ { timestamp: new Date(Date.now() - 200_000).toISOString(), endpoint: "a", method: "GET", estimatedCost: 0.10 },
118
+ ];
119
+ expect(cost.computeSpend(entries, 1_000)).toBe(0);
120
+ });
121
+ it("includes all entries when window is large enough", () => {
122
+ const now = Date.now();
123
+ const entries = [
124
+ { timestamp: new Date(now - 1_000).toISOString(), endpoint: "a", method: "GET", estimatedCost: 0.01 },
125
+ { timestamp: new Date(now - 5_000).toISOString(), endpoint: "b", method: "GET", estimatedCost: 0.02 },
126
+ ];
127
+ const spend = cost.computeSpend(entries, cost.HOUR);
128
+ expect(spend).toBeCloseTo(0.03);
129
+ });
130
+ });
131
+ describe("computeTodaySpend", () => {
132
+ it("sums entries from today", () => {
133
+ const now = new Date();
134
+ const entries = [
135
+ { timestamp: now.toISOString(), endpoint: "a", method: "GET", estimatedCost: 0.01 },
136
+ { timestamp: now.toISOString(), endpoint: "b", method: "POST", estimatedCost: 0.02 },
137
+ ];
138
+ expect(cost.computeTodaySpend(entries)).toBeCloseTo(0.03);
139
+ });
140
+ it("excludes entries from yesterday", () => {
141
+ const yesterday = new Date();
142
+ yesterday.setDate(yesterday.getDate() - 1);
143
+ yesterday.setHours(23, 59, 59, 999);
144
+ const entries = [
145
+ { timestamp: yesterday.toISOString(), endpoint: "a", method: "GET", estimatedCost: 0.50 },
146
+ ];
147
+ expect(cost.computeTodaySpend(entries)).toBe(0);
148
+ });
149
+ it("returns 0 for empty entries", () => {
150
+ expect(cost.computeTodaySpend([])).toBe(0);
151
+ });
152
+ });
153
+ describe("time constants", () => {
154
+ it("has correct values", () => {
155
+ expect(cost.HOUR).toBe(3_600_000);
156
+ expect(cost.DAY).toBe(86_400_000);
157
+ expect(cost.WEEK).toBe(7 * 86_400_000);
158
+ expect(cost.MONTH).toBe(30 * 86_400_000);
159
+ });
160
+ });
161
+ describe("formatCostFooter", () => {
162
+ it("returns empty string when no usage recorded", () => {
163
+ expect(cost.formatCostFooter()).toBe("");
164
+ });
165
+ it("formats cost breakdown by time windows", () => {
166
+ cost.logApiCall("posts.create");
167
+ cost.logApiCall("users.getMe");
168
+ const footer = cost.formatCostFooter();
169
+ expect(footer).toContain("Cost:");
170
+ expect(footer).toContain("(1h)");
171
+ expect(footer).toContain("(24h)");
172
+ expect(footer).toContain("(7d)");
173
+ expect(footer).toContain("(30d)");
174
+ expect(footer).toContain("$");
175
+ });
176
+ });
177
+ //# sourceMappingURL=cost.test.js.map